Revision history for HTML-D3 - Visualise various charts using D3
0.23 Sat Oct 17 08:28:48 AM EDT 2026
[ Enhancements ]
- The chart snippets inline each categorical scheme's colours as a literal
hex array, taken from the one definition that palette() and colour_at()
also read, instead of naming a d3.scheme* expression for the browser to
look up. A page and a map colouring the same categories therefore draw
the same numbers, and an upgrade of d3 cannot move a chart's colours: the
page no longer asks d3 for a palette at all
This affects the SCHEMES lookup table the pie, stacked bar and treemap
snippets emit, the direct scaleOrdinal() calls of the multi-series line,
scatter and binned histogram snippets, and the full-page charts that draw
category10 or the default tableau10 palette. The scheme options and the
fallback for an unknown name are unchanged, and both are still resolved
from the definition, so an unknown name draws the default rather than an
invented d3.schemeNonsense
- The d3 scheme each palette transcribes is recorded beside the definition
(and in t/palette.t), so it can still be checked against the library, but
it is no longer emitted into the page. t/d3-schemes.t continues to check
palette() against d3 itself when Node and d3 are installed
[ Tests ]
- t/palette.t now asserts the inlined arrays rather than d3.scheme*
expressions: the SCHEMES table must hold exactly the colours palette()
returns for each scheme, the snippets that draw their colours directly
must inline the requested scheme's array, and the module's code must hold
no d3.scheme* expression at all. The tests that asserted the old
expressions in snippet output were updated to the hex arrays (the key
lookups they also asserted are unchanged)
0.22 Fri Oct 16 09:50:42 PM EDT 2026
[ Tests ]
- t/d3-schemes.t checks palette() against d3 itself. It runs Node, loads
the d3 package and compares each scheme - category10, tableau10, set2,
set3 and paired - colour by colour and count by count with the array
d3.scheme* publishes. t/palette.t is an offline oracle: it holds D3's
colours as this distribution read them, so a transcription error there
and in the module would agree with itself, and this test is what catches
it. Neither Node nor d3 is a prerequisite, so the file skips with the
reason when it cannot reach them, and fails (rather than skipping) when
d3 is reached but does not publish a scheme, since that is the module
describing a palette d3 does not have
0.21 Fri Oct 16 09:19:00 PM EDT 2026
[ Enhancements ]
- The categorical colour schemes are defined once and exposed to Perl. New
methods categorical_schemes() (the names a scheme option accepts),
palette($scheme) (that scheme's colours, in D3's order) and
colour_at($scheme, $index) (the colour the scheme gives the Nth category,
cycling the palette as d3.scaleOrdinal does), so that a map, a legend or a
static renderer can colour the same data the chart does, from Perl, with no
browser. palette() and colour_at() die with 'Unknown scheme: <name>' for a
name that is not a scheme, rather than answering with the default scheme's
colours.
- The snippets' JS colour lookup is generated from that one definition.
render_pie_chart_snippet, render_stacked_bar_snippet and
render_treemap_snippet each carried its own copy of the same five scheme
names, and render_multi_series_line_chart_snippet,
render_scatter_chart_snippet and render_histogram_snippet (binned)
capitalised the scheme name they were given and looked up the expression
that spelling produced - so a name they did not know became
d3.schemeNonsense, an undefined function in the browser. All six draw with
the expression the definition holds, and a name that is not a scheme draws
with the documented default, tableau10, which is what the snippets with a
lookup table have always done. The default scheme is now named once, so it
cannot change in one snippet and not in another.
- The charts that offer no scheme option read the definition too: the two
full-page pie charts and the four multi-series line charts that predate the
option have always drawn category10, and the bar chart's
'color => categorical' fills its bars from the default scheme, and each now
takes that expression from the definition rather than writing
d3.schemeCategory10 or d3.schemeTableau10 out a second time. The HTML and
JavaScript they emit is unchanged, byte for byte; what changes is that each
palette - and the default - is now written down exactly once, so there is
no copy of it left behind when the one definition moves.
[ Documentation ]
- New POD and README sections for categorical_schemes(), palette() and
colour_at(), and the scheme options of every snippet that takes one now
point at them.
- The API SPECIFICATION postconditions of the six snippets that take a scheme
now describe the behaviour they have - a name in categorical_schemes()
draws that name's D3 expression, a name that is not one draws the default's
and invents no d3.scheme of its own - in place of the capitalisation they
replaced. render_scatter_chart_snippet and render_treemap_snippet state
their scheme postconditions for the first time.
[ Tests ]
- t/palette.t holds the palettes to D3's published schemes - the names, the
number of colours each has and the d3.scheme* expression each is - and
holds every snippet to the definition, including that an unknown name is
not added to the vocabulary by being asked for. It also counts the scheme
expressions in the module's code - POD and comments stripped - and requires
each of the five to appear exactly once and no sixth to appear at all, so a
palette written down a second time anywhere, in a snippet or in a chart
that takes no option, fails the suite. t/unit.t gains the documented
contract of the three methods.
0.20 Wed Oct 14 08:01:05 PM EDT 2026
[ Enhancements ]
- render_histogram_snippet: new 'binned' option for pre-computed bins, so a
caller can bin server-side and have the exact edges it chose rendered
without the browser re-binning. Each element of the data arrayref is a
hashref with a count plus either a label or a numeric lo/hi pair (the
label is then derived as "[lo,hi)"); d3.bin() is not called on this path
and the supplied counts are authoritative. New 'scheme' option supplies
the bar fill (first colour of the D3 categorical scheme) when 'color' is
absent. New errors: 'Data must contain at least one bin', 'Each
pre-binned entry must be a hash reference', 'Bin count must be numeric',
'Bin count must not be negative', 'Each pre-binned entry must have a
label or numeric lo and hi'. The non-binned path is unchanged, including
its 'Data must be an array of numeric values' error, and the bin-label
tooltip now escapes the label.
- render_scatter_chart_snippet: new per-point colour and size dimensions.
An optional fourth element per point may carry { series => Str, size => Num };
the new opts colour_by_series (colour from the series value, with a
legend), scheme (D3 categorical scheme, default 'tableau10'), series_col
and size_col (tooltip row labels) and radius_range ([min, max] pixel
radii mapped by area with d3.scaleSqrt over the observed size domain)
render them. Colour and size are independent: a point may carry both.
A point with no size value keeps a fixed radius and a point with no
series value keeps the default colour, so output for data with neither is
unchanged.
- render_multi_series_line_chart_snippet: new snippet method that generates
an embeddable multi-series line chart fragment rather than a whole page,
so a host application can keep its own page chrome. Accepts an arrayref
of { name, data => [ { label, value }, ... ] } series and an optional
\%opts hashref: scheme, x_label, y_label, animated, responsive, id.
Series need not share labels (the X domain is the union, in first-seen
order), each series gets a line and its own dots, and a clickable legend
toggles each series. Returns { svg_id => 'chart', html => Str }.
[ Documentation ]
- The three capabilities above are documented in the POD, README.md and the
formal specification, with new tests in t/snippet_histogram.t,
t/snippet_scatter.t and t/snippet_multi_series_line.t.
0.19 Tue Oct 13 03:35:05 PM EDT 2026
[ Enhancements ]
- render_stacked_bar_snippet: new snippet method that generates an embeddable
D3 v7 stacked bar chart. Accepts an arrayref of hashrefs (one row per
category/segment combination) and an optional \%opts hashref. Opts: cat_col,
seg_col, val_col (row-hash key names, defaults 'cat'/'seg'/'val'), orient
('v' vertical / 'h' horizontal, default 'v'), scheme (D3 categorical colour
scheme: tableau10/category10/set2/set3/paired, default 'tableau10'), x_label
(axis label, default ''), sort (undef / 'value' desc-total / 'label' alpha).
Multiple rows sharing the same cat+seg are summed on the Perl side before
encoding. Uses d3.stack() with d3.stackOrderNone / d3.stackOffsetNone.
Includes an interactive legend: clicking a segment label toggles that layer
on/off and re-stacks the visible segments. Tooltip shows category, segment,
and value on mouseover. Returns { svg_id => 'stacked_bar', html => Str }.
- render_combo_snippet: new snippet method that generates an embeddable combo
chart — bars for one measure on the left Y axis, a line for another on the
right Y axis, sharing the same X (category/band) axis. Accepts an arrayref
of hashrefs and \%opts: x_col, bar_col, line_col (row-hash key names,
defaults 'x'/'bar_val'/'line_val'), bar_label, line_label (legend/axis
labels, defaults 'Bar'/'Line'), bar_color (default 'steelblue'), line_color
(default '#e15759'). Two independent d3.scaleLinear domains (left for bars,
right for line); bars rendered first, line path and dots on top. Both axes
labelled. Tooltip on bar and dot mouseover shows all three measures.
Returns { svg_id => 'combo_chart', html => Str }.
- render_treemap_snippet: new snippet method that generates an embeddable D3
v7 treemap using d3.treemapSquarify tiling. Accepts two data formats:
(1) flat-path arrayref of hashrefs [{path => 'Root/A/X', value => N}, ...]
converted to a stratify hierarchy via d3.stratify() inside the generated JS;
(2) a nested hashref tree with name/children/value structure passed directly
to d3.hierarchy(). Format is detected by ref() of the first argument.
\%opts: path_sep (separator string for flat paths, default '/'), scheme
(categorical colour scheme, default 'tableau10'), label (bool, default 1;
shows node name inside each cell, clipped to cell width). Tooltip shows node
name and value. Returns { svg_id => 'treemap', html => Str }.
- render_histogram_snippet: new snippet method that generates an embeddable
frequency histogram. Accepts an arrayref of numeric scalars (pre-extracted
by the caller — no column-extraction logic inside the module) and \%opts:
bins (positive integer, default 20; pass undef for automatic Sturges count:
ceil(log2(n)+1)), x_label (axis label, default ''), color (bar fill colour,
default 'steelblue'). Uses d3.bin() with xScale.ticks(numBins) thresholds.
Tooltip shows bin range and count on mouseover.
Returns { svg_id => 'histogram', html => Str }.
0.18 Wed Sep 23 11:10:09 AM EDT 2026
[ Enhancements ]
- render_bar_chart_snippet: new snippet method that generates an embeddable
D3 v7 bar chart. Accepts an array of [$label, $value] (or
[$label, $value, \%extra]) pairs and an optional \%opts hashref with keys:
orientation ('vertical'/'horizontal', default 'vertical'),
sort_bars ('value'/'label'/'none', default 'none'),
max_bars (int, default 0; collapses tail into "Other"),
color (CSS colour or 'categorical' for Tableau-10 palette, default
'steelblue'), show_values (bool, default 0; prints values on bars),
animated (bool, default 0; bars grow from baseline with 800 ms stagger;
respects prefers-reduced-motion), value_label (tooltip label, default
'Value'), x_label (bottom-axis label, default ''). Returns
{ svg_id => 'bar_chart', html => Str }. Negative values are silently
converted to their absolute value; undef values are silently skipped.
X-axis labels are rotated -45 degrees when more than 8 bars are shown in
vertical orientation. Extra \%extra fields appear as additional tooltip
rows. Returns a Perl character string (UTF-8 flag set).
- render_scatter_chart_snippet: new snippet method that generates an
embeddable D3 v7 scatter chart. Accepts an array of [$x, $y] (or
[$x, $y, \%extra]) pairs; both x and y must be numeric. Optional \%opts:
id (default 'scatter_chart'), color (CSS colour, default 'steelblue'),
x_label, y_label, value_label (tooltip label, default 'Value'), animated
(bool; circles fade in from opacity 0; respects prefers-reduced-motion),
responsive. Returns { svg_id => Str, html => Str }. Uses d3.scaleLinear
for both axes; includes esc() XSS helper in generated JavaScript.
- render_table_snippet: new snippet method that generates an embeddable HTML
table with optional D3-powered column sorting. Accepts an array of array
references; the first row is the header. Optional \%opts: id (default
'data_table'), sortable (bool, default 1; adds clickable column headers
with D3 sort), caption (string; emits a <caption> element). Returns
{ table_id => Str, html => Str } (note: table_id, not svg_id). All header
and cell content is HTML-escaped.
- id opt: all five existing snippet methods (render_line_chart_snippet,
render_zoomable_line_chart_snippet, render_pie_chart_snippet,
render_heatmap_snippet, render_bar_chart_snippet) now accept an id key in
\%opts that overrides the default svg_id. The returned svg_id reflects the
override. Derived element ids (tooltip div, legend div, etc.) are computed
from the svg_id string. Fully backward-compatible: omitting id gives the
same default id as before.
- responsive opt: all render methods now support a responsive display mode.
Pass responsive => 1 to the constructor to make all full-page renders emit
viewBox="0 0 W H" width="100%" height="auto" instead of fixed pixel
dimensions on the SVG element. Snippet methods additionally accept
responsive in their per-call \%opts (takes precedence over the constructor
setting). Default is 0 (fixed dimensions), so existing callers are
unaffected. Uses new private helper _svg_attrs().
[ Bug Fixes ]
- render_pie_chart_snippet: tooltip handler was missing HTML sanitisation.
Added esc() JavaScript helper (same as bar/heatmap/scatter snippets) and
applied it to slice labels and extra-field keys and values in the
mouseover handler. Prevents XSS when label or extra fields contain HTML
special characters.
- render_line_chart_snippet: completed API documentation (Arguments, Options,
Return value, Errors, Side effects, API SPECIFICATION). Now accepts an
optional second argument \%opts with id and responsive keys.
0.17 Tue Sep 22 08:45:56 PM EDT 2026
[ Bug Fixes ]
- All render_*_snippet methods now return a Perl character string (UTF-8
flag set) in the {html} value instead of a UTF-8 byte string. The
previous use of encode_json() produced octets; any caller that
concatenated the result with a character string (e.g. a Mojolicious
Template Toolkit response) would silently double-encode non-ASCII
characters, producing mojibake. Fixed by replacing encode_json() with
a module-level JSON::MaybeXS instance configured with utf8 => 0, which
serialises to a character string.
0.16 Mon Sep 21 07:34:35 PM EDT 2026
[ Enhancements ]
- render_heatmap_snippet: new snippet method that generates an embeddable
D3 v7 grid heatmap. Accepts an array of [$x, $y, $value] triples and an
optional \%opts hashref with keys: color_scheme (YlOrRd/Blues/Greens/
Purples/RdPu/YlGnBu, default YlOrRd), x_label, y_label, val_label
(tooltip label, default 'Value'), show_values (print value in each cell,
auto-suppressed when cell < 28px), cell_padding (0-8 px gap, default 2),
legend (colour-scale bar, default on), animated (fade-in with
prefers-reduced-motion support, default off). Returns
{ svg_id => 'heatmap', html => Str }. undef values are silently skipped;
duplicate (x, y) pairs: last write wins. Requires Scalar::Util
looks_like_number (already in core distribution).
0.15 Wed Sep 16 01:51:38 PM EDT 2026
[ Enhancements ]
- render_pie_chart, render_animated_pie_chart, render_pie_chart_snippet: new
optional C<separator> key in the C<%opts> second argument controls the
character shown between label and value in the legend (default C<'/'>).
render_pie_chart and render_animated_pie_chart now accept the optional
second hashref argument for the first time; existing callers passing only
C<$data> are unaffected.
0.14 Tue Sep 15 09:12:17 PM EDT 2026
[ Enhancements ]
- render_pie_chart_snippet: major upgrade. Now accepts an optional second
argument \%opts with six keys:
- animated (bool, default 0): fan slices in from arc-length 0 using
attrTween / d3.easeBackOut.overshoot(1.2) with 800 ms staggered
transitions; legend fades in after; respects prefers-reduced-motion
- donut (bool, default 0): inner radius 38% of outer; total sum shown
in the centre hole
- sort_slices ('value'|'label'|'none', default 'none'): sort order
- max_slices (int, default 0): collapse tail into an "Other" slice
- legend (bool, default 1): HTML legend panel with colour swatches
- color_scheme (string, default 'tableau10'): supports tableau10,
category10, set2, set3, paired
Negative values are silently converted to their absolute value; zero-
value slices are silently omitted. Optional \%extra hashref per data
point is shown as additional rows in the hover tooltip.
svg_id changes from 'chart' to 'pie_chart' for namespace clarity.
(Breaking change in svg_id value; all other behaviour is backward-
compatible when opts are omitted.)
0.13 Tue Sep 15 06:05:45 PM EDT 2026
[ Enhancements ]
- render_zoomable_line_chart_snippet: accept an optional second argument
{ animated => 1 } to animate the initial line draw on page load.
The line traces itself left-to-right via stroke-dashoffset / d3.easeLinear
(1200 ms); data-point circles then fade in from opacity 0 over 300 ms
(after a 1200 ms delay so they appear only once the line reaches them).
Subsequent zoom and reset redraws are never animated.
Respects prefers-reduced-motion: when the user has requested reduced
motion the chart draws immediately at full opacity.
Omitting the second argument or passing { animated => 0 } gives the
existing behaviour unchanged (backward-compatible).
0.12 Sun Sep 13 08:14:05 AM EDT 2026
[ Enhancements ]
- Added render_animated_bar_chart(): bar chart with on-load grow animation —
each bar rises from the baseline using d3.transition() with an 800 ms
duration and a 100 ms per-bar stagger, so bars appear one after another
from left to right
- Added render_animated_line_chart(): line chart with on-load draw animation —
the line traces itself left-to-right via the stroke-dashoffset technique with
d3.easeLinear (1500 ms), followed by data-point circles fading in from
opacity 0 once the line is complete
- Added render_pie_chart(): full-page pie chart with percentage labels inside
each slice and a colour legend; slices coloured with d3.schemeCategory10
- Added render_animated_pie_chart(): same as render_pie_chart() but slices fan
out from zero on page load via attrTween/d3.interpolate (1000 ms), with
percentage labels fading in once drawing is complete
- Added render_pie_chart_snippet(): embeddable pie chart fragment returning
{svg_id, html} — no page shell, no D3 CDN tag; caller loads D3
[ Tests ]
- Test::HTML::T5 doesn't build on a number of platforms, so
remove the dependancy. Hopefully this will be fixed at sometime.
0.11 Thu Sep 10 09:32:08 PM EDT 2026
[ Bug Fixes ]
- render_zoomable_line_chart_snippet: the y-axis lower bound was
hard-coded to 0, causing data points with negative Y values to be
plotted off-screen below the x-axis. Changed to
Math.min(0, d3.min(newData, d => d.value)) so the domain extends
downward when the data contains negative values (e.g.
accounting-notation amounts).
0.10 Fri Aug 28 08:57:09 PM EDT 2026
[ Enhancements ]
- Added render_zoomable_line_chart_snippet(): embeddable line chart with brush-to-zoom — drag to select an x-axis range, animated transition to the zoomed view, incremental re-zoom on the zoomed view, and a Reset zoom button to restore the full dataset
0.09 Thu Aug 27 09:16:02 PM EDT 2026
[ Enhancements ]
- render_line_chart_snippet: accept an optional third element per data pair — a hashref of extra key/value pairs serialised as an "extra" property in the D3 data binding and appended as additional rows in the mouseover tooltip
0.08 Thu Aug 27 01:30:10 PM EDT 2026
[ Enhancements ]
- Use Params::Get
- Added test dashboard
- Added support for Object::Configure
- Added render_line_chart_snippet() returning an embeddable {svg_id, html} fragment for use in Mojolicious TT (and similar) layouts without post-processing
[ Bug Fixes ]
- Escape </b> as <\/b> in all JavaScript tooltip .html() strings to fix html-tidy '<' + '/' + letter failures on cpantesters
https://www.cpantesters.org/cpan/report/8321add6-d178-11ef-8e88-00fe6d8775ea
0.07 Fri Jan 10 21:16:49 EST 2025
Added support for interactive legends
0.06 Thu Jan 9 21:51:36 EST 2025
Added support for legends
0.05 Wed Jan 8 21:04:21 EST 2025
Added support for animated tooltips
0.04 Tue Jan 7 21:26:42 EST 2025
Added support for line charts with tooltips
Added support for multiple line charts with tooltips
0.03 Mon Jan 6 20:55:12 EST 2025
Added line chart support
Use JSON::MaybeXS instead of JSON
0.02 Mon Jan 6 10:13:11 EST 2025
Fix CI breakage
0.01 Mon Jan 6 09:47:22 EST 2025
First draft - POC: generates bar charts only
Keyboard Shortcuts
Global
s
Focus search bar
?
Bring up this help dialog
GitHub
gp
Go to pull requests
gi
Go to GitHub issues (only if GitHub is preferred repository)