### ----------------------------------------------------
### -- JavaScript::D3
### -- Licenses: Artistic-2.0
### -- Authors: Anton Antonov
### -- File: lib/JavaScript/D3/CodeSnippets.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 3
    .padding(0.05);

const y = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.y)])
    .nice()
    .range([height, 0]);

const myColor = d3.scaleOrdinal()
    .domain(data.map(d => d.group))
    .range(d3.$COLOR_SCHEME);

svg.append("g")
    .call(d3.axisLeft(y));

svg.append("g")
    .selectAll("g")
    .data(d3.group(data, d => d.x))
    .join("g")
    .attr("transform", d => `translate(${x0(d[0])},0)`)
    .selectAll("rect")
    .data(d => d[1])
    .join("rect")
    .attr("x", d => x1(d.group))
    .attr("y", d => y(d.y))
    .attr("width", x1.bandwidth())
    .attr("height", d => height - y(d.y))
    .attr("fill", d => myColor(d.group));

svg.append("g")
    .attr("transform", `translate(0,${height})`)
    .call(d3.axisBottom(x0).tickSizeOuter(0))
    .append("text")
    .attr("x", width / 2)
    .attr("y", margin.bottom - 10)
    .attr("fill", "#000")
    .attr("text-anchor", "middle")
    //.text("X Axis");
END

my $jsBarChartLabelsPart = q:to/BARCHART-PLOT-LABELS/;
svg.selectAll("mybar")
  .data(data)
  .enter()
  .append("text")
    .text(function(d) {return d.label})
    .attr("x", function(d){return x(d.x) + x.bandwidth()/2 })
    .attr("y", function(d){return y(d.y) - $PLOT_LABELS_Y_OFFSET})
    .style("fill", $PLOT_LABELS_COLOR)
    .style("stroke-width", "1px")
    .style("font-size", $PLOT_LABELS_FONT_SIZE)
    .attr("font-family", "$PLOT_LABELS_FONT_FAMILY")
    .attr("text-anchor", "middle")
BARCHART-PLOT-LABELS

my $jsBarChartHorizontalLabelsPart = q:to/BARCHARTHOR-PLOT-LABELS/;
var hy = y.bandwidth() * 0.65;

svg.selectAll("mybar")
  .data(data)
  .enter()
  .append("text")
    .text(function(d) {return d.label})
    .attr("x", function(d){return x(d.y) + $PLOT_LABELS_Y_OFFSET })
    .attr("y", function(d){return y(d.x) + hy})
    .style("fill", $PLOT_LABELS_COLOR)
    .style("stroke-width", "1px")
    .style("font-size", $PLOT_LABELS_FONT_SIZE)
    .attr("font-family", "$PLOT_LABELS_FONT_FAMILY")
    .attr("text-anchor", "left")
BARCHARTHOR-PLOT-LABELS

#============================================================
# BarChart code snippets accessors
#============================================================

our sub GetBarChartPart(Bool :$horizontal = False) {
    return $horizontal ?? $jsBarChartHorizontalPart !! $jsBarChartPart;
}

our sub GetMultiBarChartPart() {
    return $jsMultiBarChartPart;
}

our sub GetBarChartLabelsPart(Bool :$horizontal = False) {
    return $horizontal ?? $jsBarChartHorizontalLabelsPart !! $jsBarChartLabelsPart;
}

#============================================================
# Histogram code snippets
#============================================================
# See https://d3-graph-gallery.com/graph/histogram_basic.html
my $jsHistogramPart = q:to/END/;
// Obtain data
var data = $DATA

var valueMin = Math.min.apply(Math, data)
var valueMax = Math.max.apply(Math, data)

// X axis: scale and draw:
var x = d3.scaleLinear()
      .domain([valueMin, valueMax])
      .range([0, width]);
svg.append("g")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(x));

// set the parameters for the histogram
var histogram = d3.histogram()
      .value(function(d) { return d; })  // I need to give the vector of value
      .domain(x.domain())  // then the domain of the graphic
      .thresholds(x.ticks($NUMBER_OF_BINS)); // then the numbers of bins

// And apply this function to data to get the bins
var bins = histogram(data);

// Y axis: scale and draw:
var y = d3.scaleLinear()
      .range([height, 0]);
      y.domain([0, d3.max(bins, function(d) { return d.length; })]);   // d3.hist has to be called before the Y axis obviously
svg.append("g")
      .call(d3.axisLeft(y));

