Public
Edited
Feb 19, 2023
Importers
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function CalendarGrid(data, {
// X AXIS
x = (d) => d, // Given d in data, returns the (temporal) x-value
xUnit = d3.utcDay, // Provide a different unit eg. d3.utcMonth, d3.utcDay
xFormat = d3.utcFormat('%d %b'), // Provide a format function for your dates.
xScale = undefined, // Provide your own d3.scaleTime based scale for the x axis.
xDomain = undefined, // Provide your own [start date, end date] for the x axis.
// Y AXIS
y = (d) => undefined, // Given d in data, returns the (categorical/ordinal) y-value
yDomain = undefined, // Provide your own y domain to order categories how you would like. Defaults to the unique values returned by y(),
// COLOR
c = (d) => '#000', // Given d in data, returns the value that determines the color of the cell. Assumed to be continuous by default.
cFormat = d3.format('.3s'), // Provide a formatter for your values for the colors.
cScale = undefined, // Provide your own d3 scale for colors, could be d3.scaleOrdinal, d3.scaleDiverging, etc.
// Titles
title = undefined, // Given d in data, return a title
// Other Config
cellSize = 20, // Size of the cells.
cellPadding = 0.1, // Amount of padding between cells.
rx = 2, // Rounding radius for cells
width = 'auto', // Chart width in pixels or 'auto' to have it be determined by the data
height = 'auto', // Chart height or 'auto' to have it be determined by the data
margin = {top: 30, right: 10, bottom: 5, left: 60}, // Standard d3 margin conventions
responsive = true, // Whether to use the viewbox attribute to constrain the chart to the svg size.
} = {}) {

// Get our domains for each dimension.
xDomain = xDomain || d3.extent(data.map(d => x(d)));
const xUnique = xUnit.range(xDomain[0], d3.timeDay.offset(xDomain[1], 1));
const yUnique = yDomain || [... new Set(data.map(d => y(d)))];

// Set up chart width and height.
const contentSize = {
x: margin.left + margin.right + cellSize * (xUnique.length + 1),
y: margin.top + margin.bottom + cellSize * (yUnique.length + 1),
}
if (width === 'auto') width = contentSize.x;
if (height === 'auto') height = contentSize.y;
const viewBox = responsive ? contentSize : {x: width, y: height};
const svg = d3.create('svg').attr("viewBox", `0 0 ${viewBox.x} ${viewBox.y}`).attr('width', width).attr('height', height);
if (xScale === undefined) {
xScale = d3.scaleTime()
.range([margin.left, margin.left + cellSize * xUnique.length])
.domain(xDomain)
}
const yScale = d3.scaleBand()
.range([margin.top, margin.top + cellSize * yUnique.length])
.domain(yUnique)
.round(true)
.padding(cellPadding);

if (cScale === undefined) {
// Assume a symmetric diverging color scale
const cMax = d3.quantile(data.map(d => c(d)), 0.975, Math.abs);
cScale = d3.scaleDiverging().domain([-cMax, 0, cMax]).interpolator(d3.interpolatePiYG);
}
if (title === undefined) {
// Title = x, y, value
title = (d) => `${xFormat(x(d))}, ${y(d)}: ${cFormat(c(d))}`
}
// Draw our cells
const cellsGroup = svg.append('g').attr('id','cells');
cellsGroup.selectAll('g')
.data(data, d => `${x(d)}-${y(d)}`)
.join(
(enter) => {
const g = enter.append('g');
const cell = g.append('rect')
.attr('x', d => xScale(x(d)))
.attr('y', d => yScale(y(d)))
.attr('width', yScale.bandwidth())
.attr('height', yScale.bandwidth())
.attr('rx', rx)
.attr('fill', d=> cScale(c(d)))
cell.append('title').text(title)
return g;
}
)
// Draw the X axis labels.
const xLabelGroup = svg.append('g').attr('id','xLabels')
const xLabels =
xUnit === d3.utcDay ? d3.utcMonday.range(...xDomain) : // Mondays for daily data
xUnit === d3.utcWeek ? d3.utcMonth.range(...xDomain).map(d3.utcMonday) : // First monday of the month
xUnit === d3.utcMonday ? d3.utcMonth.range(...xDomain).map(d3.utcMonday) : // First monday of the month
xUnit === d3.utcMonth ? d3.utcYear.range(...xDomain) : // First monday of the year
xUnit === d3.utcYear ? d3.utcYear.range(...xDomain, 10) : // First monday of the decade
d3.utcYear.range(...xDomain);
xLabelGroup.selectAll('g').data(xLabels)
.join(
(enter) => {
const g = enter.append('g')
g.append('path').attr('d', d => d3.line()([
[xScale(d) + yScale.step() / 2, margin.top - 2],
[xScale(d) + yScale.step() / 2, margin.top - 10]]))
.attr('stroke','black')
;
g.append('text')
.text(d => xFormat(d))
.attr('x', d => xScale(d) + (yScale.step() / 2))
.attr('y', margin.top - 15)
.attr('alignment-baseline', 'bottom')
.attr('text-anchor', 'middle')
.attr('font-family', 'sans-serif')
.attr('font-size', '0.8rem');
return g;
}
)

const yLabelGroup = svg.append('g').attr('id','yLabels');
yLabelGroup.selectAll('text').data(yUnique).join(
(enter) => {
const t = enter.append('text')
.text(d => d)
.attr('x', margin.left - 5)
.attr('y', d => yScale(d) + (yScale.step() / 2))
.attr('alignment-baseline', 'middle')
.attr('text-anchor', 'end')
.attr('font-family', 'sans-serif')
.attr('font-size', '0.8rem');
return t;
}
)

return Object.assign(svg.node(), {scales: {x: xScale, y: yScale, c: cScale}});
}
Insert cell
Insert cell
ucdpPrioAcd221.filter(d => d.intensity_level == 2)
Insert cell
Insert cell
Insert cell
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