### ----------------------------------------------------
### -- JavaScript::D3
### -- Licenses: Artistic-2.0
### -- Authors: Anton Antonov
### -- File: lib/JavaScript/D3/CodeSnippets.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 3
unit module JavaScript::D3::CodeSnippets;

use JavaScript::D3::Utilities;

#============================================================
# Wrapping
#============================================================
# The core JavaScript code is wrapped with HTML or Jupyter cell pre- and post code.

our sub WrapIt(Str $code, Str :$format='jupyter', :$div-id is copy = Whatever) {

    if $div-id !~~ Str {
        warn 'The argument div-id is expected to be a string or Whatever' unless $div-id.isa(Whatever);
        $div-id = 'my-dataviz'
    }

    if $format eq 'asis' {
        return $code
    }

    my $res =
            [JavaScript::D3::CodeSnippets::GetPlotStartingCode($format),
             $code,
             JavaScript::D3::CodeSnippets::GetPlotEndingCode($format)].join("\n");


    $res = do given $format.lc {

        when $_ eq 'html' {
            $res
                    .subst(:g, '.select(element.get(0))', '.select("#' ~ $div-id ~ '")')
                    .subst(:g, / 'element.get(0)' | '"my_dataviz"' /, '"' ~ $div-id ~ '"')
                    .subst(:g, '"#my_dataviz"', '"#' ~ $div-id ~ '"');
        }

        when $_ ∈ <html-md html-markdown html-embedded html-fragment> {
            $res
                    .subst(:g, / 'element.get(0)' | '"my_dataviz"' /, '"' ~ $div-id ~ '"')
                    .subst(:g, '"#my_dataviz"', '"#' ~ $div-id ~ '"')
                    .subst(:g, '<body>')
                    .subst(:g, '</body>')
                    .subst(:g, '<!DOCTYPE html>')
                    .subst(:g, '</html>');
        }

        default { $res }
    }

    return $res;
}

#============================================================
# D3.js color palettes
#============================================================

# See : https://d3js.org/d3-scale-chromatic/sequential
my @knownSequentialSchemes =
        "Blues", "Greens", "Greys", "Oranges", "Purples", "Reds",
        "BuGn", "BuPu", "GnBu", "OrRd", "PuBuGn", "PuBu", "PuRd", "RdPu", "YlGnBu", "YlGn", "YlOrBr", "YlOrRd",
        "BrBG", "PRGn", "PiYG", "PuOr", "RdBu", "RdGy", "RdYlBu", "RdYlGn", "Spectral",
        "Cividis", "Viridis", "Inferno", "Magma", "Plasma", "Warm", "Cool", "CubehelixDefault", "Turbo",
        "Rainbow", "Sinebow";

our sub known-sequential-schemes() { return @knownSequentialSchemes; }

#============================================================
# JavaScript plot and chart template parts
#============================================================
my $jsPlotStartingHTML = q:to/END/;
<!DOCTYPE html>
<head>
    <script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>

<div id="my_dataviz"></div>

<script>
END

