Implement the history graph in the workflow status screen.

develop
Alexander Gavrilov 2012-12-01 16:50:03 +04:00
parent 05dce0d2f1
commit 58239e97ed
11 changed files with 248 additions and 96 deletions

@ -2787,6 +2787,14 @@ before rendering the token.</p>
<li><p class="first"><tt class="docutils literal">token.tile = pen</tt></p>
<p>Specifies a pen to paint as one tile before the main part of the token.</p>
</li>
<li><p class="first"><tt class="docutils literal">token.width = ...</tt></p>
<p>If specified either as a value or a callback, the text field is padded
or truncated to the specified number.</p>
</li>
<li><p class="first"><tt class="docutils literal">token.pad_char = <span class="pre">'?'</span></tt></p>
<p>If specified together with <tt class="docutils literal">width</tt>, the padding area is filled with
this character instead of just being skipped over.</p>
</li>
<li><p class="first"><tt class="docutils literal">token.key = <span class="pre">'...'</span></tt></p>
<p>Specifies the keycode associated with the token. The string description
of the key binding is added to the text content of the token.</p>
@ -2848,7 +2856,9 @@ this may be extended with mouse click support.</p>
</tr>
<tr class="field"><th class="field-name">icon_pen:</th><td class="field-body">Default pen for icons.</td>
</tr>
<tr class="field"><th class="field-name">on_select:</th><td class="field-body">Selection change callback; called as <tt class="docutils literal">on_select(index,choice)</tt>.</td>
<tr class="field"><th class="field-name">on_select:</th><td class="field-body">Selection change callback; called as <tt class="docutils literal">on_select(index,choice)</tt>.
This is also called with <em>nil</em> arguments if <tt class="docutils literal">setChoices</tt> is called
with an empty list.</td>
</tr>
<tr class="field"><th class="field-name">on_submit:</th><td class="field-body">Enter key callback; if specified, the list reacts to the key
and calls it as <tt class="docutils literal">on_submit(index,choice)</tt>.</td>
@ -2928,6 +2938,8 @@ supports:</p>
<tbody valign="top">
<tr class="field"><th class="field-name">edit_pen:</th><td class="field-body">If specified, used instead of <tt class="docutils literal">cursor_pen</tt> for the edit field.</td>
</tr>
<tr class="field"><th class="field-name">edit_below:</th><td class="field-body">If true, the edit field is placed below the list instead of above.</td>
</tr>
<tr class="field"><th class="field-name" colspan="2">not_found_label:</th></tr>
<tr class="field"><td>&nbsp;</td><td class="field-body">Specifies the text of the label shown when no items match the filter.</td>
</tr>

@ -2785,6 +2785,8 @@ It has the following attributes:
:inactive_pen: If specified, used for the cursor when the widget is not active.
:icon_pen: Default pen for icons.
:on_select: Selection change callback; called as ``on_select(index,choice)``.
This is also called with *nil* arguments if ``setChoices`` is called
with an empty list.
:on_submit: Enter key callback; if specified, the list reacts to the key
and calls it as ``on_submit(index,choice)``.
:on_submit2: Shift-Enter key callback; if specified, the list reacts to the key

