Add graph.step_hook property for more flexible drawing

There is a limit to how much one can do with step_shape callback.

By its nature it can only have three parameters, and that list
can't be extended, because the user is expected to set
the property to a function from gears.shape, all of which have
different meanings for parameters past the first 3.

Moreover step_shape requires the current coordinate system
to be modified accordingly because it draws the shape at (0,0).

Lastly it's not expected to handle NaN heights and thus is
never called for NaN values.

This makes it hard to implement things like the following:
1) drawing steps which depend on knowing their position
relative to other steps. (e.g. connecting data points with
bezier curves)
2) drawing steps while appropriately handling NaN values in
any way other than not drawing anything, which might be still
wrong or not sufficient. (e.g. interpolating data points
requires to know *where* there are gaps in data, not simply
continuing with the next present value)
3) drawing steps that need the knowledge of the exact value
that is being drawn (e.g. drawing value tooltips over bars)

The step_hook callback (name bikeshedding welcome) is designed
to solve the problems (for now only the first two of the 3).

Whenever it's set, it takes precedence over step_shape property
and is used to draw steps.
No coordinate transformation before calling it takes place
like for step_shape(). The (0, 0) is always the top-left corner
of the graph drawing area (sans borders), when it's called.

In contrast to step_shape() which only accepts three parameters
(cairo, width, height), step_hook() accepts
(cairo, x, y, baseline_y, step_width, options).

(x, y) is what would be (0, 0) in step_shape, i.e. the
coordinates of the bar top. The y parameter can be NaN,
and step_hook() is expected to handle that.

baseline_y is the y coordinate of the bar bottom.
(baseline_y - y) is what is known as `height` in step_shape.
But note that baseline_y is never NaN, so even in the
NaN case step_hook() can at least know where the baseline is.

step_width is the bar's width, just like in step_shape.

options is the same table that is passed to the
group_start()/group_finish() callbacks, it contains
some useful data for nontrivial drawing needs and it could be
extended later with more useful data at leisure,
e.g. with `value`, if such need arises, without fear of
coming in conflict with other user's parameters.
This commit is contained in:
Alex Belykh 2021-04-25 07:51:37 +07:00
parent ff882f1d80
commit 3e3b0ca9de
1 changed files with 91 additions and 22 deletions

View File