my $jsPlotStarting = q:to/END/;
(function(element) { require(['d3'], function(d3) {
END

my $jsPlotMarginsAndTitle = q:to/END/;
// set the dimensions and margins of the graph
var margin = $MARGINS,
    width = $WIDTH - margin.left - margin.right,
    height = $HEIGHT - margin.top - margin.bottom;

// append the svg object to the body of the page
var svg = d3
   .select(element.get(0))
  .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .style("background", $BACKGROUND_COLOR)
  .append("g")
    .attr("transform",
          "translate(" + margin.left + "," + margin.top + ")")

// Obtain title
var title = $TITLE
var titleFontSize = $TITLE_FONT_SIZE

if ( title.length > 0 ) {
    svg.append("text")
        .attr("x", (width / 2))
        .attr("y", 0 - (margin.top / 2))
        .attr("text-anchor", "middle")
        .style("font-size", titleFontSize.toString() + "px")
        .style("fill", $TITLE_FILL)
        .text(title);
}
END

my $jsPlotAxesLabels = q:to/END/;
// Obtain x-axis label
var xAxisLabel = $X_AXIS_LABEL
var xAxisLabelFontSize = $X_AXIS_LABEL_FONT_SIZE

if ( xAxisLabel.length > 0 ) {
    svg.append("text")
        .attr("x", (width / 2))
        .attr("y", height + margin.bottom - xAxisLabelFontSize/2)
        .attr("text-anchor", "middle")
        .style("font-size", xAxisLabelFontSize.toString() + "px")
        .style("fill", $X_AXIS_LABEL_FILL)
        .text(xAxisLabel);
}

// Obtain y-axis label
var yAxisLabel = $Y_AXIS_LABEL
var yAxisLabelFontSize = $Y_AXIS_LABEL_FONT_SIZE

if ( yAxisLabel.length > 0 ) {
    svg.append("text")
        .attr("transform", "rotate(-90)")
        .attr("x", - (height / 2))
        .attr("y", 0 - margin.left + yAxisLabelFontSize)
        .attr("text-anchor", "middle")
        .style("font-size", yAxisLabelFontSize.toString() + "px")
        .style("fill", $Y_AXIS_LABEL_FILL)
        .text(yAxisLabel);
}
END

my $jsPlotMarginsTitleAndLabels = $jsPlotMarginsAndTitle ~ "\n" ~ $jsPlotAxesLabels;

my $jsPlotDataAndScales = q:to/END/;
// Obtain data
var data = $DATA

var xMin = Math.min.apply(Math, data.map(function(o) { return o.x; }))
var xMax = Math.max.apply(Math, data.map(function(o) { return o.x; }))

var yMin = Math.min.apply(Math, data.map(function(o) { return o.y; }))
var yMax = Math.max.apply(Math, data.map(function(o) { return o.y; }))

// X scale and Axis
var x = d3.$X_AXIS_SCALE
    .domain([xMin, xMax])
    .range([0, width]);

// Y scale and Axis
var y = d3.$Y_AXIS_SCALE
    .domain([yMin, yMax])
    .range([height, 0]);
END

my $jsPlotDataAxes = q:to/END/;
svg
  .append('g')
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x))

svg
  .append('g')
  .call(d3.axisLeft(y));
END

my $jsPlotDataScalesAndAxes = $jsPlotDataAndScales ~ "\n" ~ $jsPlotDataAxes;

# See https://d3-graph-gallery.com/graph/custom_legend.html
my $jsGroupsLegend = q:to/END/;
// create a list of keys
var keys = data.map(function(o) { return o.group; })
keys = [...new Set(keys)];

// Add one dot in the legend for each name.
svg.selectAll("mydots")
  .data(keys)
  .enter()
  .append("circle")
    .attr("cx", $LEGEND_X_POS)
    .attr("cy", function(d,i){ return $LEGEND_Y_POS + i*$LEGEND_Y_GAP}) // 100 is where the first dot appears. 25 is the distance between dots
    .attr("r", 6)
    .style("fill", function(d){ return myColor(d)})

// Add one dot in the legend for each name.
svg.selectAll("mylabels")
  .data(keys)
  .enter()
  .append("text")
    .attr("x", $LEGEND_X_POS + 12)
    .attr("y", function(d,i){ return $LEGEND_Y_POS + i*$LEGEND_Y_GAP}) // 100 is where the first dot appears. 25 is the distance between dots
    .style("fill", function(d){ return myColor(d)})
    .text(function(d){ return d})
    .attr("text-anchor", "left")
    .style("alignment-baseline", "middle")
    .style("font-size", "12px")
    .attr("font-family", "Courier")
END

my $jsPlotEndingHTML = q:to/END/;
</script>
</body>
</html>
END

my $jsPlotEnding = q:to/END/;
}) })(element);
END

#============================================================
# JavaScript plot and chart snippets accessors
#============================================================

our sub GetPlotStartingCode(Str $format = 'jupyter') {
    return $format.lc ∈ <jupyter asis> ?? $jsPlotStarting !! $jsPlotStartingHTML;
}

