view release on metacpan or search on metacpan
[ 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
Changes
cpanfile
examples/animated
examples/bar
examples/interactive
examples/legends
examples/line
examples/multiple
examples/tooltip
lib/HTML/D3.pm
LICENSE
Makefile.PL
MANIFEST This list of files
MANIFEST.SKIP
README.md
t/00-load.t
t/comment-spelling.t
t/cover.t
t/critic.t
t/eof.t
t/eol.t
t/fixme.t
t/function.t
t/gv.t
t/integration.t
t/interactive.t
t/legends.t
t/line.t
t/manifest.t
t/metrics.t
t/modules-used.t
t/multiple.t
t/no404s.t
t/noplan.t
t/pod-cm.t
t/pod-coverage.t
t/pod-links.t
#### Input
$self : HTML::D3 -- required
$data : ArrayRef[ HashRef{ name: Str, data: ArrayRef[HashRef] } ] -- required
#### Output
Str -- complete HTML5 document; animated tooltip uses CSS translateY transition.
## render\_multi\_series\_line\_chart\_with\_legends
$html = $chart->render_multi_series_line_chart_with_legends($data);
Generates HTML and JavaScript code to render a chart of many lines with a static
colour legend. Each series gets a labelled colour swatch in the legend area.
Accepts the following arguments:
- `$data` - Same format as `render_multi_series_line_chart_with_tooltips`:
an array reference of `{ name, data }` series hashes.
Returns a string containing the complete HTML5 document. The stylesheet defines
a `.legend` CSS class used by the D3-generated legend elements.
### Errors
- Throws `Data must be an array of hashes` when `$data` is not an ARRAY reference.
### Side Effects
None.
### API SPECIFICATION
#### Input
$self : HTML::D3 -- required
$data : ArrayRef[ HashRef{ name: Str, data: ArrayRef[HashRef] } ] -- required
#### Output
Str -- complete HTML5 document; static colour legend rendered as SVG C<g> elements
with the C<.legend> CSS class applied via D3 C<.attr("class", "legend")>.
## render\_multi\_series\_line\_chart\_with\_interactive\_legends
$html = $chart->render_multi_series_line_chart_with_interactive_legends($data);
Generates HTML and JavaScript code to render a chart of many lines with interactive legends to filter, highlight or modify elements based on legend selections.
Accepts the following arguments:
- `$data` - Same format as `render_multi_series_line_chart_with_tooltips`:
an array reference of `{ name, data }` series hashes.
Returns a string containing the complete HTML5 document. Clicking a legend entry
toggles that series' opacity using an `isVisible` boolean flag in the D3 click
handler (opacity is set to `isVisible ? 0 : 1` on each click).
### Errors
- Throws `Data must be an array of hashes` when `$data` is not an ARRAY reference.
### Side Effects
None.
### API SPECIFICATION
#### Input
$self : HTML::D3 -- required
$data : ArrayRef[ HashRef{ name: Str, data: ArrayRef[HashRef] } ] -- required
#### Output
Str -- complete HTML5 document; legend clicks toggle series visibility.
The C<isVisible> JS variable tracks current visibility state.
Opacity toggled by C<isVisible ? 0 : 1>.
# SUPPORT
This module is provided as-is without any warranty.
Please report any bugs or feature requests to `bug-html-d3 at rt.cpan.org`,
or through the web interface at
[http://rt.cpan.org/NoAuth/ReportBug.html?Queue=HTML-D3](http://rt.cpan.org/NoAuth/ReportBug.html?Queue=HTML-D3).
## render\_multi\_series\_line\_chart\_with\_animated\_tooltips
render_multi_series_line_chart_with_animated_tooltips : HTML::D3 à (ArrayRef | undef) â Str ⪠â¥
pre ref(data) â 'ARRAY' â die "Data must be an array of hashes"
post result â Str
post "<!DOCTYPE" â result
post "translateY" â result
post "</b>" â result â§ "<\/b>" â result
## render\_multi\_series\_line\_chart\_with\_legends
render_multi_series_line_chart_with_legends : HTML::D3 à (ArrayRef | undef) â Str ⪠â¥
pre ref(data) â 'ARRAY' â die "Data must be an array of hashes"
post result â Str
post "<!DOCTYPE" â result
post ".legend" â result
## render\_multi\_series\_line\_chart\_with\_interactive\_legends
render_multi_series_line_chart_with_interactive_legends : HTML::D3 à (ArrayRef | undef) â Str ⪠â¥
pre ref(data) â 'ARRAY' â die "Data must be an array of hashes"
post result â Str
post "<!DOCTYPE" â result
post "isVisible" â result
post "isVisible ? 0 : 1" â result
post ".legend" â result
# LICENSE AND COPYRIGHT
Copyright 2025-2026 Nigel Horne.
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
examples/interactive view on Meta::CPAN
{
name => 'Product B',
data => [
{ label => 'January', value => 800 },
{ label => 'February', value => 1150 },
{ label => 'March', value => 1000 },
],
},
];
my $html_output = $chart->render_multi_series_line_chart_with_interactive_legends($data);
# Save the output as an HTML file
open my $fh, '>', 'chart.html' or die $!;
print $fh $html_output;
close $fh;
print "Chart with legends saved as 'chart.html'. Open it in a browser.\n";
examples/legends view on Meta::CPAN
}, {
name => 'Product B',
data => [
{ label => 'January', value => 800 },
{ label => 'February', value => 1150 },
{ label => 'March', value => 1000 },
],
}
];
my $html_output = $chart->render_multi_series_line_chart_with_legends($data);
# Save the output as an HTML file
open my $fh, '>', 'chart.html' or die $!;
print $fh $html_output;
close $fh;
print "HTML::D3 saved as 'chart.html'. Open it in a browser.\n";
lib/HTML/D3.pm view on Meta::CPAN
use strict;
use warnings;
use Carp qw(carp);
use JSON::MaybeXS;
use Object::Configure;
use Params::Get;
use Scalar::Util;
# TODO: add animated tooltips to charts with legends
=head1 NAME
HTML::D3 - A simple Perl module for generating charts using D3.js.
=head1 VERSION
Version 0.11
=cut
lib/HTML/D3.pm view on Meta::CPAN
});
});
</script>
</body>
</html>
HTML
return $html;
}
=head2 render_multi_series_line_chart_with_legends
$html = $chart->render_multi_series_line_chart_with_legends($data);
Generates HTML and JavaScript code to render a chart of many lines with a static
colour legend. Each series gets a labelled colour swatch in the legend area.
Accepts the following arguments:
=over 4
=item * C<$data> - Same format as C<render_multi_series_line_chart_with_tooltips>:
an array reference of C<< { name, data } >> series hashes.
=back
Returns a string containing the complete HTML5 document. The stylesheet defines
a C<.legend> CSS class used by the D3-generated legend elements.
=head3 Errors
=over 4
=item * Throws C<Data must be an array of hashes> when C<$data> is not an ARRAY reference.
=back
=head3 Side Effects
lib/HTML/D3.pm view on Meta::CPAN
=head3 API SPECIFICATION
=head4 Input
$self : HTML::D3 -- required
$data : ArrayRef[ HashRef{ name: Str, data: ArrayRef[HashRef] } ] -- required
=head4 Output
Str -- complete HTML5 document; static colour legend rendered as SVG C<g> elements
with the C<.legend> CSS class applied via D3 C<.attr("class", "legend")>.
=cut
sub render_multi_series_line_chart_with_legends {
my($self, $data) = @_;
# Validate input data
die 'Data must be an array of hashes' unless ref($data) eq 'ARRAY';
# Generate JSON for data
my $json_data = encode_json($data);
# Generate HTML and D3.js code
my $html = $self->_preamble();
lib/HTML/D3.pm view on Meta::CPAN
position: absolute;
background-color: white;
border: 1px solid #ccc;
padding: 5px;
font-size: 12px;
pointer-events: none;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.legend {
font-size: 12px;
cursor: pointer;
}
.legend rect {
stroke-width: 1;
stroke: #ccc;
}
</style>
</head>
<body>
<h1 style="text-align: center;">$self->{title}</h1>
<svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg>
<div class="tooltip" id="tooltip"></div>
<script>
lib/HTML/D3.pm view on Meta::CPAN
const svg = d3.select("#chart");
const tooltip = d3.select("#tooltip");
const margin = { top: 20, right: 120, bottom: 40, left: 40 };
const width = $self->{width} - margin.left - margin.right;
const height = $self->{height} - margin.top - margin.bottom;
const chart = svg.append("g")
.attr("transform", `translate(\${margin.left},\${margin.top})`);
const legendArea = svg.append("g")
.attr("transform", `translate(\${width + margin.left + 20},\${margin.top})`);
// Extract all labels and flatten them into a unique array
const allLabels = Array.from(new Set(data.flatMap(series => series.data.map(d => d.label))));
const x = d3.scalePoint()
.domain(allLabels)
.range([0, width]);
const y = d3.scaleLinear()
lib/HTML/D3.pm view on Meta::CPAN
.on("mousemove", (event) => {
tooltip.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 30) + "px");
})
.on("mouseout", () => {
tooltip.style("opacity", 0)
.style("transform", "translateY(-10px)");
});
});
// Add legend
data.forEach((series, i) => {
const legend = legendArea.append("g")
.attr("transform", `translate(0, \${i * 20})`)
.attr("class", "legend");
legend.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", color(i));
legend.append("text")
.attr("x", 20)
.attr("y", 10)
.text(series.name)
.style("alignment-baseline", "middle");
// Optional: Interactive legend for toggling visibility (uncomment to use)
// legend.on("click", () => {
// const visible = d3.selectAll(\`path.line-\${i}\`).style("opacity") === "1" ? 0 : 1;
// d3.selectAll(\`path.line-\${i}\`).style("opacity", visible);
// d3.selectAll(\`circle.series-\${i}\`).style("opacity", visible);
// });
});
</script>
</body>
</html>
HTML
return $html;
}
=head2 render_multi_series_line_chart_with_interactive_legends
$html = $chart->render_multi_series_line_chart_with_interactive_legends($data);
Generates HTML and JavaScript code to render a chart of many lines with interactive legends to filter, highlight or modify elements based on legend selections.
Accepts the following arguments:
=over 4
=item * C<$data> - Same format as C<render_multi_series_line_chart_with_tooltips>:
an array reference of C<< { name, data } >> series hashes.
=back
Returns a string containing the complete HTML5 document. Clicking a legend entry
toggles that series' opacity using an C<isVisible> boolean flag in the D3 click
handler (opacity is set to C<isVisible ? 0 : 1> on each click).
=head3 Errors
=over 4
=item * Throws C<Data must be an array of hashes> when C<$data> is not an ARRAY reference.
=back
lib/HTML/D3.pm view on Meta::CPAN
=head3 API SPECIFICATION
=head4 Input
$self : HTML::D3 -- required
$data : ArrayRef[ HashRef{ name: Str, data: ArrayRef[HashRef] } ] -- required
=head4 Output
Str -- complete HTML5 document; legend clicks toggle series visibility.
The C<isVisible> JS variable tracks current visibility state.
Opacity toggled by C<isVisible ? 0 : 1>.
=cut
sub render_multi_series_line_chart_with_interactive_legends
{
my ($self, $data) = @_;
# Validate input data
die 'Data must be an array of hashes' unless ref($data) eq 'ARRAY';
# Generate JSON for data
my $json_data = encode_json($data);
# Generate HTML and D3.js code
lib/HTML/D3.pm view on Meta::CPAN
position: absolute;
background-color: white;
border: 1px solid #ccc;
padding: 5px;
font-size: 12px;
pointer-events: none;
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
}
.legend {
font-size: 12px;
cursor: pointer;
}
.legend rect {
stroke-width: 1;
stroke: #ccc;
}
</style>
</head>
<body>
<h1 style="text-align: center;">$self->{title}</h1>
<svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg>
<div class="tooltip" id="tooltip"></div>
<script>
lib/HTML/D3.pm view on Meta::CPAN
const svg = d3.select("#chart");
const tooltip = d3.select("#tooltip");
const margin = { top: 20, right: 150, bottom: 40, left: 40 };
const width = $self->{width} - margin.left - margin.right;
const height = $self->{height} - margin.top - margin.bottom;
const chart = svg.append("g")
.attr("transform", `translate(\${margin.left},\${margin.top})`);
const legendArea = svg.append("g")
.attr("transform", `translate(\${width + margin.left + 20},\${margin.top})`);
// Extract all labels and flatten them into a unique array
const allLabels = Array.from(new Set(data.flatMap(series => series.data.map(d => d.label))));
const x = d3.scalePoint()
.domain(allLabels)
.range([0, width]);
const y = d3.scaleLinear()
lib/HTML/D3.pm view on Meta::CPAN
.on("mousemove", (event) => {
tooltip.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 30) + "px");
})
.on("mouseout", () => {
tooltip.style("opacity", 0)
.style("transform", "translateY(-10px)");
});
});
// Add legend with interactivity
data.forEach((series, i) => {
const legend = legendArea.append("g")
.attr("transform", `translate(0, \${i * 20})`)
.attr("class", "legend")
.on("click", () => {
const isVisible = d3.selectAll(\`path.line-\${i}\`).style("opacity") === "1";
// Toggle visibility
d3.selectAll(\`path.line-\${i}\`).style("opacity", isVisible ? 0 : 1);
d3.selectAll(\`circle.series-\${i}\`).style("opacity", isVisible ? 0 : 1);
// Dim legend if series is hidden
legend.select("text").style("opacity", isVisible ? 0.5 : 1);
});
legend.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", color(i));
legend.append("text")
.attr("x", 20)
.attr("y", 10)
.text(series.name)
.style("alignment-baseline", "middle");
});
</script>
</body>
</html>
HTML
lib/HTML/D3.pm view on Meta::CPAN
=head2 render_multi_series_line_chart_with_animated_tooltips
render_multi_series_line_chart_with_animated_tooltips : HTML::D3 à (ArrayRef | undef) â Str ⪠â¥
pre ref(data) â 'ARRAY' â die "Data must be an array of hashes"
post result â Str
post "<!DOCTYPE" â result
post "translateY" â result
post "</b>" â result â§ "<\/b>" â result
=head2 render_multi_series_line_chart_with_legends
render_multi_series_line_chart_with_legends : HTML::D3 à (ArrayRef | undef) â Str ⪠â¥
pre ref(data) â 'ARRAY' â die "Data must be an array of hashes"
post result â Str
post "<!DOCTYPE" â result
post ".legend" â result
=head2 render_multi_series_line_chart_with_interactive_legends
render_multi_series_line_chart_with_interactive_legends : HTML::D3 à (ArrayRef | undef) â Str ⪠â¥
pre ref(data) â 'ARRAY' â die "Data must be an array of hashes"
post result â Str
post "<!DOCTYPE" â result
post "isVisible" â result
post "isVisible ? 0 : 1" â result
post ".legend" â result
=head1 LICENSE AND COPYRIGHT
Copyright 2025-2026 Nigel Horne.
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
=cut
t/function.t view on Meta::CPAN
my $html = $chart->render_multi_series_line_chart_with_animated_tooltips(\@MULTI_DATA);
returns_ok($html, { type => 'string' }, 'returns a string scalar');
like($html, qr/<!DOCTYPE html>/i, 'contains DOCTYPE');
like($html, qr/translateY/, 'CSS translateY animation present');
like($html, qr/<div class="tooltip"/, 'tooltip div present');
unlike($html, qr{</b>}, 'raw </b> absent');
};
subtest 'render_multi_series_line_chart_with_legends' => sub {
my $chart = HTML::D3->new(title => 'Legends');
throws_ok(
sub { $chart->render_multi_series_line_chart_with_legends('bad') },
qr/\Q$ERR_ARRAY_OF_HASH\E/,
'dies on non-array data',
);
my $html = $chart->render_multi_series_line_chart_with_legends(\@MULTI_DATA);
returns_ok($html, { type => 'string' }, 'returns a string scalar');
like($html, qr/<!DOCTYPE html>/i, 'contains DOCTYPE');
like($html, qr/\.legend\s*\{/, 'legend CSS class defined in stylesheet');
like($html, qr/Series A/, 'first series name appears in legend area');
unlike($html, qr{</b>}, 'raw </b> absent');
};
subtest 'render_multi_series_line_chart_with_interactive_legends' => sub {
my $chart = HTML::D3->new(title => 'Interactive Legends');
throws_ok(
sub { $chart->render_multi_series_line_chart_with_interactive_legends('bad') },
qr/\Q$ERR_ARRAY_OF_HASH\E/,
'dies on non-array data',
);
my $html = $chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA);
returns_ok($html, { type => 'string' }, 'returns a string scalar');
like($html, qr/<!DOCTYPE html>/i, 'contains DOCTYPE');
like($html, qr/isVisible/, 'visibility-toggle variable present');
# The legend click handler must toggle opacity based on current visibility.
like($html, qr/isVisible\s*\?\s*0\s*:\s*1/, 'opacity toggled based on isVisible');
like($html, qr/\.legend\s*\{/, 'legend CSS class defined in stylesheet');
unlike($html, qr{</b>}, 'raw </b> absent');
};
# ---------------------------------------------------------------------------
done_testing();
t/integration.t view on Meta::CPAN
);
html_tidy_ok(
$chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA),
'render_multi_series_line_chart_with_tooltips output is valid HTML5',
);
html_tidy_ok(
$chart->render_multi_series_line_chart_with_animated_tooltips(\@MULTI_DATA),
'render_multi_series_line_chart_with_animated_tooltips output is valid HTML5',
);
html_tidy_ok(
$chart->render_multi_series_line_chart_with_legends(\@MULTI_DATA),
'render_multi_series_line_chart_with_legends output is valid HTML5',
);
html_tidy_ok(
$chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA),
'render_multi_series_line_chart_with_interactive_legends output is valid HTML5',
);
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 7. Snippet isolation: fragment methods produce no page-shell
#
# Both snippet methods must not contain any DOCTYPE, <html>, <head>, or <body>
# regardless of the object settings. They must be safe to splice directly into
# a host layout without corrupting its structure.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
t/integration.t view on Meta::CPAN
is($_, 'sentinel', '$_ is unchanged after render_line_chart_with_tooltips');
is($@, '', '$@ is unchanged after render_line_chart_with_tooltips');
$chart->render_line_chart_snippet(\@SIMPLE_DATA);
is($_, 'sentinel', '$_ is unchanged after render_line_chart_snippet');
is($@, '', '$@ is unchanged after render_line_chart_snippet');
$chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA);
is($_, 'sentinel', '$_ is unchanged after render_zoomable_line_chart_snippet');
$chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA);
is($_, 'sentinel', '$_ is unchanged after render_multi_series_*_with_interactive_legends');
is($@, '', '$@ is unchanged after render_multi_series_*_with_interactive_legends');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# 11. Extra tooltip data round-trip (snippet vs zoomable)
#
# The optional third element [label, value, \%extra] must be serialised
# consistently by both render_line_chart_snippet and
# render_zoomable_line_chart_snippet.
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
t/integration.t view on Meta::CPAN
subtest 'title propagates to page <title> and <h1> but not to fragments' => sub {
Readonly my $TITLE => 'My Dashboard Title';
my $chart = HTML::D3->new(title => $TITLE);
# Full-page methods.
for my $html (
$chart->render_bar_chart(\@SIMPLE_DATA),
$chart->render_line_chart(\@SIMPLE_DATA),
$chart->render_line_chart_with_tooltips(\@SIMPLE_DATA),
$chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA),
$chart->render_multi_series_line_chart_with_legends(\@MULTI_DATA),
$chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA),
) {
like($html, qr/<title>\Q$TITLE\E<\/title>/, 'title appears in <title> tag');
like($html, qr/\Q$TITLE\E<\/h1>/, 'title appears in visible <h1>');
}
# Fragment methods must NOT contain <title> or <h1>.
for my $html (
$chart->render_line_chart_snippet(\@SIMPLE_DATA)->{html},
$chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA)->{html},
) {
t/integration.t view on Meta::CPAN
['render_line_chart_with_tooltips',
$chart->render_line_chart_with_tooltips(\@SIMPLE_DATA)],
['render_line_chart_snippet',
$chart->render_line_chart_snippet(\@SIMPLE_DATA)->{html}],
['render_zoomable_line_chart_snippet',
$chart->render_zoomable_line_chart_snippet(\@SIMPLE_DATA)->{html}],
['render_multi_series_line_chart_with_tooltips',
$chart->render_multi_series_line_chart_with_tooltips(\@MULTI_DATA)],
['render_multi_series_line_chart_with_animated_tooltips',
$chart->render_multi_series_line_chart_with_animated_tooltips(\@MULTI_DATA)],
['render_multi_series_line_chart_with_legends',
$chart->render_multi_series_line_chart_with_legends(\@MULTI_DATA)],
['render_multi_series_line_chart_with_interactive_legends',
$chart->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA)],
) {
my ($name, $html) = @$pair;
unlike($html, qr{</b>}, "$name: no raw </b> in output");
}
};
done_testing();
t/interactive.t view on Meta::CPAN
isa_ok($chart, 'HTML::D3', 'Chart object is created');
# Check default values
is($chart->{width}, 800, 'Width is set correctly');
is($chart->{height}, 600, 'Height is set correctly');
is($chart->{title}, 'Monthly Revenue Trends by Product (With Legends)', 'Title is set correctly');
# Test chart rendering
my $html;
lives_ok { $html = $chart->render_multi_series_line_chart_with_interactive_legends($data) } 'Legend chart renders without error';
like($html, qr/<svg id="chart"/, 'HTML contains SVG element for chart');
like($html, qr/January/, 'HTML contains data label');
like($html, qr/1000/, 'HTML contains data value');
like($html, qr/<html/, 'Output contains <html> tag for HTML format');
html_tidy_ok($html, 'Output is valid HTML');
like($html, qr/Monthly Revenue .+<\/h1>/, 'Title is included');
like($html, qr/transform: /, 'Animation included');
like($html, qr/<div class="tooltip" id="tooltip">/, 'Tooltips included');
like($html, qr/legend\.append/, 'Legends included');
like($html, qr/Toggle visibility/, 'Legends are interactive');
# Test for invalid data
throws_ok {
$chart->render_multi_series_line_chart_with_interactive_legends('Invalid data');
} qr/Data must be an array of hashes/, 'Dies on invalid data';
t/legends.t view on Meta::CPAN
isa_ok($chart, 'HTML::D3', 'Chart object is created');
# Check default values
is($chart->{width}, 800, 'Width is set correctly');
is($chart->{height}, 600, 'Height is set correctly');
is($chart->{title}, 'Monthly Revenue Trends by Product (With Legends)', 'Title is set correctly');
# Test chart rendering
my $html;
lives_ok { $html = $chart->render_multi_series_line_chart_with_legends($data) } 'Legend chart renders without error';
like($html, qr/<svg id="chart"/, 'HTML contains SVG element for chart');
like($html, qr/January/, 'HTML contains data label');
like($html, qr/1000/, 'HTML contains data value');
like($html, qr/<html/, 'Output contains <html> tag for HTML format');
html_tidy_ok($html, 'Output is valid HTML');
like($html, qr/Monthly Revenue .+<\/h1>/, 'Title is included');
like($html, qr/transform: /, 'Animation included');
like($html, qr/<div class="tooltip" id="tooltip">/, 'Tooltips included');
like($html, qr/legend\.append/, 'Legends included');
# Test for invalid data
throws_ok {
$chart->render_multi_series_line_chart_with_legends('Invalid data');
} qr/Data must be an array of hashes/, 'Dies on invalid data';
'ms-tt: escaped <\/b> present' => 1,
# render_multi_series_line_chart_with_animated_tooltips
'ms-anim: die non-array' => 1,
'ms-anim: returns string' => 1,
'ms-anim: DOCTYPE present' => 1,
'ms-anim: translateY animation present' => 1,
'ms-anim: no raw </b>' => 1,
'ms-anim: escaped <\/b> present' => 1,
# render_multi_series_line_chart_with_legends
'ms-leg: die non-array' => 1,
'ms-leg: returns string' => 1,
'ms-leg: DOCTYPE present' => 1,
'ms-leg: legend CSS class in stylesheet' => 1,
'ms-leg: series name in output' => 1,
'ms-leg: no raw </b>' => 1,
'ms-leg: escaped <\/b> present' => 1,
# render_multi_series_line_chart_with_interactive_legends
'ms-int: die non-array' => 1,
'ms-int: returns string' => 1,
'ms-int: DOCTYPE present' => 1,
'ms-int: isVisible variable present' => 1,
'ms-int: isVisible ? 0 : 1 toggle present' => 1,
'ms-int: legend CSS class in stylesheet' => 1,
'ms-int: no raw </b>' => 1,
'ms-int: escaped <\/b> present' => 1,
);
# Marks a ledger entry as verified. Calling with an unknown key catches typos.
sub mark {
my ($key) = @_;
exists $LEDGER{$key}
? delete $LEDGER{$key}
: fail("BUG: unknown ledger key '$key'");
mark('ms-anim: translateY animation present');
unlike($html, qr{</b>}, 'raw </b> absent');
mark('ms-anim: no raw </b>');
like($html, qr{<\\/b>}, 'escaped <\/b> present');
mark('ms-anim: escaped <\/b> present');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# render_multi_series_line_chart_with_legends()
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'render_multi_series_line_chart_with_legends() -- validation' => sub {
throws_ok(
sub { HTML::D3->new()->render_multi_series_line_chart_with_legends('bad') },
qr/\Q$ERR_ARRAY_OF_HASH\E/,
'non-array data dies',
);
mark('ms-leg: die non-array');
};
subtest 'render_multi_series_line_chart_with_legends() -- output' => sub {
my $html = HTML::D3->new(title => 'Legends')
->render_multi_series_line_chart_with_legends(\@MULTI_DATA);
returns_ok($html, { type => 'string' }, 'return value is a plain string');
mark('ms-leg: returns string');
like($html, qr/<!DOCTYPE html>/i, 'HTML5 DOCTYPE present');
mark('ms-leg: DOCTYPE present');
# POD specifies a .legend CSS class defined in the stylesheet.
like($html, qr/\.legend\s*\{/, '.legend CSS class defined in <style> block');
mark('ms-leg: legend CSS class in stylesheet');
like($html, qr/Series A/, 'series name present in output');
mark('ms-leg: series name in output');
unlike($html, qr{</b>}, 'raw </b> absent');
mark('ms-leg: no raw </b>');
like($html, qr{<\\/b>}, 'escaped <\/b> present');
mark('ms-leg: escaped <\/b> present');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# render_multi_series_line_chart_with_interactive_legends()
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
subtest 'render_multi_series_line_chart_with_interactive_legends() -- validation' => sub {
throws_ok(
sub { HTML::D3->new()->render_multi_series_line_chart_with_interactive_legends('bad') },
qr/\Q$ERR_ARRAY_OF_HASH\E/,
'non-array data dies',
);
mark('ms-int: die non-array');
};
subtest 'render_multi_series_line_chart_with_interactive_legends() -- output' => sub {
my $html = HTML::D3->new(title => 'Interactive')
->render_multi_series_line_chart_with_interactive_legends(\@MULTI_DATA);
returns_ok($html, { type => 'string' }, 'return value is a plain string');
mark('ms-int: returns string');
like($html, qr/<!DOCTYPE html>/i, 'HTML5 DOCTYPE present');
mark('ms-int: DOCTYPE present');
# POD: "isVisible variable tracks current visibility state"
like($html, qr/isVisible/, 'isVisible variable declared in legend click handler');
mark('ms-int: isVisible variable present');
# POD: "opacity toggled by isVisible ? 0 : 1"
like($html, qr/isVisible\s*\?\s*0\s*:\s*1/, 'isVisible ? 0 : 1 opacity toggle present');
mark('ms-int: isVisible ? 0 : 1 toggle present');
like($html, qr/\.legend\s*\{/, '.legend CSS class defined in <style> block');
mark('ms-int: legend CSS class in stylesheet');
unlike($html, qr{</b>}, 'raw </b> absent');
mark('ms-int: no raw </b>');
like($html, qr{<\\/b>}, 'escaped <\/b> present');
mark('ms-int: escaped <\/b> present');
};
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
# Ledger check -- every documented condition must have been exercised.