// append the bar rectangles to the svg element
svg.selectAll("rect")
      .data(bins)
      .enter()
      .append("rect")
        .attr("x", 1)
        .attr("transform", function(d) { return "translate(" + x(d.x0) + "," + y(d.length) + ")"; })
        .attr("width", function(d) { return x(d.x1) - x(d.x0) -1 ; })
        .attr("height", function(d) { return height - y(d.length); })
        .style("fill", $FILL_COLOR)
END

#============================================================
# Histogram code snippets accessors
#============================================================

our sub GetHistogramPart() {
    return $jsHistogramPart;
}

#============================================================
# BubbleChart code snippets
#============================================================

# See https://d3-graph-gallery.com/graph/bubble_basic.html
my $jsBubbleChartPart = q:to/END/;
var zMin = Math.min.apply(Math, data.map(function(o) { return o.z; }))
var zMax = Math.max.apply(Math, data.map(function(o) { return o.z; }))

// Add a scale for bubble size
const z = d3.scaleLinear()
    .domain([zMin, zMax])
    .range([$Z_RANGE_MIN, $Z_RANGE_MAX]);

// Add dots
svg.append('g')
    .selectAll("dot")
    .data(data)
    .join("circle")
      .attr("cx", d => x(d.x))
      .attr("cy", d => y(d.y))
      .attr("r",  d => z(d.z))
      .style("fill", $FILL_COLOR)
      .style("opacity", "0.7")
      .attr("stroke", "black")
END

# See https://d3-graph-gallery.com/graph/bubble_color.html
my $jsMultiBubbleChartPart = q:to/END/;
var zMin = Math.min.apply(Math, data.map(function(o) { return o.z; }))
var zMax = Math.max.apply(Math, data.map(function(o) { return o.z; }))

// Add a scale for bubble size
const z = d3.scaleLinear()
    .domain([zMin, zMax])
    .range([$Z_RANGE_MIN, $Z_RANGE_MAX]);

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

// Add dots
svg.append('g')
    .selectAll("dot")
    .data(data)
    .join("circle")
      .attr("cx", d => x(d.x))
      .attr("cy", d => y(d.y))
      .attr("r",  d => z(d.z))
      .style("fill", function (d) { return myColor(d.group); } )
      .style("opacity", $OPACITY)
      .attr("stroke", $STROKE_COLOR)
    // Trigger the tooltip functions
END

#============================================================
# BubbleChart code snippets accessors
#============================================================

our sub GetBubbleChartPart() {
    return $jsBubbleChartPart;
}

our sub GetMultiBubbleChartPart() {
    return $jsMultiBubbleChartPart;
}

our sub GetTooltipMultiBubbleChartPart() {
    #return $jsTooltipMultiBubbleChartPart;
    my $res =  GetTooltipPart() ~ "\n\n" ~ GetMultiBubbleChartPart();
    my $marker = '// Trigger the tooltip functions';
    $res .= subst($marker, $marker ~ "\n" ~ GetTooltipMousePart());
    return $res;
}

#============================================================
# Bind2D code snippets
#============================================================

# See https://d3-graph-gallery.com/graph/density2d_hexbin.html
my $jsHexbinChartPart = q:to/END/;
  // Reformat the data: d3.hexbin() needs a specific format
  const inputForHexbinFun = []
  data.forEach(function(d) {
    inputForHexbinFun.push( [x(d.x), y(d.y)] )  // Note that we had the transform value of X and Y !
  })

  // Prepare a color palette
  const color = d3.scaleLinear()
      .domain([0, 500]) // Number of points in the bin?
      .range(["transparent",  "#69b3a2"])

  // Compute the hexbin data
  const hexbin = d3.hexbin()
    .radius(9) // size of the bin in px
    .extent([ [0, 0], [width, height] ])

  // Plot the hexbins
  svg.append("clipPath")
      .attr("id", "clip")
    .append("rect")
      .attr("width", width)
      .attr("height", height)

  svg.append("g")
    .attr("clip-path", "url(#clip)")
    .selectAll("path")
    .data( hexbin(inputForHexbinFun) )
    .join("path")
      .attr("d", hexbin.hexagon())
      .attr("transform", function(d) { return `translate(${d.x}, ${d.y})`})
      .attr("fill", function(d) { return color(d.length); })
      .attr("stroke", "black")
      .attr("stroke-width", "0.1")

END