our sub GetPlotEndingCode(Str $format = 'jupyter') {
    return $format.lc ∈ <jupyter asis> ?? $jsPlotEnding !! $jsPlotEndingHTML;
}

our sub GetPlotMarginsAndTitle(Str $format = 'jupyter') {
    return
            $format.lc ∈ <jupyter asis> ??
            $jsPlotMarginsAndTitle !! $jsPlotMarginsAndTitle.subst(:g, 'element.get(0)', '"#my_dataviz"');
}

our sub GetPlotMarginsTitleAndLabelsCode(Str $format = 'jupyter') {
    return
            $format.lc ∈ <jupyter asis> ??
            $jsPlotMarginsTitleAndLabels !! $jsPlotMarginsTitleAndLabels.subst(:g, 'element.get(0)', '"#my_dataviz"');
}

our sub ProcessAxisScale($scale = Whatever) {
    return do given $scale {
        when Whatever { 'scaleLinear()' }
        when WhateverCode { 'scaleLinear()' }
        when $_ ~~ Str:D && $_.lc ∈ <line linear> { 'scaleLinear()' }
        when $_ ~~ Str:D && $_.lc ∈ <log logarith logarithmic> { 'scaleLog()' }
        default { $_ }
    }
}
our sub GetPlotDataAndScalesCode(
        :$x-axis-scale = Whatever,
        :$y-axis-scale = Whatever) {
    return
            $jsPlotDataAndScales
            .subst('$X_AXIS_SCALE', ProcessAxisScale($x-axis-scale))
            .subst('$Y_AXIS_SCALE', ProcessAxisScale($y-axis-scale));
}

our sub GetPlotDataScalesAndAxesCode(
        UInt $nXTicks = 0,
        UInt $nYTicks = 0,
        Str $codeFragment = $jsPlotDataScalesAndAxes,
        :$x-axis-scale = Whatever,
        :$y-axis-scale = Whatever) {
    my $res = $codeFragment;
    if $nXTicks > 0 {
        $res = $res.subst('.call(d3.axisBottom(x))', ".call(d3.axisBottom(x).ticks($nXTicks).tickSizeInner(-height))");
    }
    if $nYTicks > 0 {
        $res = $res.subst('.call(d3.axisLeft(y))', ".call(d3.axisLeft(y).ticks($nYTicks).tickSizeInner(-width))");
    }
    $res = $res
            .subst('$X_AXIS_SCALE', ProcessAxisScale($x-axis-scale))
            .subst('$Y_AXIS_SCALE', ProcessAxisScale($y-axis-scale));
    return $res;
}

our sub GetPlotPreparationCode(
        Str $format = 'jupyter',
        UInt $nXTicks = 0,
        UInt $nYTicks = 0,
        Bool :$axes = True,
        :$x-axis-scale = Whatever,
        :$y-axis-scale = Whatever) {
    return [GetPlotMarginsTitleAndLabelsCode($format),
            $axes
            ?? GetPlotDataScalesAndAxesCode($nXTicks, $nYTicks, :$x-axis-scale, :$y-axis-scale)
            !! GetPlotDataAndScalesCode(:$x-axis-scale, :$y-axis-scale)].join("\n");
}

our sub GetLegendCode() {
    return $jsGroupsLegend;
}


#============================================================
# Tooltip code snippets
#============================================================

my $jsTooltipPart = q:to/END/;
// -1- Create a tooltip div that is hidden by default:
const tooltip = d3.select(element.get(0))
    .append("div")
      .style("opacity", 0)
      .attr("class", "tooltip")
      .style("background-color", $TOOLTIP_BACKGROUND_COLOR)
      .style("border-radius", "5px")
      .style("padding", "10px")
      .style("color", $TOOLTIP_COLOR)

