Public
Edited
Jul 29, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
datayearly = FileAttachment("stationAggYearly2011_2020.csv").csv({typed: true})
Insert cell
dataMonthly = FileAttachment("stationAggYearMonthly2011_2020.csv").csv({typed: true})
Insert cell
dataDaily = FileAttachment("stationAggDaytoDay2011_2020May12.csv").csv({typed: true})
Insert cell
stations = [...new Set(datayearly.map(x => x["Station Name"]))]
Insert cell
data = [datayearly, dataMonthly, dataDaily]
Insert cell
Insert cell
viewof aggToggle1 = Inputs.toggle({label: "Overlay station average", value: true})
Insert cell
viewof panningRate1 = Inputs.range([1, 30], {label: "Panning rate", step: 1, value: 1})
Insert cell
viewof options1 = Inputs.radio(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: "Variable"
})
Insert cell
viewof rollingAverage1 = Inputs.radio(new Map([["daily", "StartDate"], ["monthly", "x"], ["yearly", "StartYear"]]), {value: "x", label: "Rolling average"})
Insert cell
viewof curve1 = Inputs.radio(["curveLinear", "curveCardinal", "curveStep"], {value: "curveStep", label: "Curve type"})
Insert cell
Insert cell
height = 600;
Insert cell
totalWidth = width * panningRate1;
Insert cell
margins = ({
marginTop: 20,
marginRight: 20,
marginBottom: 30,
marginLeft: 50
})
Insert cell
Insert cell
scalex = rollingAverage == "StartDate"? d3.scaleUtc()
.domain(d3.extent(dataDaily, d => d.StartDate))
.range([margins.marginLeft, totalWidth - margins.marginRight]): d3.scaleLinear()
.domain(d3.extent(data[averageCall(rollingAverage)], d => d[rollingAverage]))
.range([margins.marginLeft, totalWidth - margins.marginRight]);
Insert cell
scaley = d3.scaleLinear()
.domain([0, d3.max(data[averageCall(rollingAverage)], d => d[options])]).nice()
.range([height - margins.marginBottom, margins.marginTop]);
Insert cell
Insert cell
figureChart = {

const highlightColor = "#47A8BD"
const unhighlightColor = "#ddd"
const AVGcolor = "#d62728"
const stationColor = "yellow"

// Data processing
const points = data[averageCall(rollingAverage)].map((d) => [scalex(d[rollingAverage]), scaley(d[options]), d["Station Name"]]);
const groups = d3.rollup(points, v => Object.assign(v, {z: v[0][2]}), d => d[2]);

const rollupEVdata = Array.from(d3.rollup(data[averageCall(rollingAverage)], v => d3.mean(v, d => d[options]), d => d[rollingAverage]), ([rollingAverage, mean]) => ({rollingAverage, mean})).sort((a, b) => d3.ascending(a.rollingAverage, b.rollingAverage))

// 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(scaley))
.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")

// Add the horizontal axis.
const xAxis = svg.append("g")
.attr("transform", `translate(0, ${height - margins.marginBottom})`)
.call(rollingAverage == "StartDate"? d3.axisBottom(scalex).ticks(width / 40).tickSizeOuter(0): d3.axisBottom(scalex).ticks(width / 100).tickSizeOuter(0).tickFormat(d3.format(".0f")))

// Draw the lines.
const line = d3.line().curve(d3[curve])
const path = svg.append("g")
.attr("fill", "none")
.attr("stroke", aggToggle ? unhighlightColor : highlightColor)
.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);

// Draw the aggregate line
const lineAgg = d3.line().curve(d3[curve])
.x(d => scalex(d.rollingAverage))
.y(d => scaley(d.mean))

// Append a path for the line.
const aggPath = svg.append("path")
.attr("fill", "none")
.attr("stroke", AVGcolor)
.attr("stroke-width", aggToggle ? 2 : 0)
.attr("d", lineAgg(rollupEVdata))
.attr("opacity", 0.8)

// 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 ? highlightColor : unhighlightColor).filter(({z}) => z === k).raise();
dot.attr("transform", `translate(${x},${y})`);
dot.select("text").text(k).attr("font-size", 12).attr("font-weight", 5)
svg.property("value", data[averageCall("year")][i]).dispatch("input", {bubbles: true});
}

function pointerentered() {
path.style("mix-blend-mode", null).style("stroke", unhighlightColor);
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
Insert cell
Insert cell
Insert cell
Insert cell
import {toc} from "@jonfroehlich/collapsible-toc"
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