my $jsRectbinChartPart = q:to/END/;
  // Reformat the data: d3.rectbin() needs a specific format
  var inputForRectBinning = []
  data.forEach(function(d) {
    inputForRectBinning.push( [+d.x, +d.y] )  // Note that we had the transform value of X and Y !
  })

  // Compute the rectbin
  var size = 0.5
  var rectbinData = d3.rectbin()
    .dx(size)
    .dy(size)
    (inputForRectBinning)

  // Prepare a color palette
  var color = d3.scaleLinear()
      .domain([0, 350]) // Number of points in the bin?
      .range(["transparent",  "#69a3b2"])

  // What is the height of a square in px?
  heightInPx = y( yLim[1]-size )

  // What is the width of a square in px?
  var widthInPx = x(xLim[0]+size)

  // Now we can add the squares
  svg.append("clipPath")
      .attr("id", "clip")
    .append("rect")
      .attr("width", width)
      .attr("height", height)
  svg.append("g")
      .attr("clip-path", "url(#clip)")
    .selectAll("myRect")
    .data(rectbinData)
    .enter().append("rect")
      .attr("x", function(d) { return x(d.x) })
      .attr("y", function(d) { return y(d.y) - heightInPx })
      .attr("width", widthInPx )
      .attr("height", heightInPx )
      .attr("fill", function(d) { return color(d.length); })
      .attr("stroke", "black")
      .attr("stroke-width", "0.4")
END

#============================================================
# Bind2D code snippets accessors
#============================================================

our sub GetHexbinChartPart() {
    return $jsHexbinChartPart;
}

our sub GetRectbinChartPart() {
    return $jsRectbinChartPart;
}

#============================================================
# Image display code snippets
#============================================================

my $jsImageDisplayPart = q:to/END/;
const imagePath = '$IMAGE_PATH'
d3.select(element.get(0)).append('img').attr("src", imagePath).attr("width", "$WIDTH").attr("height", "$HEIGHT")
END

#============================================================
# Image display code snippets accessors
#============================================================

our sub GetImageDisplayPart() {
    return $jsImageDisplayPart;
}

#============================================================
# Image code snippets
#============================================================

my $jsImagePart = q:to/END/;
const color = d3.scaleSequential([$LOW_VALUE, $HIGH_VALUE], d3.interpolate$COLOR_PALETTE)

// Obtain data
var data = $DATA

var n = data.width,
  m = data.height;

var canvas = d3.select("canvas")
  .attr("width", n)
  .attr("height", m);

var context = canvas.node().getContext("2d"),
  image = context.createImageData(n, m);

for (var j = 0, k = 0, l = 0; j < m; ++j) {
    for (var i = 0; i < n; ++i, ++k, l += 4) {
      var c = d3.rgb(color(data.values[k]));
      image.data[l + 0] = c.r;
      image.data[l + 1] = c.g;
      image.data[l + 2] = c.b;
      image.data[l + 3] = 255;
    }
}

context.putImageData(image, 0, 0);
END

#============================================================
# Image code snippets accessors
#============================================================

our sub GetImagePart() {
    return $jsImagePart;
}

#============================================================
# Heatmap code snippets
#============================================================

my $jsTooltipHeatmapPart = q:to/HEATMAP-PART-END/;
// Obtain data
var data = $DATA;

var myGroups = $X_TICK_LABELS;
var myVars = $Y_TICK_LABELS;

if (myGroups.length === 0) {
    myGroups = Array.from(new Set(data.map(d => d.x)));
}
if (myVars.length === 0) {
    myVars = Array.from(new Set(data.map(d => d.y)));
}

if ($SORT_TICK_LABELS) {
    myGroups = myGroups.sort(d3.ascending);
    myVars = myVars.sort(d3.ascending);
}

// Build X scales and axis:
var x = d3.scaleBand()
    .range([0, width])
    .domain(myGroups)
    .padding(0.05);

svg.append("g")
    .style("font-size", $TICK_LABELS_FONT_SIZE)
    .style("stroke", $TICK_LABELS_COLOR)
    .style("stroke-width", "1px")
    .attr("font-family", $TICK_LABELS_FONT_FAMILY)
    .attr("transform", `translate(0, ${height})`)
    .call(d3.axisBottom(x).tickSize(0))
    .select(".domain").remove();

// Build Y scales and axis:
var y = d3.scaleBand()
    .range([height, 0])
    .domain(myVars)
    .padding(0.05);