// -2- Create 3 functions to show / update (when mouse move but stay on same circle) / hide the tooltip
const showTooltip = function(event, d) {
    tooltip
      .transition()
      .duration($TOOLTIP_DURATION);

    var tooltipContent
    if (d.tooltip) {
        tooltipContent = d.tooltip;
    } else if (d.label) {
        tooltipContent = "Label: " + d.label;
    } else if (d.group && d.x && d.y && d.z) {
        tooltipContent = "Group: " + d.group + '<br/>z: ' + d.z.toString() + '<br/>x: ' + d.x.toString() + '<br/>y: ' + d.y.toString();
    } else if (d.group && d.x && d.y) {
        tooltipContent = "Group: " + d.group + '<br/>x: ' + d.x.toString() + '<br/>y: ' + d.y.toString();
    }

    tooltip
      .style("opacity", 1)
      .html(tooltipContent)
      .style("left", (event.x)/2 + "px")
      .style("top", (event.y)/2 + 10 + "px");
};

const moveTooltip = function(event, d) {
tooltip
  .style("left", (event.x)/2 + "px")
  .style("top", (event.y)/2+10 + "px")
};

const hideTooltip = function(event, d) {
tooltip
  .transition()
  .duration($TOOLTIP_DURATION)
  .style("opacity", 0)
};
END

my $jsTooltipMousePart = q:to/END/;
    .on("mouseover", showTooltip )
    .on("mousemove", moveTooltip )
    .on("mouseleave", hideTooltip )
END

#============================================================
# Tooltip code snippets
#============================================================

our sub GetTooltipPart(UInt :duration(:$tooltip-duration) = 200) {
    my $res = $jsTooltipPart
        .subst('$TOOLTIP_DURATION', $tooltip-duration):g;

    return $res;
}

our sub GetTooltipMousePart() {
    return $jsTooltipMousePart;
}

#============================================================
# ListPlot code snippets
#============================================================

my $jsScatterPlotPart = q:to/END/;
// Add dots
svg
  .selectAll("dot")
  .data(data)
  .enter()
  .append("circle")
    .attr("cx", function(d){ return x(d.x) })
    .attr("cy", function(d){ return y(d.y) })
    .attr("r", $POINT_RADIUS)
    .attr("color", "blue")
    .attr("fill", $POINT_COLOR)
  // Trigger the tooltip functions
END

my $jsMultiScatterPlotPart = q:to/END/;
// Add a scale for dot color
var myColor = d3.scaleOrdinal()
    .domain(data.map(function(o) { return o.group; }))
    .range(d3.$COLOR_SCHEME);

// Add dots
svg
  .selectAll("whatever")
  .data(data)
  .enter()
  .append("circle")
    .attr("cx", function(d){ return x(d.x) })
    .attr("cy", function(d){ return y(d.y) })
    .attr("r", $POINT_RADIUS)
    .attr("color", "blue")
    .attr("fill", function (d) { return myColor(d.group) } )
  // Trigger the tooltip functions
END

#============================================================
# ListPlot code snippets accessors
#============================================================
our sub GetScatterPlotPart() {
    return $jsScatterPlotPart;
}

our sub GetMultiScatterPlotPart() {
    return $jsMultiScatterPlotPart;
}

#============================================================
# ListLinePlot code snippets
#============================================================
my $jsPathPlotPart = q:to/END/;
// prepare a helper function
var lineFunc = d3.line()
  .x(function(d) { return x(d.x) })
  .y(function(d) { return y(d.y) })

// Add the path using this helper function
svg.append('path')
  .attr('d', lineFunc(data))
  .attr("stroke-width", $STROKE_WIDTH)
  .attr('stroke', $LINE_COLOR)
  .attr('fill', 'none');
END

my $jsFilledPathPlotPart = q:to/END/;
// prepare a helper function
var areaFunc = d3.area()
  .x(function(d) { return x(d.x) })
  .y0(function(d) { return y(0) })
  .y1(function(d) { return y(d.y) })

// Add the path using this helper function
svg.append('path')
  .attr('d', areaFunc(data))
  .attr("stroke-width", $STROKE_WIDTH)
  .attr('stroke', $LINE_COLOR)
  .attr('fill', $FILL_COLOR);
END

