aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-17 19:03:03 +0200
committerDanilo M. <danix@danix.xyz>2026-09-17 19:03:03 +0200
commit1aa67dbe3e711617895d7b1d6e4dffccc74fa95a (patch)
tree792a15a803602dc318daa51019d7433a1cf4fc14
parentd9fb5c42e8a8deed21d93b6961a0775acea5540a (diff)
downloadconky-theme-udt-1aa67dbe3e711617895d7b1d6e4dffccc74fa95a.tar.gz
conky-theme-udt-1aa67dbe3e711617895d7b1d6e4dffccc74fa95a.zip
feat: add card.plot, a shared-scale line chart
Several series on one baseline and one ceiling. The shared ceiling is the point: scaled independently, a 200kB/s upload draws the same height as a 40MB/s download, which is a lie about a link's shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--lib/card.lua54
1 files changed, 54 insertions, 0 deletions
diff --git a/lib/card.lua b/lib/card.lua
index f25c393..337347e 100644
--- a/lib/card.lua
+++ b/lib/card.lua
@@ -270,4 +270,58 @@ function M.truncate(s, limit)
return s:sub(1, cut - 1) .. '\u{2026}'
end
+-- A line chart: several series sharing one baseline and one vertical scale.
+--
+-- `series` is a list of { values = <array>, colour = <rgb> }. `max` is the
+-- shared ceiling; passing one rather than computing per series is the whole
+-- point, because two series scaled independently lie about their relative
+-- size: a 200kB/s upload would draw the same height as a 40MB/s download.
+--
+-- Values are drawn oldest-left, one per array entry, so a caller sizing its
+-- history to the pixel width gets one sample per column and no interpolation.
+-- A series shorter than its buffer draws only what it has, which is what a
+-- freshly started dashboard shows while the window fills.
+function M.plot(cr, x, y, w, h, series, max, colors)
+ if not (w > 0 and h > 0) then return end
+ max = tonumber(max) or 0
+ if max <= 0 then max = 1 end -- an idle link is a flat line, not a division by zero
+
+ local prev_width = cairo_get_line_width(cr)
+ local prev_cap = cairo_get_line_cap(cr)
+
+ -- The baseline, so an empty chart still reads as a chart rather than a gap.
+ M.rgba(cr, colors.rule, 0.5)
+ cairo_new_path(cr)
+ cairo_set_line_width(cr, 1)
+ cairo_move_to(cr, x, y + h)
+ cairo_line_to(cr, x + w, y + h)
+ cairo_stroke(cr)
+
+ cairo_set_line_width(cr, math.max(1.5, h * 0.02))
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND)
+
+ for _, s in ipairs(series) do
+ local v = s.values
+ local n = #v
+ if n >= 2 then
+ -- One sample per column when the caller sized its buffer to the width.
+ local step = w / math.max(n - 1, 1)
+ M.rgba(cr, s.colour, 1)
+ cairo_new_path(cr)
+ for i = 1, n do
+ local frac = v[i] / max
+ if frac < 0 then frac = 0 elseif frac > 1 then frac = 1 end
+ local px = x + (i - 1) * step
+ local py = y + h - frac * h
+ if i == 1 then cairo_move_to(cr, px, py) else cairo_line_to(cr, px, py) end
+ end
+ cairo_stroke(cr)
+ end
+ end
+
+ -- Leave the line state as it was found, for the same reason card.ring does.
+ cairo_set_line_width(cr, prev_width)
+ cairo_set_line_cap(cr, prev_cap)
+end
+
return M