svg.append("g")
    .style("font-size", $TICK_LABELS_FONT_SIZE)
    .style("stroke", $TICK_LABELS_COLOR)
    .style("stroke-width", "1px")
    .attr("font-family", $TICK_LABELS_FONT_FAMILY)
    .call(d3.axisLeft(y).tickSize(0))
    .select(".domain").remove();

// Build color scale
var myColor = d3.scaleSequential()
    .interpolator(d3.interpolate$COLOR_PALETTE)
    .domain([$LOW_VALUE, $HIGH_VALUE]);

// tooltip-code-begin
// create a tooltip
var tooltip = d3.select(element.get(0))
    .append("div")
    .style("opacity", 0)
    .attr("class", "tooltip")
    .style("background-color", $TOOLTIP_BACKGROUND_COLOR)
    .style("border", "solid")
    .style("border-width", "2px")
    .style("border-radius", "5px")
    .style("padding", "5px")
    .style("color", $TOOLTIP_COLOR);

// Three functions that change the tooltip when user hover / move / leave a cell
var mouseover = function(event, d) {
    tooltip
        .style("opacity", 1);
    d3.select(this)
        .style("stroke", "black")
        .style("opacity", 1);
};

var mousemove = function(event, d) {
    tooltip
        .html((d.tooltip ? String(d.tooltip) : String(d.z)))
        .style("left", (event.pageX) + "px")
        .style("top", (event.pageY) + "px");
};

var mouseleave = function(event, d) {
    tooltip
        .style("opacity", 0);
    d3.select(this)
        .style("stroke", "none")
        .style("opacity", 0.8);
};
// tooltip-code-end

// add the squares
svg.selectAll()
    .data(data, d => d.x + ':' + d.y)
    .join("rect")
    .attr("x", d => x(d.x))
    .attr("y", d => y(d.y))
    .attr("rx", 4)
    .attr("ry", 4)
    .attr("width", x.bandwidth())
    .attr("height", y.bandwidth())
    .style("fill", d => myColor(d.z))
    .style("stroke-width", 4)
    .style("stroke", "none")
    .style("opacity", $OPACITY)
    .on("mouseover", mouseover)
    .on("mousemove", mousemove)
    .on("mouseleave", mouseleave);

// add the grid
if ( $GRID_LINES) {
    svg.selectAll()
        .data(data, d => d.x + ':' + d.y)
        .join("rect")
        .attr("x", d => x(d.x))
        .attr("y", d => y(d.y))
        .attr("rx", 0)
        .attr("ry", 0)
        .attr("width", x.bandwidth())
        .attr("height", y.bandwidth())
        .style("fill", "none")
        .style("stroke-width", $GRID_LINES_WIDTH)
        .style("stroke", $GRID_LINES_COLOR)
        .style("opacity", 1)
}
HEATMAP-PART-END

#============================================================
# Heatmap code snippets accessors
#============================================================

our sub GetTooltipHeatmapPart() {
    return $jsTooltipHeatmapPart;
}

#============================================================
# Chessboard code snippets
#============================================================

my $jsTooltipHeatmapPlotLabelsPart = q:to/HEATMAP-PLOT-LABELS/;
var plotLabelData = $PLOT_LABELS_DATA

var hx = x.bandwidth()
var hy = y.bandwidth()

svg.selectAll()
.data(plotLabelData, function(d) {return d.x+':'+d.y;})
.join("text")
  .attr("x", function(d) { return x(d.x) + hx/2 })
  .attr("y", function(d) { return y(d.y) + hy/2 + $PLOT_LABELS_Y_OFFSET })
  .attr("text-anchor", "middle")
  .style("alignment-baseline", "middle")
  .style("fill", $PLOT_LABELS_COLOR)
  .style("stroke", $PLOT_LABELS_COLOR)
  .style("stroke-width", "1px")
  .style("font-size", $PLOT_LABELS_FONT_SIZE)
  .attr("font-family", "$PLOT_LABELS_FONT_FAMILY")
  .attr("font-weight", 100)
  .html(function(d){ return d.z });
HEATMAP-PLOT-LABELS

#============================================================
# Chessboard code snippets accessors
#============================================================

our sub GetTooltipHeatmapPlotLabelsPart() {
    return $jsTooltipHeatmapPlotLabelsPart;
}

#============================================================
# Graph-force code snippets
#============================================================
# For the force settings see GitHub gist:
# https://gist.github.com/steveharoz/8c3e2524079a8c440df60c1ab72b5d03