@ -174,6 +174,8 @@ local graph = { mt = {} }
--- The step shape.
--
-- If `step_hook` property is also set, this property is ignored.
--
--@DOC_wibox_widget_graph_step_shape_EXAMPLE@
--
-- @property step_shape
@ -297,6 +299,59 @@ local graph = { mt = {} }
-- @tparam @{draw_callback_options} options Additional info (@{draw_callback_options})
-- @see group_finish
--- Control what is done to draw each value.
--
-- This property can be set to a `step_hook_callback` function or nil.
--
-- When `step_hook` is nil (default), the default implementation
-- is used, which draws the values as `step_shape` bars.
--
-- @DOC_wibox_widget_graph_bezier_curve_EXAMPLE@
--
-- @property step_hook
-- @within Advanced drawing
-- @tparam function|nil step_hook
-- @propemits true false
--- User callback for drawing a value.
--
-- This function is called in succession to draw the values of a data group,
-- starting with the newest value.
--
-- Prior to this, the `group_start_callback` will be called once for
-- the data group. And after `step_hook` was called for all values
-- necessary, the `group_finish_callback` will be called once.
--
-- A drawing session consists of repeating the above steps for each drawn
-- data group. The order of drawing of data groups is unspecified.
--
-- This function can assume, that nothing except itself interferes with
-- the widget or cairo context state between the paired
-- `group_start_callback`/`group_finish_callback` calls.
--
-- The coordinate system of the cairo context during all calls is the
-- one with (0,0) and (width, height) corresponding to the top-left
-- and the bottom-right corner of the graph's value drawing area respectively.
--
-- The parameters of this callback are pretty straightforward.
-- It deserves a note though, that the baseline\_y parameter could vary between
-- successive calls, because e.g. for stacked graphs, baseline\_y corresponds
-- to the tops of the bars of the lower data group.
--
-- It's also not necessary that `baseline_y >= value_y`, the opposite holds e.g.
-- for graphs with values smaller than `baseline_value`.
--
-- @callback step_hook_callback
-- @within Advanced drawing
-- @tparam cairo.Context cr Cairo context
-- @tparam number x The horizontal coordinate of the left edge of the bar.
-- @tparam number value_y The vertical coordinate corresponding to the drawn value.
-- Can be a NaN.
-- @tparam number baseline_y The vertical coordinate corresponding to the baseline.
-- @tparam number step_width Same as `step_width` (but never nil).
-- @tparam @{draw_callback_options} options Additional info (@{draw_callback_options}).
-- @see step_hook
--- The graph foreground color
-- Used, when the `color` property isn't set.
--
@ -321,7 +376,7 @@ local properties = { "width", "height", "border_color", "stack",
"step_spacing", "step_width", "border_width",
"clamp_bars", "baseline_value",
"capacity", "nan_color", "nan_indication",
"group_start", "group_finish",
"group_start", "group_finish", "step_hook",
"group_colors",
}
@ -597,19 +652,13 @@ end
local function graph_draw_values(self, cr, width, height, drawn_values_num)
local values = self._private.values
local step_shape = self._private.step_shape
local step_spacing = self._private.step_spacing or prop_fallbacks.step_spacing
local step_width = self._private.step_width or prop_fallbacks.step_width
-- Cache methods used in the inner loop for a 3x performance boost
local cairo_rectangle = cr.rectangle
local cairo_translate = cr.translate
local cairo_set_matrix = cr.set_matrix
local map_coords = graph_map_value_to_widget_coordinates
-- Preserve the transform centered at the top-left corner of the graph
local pristine_transform = step_shape and cr:get_matrix()
local drawn_values, scaling_values = graph_preprocess_values(self, values, drawn_values_num)
-- If preprocessor returned drawn_values = nil, then simply draw the values we have
drawn_values = drawn_values or values
@ -662,6 +711,29 @@ local function graph_draw_values(self, cr, width, height, drawn_values_num)
end
end
-- The user callback for drawing each data bar
local step_hook = self._private.step_hook
if not step_hook then
local step_shape = self._private.step_shape
if step_shape then
-- Preserve the transform centered at the top-left corner of the graph
local pristine_transform = cr:get_matrix()
local cairo_translate = cr.translate
local cairo_set_matrix = cr.set_matrix
step_hook = function(cr, x, value_y, baseline_y, step_width, _options) --luacheck: ignore 431 432 212/_.*
local step_height = baseline_y - value_y
if not (step_height == step_height) then
return -- is NaN
end
-- Shift to the bar beginning
cairo_translate(cr, x, value_y)
step_shape(cr, step_width, step_height)
-- Undo the shift
cairo_set_matrix(cr, pristine_transform)
end
end
end
local nan_x = self._private.nan_indication and {}
local prev_y = self._private.stack and {}
@ -682,24 +754,21 @@ local function graph_draw_values(self, cr, width, height, drawn_values_num)
-- The coordinate of the i-th bar's left edge
local x = (i-1)*(step_width + step_spacing) + offset_x
if not_nan then
local base_y = baseline_y
if prev_y then
-- Draw from where the previous stacked series left off
base_y = prev_y[i] or base_y
-- Save our y for the next stacked series
local base_y = baseline_y
if prev_y then
-- Draw from where the previous stacked series left off
base_y = prev_y[i] or base_y
-- Save our y for the next stacked series
if not_nan then
prev_y[i] = value_y
end
end
if step_shape then
-- Shift to the bar beginning
cairo_translate(cr, x, value_y)
step_shape(cr, step_width, base_y - value_y)
-- Undo the shift
cairo_set_matrix(cr, pristine_transform)
else
cairo_rectangle(cr, x, value_y, step_width, base_y - value_y)
end
if step_hook then
-- step_hook() is expected to handle NaNs itself
step_hook(cr, x, value_y, base_y, step_width, options)
elseif not_nan then
cairo_rectangle(cr, x, value_y, step_width, base_y - value_y)
end
if not not_nan and nan_x then