Published
Edited
Jul 3, 2022
Importers
Insert cell
Insert cell
dateLinear = (data, axes, palette, [yMin = null, yMax = null] = []) => {
// Margin information around the plot, values in pixels.
const margin = { top: 10, right: 10, bottom: 40, left: 50 };
const w = width - margin.left - margin.right;
const h = 450 - margin.top - margin.bottom;
// w and h are dimensions of plot itself, excluding margins.

// Create wrapper element encompassing plot and margins
const chartBody = document.createElement("div");
chartBody.id = "chartBody";

// Create plot itself. D3 plots are svgs.
const svg = d3.create("svg").attr("viewBox", [0, 0, w, h]);
chartBody.appendChild(svg.node());

// governs if user can select a line on the plot.
let canSelect = true;

/* Make x-scale using d3. Scales are methods that take values and output
the designated location on the svg of that value.
Note: x starts at initial date since all dots start at zero before the
animation moves them to the correct location later.
*/
const x = d3
.scaleTime()
.domain([new Date("2016-01-01"), new Date("2016-01-01")])
.range([margin.left, margin.left]);

// Make xAxis svg element using the x-scale.
const xAxis = d3
.axisBottom(x)
.tickSizeInner(-h * 1.3)
.tickSizeOuter(6)
.tickPadding(10)
.ticks(10);

// Append the xAxis to the plot
svg
.append("g")
.attr("id", "x-axis")
.attr("transform", `translate(0, ${h - margin.bottom})`)
.call(xAxis);

// Append the xAxis titles to plot
svg
.append("text")
.attr("text-anchor", "end")
.attr("x", w)
.attr("y", h - 5)
.text(axes.yTitle);

console.log(yMin);
console.log(d3.max(data, (d) => d.y));

const yExtent = [
yMin == null ? d3.min(data, (d) => d.y) : yMin,
yMax == null ? d3.max(data, (d) => d.y) : yMax
];

console.log(yExtent);

// Make y-scale using d3.
const y = d3
.scaleLinear()
.domain(yExtent)
.range([h - margin.bottom, 0]);

// Make y-axis svg element using the defined y-scale
const yAxis = d3
.axisLeft(y)
.tickSizeInner(-w * 1.3)
.tickSizeOuter(6)
.tickPadding(5)
.ticks(null);

// Append y-axis to plot
svg
.append("g")
.attr("id", "y-axis")
.attr("transform", `translate( ${margin.left}, 0)`)
.call(yAxis)
.call((g) =>
g
.selectAll(".tick line")
.clone()
.attr("stroke-opacity", 0.1)
.attr("x2", w)
);

// Append y-axis title to plot.
svg
.append("text")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("y", margin.left - 35)
.attr("x", -margin.top)
.text(axes.xTitle);

// Create color scale for dots. Scale Ordinal creates a scale with discrete color values (in hex)
const color = d3.scaleOrdinal().domain(palette.label).range(palette.color);

// Create tooltip element and append it to the wrapper element. We don't append to plot since we
// want the tooltip to show up on the margins too.
const tooltip = d3.create("div").attr("id", "tooltip");
chartBody.appendChild(tooltip.node());

// Create legend element using basicLegend import (see basicLegend docs)
const legend = basicLegend(palette.text, palette.label, palette.color);
chartBody.appendChild(legend);

// Create an exit button to exit from selection using d3 createDiv tools. Append to wrapper.
const exitBtn = d3
.create("div")
.attr("id", "exitBtn")
.attr(
"style",
"position: absolute; top: 10px; right: 0px; width: 20px; height: 20px; border-radius: 2px; background-color: #eee; display: flex; justify-content: center; align-items: center; cursor: pointer;"
)
.text("x")
.style("visibility", "hidden")
.on("click", () => {
exitBtn.style("visibility", "visible");
deHighlightChart();
resetBounds();
})
.on("mouseover", (d) => {
tooltip.style("position", "fixed");
tooltip.style("visibility", "visible");
})
.on("mouseleave", (d) => {
tooltip.style("position", "absolute");
tooltip.style("visibility", "hidden");
})
.on("mousemove", (d) => {
tooltip
.html(`Clear selection`)
.style("left", d.clientX + 10 + "px")
.style("top", d.clientY + 10 + "px");
});
chartBody.appendChild(exitBtn.node());

// Create a clipPath so dots and lines aren't draw outside of axises.
const clip = svg
.append("defs")
.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", w + 5)
.attr("height", h + 10)
.attr("x", margin.left - 5)
.attr("y", 0);

// Create a d3 line-drawer function. This takes values and draws a line with each value's
// respective x-y positions.
const line = d3
.line()
.curve(d3.curveBasis)
.x((d) => x(d.x))
.y((d) => y(d.rolling));

// Create a group tied to the clipPath to draw scatterplot points.
const dotGroup = svg.append("g").attr("clip-path", "url(#clip)");

/* Draw points on scatterplot based on y-values. All x-positions are initially
set to zero to start off the animation. The animation will move them to their
correct locations. */
dotGroup
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", (d) => "dot-" + d.color)
.attr("cx", margin.left)
.attr("cy", (d) => y(d.y))
.attr("r", 3)
.style("fill", (d) => color(d.color))
.style("opacity", 0.5);

var idleTimeout;
function idled() {
idleTimeout = null;
}

// Create brush that tracks user selection. After user finishes selection,
// call updateChart, which zooms in on selected location.
const brush = d3
.brushX()
.extent([
[margin.left, 0],
[w, h - margin.bottom]
])
.on("end", updateChart);

// Add brush to dotGroup.
dotGroup.append("g").attr("class", "brush").call(brush);

/* !!! Begin initial Animation. !!! */

// Expand x-axis to correct size with an ease-in transition
x.domain(d3.extent(data, (d) => d.x)).range([margin.left, w]);
svg
.select("#x-axis")
.transition("opacity")
.duration(2000)
.attr("opacity", "1")
.call(xAxis);

// Assign correct x-position values for all points with move-in transition
dotGroup
.selectAll("circle")
.transition("xmovement")
.delay((d, i) => i * 3)
.duration(2000)
.attr("cx", (d) => x(d.x));

/* !!! End initial Animation. !!! */

// Create a group tied to clipPath to draw lines.
const lineGroup = svg.append("g").attr("clip-path", "url(#clip)");

/* Draw lines after initial animation is done. Assign mouse events, including
category selection and opacity/color stylization. Aslo assign mouse events tied
to the tooltip.
preHighlightChart -> preview highlight of category (turns all lines not highlighted
to grey)
highlightChart -> remove all other lines and dots, zooming in on current selected line.

deHighlightChart -> deselects current category and returns all other lines */
console.log(d3.group(data, (d) => d.color));
lineGroup
.selectAll("path")
.data(d3.group(data, (d) => d.color))
.enter()
.append("path")
.attr("id", (d) => "line-" + d[0])
.attr("fill", "none")
.attr("stroke", (d) => color(d[0]))
.attr("stroke-width", 3)
.attr("d", (d) => {
return line(d[1]);
})
.attr("opacity", 0)
.on("mouseover", (d) => {
preHighlightChart(d);
tooltip.style("opacity", 1);
})
.on("mouseout", () => {
deHighlightChart();
tooltip.style("opacity", 0);
})
.on("mousemove", (event, d) => {
tooltip
.attr(
"style",
`position: absolute; left: ${event.x}px; top: ${event.offsetY}px; background-color: #eee;`
)
.text(event.target.id.replace("line-", ""));
})
.on("click", highlightChart);

// Assign mouse-event to reset chart completely upon user double click
svg.on("dblclick", () => {
deHighlightChart();
resetBounds();
});

// Assign mouse-event to reset chart completely upon user click on exit button
exitBtn.on("click", () => {
deHighlightChart();
resetBounds();
});

// Fade in lineGroup after initial Animation is done
lineGroup
.selectAll("path")
.transition("fadeIn")
.delay(2900)
.duration(500)
.attr("opacity", 1);

d3.selectAll(".overlay").on("mouseover", deHighlightChart);

// Stylization of axes, removing unnecessary lines from default style.
svg.selectAll(".tick line").attr("stroke", "#EBEBEB");
svg.selectAll(".domain").remove();

/**
* Internal function that deselects any selected category and returns lines
* back to their respective colors-- NOT VISIBILITIES. Meant to be used in counter
* to preHighlightChart.
*/
function deHighlightChart() {
canSelect = true;
dotGroup
.selectAll("circle")
.transition("recolorDots")
.duration(100)
.style("fill", (d) => color(d.color))
.style("opacity", 1)
.attr("r", 3);

lineGroup
.selectAll("path")
.transition("recolorLines")
.duration(100)
.style("opacity", 1)
.attr("stroke-width", 3)
.style("stroke", (d) => color(d[0]));
}

/**
* Internal function that deselects any category and returns lines to their
* respective visibilities-- NOT COLORS. Meant to be used in counter to
* highlightChart.
*/
function resetBounds() {
canSelect = true;

exitBtn.style("visibility", "hidden");
dotGroup
.selectAll("circle")
.transition()
.duration(200)
.delay(300)
.style("opacity", 1)
.style("visibility", "visible");

lineGroup
.selectAll("path")
.transition()
.duration(200)
.delay(300)
.style("opacity", 1)
.style("visibility", "visible");
x.domain(d3.extent(data, (d) => d.x));
y.domain(d3.extent(data, (d) => d.y));

updateMaterials();
}

/**
* Internal function that highlights current line and greys out all other lines.
* Meant to be used as a hover preview.
* @param d Info object of line element that was hovered on
*/
function preHighlightChart(d) {
let selected = d.target.id.replace("line-", "");
let transDur = 70;

dotGroup
.selectAll("circle")
.transition()
.duration(transDur)
.style("fill", "lightgrey")
.attr("r", 3);

svg
.selectAll(".dot-" + selected)
.transition()
.duration(transDur)
.style("fill", color(selected))
.attr("r", 5);

lineGroup
.selectAll("path")
.transition()
.duration(transDur)
.style("stroke", "lightgrey");

svg
.select("#line-" + selected)
.transition()
.duration(transDur)
.style("stroke", (d) => color(d[0]))
.attr("stroke-width", 4);
}

/**
* Internal function that zooms in on current line and disappears all other lines.
* Meant to be used as a selection.
* @param d Info object of line element that was clicked on.
*/
function highlightChart(d) {
if (canSelect) {
canSelect = false;
deHighlightChart();
let selected = d.target.id.replace("line-", "");
let transDur = 250;

exitBtn.style("opacity", 1);

dotGroup
.selectAll(`circle:not(.dot-${selected})`)
.transition("hideout")
.duration(transDur)
.style("opacity", 0)
.style("visibility", "hidden");

lineGroup
.selectAll(`path:not(#line-${selected})`)
.transition("hideout")
.duration(transDur)
.style("opacity", 0)
.style("visibility", "hidden");

y.domain(
d3.extent(
data.filter((d) => d.color == selected),
(d) => d.y
)
);

updateMaterials();
}
}

/**
* Internal function that updates the Chart upon user selection with
* the brush tool.
*/
function updateChart() {
const extent = d3.brushSelection(this);
deHighlightChart();

// Reset to initial bounds if timeout or selection was zero. Otherwise, zoom to
// specified x boundaries.
if (!extent) {
if (!idleTimeout) return (idleTimeout = setTimeout(idled, 350));
x.domain(d3.extent(data, (d) => d.x));
} else {
x.domain([x.invert(extent[0]), x.invert(extent[1])]);
dotGroup.select(".brush").call(brush.move, null);
}
updateMaterials();
}

/**
* Internal function that updates all changes made programmatically to
* the visualized graph, with a small transition.
*/
function updateMaterials() {
svg.select("#x-axis").transition().duration(1000).call(xAxis);
svg.select("#y-axis").transition().duration(1000).call(yAxis);
svg.selectAll(".tick line").attr("stroke", "#EBEBEB");
svg.selectAll(".domain").remove();
dotGroup
.selectAll("circle")
.transition("xmovement")
.duration(1000)
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => y(d.y));

lineGroup
.selectAll("path")
.transition("pathmovement")
.duration(1000)
.attr("d", (d) => line(d[1]));
}

return chartBody;
}
Insert cell
Insert cell
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