HTML-D3

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

	[ 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

MANIFEST  view on Meta::CPAN

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

MANIFEST  view on Meta::CPAN

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

README.md  view on Meta::CPAN


#### 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).

README.md  view on Meta::CPAN

## 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';

t/unit.t  view on Meta::CPAN

	'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'");

t/unit.t  view on Meta::CPAN

	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.



( run in 3.459 seconds using v1.01-cache-2.11-cpan-364913b4093 )