TER1 (Statement): 97.85%
TER2 (Branch): 92.31%
TER3 (LCSAJ): 100.0% (1/1)
Approximate LCSAJ segments: 27
โ Covered โ this LCSAJ path was executed during testing.
โ Not covered โ this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start โ end โ jump
Uncovered paths show [NOT COVERED] in the tooltip.
1: package HTML::D3; 2: 3: use strict; 4: use warnings; 5: 6: use JSON::MaybeXS; 7: use Object::Configure; 8: use Params::Get; 9: use Scalar::Util; 10: 11: # TODO: add animated tooltips to charts with legends 12: 13: =head1 NAME 14: 15: HTML::D3 - A simple Perl module for generating charts using D3.js. 16: 17: =head1 VERSION 18: 19: Version 0.09 20: 21: =cut 22: 23: our $VERSION = '0.09'; 24: 25: =head1 SYNOPSIS 26: 27: use HTML::D3; 28: 29: my $chart = HTML::D3->new( 30: width => 1024, 31: height => 768, 32: title => 'Sample Bar Chart' 33: ); 34: 35: my $data = [ 36: ['Category 1', 10], 37: ['Category 2', 20], 38: ['Category 3', 30] 39: ]; 40: 41: my $html = $chart->render_bar_chart($data); 42: print $html; 43: 44: $chart = HTML::D3->new(title => 'Sales Data'); 45: 46: $data = [ 47: ['Product A', 100], 48: ['Product B', 150], 49: ['Product C', 200] 50: ]; 51: 52: $html = $chart->render_line_chart($data); 53: print $html; 54: 55: =head1 DESCRIPTION 56: 57: HTML::D3 is a Perl module that provides functionality to create simple charts using D3.js. 58: The module generates HTML and JavaScript code to render the chart in a web browser. 59: 60: =head1 METHODS 61: 62: =head2 new 63: 64: my $chart = HTML::D3->new(%args); 65: 66: Creates a new HTML::D3 object. 67: Accepts the following optional arguments: 68: 69: =over 4 70: 71: =item * C<width> - The width of the chart (default: 800). 72: 73: =item * C<height> - The height of the chart (default: 600). 74: 75: =item * C<title> - The title of the chart (default: 'Chart'). 76: 77: =back 78: 79: =cut 80: 81: # Constructor to initialize chart properties 82: sub new 83: { โ84 โ 89 โ 102 84: my $class = shift; 85: 86: # Handle hash or hashref arguments 87: my $params = Params::Get::get_params(undef, @_) || {}; 88: 89: if(!defined($class)) {Mutants (Total: 1, Killed: 1, Survived: 0)
90: if((scalar keys %{$params}) > 0) {
91: # Using HTML::D3->new(), not HTML::D3::new() 92: carp(__PACKAGE__, ' use ->new() not ::new() to instantiate'); 93: return; 94: } 95: # FIXME: this only works when no arguments are given 96: $class = __PACKAGE__; 97: } elsif(Scalar::Util::blessed($class)) { 98: # If $class is an object, clone it with new arguments 99: return bless { %{$class}, %{$params} }, ref($class);Mutants (Total: 4, Killed: 1, Survived: 3)
- NUM_BOUNDARY_90_31_<: Numeric boundary flip > to <
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_90_31_>=: Numeric boundary flip > to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_90_31_<=: Numeric boundary flip > to <=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );Mutants (Total: 2, Killed: 2, Survived: 0)
100: } 101: 102: $params = Object::Configure::configure($class, $params); 103: 104: # Return the blessed object 105: return bless {
Mutants (Total: 2, Killed: 2, Survived: 0)
106: width => $params->{width} || 800, # Default chart width 107: height => $params->{height} || 600, # Default chart height 108: title => $params->{title} || 'Chart', # Default chart title 109: }, $class; 110: } 111: 112: =head2 render_bar_chart 113: 114: my $html = $chart->render_bar_chart($data); 115: 116: Generates HTML and JavaScript code to render a bar chart. Accepts the following arguments: 117: 118: =over 4 119: 120: =item * C<$data> - An array reference containing data points. Each data point should 121: be an array reference with two elements: the label (string) and the value (numeric). 122: 123: =back 124: 125: Returns a string containing the HTML and JavaScript code for the chart. 126: 127: =cut 128: 129: # Method to render a bar chart with given data 130: sub render_bar_chart { 131: my ($self, $data) = @_; 132: 133: die 'Data is not optional' if(!defined($data)); 134: 135: # Validate input data to ensure it is an array of arrays 136: die 'Data must be an array of arrays' unless ref($data) eq 'ARRAY'; 137: 138: # Generate JSON representation of data 139: my $json_data = encode_json([ 140: map { { label => $_->[0], value => $_->[1] } } @$data 141: ]); 142: 143: # Generate HTML and D3.js JavaScript for rendering the bar chart 144: my $html = $self->_preamble(); 145: $html .= $self->_head(); 146: $html .= <<"HTML"; 147: <body> 148: <h1 style="text-align: center;">$self->{title}</h1> 149: <svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg> 150: <script> 151: const data = $json_data; 152: 153: const svg = d3.select("#chart"); 154: const margin = { top: 20, right: 30, bottom: 40, left: 40 }; 155: const width = $self->{width} - margin.left - margin.right; 156: const height = $self->{height} - margin.top - margin.bottom; 157: 158: // Set up scales for x and y axes 159: const x = d3.scaleBand() 160: .domain(data.map(d => d.label)) 161: .range([0, width]) 162: .padding(0.1); 163: 164: const y = d3.scaleLinear() 165: .domain([0, d3.max(data, d => d.value)]) 166: .nice() 167: .range([height, 0]); 168: 169: const chart = svg.append("g") 170: .attr("transform", `translate(\${margin.left},\${margin.top})`); 171: 172: // Add bars to the chart 173: chart.append("g") 174: .selectAll("rect") 175: .data(data) 176: .join("rect") 177: .attr("x", d => x(d.label)) 178: .attr("y", d => y(d.value)) 179: .attr("height", d => height - y(d.value)) 180: .attr("width", x.bandwidth()) 181: .attr("fill", "steelblue"); 182: 183: // Add the y-axis 184: chart.append("g") 185: .call(d3.axisLeft(y)); 186: 187: // Add the x-axis with labels rotated for better readability 188: chart.append("g") 189: .attr("transform", `translate(0,\${height})`) 190: .call(d3.axisBottom(x)) 191: .selectAll("text") 192: .attr("transform", "rotate(-45)") 193: .style("text-anchor", "end"); 194: </script> 195: </body> 196: </html> 197: HTML 198: 199: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
200: } 201: 202: =head2 render_line_chart 203: 204: my $html = $chart->render_line_chart($data); 205: 206: Generates HTML and JavaScript code to render a line chart. Accepts the following arguments: 207: 208: =over 4 209: 210: =item * C<$data> - An array reference containing data points. Each data point should 211: be an array reference with two elements: the label (string) and the value (numeric). 212: 213: =back 214: 215: Returns a string containing the HTML and JavaScript code for the chart. 216: 217: =cut 218: 219: sub render_line_chart { 220: my ($self, $data) = @_; 221: 222: # Validate input data 223: die 'Data must be an array of arrays' unless ref($data) eq 'ARRAY'; 224: 225: # Generate JSON for data 226: my $json_data = encode_json([ 227: map { { label => $_->[0], value => $_->[1] } } @$data 228: ]); 229: 230: # Generate HTML and D3.js code 231: my $html = $self->_preamble(); 232: $html .= $self->_head(); 233: $html .= <<"HTML"; 234: <body> 235: <h1 style="text-align: center;">$self->{title}</h1> 236: <svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg> 237: <script> 238: const data = $json_data; 239: 240: const svg = d3.select("#chart"); 241: const margin = { top: 20, right: 30, bottom: 40, left: 40 }; 242: const width = $self->{width} - margin.left - margin.right; 243: const height = $self->{height} - margin.top - margin.bottom; 244: 245: const x = d3.scalePoint() 246: .domain(data.map(d => d.label)) 247: .range([0, width]); 248: 249: const y = d3.scaleLinear() 250: .domain([0, d3.max(data, d => d.value)]) 251: .nice() 252: .range([height, 0]); 253: 254: const chart = svg.append("g") 255: .attr("transform", `translate(\${margin.left},\${margin.top})`); 256: 257: // Draw line 258: const line = d3.line() 259: .x(d => x(d.label)) 260: .y(d => y(d.value)); 261: 262: chart.append("path") 263: .datum(data) 264: .attr("fill", "none") 265: .attr("stroke", "steelblue") 266: .attr("stroke-width", 2) 267: .attr("d", line); 268: 269: // Add points to the line 270: chart.selectAll("circle") 271: .data(data) 272: .join("circle") 273: .attr("cx", d => x(d.label)) 274: .attr("cy", d => y(d.value)) 275: .attr("r", 4) 276: .attr("fill", "steelblue"); 277: 278: // Add axes 279: chart.append("g") 280: .call(d3.axisLeft(y)); 281: 282: chart.append("g") 283: .attr("transform", `translate(0,\${height})`) 284: .call(d3.axisBottom(x)) 285: .selectAll("text") 286: .attr("transform", "rotate(-45)") 287: .style("text-anchor", "end"); 288: </script> 289: </body> 290: </html> 291: HTML 292: 293: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
294: } 295: 296: =head2 render_line_chart_with_tooltips 297: 298: $html = $chart->render_line_chart_with_tooltips($data); 299: 300: Generates HTML and JavaScript code to render a line chart with mouseover tooltips. 301: Accepts the following arguments: 302: 303: =over 4 304: 305: =item * C<$data> - An array reference containing data points. Each data point should 306: be an array reference with two elements: the label (string) and the value (numeric). 307: 308: =back 309: 310: Returns a string containing the HTML and JavaScript code for the chart. 311: 312: =cut 313: 314: sub render_line_chart_with_tooltips 315: { 316: my ($self, $data) = @_; 317: 318: # Validate input data 319: die 'Data must be an array of arrays' unless ref($data) eq 'ARRAY'; 320: 321: # Generate JSON for data 322: my $json_data = encode_json([ 323: map { { label => $_->[0], value => $_->[1] } } @$data 324: ]); 325: 326: # Generate HTML and D3.js code 327: my $html = $self->_preamble(); 328: $html .= <<"HTML"; 329: <head> 330: <meta charset="UTF-8"> 331: <meta name="viewport" content="width=device-width, initial-scale=1.0"> 332: <title>$self->{title}</title> 333: <script src="https://d3js.org/d3.v7.min.js"></script> 334: <style> 335: .tooltip { 336: position: absolute; 337: background-color: white; 338: border: 1px solid #ccc; 339: padding: 5px; 340: font-size: 12px; 341: pointer-events: none; 342: opacity: 0; 343: transition: opacity 0.2s ease-in-out; 344: } 345: </style> 346: </head> 347: <body> 348: <h1 style="text-align: center;">$self->{title}</h1> 349: <svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg> 350: <div class="tooltip" id="tooltip"></div> 351: <script> 352: const data = $json_data; 353: 354: const svg = d3.select("#chart"); 355: const tooltip = d3.select("#tooltip"); 356: const margin = { top: 20, right: 30, bottom: 40, left: 40 }; 357: const width = $self->{width} - margin.left - margin.right; 358: const height = $self->{height} - margin.top - margin.bottom; 359: 360: const x = d3.scalePoint() 361: .domain(data.map(d => d.label)) 362: .range([0, width]); 363: 364: const y = d3.scaleLinear() 365: .domain([0, d3.max(data, d => d.value)]) 366: .nice() 367: .range([height, 0]); 368: 369: const chart = svg.append("g") 370: .attr("transform", `translate(\${margin.left},\${margin.top})`); 371: 372: // Draw line 373: const line = d3.line() 374: .x(d => x(d.label)) 375: .y(d => y(d.value)); 376: 377: chart.append("path") 378: .datum(data) 379: .attr("fill", "none") 380: .attr("stroke", "steelblue") 381: .attr("stroke-width", 2) 382: .attr("d", line); 383: 384: // Add points to the line 385: chart.selectAll("circle") 386: .data(data) 387: .join("circle") 388: .attr("cx", d => x(d.label)) 389: .attr("cy", d => y(d.value)) 390: .attr("r", 4) 391: .attr("fill", "steelblue") 392: .on("mouseover", (event, d) => { 393: tooltip.style("opacity", 1) 394: .html(`Label: <b>\${d.label}<\/b><br>Value: <b>\${d.value}<\/b>`) 395: .style("left", (event.pageX + 10) + "px") 396: .style("top", (event.pageY - 30) + "px"); 397: }) 398: .on("mousemove", (event) => { 399: tooltip.style("left", (event.pageX + 10) + "px") 400: .style("top", (event.pageY - 30) + "px"); 401: }) 402: .on("mouseout", () => { 403: tooltip.style("opacity", 0); 404: }); 405: 406: // Add axes 407: chart.append("g") 408: .call(d3.axisLeft(y)); 409: 410: chart.append("g") 411: .attr("transform", `translate(0,\${height})`) 412: .call(d3.axisBottom(x)) 413: .selectAll("text") 414: .attr("transform", "rotate(-45)") 415: .style("text-anchor", "end"); 416: </script> 417: </body> 418: </html> 419: HTML 420: 421: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
422: } 423: 424: =head2 render_line_chart_snippet 425: 426: my $fragment = $chart->render_line_chart_snippet($data); 427: # $fragment->{svg_id} - the id attribute of the <svg> element 428: # $fragment->{html} - embeddable HTML fragment (style + svg + script) 429: 430: Generates an embeddable HTML fragment for a line chart with mouseover tooltips. 431: Unlike C<render_line_chart_with_tooltips>, this method returns a fragment with 432: no C<<!DOCTYPE>>, C<<html>>, C<<head>>, or C<<body>> wrapper, suitable for 433: splicing directly into a Mojolicious TT (or any other) layout. 434: 435: The caller is responsible for loading D3 in the page C<<head>>, e.g.: 436: 437: <script src="https://d3js.org/d3.v7.min.js"></script> 438: 439: Accepts the following arguments: 440: 441: =over 4 442: 443: =item * C<$data> - An array reference of data points. Each point is an array 444: reference with two required elements - the label (string) and the value 445: (numeric) - and an optional third element: a hash reference of extra key/value 446: pairs to display in the tooltip after the label and value rows. 447: 448: [$x, $y] # basic point 449: [$x, $y, \%row] # point with extra tooltip data 450: 451: =back 452: 453: Returns a hash reference with: 454: 455: =over 4 456: 457: =item * C<svg_id> - The C<id> attribute used on the C<<svg>> element. 458: 459: =item * C<html> - The embeddable fragment string. 460: 461: =back 462: 463: =cut 464: 465: sub render_line_chart_snippet 466: { 467: my ($self, $data) = @_; 468: 469: die 'Data must be an array of arrays' unless ref($data) eq 'ARRAY'; 470: 471: my $json_data = encode_json([ 472: map { 473: my $point = { label => $_->[0], value => $_->[1] }; 474: $point->{extra} = $_->[2] if ref($_->[2]) eq 'HASH'; 475: $point 476: } @$data 477: ]); 478: 479: my $svg_id = 'chart'; 480: 481: my $html = <<"HTML"; 482: <style> 483: .tooltip { 484: position: absolute; 485: background-color: white; 486: border: 1px solid #ccc; 487: padding: 5px; 488: font-size: 12px; 489: pointer-events: none; 490: opacity: 0; 491: transition: opacity 0.2s ease-in-out; 492: } 493: </style> 494: <svg id="$svg_id" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg> 495: <div class="tooltip" id="tooltip"></div> 496: <script> 497: const data = $json_data; 498: 499: const svg = d3.select("#$svg_id"); 500: const tooltip = d3.select("#tooltip"); 501: const margin = { top: 20, right: 30, bottom: 40, left: 40 }; 502: const width = $self->{width} - margin.left - margin.right; 503: const height = $self->{height} - margin.top - margin.bottom; 504: 505: const x = d3.scalePoint() 506: .domain(data.map(d => d.label)) 507: .range([0, width]); 508: 509: const y = d3.scaleLinear() 510: .domain([0, d3.max(data, d => d.value)]) 511: .nice() 512: .range([height, 0]); 513: 514: const chart = svg.append("g") 515: .attr("transform", `translate(\${margin.left},\${margin.top})`); 516: 517: const line = d3.line() 518: .x(d => x(d.label)) 519: .y(d => y(d.value)); 520: 521: chart.append("path") 522: .datum(data) 523: .attr("fill", "none") 524: .attr("stroke", "steelblue") 525: .attr("stroke-width", 2) 526: .attr("d", line); 527: 528: chart.selectAll("circle") 529: .data(data) 530: .join("circle") 531: .attr("cx", d => x(d.label)) 532: .attr("cy", d => y(d.value)) 533: .attr("r", 4) 534: .attr("fill", "steelblue") 535: .on("mouseover", (event, d) => { 536: let ttHtml = `Label: <b>\${d.label}<\/b><br>Value: <b>\${d.value}<\/b>`; 537: if (d.extra) { 538: Object.entries(d.extra).forEach(([k, v]) => { 539: ttHtml += `<br>\${k}: <b>\${v}<\/b>`; 540: }); 541: } 542: tooltip.style("opacity", 1) 543: .html(ttHtml) 544: .style("left", (event.pageX + 10) + "px") 545: .style("top", (event.pageY - 30) + "px"); 546: }) 547: .on("mousemove", (event) => { 548: tooltip.style("left", (event.pageX + 10) + "px") 549: .style("top", (event.pageY - 30) + "px"); 550: }) 551: .on("mouseout", () => { 552: tooltip.style("opacity", 0); 553: }); 554: 555: chart.append("g") 556: .call(d3.axisLeft(y)); 557: 558: chart.append("g") 559: .attr("transform", `translate(0,\${height})`) 560: .call(d3.axisBottom(x)) 561: .selectAll("text") 562: .attr("transform", "rotate(-45)") 563: .style("text-anchor", "end"); 564: </script> 565: HTML 566: 567: return { svg_id => $svg_id, html => $html }; 568: } 569: 570: =head2 render_multi_series_line_chart_with_tooltips 571: 572: $html = $chart->render_multi_series_line_chart_with_tooltips($data); 573: 574: Generates HTML and JavaScript code to render a chart of many lines with mouseover tooltips. 575: 576: Accepts the following arguments: 577: 578: =over 4 579: 580: =item * C<$data> - An reference to an array of hashes containing data points. 581: Each data point should be an array reference with two elements: the label (string) and the value (numeric). 582: 583: =back 584: 585: Returns a string containing the HTML and JavaScript code for the chart. 586: 587: =cut 588: 589: sub render_multi_series_line_chart_with_tooltips 590: { 591: my ($self, $data) = @_; 592: 593: # Validate input data 594: die 'Data must be an array of hashes' unless ref($data) eq 'ARRAY'; 595: 596: my $json_data = encode_json($data); 597: 598: # Generate HTML and D3.js code 599: my $html = $self->_preamble(); 600: $html .= <<"HTML"; 601: <head> 602: <meta charset="UTF-8"> 603: <meta name="viewport" content="width=device-width, initial-scale=1.0"> 604: <title>$self->{title}</title> 605: <script src="https://d3js.org/d3.v7.min.js"></script> 606: <style> 607: .tooltip { 608: position: absolute; 609: background-color: white; 610: border: 1px solid #ccc; 611: padding: 5px; 612: font-size: 12px; 613: pointer-events: none; 614: opacity: 0; 615: transition: opacity 0.2s ease-in-out; 616: } 617: </style> 618: </head> 619: <body> 620: <h1 style="text-align: center;">$self->{title}</h1> 621: <svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg> 622: <div class="tooltip" id="tooltip"></div> 623: <script> 624: const data = $json_data; 625: 626: const svg = d3.select("#chart"); 627: const tooltip = d3.select("#tooltip"); 628: const margin = { top: 20, right: 30, bottom: 40, left: 40 }; 629: const width = $self->{width} - margin.left - margin.right; 630: const height = $self->{height} - margin.top - margin.bottom; 631: 632: const chart = svg.append("g") 633: .attr("transform", `translate(\${margin.left},\${margin.top})`); 634: 635: // Extract all labels and flatten them into a unique array 636: const allLabels = Array.from(new Set(data.flatMap(series => series.data.map(d => d.label)))); 637: 638: const x = d3.scalePoint() 639: .domain(allLabels) 640: .range([0, width]); 641: 642: const y = d3.scaleLinear() 643: .domain([0, d3.max(data.flatMap(series => series.data.map(d => d.value)))]) 644: .nice() 645: .range([height, 0]); 646: 647: // Define color scale for series 648: const color = d3.scaleOrdinal(d3.schemeCategory10); 649: 650: // Add axes 651: chart.append("g") 652: .call(d3.axisLeft(y)); 653: 654: chart.append("g") 655: .attr("transform", `translate(0,\${height})`) 656: .call(d3.axisBottom(x)) 657: .selectAll("text") 658: .attr("transform", "rotate(-45)") 659: .style("text-anchor", "end"); 660: 661: // Draw lines for each series 662: data.forEach((series, i) => { 663: const line = d3.line() 664: .x(d => x(d.label)) 665: .y(d => y(d.value)); 666: 667: // Add line 668: chart.append("path") 669: .datum(series.data) 670: .attr("fill", "none") 671: .attr("stroke", color(i)) 672: .attr("stroke-width", 2) 673: .attr("d", line); 674: 675: // Add points and tooltips 676: chart.selectAll(\`circle.series-\${i}\`) 677: .data(series.data) 678: .join("circle") 679: .attr("class", \`series-\${i}\`) 680: .attr("cx", d => x(d.label)) 681: .attr("cy", d => y(d.value)) 682: .attr("r", 4) 683: .attr("fill", color(i)) 684: .on("mouseover", (event, d) => { 685: tooltip.style("opacity", 1) 686: .html(\`Series: <b>\${series.name}<\/b><br>Label: <b>\${d.label}<\/b><br>Value: <b>\${d.value}<\/b>\`) 687: .style("left", (event.pageX + 10) + "px") 688: .style("top", (event.pageY - 30) + "px"); 689: }) 690: .on("mousemove", (event) => { 691: tooltip.style("left", (event.pageX + 10) + "px") 692: .style("top", (event.pageY - 30) + "px"); 693: }) 694: .on("mouseout", () => { 695: tooltip.style("opacity", 0); 696: }); 697: }); 698: </script> 699: </body> 700: </html> 701: HTML 702: 703: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
704: } 705: 706: =head2 render_multi_series_line_chart_with_animated_tooltips 707: 708: $html = $chart->render_multi_series_line_chart_with_animated_tooltips($data); 709: 710: Generates HTML and JavaScript code to render a chart of many lines with animated mouseover tooltips. 711: 712: Accepts the following arguments: 713: 714: =over 4 715: 716: =item * C<$data> - An reference to an array of hashes containing data points. 717: Each data point should be an array reference with two elements: the label (string) and the value (numeric). 718: 719: =back 720: 721: Returns a string containing the HTML and JavaScript code for the chart. 722: 723: =cut 724: 725: sub render_multi_series_line_chart_with_animated_tooltips 726: { 727: my ($self, $data) = @_; 728: 729: # Validate input data 730: die 'Data must be an array of hashes' unless ref($data) eq 'ARRAY'; 731: 732: # Generate JSON for data 733: my $json_data = encode_json($data); 734: 735: # Generate HTML and D3.js code 736: my $html = $self->_preamble(); 737: $html .= <<"HTML"; 738: <head> 739: <meta charset="UTF-8"> 740: <meta name="viewport" content="width=device-width, initial-scale=1.0"> 741: <title>$self->{title}</title> 742: <script src="https://d3js.org/d3.v7.min.js"></script> 743: <style> 744: .tooltip { 745: position: absolute; 746: background-color: white; 747: border: 1px solid #ccc; 748: padding: 5px; 749: font-size: 12px; 750: pointer-events: none; 751: opacity: 0; 752: transform: translateY(-10px); 753: transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; 754: } 755: </style> 756: </head> 757: <body> 758: <h1 style="text-align: center;">$self->{title}</h1> 759: <svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg> 760: <div class="tooltip" id="tooltip"></div> 761: <script> 762: const data = $json_data; 763: 764: const svg = d3.select("#chart"); 765: const tooltip = d3.select("#tooltip"); 766: const margin = { top: 20, right: 30, bottom: 40, left: 40 }; 767: const width = $self->{width} - margin.left - margin.right; 768: const height = $self->{height} - margin.top - margin.bottom; 769: 770: const chart = svg.append("g") 771: .attr("transform", `translate(\${margin.left},\${margin.top})`); 772: 773: // Extract all labels and flatten them into a unique array 774: const allLabels = Array.from(new Set(data.flatMap(series => series.data.map(d => d.label)))); 775: 776: const x = d3.scalePoint() 777: .domain(allLabels) 778: .range([0, width]); 779: 780: const y = d3.scaleLinear() 781: .domain([0, d3.max(data.flatMap(series => series.data.map(d => d.value)))]) 782: .nice() 783: .range([height, 0]); 784: 785: // Define color scale for series 786: const color = d3.scaleOrdinal(d3.schemeCategory10); 787: 788: // Add axes 789: chart.append("g") 790: .call(d3.axisLeft(y)); 791: 792: chart.append("g") 793: .attr("transform", `translate(0,\${height})`) 794: .call(d3.axisBottom(x)) 795: .selectAll("text") 796: .attr("transform", "rotate(-45)") 797: .style("text-anchor", "end"); 798: 799: // Draw lines for each series 800: data.forEach((series, i) => { 801: const line = d3.line() 802: .x(d => x(d.label)) 803: .y(d => y(d.value)); 804: 805: // Add line 806: chart.append("path") 807: .datum(series.data) 808: .attr("fill", "none") 809: .attr("stroke", color(i)) 810: .attr("stroke-width", 2) 811: .attr("d", line); 812: 813: // Add points and tooltips 814: chart.selectAll(\`circle.series-\${i}\`) 815: .data(series.data) 816: .join("circle") 817: .attr("class", \`series-\${i}\`) 818: .attr("cx", d => x(d.label)) 819: .attr("cy", d => y(d.value)) 820: .attr("r", 4) 821: .attr("fill", color(i)) 822: .on("mouseover", (event, d) => { 823: tooltip.style("opacity", 1) 824: .style("transform", "translateY(0)") 825: .html(\`Series: <b>\${series.name}<\/b><br>Label: <b>\${d.label}<\/b><br>Value: <b>\${d.value}<\/b>\`) 826: .style("left", (event.pageX + 10) + "px") 827: .style("top", (event.pageY - 30) + "px"); 828: }) 829: .on("mousemove", (event) => { 830: tooltip.style("left", (event.pageX + 10) + "px") 831: .style("top", (event.pageY - 30) + "px"); 832: }) 833: .on("mouseout", () => { 834: tooltip.style("opacity", 0) 835: .style("transform", "translateY(-10px)"); 836: }); 837: }); 838: </script> 839: </body> 840: </html> 841: HTML 842: 843: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
844: } 845: 846: =head2 render_multi_series_line_chart_with_legends 847: 848: $html = $chart->render_multi_series_line_chart_with_legends($data); 849: 850: Generates HTML and JavaScript code to render a chart of many lines with animated mouseover tooltips. 851: 852: Accepts the following arguments: 853: 854: =over 4 855: 856: =item * C<$data> - An reference to an array of hashes containing data points. 857: Each data point should be an array reference with two elements: the label (string) and the value (numeric). 858: 859: =back 860: 861: Returns a string containing the HTML and JavaScript code for the chart. 862: 863: =cut 864: 865: sub render_multi_series_line_chart_with_legends { 866: my($self, $data) = @_; 867: 868: # Validate input data 869: die 'Data must be an array of hashes' unless ref($data) eq 'ARRAY'; 870: 871: # Generate JSON for data 872: my $json_data = encode_json($data); 873: 874: # Generate HTML and D3.js code 875: my $html = $self->_preamble(); 876: $html .= <<"HTML"; 877: <head> 878: <meta charset="UTF-8"> 879: <meta name="viewport" content="width=device-width, initial-scale=1.0"> 880: <title>$self->{title}</title> 881: <script src="https://d3js.org/d3.v7.min.js"></script> 882: <style> 883: .tooltip { 884: position: absolute; 885: background-color: white; 886: border: 1px solid #ccc; 887: padding: 5px; 888: font-size: 12px; 889: pointer-events: none; 890: opacity: 0; 891: transform: translateY(-10px); 892: transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; 893: } 894: .legend { 895: font-size: 12px; 896: cursor: pointer; 897: } 898: .legend rect { 899: stroke-width: 1; 900: stroke: #ccc; 901: } 902: </style> 903: </head> 904: <body> 905: <h1 style="text-align: center;">$self->{title}</h1> 906: <svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg> 907: <div class="tooltip" id="tooltip"></div> 908: <script> 909: const data = $json_data; 910: 911: const svg = d3.select("#chart"); 912: const tooltip = d3.select("#tooltip"); 913: const margin = { top: 20, right: 120, bottom: 40, left: 40 }; 914: const width = $self->{width} - margin.left - margin.right; 915: const height = $self->{height} - margin.top - margin.bottom; 916: 917: const chart = svg.append("g") 918: .attr("transform", `translate(\${margin.left},\${margin.top})`); 919: 920: const legendArea = svg.append("g") 921: .attr("transform", `translate(\${width + margin.left + 20},\${margin.top})`); 922: 923: // Extract all labels and flatten them into a unique array 924: const allLabels = Array.from(new Set(data.flatMap(series => series.data.map(d => d.label)))); 925: 926: const x = d3.scalePoint() 927: .domain(allLabels) 928: .range([0, width]); 929: 930: const y = d3.scaleLinear() 931: .domain([0, d3.max(data.flatMap(series => series.data.map(d => d.value)))]) 932: .nice() 933: .range([height, 0]); 934: 935: // Define color scale for series 936: const color = d3.scaleOrdinal(d3.schemeCategory10); 937: 938: // Add axes 939: chart.append("g") 940: .call(d3.axisLeft(y)); 941: 942: chart.append("g") 943: .attr("transform", `translate(0,\${height})`) 944: .call(d3.axisBottom(x)) 945: .selectAll("text") 946: .attr("transform", "rotate(-45)") 947: .style("text-anchor", "end"); 948: 949: // Draw lines for each series 950: data.forEach((series, i) => { 951: const line = d3.line() 952: .x(d => x(d.label)) 953: .y(d => y(d.value)); 954: 955: // Add line 956: chart.append("path") 957: .datum(series.data) 958: .attr("fill", "none") 959: .attr("stroke", color(i)) 960: .attr("stroke-width", 2) 961: .attr("class", \`line-\${i}\`) 962: .attr("d", line); 963: 964: // Add points and tooltips 965: chart.selectAll(\`circle.series-\${i}\`) 966: .data(series.data) 967: .join("circle") 968: .attr("class", \`series-\${i}\`) 969: .attr("cx", d => x(d.label)) 970: .attr("cy", d => y(d.value)) 971: .attr("r", 4) 972: .attr("fill", color(i)) 973: .on("mouseover", (event, d) => { 974: tooltip.style("opacity", 1) 975: .style("transform", "translateY(0)") 976: .html(\`Series: <b>\${series.name}<\/b><br>Label: <b>\${d.label}<\/b><br>Value: <b>\${d.value}<\/b>\`) 977: .style("left", (event.pageX + 10) + "px") 978: .style("top", (event.pageY - 30) + "px"); 979: }) 980: .on("mousemove", (event) => { 981: tooltip.style("left", (event.pageX + 10) + "px") 982: .style("top", (event.pageY - 30) + "px"); 983: }) 984: .on("mouseout", () => { 985: tooltip.style("opacity", 0) 986: .style("transform", "translateY(-10px)"); 987: }); 988: }); 989: 990: // Add legend 991: data.forEach((series, i) => { 992: const legend = legendArea.append("g") 993: .attr("transform", `translate(0, \${i * 20})`) 994: .attr("class", "legend"); 995: 996: legend.append("rect") 997: .attr("width", 12) 998: .attr("height", 12) 999: .attr("fill", color(i)); 1000: 1001: legend.append("text") 1002: .attr("x", 20) 1003: .attr("y", 10) 1004: .text(series.name) 1005: .style("alignment-baseline", "middle"); 1006: 1007: // Optional: Interactive legend for toggling visibility (uncomment to use) 1008: // legend.on("click", () => { 1009: // const visible = d3.selectAll(\`path.line-\${i}\`).style("opacity") === "1" ? 0 : 1; 1010: // d3.selectAll(\`path.line-\${i}\`).style("opacity", visible); 1011: // d3.selectAll(\`circle.series-\${i}\`).style("opacity", visible); 1012: // }); 1013: }); 1014: </script> 1015: </body> 1016: </html> 1017: HTML 1018: 1019: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
1020: } 1021: 1022: =head2 render_multi_series_line_chart_with_interactive_legends 1023: 1024: $html = $chart->render_multi_series_line_chart_with_interactive_legends($data); 1025: 1026: 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. 1027: 1028: Accepts the following arguments: 1029: 1030: =over 4 1031: 1032: =item * C<$data> - An reference to an array of hashes containing data points. 1033: Each data point should be an array reference with two elements: the label (string) and the value (numeric). 1034: 1035: =back 1036: 1037: Returns a string containing the HTML and JavaScript code for the chart. 1038: 1039: =cut 1040: 1041: sub render_multi_series_line_chart_with_interactive_legends 1042: { 1043: my ($self, $data) = @_; 1044: 1045: # Validate input data 1046: die 'Data must be an array of hashes' unless ref($data) eq 'ARRAY'; 1047: 1048: # Generate JSON for data 1049: my $json_data = encode_json($data); 1050: 1051: # Generate HTML and D3.js code 1052: my $html = $self->_preamble(); 1053: $html .= <<"HTML"; 1054: <head> 1055: <meta charset="UTF-8"> 1056: <meta name="viewport" content="width=device-width, initial-scale=1.0"> 1057: <title>$self->{title}</title> 1058: <script src="https://d3js.org/d3.v7.min.js"></script> 1059: <style> 1060: .tooltip { 1061: position: absolute; 1062: background-color: white; 1063: border: 1px solid #ccc; 1064: padding: 5px; 1065: font-size: 12px; 1066: pointer-events: none; 1067: opacity: 0; 1068: transform: translateY(-10px); 1069: transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; 1070: } 1071: .legend { 1072: font-size: 12px; 1073: cursor: pointer; 1074: } 1075: .legend rect { 1076: stroke-width: 1; 1077: stroke: #ccc; 1078: } 1079: </style> 1080: </head> 1081: <body> 1082: <h1 style="text-align: center;">$self->{title}</h1> 1083: <svg id="chart" width="$self->{width}" height="$self->{height}" style="border: 1px solid black;"></svg> 1084: <div class="tooltip" id="tooltip"></div> 1085: <script> 1086: const data = $json_data; 1087: 1088: const svg = d3.select("#chart"); 1089: const tooltip = d3.select("#tooltip"); 1090: const margin = { top: 20, right: 150, bottom: 40, left: 40 }; 1091: const width = $self->{width} - margin.left - margin.right; 1092: const height = $self->{height} - margin.top - margin.bottom; 1093: 1094: const chart = svg.append("g") 1095: .attr("transform", `translate(\${margin.left},\${margin.top})`); 1096: 1097: const legendArea = svg.append("g") 1098: .attr("transform", `translate(\${width + margin.left + 20},\${margin.top})`); 1099: 1100: // Extract all labels and flatten them into a unique array 1101: const allLabels = Array.from(new Set(data.flatMap(series => series.data.map(d => d.label)))); 1102: 1103: const x = d3.scalePoint() 1104: .domain(allLabels) 1105: .range([0, width]); 1106: 1107: const y = d3.scaleLinear() 1108: .domain([0, d3.max(data.flatMap(series => series.data.map(d => d.value)))]) 1109: .nice() 1110: .range([height, 0]); 1111: 1112: // Define color scale for series 1113: const color = d3.scaleOrdinal(d3.schemeCategory10); 1114: 1115: // Add axes 1116: chart.append("g") 1117: .call(d3.axisLeft(y)); 1118: 1119: chart.append("g") 1120: .attr("transform", `translate(0,\${height})`) 1121: .call(d3.axisBottom(x)) 1122: .selectAll("text") 1123: .attr("transform", "rotate(-45)") 1124: .style("text-anchor", "end"); 1125: 1126: // Draw lines for each series 1127: data.forEach((series, i) => { 1128: const line = d3.line() 1129: .x(d => x(d.label)) 1130: .y(d => y(d.value)); 1131: 1132: // Add line 1133: chart.append("path") 1134: .datum(series.data) 1135: .attr("fill", "none") 1136: .attr("stroke", color(i)) 1137: .attr("stroke-width", 2) 1138: .attr("class", \`line-\${i}\`) 1139: .attr("d", line); 1140: 1141: // Add points and tooltips 1142: chart.selectAll(\`circle.series-\${i}\`) 1143: .data(series.data) 1144: .join("circle") 1145: .attr("class", \`series-\${i}\`) 1146: .attr("cx", d => x(d.label)) 1147: .attr("cy", d => y(d.value)) 1148: .attr("r", 4) 1149: .attr("fill", color(i)) 1150: .on("mouseover", (event, d) => { 1151: tooltip.style("opacity", 1) 1152: .style("transform", "translateY(0)") 1153: .html(\`Series: <b>\${series.name}<\/b><br>Label: <b>\${d.label}<\/b><br>Value: <b>\${d.value}<\/b>\`) 1154: .style("left", (event.pageX + 10) + "px") 1155: .style("top", (event.pageY - 30) + "px"); 1156: }) 1157: .on("mousemove", (event) => { 1158: tooltip.style("left", (event.pageX + 10) + "px") 1159: .style("top", (event.pageY - 30) + "px"); 1160: }) 1161: .on("mouseout", () => { 1162: tooltip.style("opacity", 0) 1163: .style("transform", "translateY(-10px)"); 1164: }); 1165: }); 1166: 1167: // Add legend with interactivity 1168: data.forEach((series, i) => { 1169: const legend = legendArea.append("g") 1170: .attr("transform", `translate(0, \${i * 20})`) 1171: .attr("class", "legend") 1172: .on("click", () => { 1173: const isVisible = d3.selectAll(\`path.line-\${i}\`).style("opacity") === "1"; 1174: 1175: // Toggle visibility 1176: d3.selectAll(\`path.line-\${i}\`).style("opacity", isVisible ? 0 : 1); 1177: d3.selectAll(\`circle.series-\${i}\`).style("opacity", isVisible ? 0 : 1); 1178: 1179: // Dim legend if series is hidden 1180: legend.select("text").style("opacity", isVisible ? 0.5 : 1); 1181: }); 1182: 1183: legend.append("rect") 1184: .attr("width", 12) 1185: .attr("height", 12) 1186: .attr("fill", color(i)); 1187: 1188: legend.append("text") 1189: .attr("x", 20) 1190: .attr("y", 10) 1191: .text(series.name) 1192: .style("alignment-baseline", "middle"); 1193: }); 1194: </script> 1195: </body> 1196: </html> 1197: HTML 1198: 1199: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
1200: } 1201: 1202: sub _preamble 1203: { 1204: my $html = <<'HTML'; 1205: <!DOCTYPE html> 1206: <html lang="en"> 1207: HTML 1208: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
1209: } 1210: 1211: sub _head 1212: { 1213: my $self = shift; 1214: 1215: my $html = <<"HTML"; 1216: <head> 1217: <meta charset="UTF-8"> 1218: <meta name="viewport" content="width=device-width, initial-scale=1.0"> 1219: <title>$self->{title}</title> 1220: <script src="https://d3js.org/d3.v7.min.js"></script> 1221: </head> 1222: HTML 1223: return $html;
Mutants (Total: 2, Killed: 2, Survived: 0)
1224: } 1225: 1226: =head1 SUPPORT 1227: 1228: This module is provided as-is without any warranty. 1229: 1230: Please report any bugs or feature requests to C<bug-html-d3 at rt.cpan.org>, 1231: or through the web interface at 1232: L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=HTML-D3>. 1233: I will be notified, and then you'll 1234: automatically be notified of progress on your bug as I make changes. 1235: 1236: You can find documentation for this module with the perldoc command. 1237: 1238: perldoc HTML::D3 1239: 1240: You can also look for information at: 1241: 1242: =head1 BUGS 1243: 1244: It would help to have the render routine to return the head and body components separately. 1245: 1246: =head1 SEE ALSO 1247: 1248: =over 4 1249: 1250: =item * L<Configure an Object at Runtime|Object::Configure> 1251: 1252: =item * L<Test Dashboard|https://nigelhorne.github.io/HTML-D3/coverage/> 1253: 1254: =back 1255: 1256: =head1 AUTHOR 1257: 1258: Nigel Horne <njh@nigelhorne.com> 1259: 1260: =head1 LICENSE AND COPYRIGHT 1261: 1262: Copyright 2025-2026 Nigel Horne. 1263: 1264: Usage is subject to the GPL2 licence terms. 1265: If you use it, 1266: please let me know. 1267: 1268: =cut 1269: 1270: 1;