@ -3034,11 +3034,11 @@ current job, and their current status.</p>
current count is below the lower bound of the range, the job is resumed; if it
is above or equal to the top bound, it will be suspended. Within the range, the
specific constraint has no effect on the job; others may still affect it.</p>
<p>Pressing 'c' switches the current constraint between counting stacks or items.
Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting
items and expanding the range each gives a 5x bonus).</p>
<p>Pressing 'n' produces a list of possible outputs of this job as guessed by
<p>Pressing 'I' switches the current constraint between counting stacks or items.
Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
items and expanding the range each gives a 2x bonus).</p>
<p>Pressing 'A' produces a list of possible outputs of this job as guessed by
workflow, and lets you create a new constraint by choosing one as template. If you
don't see the choice you want in the list, it likely means you have to adjust
the job material first using <tt class="docutils literal">job <span class="pre">item-material</span></tt> or <tt class="docutils literal"><span class="pre">gui/workshop-job</span></tt>,
@ -3050,6 +3050,23 @@ added to the list. If you use Shift-Enter, the interface proceeds to the
next dialog, which allows you to edit the suggested constraint parameters to
suit your need, and set the item count range.</p>
<img alt="images/workflow-new2.png" src="images/workflow-new2.png" />
<p>Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen)
opens the overall status screen, which was copied from the C++ implementation
by falconne for better integration with the rest of the lua script:</p>
<img alt="images/workflow-status.png" src="images/workflow-status.png" />
<p>This screen shows all currently existing workflow constraints, and allows
monitoring and/or changing them from one screen. The constraint list can
be filtered by typing text in the field below.</p>
<p>The color of the stock level number indicates how &quot;healthy&quot; the stock level
is, based on current count and trend. Bright green is very good, green is good,
red is bad, bright red is very bad.</p>
<p>The limit number is also color-coded. Red means that there are currently no
workshops producing that item (i.e. no jobs). If it's yellow, that means the
production has been delayed, possibly due to lack of input materials.</p>
<p>The chart on the right is a plot of the last 14 days (28 half day plots) worth
of stock history for the selected item, with the rightmost point representing
the current stock value. The bright green dashed line is the target
limit (maximum) and the dark green line is that minus the gap (minimum).</p>
</div>
<div class="section" id="gui-assign-rack">
<h2><a class="toc-backref" href="#id146">gui/assign-rack</a></h2>

@ -2295,12 +2295,12 @@ current count is below the lower bound of the range, the job is resumed; if it
is above or equal to the top bound, it will be suspended. Within the range, the
specific constraint has no effect on the job; others may still affect it.
Pressing 'c' switches the current constraint between counting stacks or items.
Pressing 'm' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 1, 5, or 25 depending on the direction and the 'c' setting (counting
items and expanding the range each gives a 5x bonus).
Pressing 'I' switches the current constraint between counting stacks or items.
Pressing 'R' lets you input the range directly; 'e', 'r', 'd', 'f' adjust the
bounds by 5, 10, or 20 depending on the direction and the 'I' setting (counting
items and expanding the range each gives a 2x bonus).
Pressing 'n' produces a list of possible outputs of this job as guessed by
Pressing 'A' produces a list of possible outputs of this job as guessed by
workflow, and lets you create a new constraint by choosing one as template. If you
don't see the choice you want in the list, it likely means you have to adjust
the job material first using ``job item-material`` or ``gui/workshop-job``,
@ -2316,6 +2316,29 @@ suit your need, and set the item count range.
.. image:: images/workflow-new2.png
Pressing 'S' (or, with the example config, Alt-W in the 'z' stocks screen)
opens the overall status screen, which was copied from the C++ implementation
by falconne for better integration with the rest of the lua script:
.. image:: images/workflow-status.png
This screen shows all currently existing workflow constraints, and allows
monitoring and/or changing them from one screen. The constraint list can
be filtered by typing text in the field below.
The color of the stock level number indicates how "healthy" the stock level
is, based on current count and trend. Bright green is very good, green is good,
red is bad, bright red is very bad.
The limit number is also color-coded. Red means that there are currently no
workshops producing that item (i.e. no jobs). If it's yellow, that means the
production has been delayed, possibly due to lack of input materials.
The chart on the right is a plot of the last 14 days (28 half day plots) worth
of stock history for the selected item, with the rightmost point representing
the current stock value. The bright green dashed line is the target
limit (maximum) and the dark green line is that minus the gap (minimum).
gui/assign-rack
===============

@ -91,6 +91,7 @@ keybinding add Alt-A@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workshop-job
# workflow front-end
keybinding add Alt-W@dwarfmode/QueryBuilding/Some/Workshop/Job gui/workflow
keybinding add Alt-W@overallstatus "gui/workflow status"
# assign weapon racks to squads so that they can be used
keybinding add P@dwarfmode/QueryBuilding/Some/Weaponrack gui/assign-rack

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