my $jsGraphPart = q:to/GRAPH-END/;
const edges = $DATA;

const nodes = Array.from(new Set(edges.flatMap(e => [e.from, e.to])), id => ({id}));

const highlightSpecs = $HIGHLIGHT_SPEC;

const links = edges.map(e => ({
  source: e.from,
  target: e.to,
  weight: e.weight,
  label: e.label,
}));

//.force("link", d3.forceLink(links).id(d => d.id).distance(d => Math.max(d.weight * 20, $NODE_SIZE * 4)))
const simulation = d3.forceSimulation(nodes)
    .force("link", d3.forceLink(links).id(d => d.id).distance($FORCE_LINK_DISTANCE).iterations($FORCE_LINK_ITER))
    .force("charge", d3.forceManyBody().strength($FORCE_CHARGE_STRENGTH).distanceMin($FORCE_CHARGE_DIST_MIN).distanceMax($FORCE_CHARGE_DIST_MAX))
    .force("x", d3.forceX().strength($FORCE_X_STRENGTH).x($FORCE_X))
    .force("y", d3.forceY().strength($FORCE_Y_STRENGTH).y($FORCE_Y))
    .force("collision", d3.forceCollide().strength($FORCE_COLLIDE_STRENGTH).radius($FORCE_COLLIDE_RADIUS).iterations($FORCE_COLLIDE_ITER))
    .force("center", d3.forceCenter($FORCE_CENTER_X, $FORCE_CENTER_Y));

svg.append('defs').append('marker')
    .attr("id",'arrowhead')
    .attr('viewBox','-0 -5 10 10') //the bound of the SVG viewport for the current SVG fragment. defines a coordinate system 10 wide and 10 high starting on (0,-5)
     .attr('refX', $ARROWHEAD_OFFSET) // x coordinate for the reference point of the marker. If circle is bigger, this need to be bigger.
     .attr('refY',0)
     .attr('orient','auto')
        .attr('markerWidth', $ARROWHEAD_SIZE)
        .attr('markerHeight', $ARROWHEAD_SIZE)
        .attr('xoverflow','visible')
    .append('svg:path')
    .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
    .attr('fill', $LINK_STROKE_COLOR)
    .style('stroke','none');

const link = svg.append("g")
    .attr("class", "links")
  .selectAll("line")
  .data(links)
  .enter().append("line")
    .attr("class", "link")
    .attr("stroke", d => {
        for (const [color, items] of Object.entries(highlightSpecs)) {
            if (items.some(item => Array.isArray(item) && item[0] === d.source.id && item[1] === d.target.id)) {
                return color;
            }
        }
        return $LINK_STROKE_COLOR;
    })
    .attr("stroke-width", $LINK_STROKE_WIDTH)
    .attr('marker-end','url(#arrowhead)')

const node = svg.append("g")
    .attr("class", "nodes")
  .selectAll("circle")
  .data(nodes)
  .enter().append("circle")
    .attr("class", "node")
    .attr("r", $NODE_SIZE)
    .attr("stroke", d => {
       for (const [color, items] of Object.entries(highlightSpecs)) {
         if (items.includes(d.id)) {
            return color;
         }
       }
       return $NODE_STROKE_COLOR;
    })
    .attr("fill", d => {
       for (const [color, items] of Object.entries(highlightSpecs)) {
         if (items.includes(d.id)) {
            return color;
         }
       }
       return $NODE_FILL_COLOR;
    })
    .call(drag(simulation));

node.append("title")
    .text(d => d.id);

const nodeLabel = svg.append("g")
    .attr("class", "node-labels")
  .selectAll("text")
  .data(nodes)
  .enter().append("text")
    .attr("class", "node-label")
    .style("font-size", $NODE_LABEL_FONT_SIZE)
    .attr("font-family", $NODE_LABEL_FONT_FAMILY)
    .attr("font-weight", 100)
    .attr("dy", -10)
    .attr('fill', $NODE_LABEL_STROKE_COLOR)
    .attr('stroke', $NODE_LABEL_STROKE_COLOR)
    .text(d => d.id);

const linkLabel = svg.append("g")
    .attr("class", "link-labels")
  .selectAll("text")
  .data(links)
  .enter().append("text")
    .filter(d => d.label !== "")
    .attr("class", "link-label")
    .style("font-size", $LINK_LABEL_FONT_SIZE)
    .attr("font-family", "Courier")
    .attr('fill', $LINK_LABEL_STROKE_COLOR)
    .attr('stroke', $LINK_LABEL_STROKE_COLOR)
    .text(d => d.label);

