pannableChart = {
const totalWidth = width * panningRate;
const x = d3.scaleUtc()
.domain(d3.extent(EVchargingData, d => d.StartDate))
.range([margins.marginLeft, totalWidth - margins.marginRight]);
const y = d3.scaleLinear()
.domain([0, d3.max(EVchargingData, d => d[options])]).nice()
.range([height - margins.marginBottom, margins.marginTop]);
const points = EVchargingData.map((d) => [x(d.StartDate), y(d[options]), d["Station Name"]]);
const groups = d3.rollup(points, v => Object.assign(v, {z: v[0][2]}), d => d[2]);
const parent = d3.create("div");
const totalSVG = parent.append("svg")
.attr("width", width)
.attr("height", height)
.style("position", "absolute")
.style("pointer-events", "none")
.style("z-index", 1)
const yAxis = totalSVG.append("g")
.attr("transform", `translate(${margins.marginLeft},0)`)
.call(d3.axisLeft(y))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - margins.marginLeft - margins.marginRight)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", -margins.marginLeft)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text(`↑ ${options}`))
// Create a scrolling div containing the area shape and the horizontal axis.
const body = parent.append("div")
.style("overflow-x", "scroll")
.style("-webkit-overflow-scrolling", "touch");
const svg = body.append("svg")
.attr("width", totalWidth)
.attr("height", height)
.style("display", "block")
const xAxis = svg.append("g")
.attr("transform", `translate(0, ${height - margins.marginBottom})`)
.call(d3.axisBottom(x).ticks(panningRate > 4? d3.utcMonth.every(1200 / width): width / 80).tickSizeOuter(0));
// Draw the lines.
const line = d3.line();
const path = svg.append("g")
.attr("fill", "none")
.attr("stroke", "teal")
.attr("stroke-width", 0.8)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.selectAll("path")
.data(groups.values())
.join("path")
.style("mix-blend-mode", "multiply")
.attr("d", line)
.attr("opacity", 0.5)
// Add an invisible layer for the interactive tip.
const dot = svg.append("g")
.attr("display", "none");
dot.append("circle")
.attr("r", 2.5);
dot.append("text")
.attr("text-anchor", "middle")
.attr("y", -8);
svg
.on("pointerenter", pointerentered)
.on("pointermove", pointermoved)
.on("pointerleave", pointerleft)
.on("touchstart", event => event.preventDefault());
yield parent.node();
// Initialize the scroll offset after yielding the chart to the DOM.
body.node().scrollBy(totalWidth, 0);
// When the pointer moves, find the closest point, update the interactive tip, and highlight
// the corresponding line. Note: we don't actually use Voronoi here, since an exhaustive search
// is fast enough.
function pointermoved(event) {
const [xm, ym] = d3.pointer(event);
const i = d3.leastIndex(points, ([x, y]) => Math.hypot(x - xm, y - ym));
const [x, y, k] = points[i];
path.style("stroke", ({z}) => z === k ? null : "#ddd").filter(({z}) => z === k).raise();
dot.attr("transform", `translate(${x},${y})`);
dot
.select("text")
.text(k)
.style("font-size", 10)
svg.property("value", EVchargingData[i]).dispatch("input", {bubbles: true});
}
function pointerentered() {
path.style("mix-blend-mode", null).style("stroke", "#ddd");
dot.attr("display", null);
}
function pointerleft() {
path.style("mix-blend-mode", "multiply").style("stroke", null);
dot.attr("display", "none");
svg.node().value = null;
svg.dispatch("input", {bubbles: true});
}
}