# See https://d3-graph-gallery.com/graph/line_several_group.html
my $jsMultiPathPlotPart = q:to/END/;
// group the data: I want to draw one line per group
var sumstat = d3.group(data, d => d.group);

// Add a scale for line color
var myColor = d3.scaleOrdinal()
    .domain(data.map(function(o) { return o.group; }))
    .range(d3.$COLOR_SCHEME);

// Draw the line
svg.selectAll(".line")
      .data(sumstat)
      .join("path")
        .attr("fill", "none")
        .attr("stroke", function(d){ return myColor(d[0]) })
        .attr("stroke-width", $STROKE_WIDTH)
        .attr("d", function(d){
          return d3.line()
            .x(function(d) { return x(d.x); })
            .y(function(d) { return y(+d.y); })
            (d[1])
        })
END

#============================================================
# ListLinePlot code snippets accessors
#============================================================

our sub GetPathPlotPart(Bool :$filled = False) {
    return $filled ?? $jsFilledPathPlotPart !! $jsPathPlotPart;
}

our sub GetMultiPathPlotPart() {
    return $jsMultiPathPlotPart;
}

#============================================================
# DateListPlot code snippets
#============================================================

# See https://d3-graph-gallery.com/graph/line_basic.html
my $jsPlotDateDataAndScales = q:to/END/;
// Obtain data
var data = $DATA

data = data.map(function(d){
  var d2 = d;
  d2["x"] = d3.timeParse("%Y-%m-%d")(d.date);
  if ( "value" in d ) { d2["y"] = d.value; }
  return d2
})

var yMin = Math.min.apply(Math, data.map(function(o) { return o.y; }))
var yMax = Math.max.apply(Math, data.map(function(o) { return o.y; }))

// Add X axis --> it is a date format
var x = d3.scaleTime()
      .domain(d3.extent(data, function(d) { return d.x; }))
      .range([ 0, width ]);

// Y scale and Axis
var y = d3.$Y_AXIS_SCALE
    .domain([yMin, yMax])
    .range([height, 0]);
END

my $jsPlotDateAxes = q:to/END/;
svg
  .append('g')
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x))

svg
  .append('g')
  .call(d3.axisLeft(y));
END

#============================================================
# DateListPlot code snippets accessors
#============================================================

our sub GetPlotDateDataAndScales(:$y-axis-scale = Whatever) {
    return $jsPlotDateDataAndScales.subst('$Y_AXIS_SCALE', ProcessAxisScale($y-axis-scale));
}

our sub GetPlotDateDataScalesAndAxes(:$y-axis-scale = Whatever) {
    return GetPlotDateDataAndScales(:$y-axis-scale) ~ "\n" ~ $jsPlotDateAxes;
}

#============================================================
# Box-Whisker code snippets
#============================================================

my $jsBoxWhiskerPlotBoxData = q:to/END/;
// Obtain data
var data = $DATA

// Compute summary statistics used for the box:
var data_sorted = data.sort(d3.ascending)
var q1 = d3.quantile(data_sorted, .25)
var median = d3.quantile(data_sorted, .5)
var q3 = d3.quantile(data_sorted, .75)
var interQuantileRange = q3 - q1
var min = q1 - 1.5 * interQuantileRange
var max = q1 + 1.5 * interQuantileRange

var outliers = data.filter(d => d < min || max < d);

var valueMin = Math.min.apply(Math, data.map(function(o) { return o; }))
var valueMax = Math.max.apply(Math, data.map(function(o) { return o; }))

const tooltipContent = `
  <table>
    <tr><td><b>max</b></td><td>${valueMax}</td></tr>
    <tr><td><b>75%</b></td><td>${q3}</td></tr>
    <tr><td><b>median</b></td><td>${median}</td></tr>
    <tr><td><b>25%</b></td><td>${q1}</td></tr>
    <tr><td><b>min</b></td><td>${valueMin}</td></tr>
  </table>
`;

const findClosestLarger = (data, min) => {
  return data.filter(d => d > min).reduce((prev, curr) =>
    (Math.abs(curr - min) < Math.abs(prev - min) ? curr : prev), Infinity);
};

