HTML-D3

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

Revision history for HTML-D3 - Visualise various charts using D3

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
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/animated.t
t/bar.t
t/comment-spelling.t
t/cover.t
t/critic.t
t/eof.t
t/eol.t
t/fixme.t
t/gv.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


Generates HTML and JavaScript code to render a chart of many lines with animated mouseover tooltips.

Accepts the following arguments:

- `$data` - An reference to an array of hashes containing data points.
Each data point should be an array reference with two elements: the label (string) and the value (numeric).

Returns a string containing the HTML and JavaScript code for the chart.

## 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 animated mouseover tooltips.

Accepts the following arguments:

- `$data` - An reference to an array of hashes containing data points.
Each data point should be an array reference with two elements: the label (string) and the value (numeric).

Returns a string containing the HTML and JavaScript code for the chart.

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

package HTML::D3;

use strict;
use warnings;

use JSON::MaybeXS;
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.07

=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 animated mouseover tooltips.

Accepts the following arguments:

=over 4

=item * C<$data> - An reference to an array of hashes containing data points.
Each data point should be an array reference with two elements: the label (string) and the value (numeric).

=back

Returns a string containing the HTML and JavaScript code for the chart.

=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;
}

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

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



( run in 2.948 seconds using v1.01-cache-2.11-cpan-49f99fa48dc )