@ -418,7 +418,13 @@ List.ATTRS{
function List:init(info)
self.page_top = 1
self.page_size = 1
self:setChoices(info.choices, info.selected)
if info.choices then
self:setChoices(info.choices, info.selected)
else
self.choices = {}
self.selected = 1
end
end
function List:setChoices(choices, selected)
@ -481,6 +487,9 @@ function List:moveCursor(delta, force_cb)
if cnt < 1 then
self.page_top = 1
self.selected = 1
if force_cb and self.on_select then
self.on_select(nil,nil)
end
return
end
@ -657,13 +666,17 @@ function FilteredList:init(info)
end
end
self.not_found = Label{
visible = false,
visible = true,
text = info.not_found_label or 'No matches',
text_pen = COLOR_LIGHTRED,
frame = { l = info.icon_width, t = self.list.frame.t },
}
self:addviews{ self.edit, self.list, self.not_found }
self:setChoices(info.choices, info.selected)
if info.choices then
self:setChoices(info.choices, info.selected)
else
self.choices = {}
end
end
function FilteredList:getChoices()

@ -130,12 +130,20 @@ RangeEditor = defclass(RangeEditor, widgets.Label)
RangeEditor.ATTRS {
get_cb = DEFAULT_NIL,
save_cb = DEFAULT_NIL
save_cb = DEFAULT_NIL,
keys = {
count = 'CUSTOM_SHIFT_I',
modify = 'CUSTOM_SHIFT_R',
min_dec = 'BUILDING_TRIGGER_MIN_SIZE_DOWN',
min_inc = 'BUILDING_TRIGGER_MIN_SIZE_UP',
max_dec = 'BUILDING_TRIGGER_MAX_SIZE_DOWN',
max_inc = 'BUILDING_TRIGGER_MAX_SIZE_UP',
}
}
function RangeEditor:init(args)
self:setText{
{ key = 'BUILDING_TRIGGER_ENABLE_CREATURE',
{ key = self.keys.count,
text = function()
local cons = self.get_cb() or null_cons
if cons.goal_by_count then
@ -145,21 +153,21 @@ function RangeEditor:init(args)
end
end,
on_activate = self:callback('onChangeUnit') },
{ key = 'BUILDING_TRIGGER_ENABLE_MAGMA', text = ': Modify',
{ key = self.keys.modify, text = ': Range',
on_activate = self:callback('onEditRange') },
NEWLINE, ' ',
{ key = 'BUILDING_TRIGGER_MIN_SIZE_DOWN',
on_activate = self:callback('onIncRange', 'goal_gap', 5) },
{ key = 'BUILDING_TRIGGER_MIN_SIZE_UP',
{ key = self.keys.min_dec,
on_activate = self:callback('onIncRange', 'goal_gap', 2) },
{ key = self.keys.min_inc,
on_activate = self:callback('onIncRange', 'goal_gap', -1) },
{ text = function()
local cons = self.get_cb() or null_cons
return string.format(': Min %-4d ', cons.goal_value - cons.goal_gap)
end },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_DOWN',
{ key = self.keys.max_dec,
on_activate = self:callback('onIncRange', 'goal_value', -1) },
{ key = 'BUILDING_TRIGGER_MAX_SIZE_UP',
on_activate = self:callback('onIncRange', 'goal_value', 5) },
{ key = self.keys.max_inc,
on_activate = self:callback('onIncRange', 'goal_value', 2) },
{ text = function()
local cons = self.get_cb() or null_cons
return string.format(': Max %-4d', cons.goal_value)
@ -200,9 +208,9 @@ end
function RangeEditor:onIncRange(field, delta)
local cons = self.get_cb()
if not cons.goal_by_count then
delta = delta * 5
delta = delta * 2
end
cons[field] = math.max(1, cons[field] + delta)
cons[field] = math.max(1, cons[field] + delta*5)
self.save_cb(cons)
end
@ -295,7 +303,7 @@ function NewConstraint:init(args)
}
},
widgets.Label{
frame = { l = 0, t = 13 },
frame = { l = 0, t = 14 },
text = {
'Desired range: ',
{ pen = COLOR_LIGHTCYAN,
@ -311,7 +319,7 @@ function NewConstraint:init(args)
}
},
RangeEditor{
frame = { l = 1, t = 15 },
frame = { l = 1, t = 16 },
get_cb = self:cb_getfield('constraint'),
save_cb = self:callback('onRangeChange'),
},
@ -353,7 +361,7 @@ function NewConstraint:postinit()
end
function NewConstraint:isValid()
return self.constraint.item_type >= 0
return self.constraint.item_type >= 0 or self.constraint.is_craft
end
function NewConstraint:onChange()
@ -455,6 +463,59 @@ function NewConstraint:onRangeChange()
cons.goal_gap = math.max(1, math.min(cons.goal_gap, cons.goal_value-1))
end
------------------------------
-- CONSTRAINT HISTORY GRAPH --
------------------------------
HistoryGraph = defclass(HistoryGraph, widgets.Widget)
HistoryGraph.ATTRS {
frame_inset = 1,
history_pen = COLOR_CYAN,
}
function HistoryGraph:init(info)
end
function HistoryGraph:setData(history, bars)
self.history = history or {}
self.bars = bars or {}
local maxval = 1
for i,v in ipairs(self.history) do
maxval = math.max(maxval, v)
end
for i,v in ipairs(self.bars) do
maxval = math.max(maxval, v.value)
end
self.max_value = maxval
end
function HistoryGraph:onRenderFrame(dc,rect)
dc:fill(rect.x1,rect.y1,rect.x1,rect.y2,{ch='\xb3', fg=COLOR_BROWN})
dc:fill(rect.x1,rect.y2,rect.x2,rect.y2,{ch='\xc4', fg=COLOR_BROWN})
dc:seek(rect.x1,rect.y1):char('\x1e', COLOR_BROWN)
dc:seek(rect.x1,rect.y2):char('\xc5', COLOR_BROWN)
dc:seek(rect.x2,rect.y2):char('\x10', COLOR_BROWN)
dc:seek(rect.x1,rect.y2-1):char('0', COLOR_BROWN)
end
function HistoryGraph:onRenderBody(dc)
local coeff = (dc.height-1)/self.max_value
for i,v in ipairs(self.bars) do
local y = dc.height-1-math.floor(0.5 + coeff*v.value)
dc:fill(0,y,dc.width-1,y,v.pen or {ch='-', fg=COLOR_GREEN})
end
local xbase = dc.width-1-#self.history
for i,v in ipairs(self.history) do
local x = xbase + i
local y = dc.height-1-math.floor(0.5 + coeff*v)
dc:seek(x,y):char('*', self.history_pen)
end
end
------------------------------
-- GLOBAL CONSTRAINT SCREEN --
------------------------------
@ -478,47 +539,7 @@ function ConstraintList:init(args)
self:addviews{
widgets.Panel{
frame = { w = 31, r = 0, h = 6, t = 0 },
frame_inset = 1,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
enabled = self:callback('isAnySelected'),
text = {
{ text = function()
local cur = self:getCurConstraint()
if cur then
return string.format(
'Currently %d (%d in use)',
current_stock(cur),
if_by_count(cur, cur.cur_in_use_count, cur.cur_in_use_amount)
)
else
return 'No constraint selected'
end
end }
}
},
RangeEditor{
frame = { l = 0, t = 2 },
enabled = self:callback('isAnySelected'),
get_cb = self:callback('getCurConstraint'),
save_cb = self:callback('saveConstraint'),
},
}
},
widgets.Widget{
active = false,
frame = { w = 1, r = 31 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Widget{
active = false,
frame = { w = 31, r = 0, h = 1, t = 6 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Panel{
frame = { l = 0, r = 32 },
frame = { l = 0, r = 31 },
frame_inset = 1,
on_layout = function(body)
self.fwidth = body.width - (12+1+1+7+1+1+1+7)
@ -541,6 +562,7 @@ function ConstraintList:init(args)
edit_pen = COLOR_LIGHTCYAN,
text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK },
cursor_pen = { fg = COLOR_WHITE, bg = COLOR_GREEN },
on_select = self:callback('onSelectConstraint'),
},
widgets.Label{
frame = { b = 0, h = 1 },
@ -557,27 +579,66 @@ function ConstraintList:init(args)
else
return COLOR_WHITE
end
end }, ', ',
{ key = 'CUSTOM_SHIFT_S', text = ': Search',
on_activate = function()
self.subviews.list.edit.active = not self.subviews.list.edit.active
end,
pen = function()
if self.subviews.list.edit.active then
return COLOR_LIGHTCYAN
end },
}
}
}
},
widgets.Panel{
frame = { w = 30, r = 0, h = 6, t = 0 },
frame_inset = 1,
subviews = {
widgets.Label{
frame = { l = 0, t = 0 },
enabled = self:callback('isAnySelected'),
text = {
{ text = function()
local cur = self:getCurConstraint()
if cur then
return string.format(
'Currently %d (%d in use)',
current_stock(cur),
if_by_count(cur, cur.cur_in_use_count, cur.cur_in_use_amount)
)
else
return COLOR_WHITE
return 'No constraint selected'
end
end }
}
}
},
RangeEditor{
frame = { l = 0, t = 2 },
enabled = self:callback('isAnySelected'),
get_cb = self:callback('getCurConstraint'),
save_cb = self:callback('saveConstraint'),
keys = {
count = 'CUSTOM_SHIFT_I',
modify = 'CUSTOM_SHIFT_R',
min_dec = 'SECONDSCROLL_PAGEUP',
min_inc = 'SECONDSCROLL_PAGEDOWN',
max_dec = 'SECONDSCROLL_UP',
max_inc = 'SECONDSCROLL_DOWN',
}
},
}
},
widgets.Widget{
active = false,
frame = { w = 1, r = 30 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
widgets.Widget{
active = false,
frame = { w = 30, r = 0, h = 1, t = 6 },
frame_background = gui.BOUNDARY_FRAME.frame_pen,
},
HistoryGraph{
view_id = 'graph',
frame = { w = 30, r = 0, t = 7, b = 0 },
}
}
self.subviews.list.edit.active = false
self:initListChoices()
self:initListChoices(nil, args.select_token)
end
function stock_trend_color(cons)
@ -733,6 +794,29 @@ function ConstraintList:onDeleteConstraint()
)
end
function ConstraintList:onSelectConstraint(idx,item)
local history, bars
if item then
local cons = item.obj
local vfield = if_by_count(cons, 'cur_count', 'cur_amount')
bars = {
{ value = cons.goal_value - cons.goal_gap, pen = {ch='-', fg=COLOR_GREEN} },
{ value = cons.goal_value, pen = {ch='-', fg=COLOR_LIGHTGREEN} },
}
history = {}
for i,v in ipairs(cons.history or {}) do
table.insert(history, v[vfield])
end
table.insert(history, cons[vfield])
end
self.subviews.graph:setData(history, bars)
end
-------------------------------
-- WORKSHOP JOB INFO OVERLAY --
-------------------------------
@ -772,14 +856,20 @@ function JobConstraints:init(args)
widgets.Label{
frame = { l = 0, b = 0 },
text = {
{ key = 'CUSTOM_N', text = ': New limit, ',
{ key = 'CUSTOM_SHIFT_A', text = ': Add limit, ',
on_activate = self:callback('onNewConstraint') },
{ key = 'CUSTOM_X', text = ': Delete',
{ key = 'CUSTOM_SHIFT_X', text = ': Delete',
enabled = self:callback('isAnySelected'),
on_activate = self:callback('onDeleteConstraint') },
NEWLINE, NEWLINE,
{ key = 'LEAVESCREEN', text = ': Back',
on_activate = self:callback('dismiss') }
on_activate = self:callback('dismiss') },
' ',
{ key = 'CUSTOM_SHIFT_S', text = ': Status',
on_activate = function()
local sel = self:getCurConstraint()
ConstraintList{ select_token = (sel or {}).token }:show()
end }
}
},
}
@ -873,22 +963,16 @@ function JobConstraints:onNewConstraint()
local choices = {}
for i,cons in ipairs(variants) do
local itemstr = describe_item_type(cons)
local matstr = describe_material(cons)
local matflags = utils.list_bitfield_flags(cons.mat_mask)
if #matflags > 0 then
local fstr = table.concat(matflags, '/')
if matstr == 'any material' then
matstr = 'any '..fstr
else
matstr = 'any '..fstr..' '..matstr
end
local matstr,matflags = describe_material(cons)
if matflags then
matstr = matflags..' '..matstr
end
table.insert(choices, { text = itemstr..' of '..matstr, obj = cons })
end
dlg.ListBox{
frame_title = 'New limit',
frame_title = 'Add limit',
text = 'Select one of the possible outputs:',
text_pen = COLOR_WHITE,
choices = choices,
@ -930,7 +1014,7 @@ end
local args = {...}
if args[1] == 'list' then
if args[1] == 'status' then
check_enabled(function() ConstraintList{}:show() end)
else
if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then