const findClosestSmaller = (data, max) => {
  return data.filter(d => d < max).reduce((prev, curr) =>
    (Math.abs(curr - max) < Math.abs(prev - max) ? curr : prev), Infinity);
};

const minWhisker = findClosestLarger(data, min);
const maxWhisker = findClosestSmaller(data, max);

var boxData = [{"q1" : q1, "q3" : q3, "minWhisker" : minWhisker, "maxWhisker" : maxWhisker, "median" : median, "tooltip" : tooltipContent }];
END

my $jsVerticalBoxWhiskerPlot = q:to/END/;
// Show the Y scale
var y = d3.scaleLinear()
  .domain([valueMin,valueMax])
  .range([height, 0]);
svg.call(d3.axisLeft(y))

// a few features for the box
var center = width / 2
var boxWidth = $BOX_WIDTH

// Show the main vertical line
svg
.append("line")
  .attr("x1", center)
  .attr("x2", center)
  .attr("y1", y(minWhisker) )
  .attr("y2", y(maxWhisker) )
  .attr("stroke", $STROKE_COLOR)

svg.append('g')
    .selectAll('rect')
    .data(boxData)
    .join("rect")
        .attr("x", center - boxWidth/2)
        .attr("y", d => y(d.q3) )
        .attr("height", d => (y(d.q1)-y(d.q3)) )
        .attr("width", boxWidth )
        .attr("stroke", $STROKE_COLOR)
        .style("fill", $FILL_COLOR)
    // Trigger the tooltip functions

if ($OUTLIERS) {
    svg
      .selectAll("whatever")
      .data(outliers)
      .enter()
      .append("circle")
        .attr("cy", function(d){ return y(d) })
        .attr("cx", center)
        .attr("r", 2)
        .attr("color", $STROKE_COLOR)
        .attr("fill", $STROKE_COLOR)
}

// show median, min and max horizontal lines
svg
.selectAll("toto")
.data([minWhisker, median, maxWhisker])
.enter()
.append("line")
  .attr("x1", center-boxWidth/2)
  .attr("x2", center+boxWidth/2)
  .attr("y1", function(d){ return(y(d))} )
  .attr("y2", function(d){ return(y(d))} )
  .attr("stroke", $STROKE_COLOR)
END

my $jsHorizonalBoxWhiskerPlot = q:to/END/;
// Show the X scale
var x = d3.scaleLinear()
  .domain([valueMin,valueMax])
  .range([0, width]);

svg.call(d3.axisBottom(x))

// a few features for the box
var center = height / 2
var boxWidth = $BOX_WIDTH

// Show the main vertical line
svg
.append("line")
  .attr("y1", center)
  .attr("y2", center)
  .attr("x1", x(minWhisker) )
  .attr("x2", x(maxWhisker) )
  .attr("stroke", $STROKE_COLOR)

svg.append('g')
    .selectAll('rect')
    .data(boxData)
    .join("rect")
        .attr("x", d => x(d.q1) )
        .attr("y", center - boxWidth/2)
        .attr("width", d => (x(d.q3)-x(d.q1)) )
        .attr("height", boxWidth )
        .attr("stroke", $STROKE_COLOR)
        .style("fill", $FILL_COLOR)
    // Trigger the tooltip functions

if ($OUTLIERS) {
    svg
      .selectAll("whatever")
      .data(outliers)
      .enter()
      .append("circle")
        .attr("cx", function(d){ return x(d) })
        .attr("cy", center)
        .attr("r", 2)
        .attr("color", $STROKE_COLOR)
        .attr("fill", $STROKE_COLOR)
}

// show median, min and max horizontal lines
svg
.selectAll("toto")
.data([minWhisker, median, maxWhisker])
.enter()
.append("line")
  .attr("y1", center-boxWidth/2)
  .attr("y2", center+boxWidth/2)
  .attr("x1", function(d){ return(x(d))} )
  .attr("x2", function(d){ return(x(d))} )
  .attr("stroke", $STROKE_COLOR)
END