simulation.on("tick", () => {
  link
      .attr("x1", d => d.source.x)
      .attr("y1", d => d.source.y)
      .attr("x2", d => d.target.x)
      .attr("y2", d => d.target.y);

  node
      .attr("cx", d => d.x)
      .attr("cy", d => d.y);

  nodeLabel
      .attr("x", d => d.x)
      .attr("y", d => d.y);

  linkLabel
      .attr("x", d => (d.source.x + d.target.x) / 2)
      .attr("y", d => (d.source.y + d.target.y) / 2);
});

function drag(simulation) {
  function dragstarted(event, d) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }

  function dragged(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }

  function dragended(event, d) {
    if (!event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }

  return d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended);
}
GRAPH-END


#============================================================
# Graph code snippets accessors
#============================================================

our sub GetGraphPart() {
    return $jsGraphPart;
}


#============================================================
# Graph-with-coordinates code snippets
#============================================================

my $jsGraphWithCoordsPart = q:to/GRAPH-COORDS-END/;
const edges = $DATA;

const isDirected = $IS_DIRECTED;

const vertexCoordinates = $VERTEX_COORDINATES;

const highlightSpecs = $HIGHLIGHT_SPEC;

const links = edges.map(e => ({
  source: e.from,
  target: e.to,
  weight: e.weight,
  label: e.label
}));

const nodes = Object.keys(vertexCoordinates).map(key => ({
  id: key,
  x: vertexCoordinates[key].x,
  y: vertexCoordinates[key].y
}));

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

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

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

// Y scale and Axis
var yScale = d3.scaleLinear()
    .domain([yMin, yMax])
    .range([height, 0]);

nodes.forEach(node => {
  node.x = xScale(node.x);
  node.y = yScale(node.y);
});

const link = svg.append("g")
    .attr("class", "links")
  .selectAll("line")
  .data(links)
  .enter().append("line")
    .attr("class", "link")
    .attr("stroke", d => {
        for (const [color, items] of Object.entries(highlightSpecs)) {
            if (items.some(item => Array.isArray(item) && item[0] === d.source && item[1] === d.target)) {
                return color;
            }
        }
        return $LINK_STROKE_COLOR;
    })
    .attr("stroke-width", $LINK_STROKE_WIDTH)
    .attr("x1", d => nodes.find(n => n.id === d.source).x)
    .attr("y1", d => nodes.find(n => n.id === d.source).y)
    .attr("x2", d => nodes.find(n => n.id === d.target).x)
    .attr("y2", d => nodes.find(n => n.id === d.target).y);

if (isDirected) {
  link.attr("marker-end", "url(#arrow)");
  svg.append("defs").append("marker")
    .attr("id", "arrow")
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", $ARROWHEAD_OFFSET)
    .attr("refY", 0)
    .attr("markerWidth", $ARROWHEAD_SIZE)
    .attr("markerHeight", $ARROWHEAD_SIZE)
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M0,-5L10,0L0,5")
    .attr("fill", $LINK_STROKE_COLOR);
}

const node = svg.append("g")
    .attr("class", "nodes")
  .selectAll("circle")
  .data(nodes)
  .enter().append("circle")
    .attr("class", "node")
    .attr("r", $NODE_SIZE)
    .attr("cx", d => d.x)
    .attr("cy", d => d.y)
    .attr("stroke", d => {
       for (const [color, items] of Object.entries(highlightSpecs)) {
         if (items.includes(d.id)) {
            return color;
         }
       }
       return $NODE_STROKE_COLOR;
    })
    .attr("fill", d => {
       for (const [color, items] of Object.entries(highlightSpecs)) {
         if (items.includes(d.id)) {
            return color;
         }
       }
       return $NODE_FILL_COLOR;
    })
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended));

node.append("title")
    .text(d => d.id);

const nodeLabel = svg.append("g")
    .attr("class", "node-labels")
  .selectAll("text")
  .data(nodes)
  .enter().append("text")
    .attr("class", "node-label")
    .style("font-size", $NODE_LABEL_FONT_SIZE)
    .attr("font-family", $NODE_LABEL_FONT_FAMILY)
    .attr("font-weight", 100)
    .attr("dy", -10)
    .attr("x", d => d.x)
    .attr("y", d => d.y)
    .attr('fill', $NODE_LABEL_STROKE_COLOR)
    .attr('stroke', $NODE_LABEL_STROKE_COLOR)
    .text(d => d.id);

