Public
Edited
Apr 10, 2024
Insert cell
Insert cell
Insert cell
EVchargingData = FileAttachment("stationAggYearDaily2011_2020@2.csv").csv({typed: true})
Insert cell
Insert cell
points = EVchargingData.map((d) => [x(d.StartDate), y(d.counts), d["Station Name"]]);
Insert cell
groups = d3.rollup(points, v => Object.assign(v, {z: v[0][2]}), d => d[2]);
Insert cell
Insert cell
width
Insert cell
height = 600;
Insert cell
margins = ({
marginTop: 20,
marginRight: 20,
marginBottom: 30,
marginLeft: 30
})
Insert cell
Insert cell
x = d3.scaleUtc()
.domain(d3.extent(EVchargingData, d => d.StartDate))
.range([margins.marginLeft, width - margins.marginRight]);
Insert cell
y = d3.scaleLinear()
.domain([0, d3.max(EVchargingData, d => d.counts)]).nice()
.range([height - margins.marginBottom, margins.marginTop]);
Insert cell
Insert cell
baseChart = {
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; overflow: visible; font: 10px sans-serif;");

// Add the horizontal axis.
svg.append("g")
.attr("transform", `translate(0, ${height - margins.marginBottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0));

// Add the vertical axis.
svg.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("↑ Counts"))

return svg.node()

}
Insert cell
Insert cell
lineChart = {
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; overflow: visible; font: 10px sans-serif;");

// Add the horizontal axis.
svg.append("g")
.attr("transform", `translate(0, ${height - margins.marginBottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0));

// Add the vertical axis.
svg.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("↑ Counts"))

// Draw the lines.
const line = d3.line();
const path = svg.append("g")
.attr("fill", "none")
.attr("stroke", "teal")
.attr("stroke-width", 1)
.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)

return svg.node()

}
Insert cell
Insert cell
tooltipChart = {
// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; overflow: visible; font: 10px sans-serif;");

// Add the horizontal axis.
svg.append("g")
.attr("transform", `translate(0, ${height - margins.marginBottom})`)
.call(d3.axisBottom(x).ticks(width / 80).tickSizeOuter(0));

// Add the vertical axis.
svg.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("↑ Counts"))

// 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());

return svg.node();

// 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);
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});
}

}
Insert cell
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
viewof panningRate = Inputs.range([1, 10], {step: 1})
Insert cell
viewof options = Inputs.select(new Map([["Number of charging events", "counts"], ["Energy (kWh)", "Energy (kWh)"], ["GHG Savings (kg)", "GHG Savings (kg)"] ,["Gasoline Savings (gallons)", "Gasoline Savings (gallons)"]]), {
value: "Energy (kWh)",
label: "See different options"
})
Insert cell
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]);

// Create a div that holds two svg elements: one for the main chart and horizontal axis,
// which moves as the user scrolls the content; the other for the vertical axis (which
// doesn’t scroll).
const parent = d3.create("div");

// Create the svg with the vertical axis.
const totalSVG = parent.append("svg")
.attr("width", width)
.attr("height", height)
.style("position", "absolute")
.style("pointer-events", "none")
.style("z-index", 1)

// Add the vertical axis.
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});
}

}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more