my $jsMultiBoxWhiskerPlot = q:to/END/;
// Obtain data
var data = $DATA

var groupColumn = $GROUP_COLUMN;
var valueColumn = $VALUE_COLUMN;

var valueMin = Math.min.apply(Math, data.map(function(o) { return o[valueColumn]; }))
var valueMax = Math.max.apply(Math, data.map(function(o) { return o[valueColumn]; }))

var uniqueValues = [...new Set(data.map(d => d[groupColumn]))];

// Compute quartiles, median, inter quantile range min and max --> these info are then used to draw the box.
var sumstat = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d[groupColumn];})
.rollup(function(d) {
  q1 = d3.quantile(d.map(function(g) { return g[valueColumn];}).sort(d3.ascending),.25)
  median = d3.quantile(d.map(function(g) { return g[valueColumn];}).sort(d3.ascending),.5)
  q3 = d3.quantile(d.map(function(g) { return g[valueColumn];}).sort(d3.ascending),.75)
  interQuantileRange = q3 - q1
  min = q1 - 1.5 * interQuantileRange
  max = q3 + 1.5 * interQuantileRange
  return({q1: q1, median: median, q3: q3, interQuantileRange: interQuantileRange, min: min, max: max})
})
.entries(data)

// Show the X scale
var x = d3.scaleBand()
.range([ 0, width ])
.domain(uniqueValues)
.paddingInner(1)
.paddingOuter(.5)
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))

// Show the Y scale
var y = d3.scaleLinear()
.domain([valueMin * 0.95, valueMax * 1.05])
.range([height, 0])
svg.append("g").call(d3.axisLeft(y))

// Show the main vertical line
svg
.selectAll("vertLines")
.data(sumstat)
.enter()
.append("line")
  .attr("x1", function(d){return(x(d.key))})
  .attr("x2", function(d){return(x(d.key))})
  .attr("y1", function(d){return(y(d.value.min))})
  .attr("y2", function(d){return(y(d.value.max))})
  .attr("stroke", "black")
  .style("width", 40)

// rectangle for the main box
var boxWidth = $BOX_WIDTH
svg
.selectAll("boxes")
.data(sumstat)
.enter()
.append("rect")
    .attr("x", function(d){return(x(d.key)-boxWidth/2)})
    .attr("y", function(d){return(y(d.value.q3))})
    .attr("height", function(d){return(y(d.value.q1)-y(d.value.q3))})
    .attr("width", boxWidth )
    .attr("stroke", "black")
    .style("fill", "#69b3a2")

// Show the median
svg
.selectAll("medianLines")
.data(sumstat)
.enter()
.append("line")
  .attr("x1", function(d){return(x(d.key)-boxWidth/2) })
  .attr("x2", function(d){return(x(d.key)+boxWidth/2) })
  .attr("y1", function(d){return(y(d.value.median))})
  .attr("y2", function(d){return(y(d.value.median))})
  .attr("stroke", "white")
  .style("width", 80)
END

#============================================================
# Box-Whisker code accessors
#============================================================

our sub GetBoxWhiskerChartPart(Bool :$horizontal=False) {
    my $res = [
        GetTooltipPart(),
        $jsBoxWhiskerPlotBoxData,
        $horizontal ?? $jsHorizonalBoxWhiskerPlot !! $jsVerticalBoxWhiskerPlot
    ].join("\n\n");

    my $marker = '// Trigger the tooltip functions';
    $res .= subst($marker, $marker ~ "\n" ~ GetTooltipMousePart());

    return $res;
}

our sub GetMultiBoxWhiskerChartPart() {
    return $jsMultiBoxWhiskerPlot;
}

#============================================================
# Linear Gradient code snippets
#============================================================