const linkLabel = svg.append("g")
    .attr("class", "link-labels")
  .selectAll("text")
  .data(links)
  .enter().append("text")
    .filter(d => d.label !== "")
    .attr("class", "link-label")
    .style("font-size", $LINK_LABEL_FONT_SIZE)
    .attr("font-family", $LINK_LABEL_FONT_FAMILY)
    .attr('fill', $LINK_LABEL_STROKE_COLOR)
    .attr('stroke', $LINK_LABEL_STROKE_COLOR)
    .attr("font-weight", 100)
    .attr("dy", -10)
    .attr("x", d => xScale((vertexCoordinates[d.source].x + vertexCoordinates[d.target].x) / 2))
    .attr("y", d => yScale((vertexCoordinates[d.source].y + vertexCoordinates[d.target].y) / 2))
    .text(d => d.label);

function dragstarted(event, d) {
  d3.select(this).raise().attr("stroke", $NODE_STROKE_COLOR);
}

function dragged(event, d) {
  d.x = event.x;
  d.y = event.y;
  d3.select(this).attr("cx", d.x).attr("cy", d.y);
  link
    .attr("x1", l => nodes.find(n => n.id === l.source).x)
    .attr("y1", l => nodes.find(n => n.id === l.source).y)
    .attr("x2", l => nodes.find(n => n.id === l.target).x)
    .attr("y2", l => nodes.find(n => n.id === l.target).y);
}

function dragended(event, d) {
  d3.select(this).attr("stroke", null);
}
GRAPH-COORDS-END

#============================================================
# Graph code snippets accessors
#============================================================

our sub GetGraphWithCoordsPart() {
    return $jsGraphWithCoordsPart;
}

#============================================================
# Mondrian code snippet
#============================================================
my $jsMondrianPart = q:to/MONDRIAN-END/;
const data = $DATA;

const colorScheme = $COLOR_SCHEME;

var color;
if (colorScheme.toLowerCase()  === 'none') {
    color = d3.scaleOrdinal(d3.schemeAccent)
} else {
    color = d3.scaleOrdinal(d3[colorScheme])
};

const weightedColors = $COLOR_PALETTE;

function getRandomWeightedColor() {
    const colors = [];
    for (const [color, weight] of Object.entries(weightedColors)) {
        for (let i = 0; i < weight; i++) {
            colors.push(color);
        }
    }
    return colors[Math.floor(Math.random() * colors.length)];
}

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

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

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

// Y scale and Axis
var yScale = d3.scaleLinear()
    .domain([yMin, yMax])
    .range([0, height]);

svg.selectAll("rect")
    .data(data)
    .enter()
    .append("rect")
    .attr("x", d => xScale(d.x1))
    .attr("y", d => yScale(d.y1))
    .attr("width", d => xScale(d.x2) - xScale(d.x1))
    .attr("height", d => yScale(d.y2) - yScale(d.y1))
    .attr("fill", (d, i) => {
        if (colorScheme.toLowerCase() === 'none') {
            return getRandomWeightedColor();
        } else {
            return color(Math.random());
        }
    })
    .attr("stroke-width", $STROKE_WIDTH)
    .attr("stroke", $STROKE_COLOR);
MONDRIAN-END

#============================================================
# Mondrian code snippet accessor
#============================================================

our sub GetMondrianPart() {
    return $jsMondrianPart;
}

#============================================================
# Clock Gauge snippet
#============================================================

my $jsClockGauge = q:to/GAUGE-CLOCK/;
const radius = Math.min(width, height) / 2;

// Rescaling
svg = svg
    .attr("viewBox", `0 0 ${width} ${height}`)
    .append("g")
    .attr("transform", `translate(${width / 2}, ${height / 2})`);

const hour   = $HOUR;   //new Date().getHours();
const minute = $MINUTE; //new Date().getMinutes();
const second = $SECOND; //new Date().getSeconds();
const updateInterval = $UPDATE_INTERVAL; // Update, say, every second

//const scaleRanges = [[[20, 30], [0, 0.05]], [[30, 45], [0, 0.25]], [[45, 55], [0, 0.25]], [[55, 60], [0, 0.05