my $jsLinearGradient = q:to/END/;
// Set the gradient
svg.append("linearGradient")
  .attr("id", "line-gradient")
  .attr("gradientUnits", "userSpaceOnUse")
  .attr("x1", $LINEAR_GRADIENT_MIN_X)
  .attr("y1", $LINEAR_GRADIENT_MIN_Y)
  .attr("x2", $LINEAR_GRADIENT_MAX_X)
  .attr("y2", $LINEAR_GRADIENT_MAX_Y)
  .selectAll("stop")
    .data([
      {offset: "0%", color: "$LINEAR_GRADIENT_COLOR_0"},
      {offset: "100%", color: "$LINEAR_GRADIENT_COLOR_100"}
    ])
  .enter().append("stop")
    .attr("offset", function(d) { return d.offset; })
    .attr("stop-color", function(d) { return d.color; });
END

#============================================================
# Linear Gradient accessors
#============================================================
our sub GetLinearGradientCode(:$minX='x(xMin)', :$maxX='x(xMax)',
                              :$minY='y(yMin)', :$maxY='y(yMax)',
                              :$color0 = 'blue', :$color100 = 'red') {
    return
            $jsLinearGradient
            .subst('$LINEAR_GRADIENT_MIN_X', $minX)
            .subst('$LINEAR_GRADIENT_MAX_X', $maxX)
            .subst('$LINEAR_GRADIENT_MIN_Y', $minY)
            .subst('$LINEAR_GRADIENT_MAX_Y', $maxY)
            .subst('$LINEAR_GRADIENT_COLOR_0', $color0)
            .subst('$LINEAR_GRADIENT_COLOR_100', $color100);
}

#============================================================
# BarChart code snippets
#============================================================
# See https://d3-graph-gallery.com/graph/barplot_basic.html

my $jsBarChartPart = q:to/END/;
// Obtain data
var data = $DATA

var valueMin = Math.min.apply(Math, data.map(function(o) { return o.y; }))
var valueMax = Math.max.apply(Math, data.map(function(o) { return o.y; }))

// X axis
var x = d3.scaleBand()
  .range([ 0, width ])
  .domain(data.map(function(d) { return d.x; }))
  .padding(0.2);
svg.append("g")
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x))
  .selectAll("text")
    .attr("transform", "translate(-10,0)rotate(-45)")
    .style("text-anchor", "end");

// Add Y axis
var y = d3.scaleLinear()
  .domain([0, valueMax])
  .range([ height, 0]);
svg.append("g")
  .call(d3.axisLeft(y));

// Bars
svg.selectAll("mybar")
  .data(data)
  .enter()
  .append("rect")
    .attr("x", function(d) { return x(d.x); })
    .attr("y", function(d) { return y(d.y); })
    .attr("width", x.bandwidth())
    .attr("height", function(d) { return height - y(d.y); })
    .attr("fill", $FILL_COLOR)
END

my $jsBarChartHorizontalPart = q:to/END/;
// Obtain data
var data = $DATA

var valueMin = Math.min.apply(Math, data.map(function(o) { return o.y; }))
var valueMax = Math.max.apply(Math, data.map(function(o) { return o.y; }))

// Y axis
var y = d3.scaleBand()
  .range([0, height])
  .domain(data.map(function(d) { return d.x; }))
  .padding(0.2);

svg.append("g")
  .call(d3.axisLeft(y))
  .selectAll("text")
    .attr("transform", "translate(-10,0)")
    .style("text-anchor", "end");

// Add X axis
var x = d3.scaleLinear()
  .domain([0, valueMax])
  .range([0, width]);

svg.append("g")
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x));

// Bars
svg.selectAll("mybar")
  .data(data)
  .enter()
  .append("rect")
    .attr("x", function(d) { return 0; })
    .attr("y", function(d) { return y(d.x); })
    .attr("height", y.bandwidth())
    .attr("width", function(d) { return x(d.y); })
    .attr("fill", $FILL_COLOR)
END

my $jsMultiBarChartPart = q:to/END/;
// Obtain data
var data = $DATA

svg = svg.attr("transform", `translate(${margin.left},${margin.top})`);

const x0 = d3.scaleBand()
    .domain(data.map(d => d.x))
    .range([0, width])
    .paddingInner(0.1);

const x1 = d3.scaleBand()
    .domain(data.map(d => d.group))
    .range([0, x0.bandwidth()])
