I’m using this Observable post to create a calendar heatmap with D3.js. My problem is that the calendar is not appearing once it has been created. I have a demo set up on StackBlitz that is set up as suggested in the blog post. I’m not sure if I missed something in the post or if something isn’t set up properly, but any advice or direction would be greatly appreciated.
index.js
import * as d3 from 'd3'; import dji from './dji.json'; import Calendar from './Calendar'; const chart = Calendar(dji, { x: (d) => d.Date, y: (d, i, data) => { return i > 0 ? (d.Close - data[i - 1].Close) / data[i - 1].Close : NaN; }, // relative change yFormat: '+%', // show percent change on hover weekday: 'weekday', /* width, */ });
Calendar.js
import * as d3 from 'd3'; export default function Calendar( data, { x = ([x]) => x, // given d in data, returns the (temporal) x-value y = ([, y]) => y, // given d in data, returns the (quantitative) y-value title, // given d in data, returns the title text width = 928, // width of the chart, in pixels cellSize = 17, // width and height of an individual day, in pixels weekday = 'monday', // either: weekday, sunday, or monday formatDay = (i) => 'SMTWTFS'[i], // given a day number in [0, 6], the day-of-week label formatMonth = '%b', // format specifier string for months (above the chart) yFormat, // format specifier string for values (in the title) colors = d3.interpolatePiYG, } = {} ) { // Compute values. const X = d3.map(data, x); const Y = d3.map(data, y); const I = d3.range(X.length); const countDay = weekday === 'sunday' ? (i) => i : (i) => (i + 6) % 7; const timeWeek = weekday === 'sunday' ? d3.utcSunday : d3.utcMonday; const weekDays = weekday === 'weekday' ? 5 : 7; const height = cellSize * (weekDays + 2); // Compute a color scale. This assumes a diverging color scheme where the pivot // is zero, and we want symmetric difference around zero. const max = d3.quantile(Y, 0.9975, Math.abs); const color = d3.scaleSequential([-max, +max], colors).unknown('none'); // Construct formats. formatMonth = d3.utcFormat(formatMonth); // Compute titles. if (title === undefined) { const formatDate = d3.utcFormat('%B %-d, %Y'); const formatValue = color.tickFormat(100, yFormat); title = (i) => `${formatDate(X[i])}n${formatValue(Y[i])}`; } if (title !== null) { const T = d3.map(data, title); title = (i) => T[i]; } // Group the index by year, in reverse input order. (Assuming that the input is // chronological, this will show years in reverse chronological order.) const years = d3 .groups(I, (i) => { const x = new Date(X[i]); return x.getUTCFullYear(); }) .reverse(); function pathMonth(t) { const d = Math.max(0, Math.min(weekDays, countDay(t.getUTCDay()))); const w = timeWeek.count(d3.utcYear(t), t); return `${ d === 0 ? `M${w * cellSize},0` : d === weekDays ? `M${(w + 1) * cellSize},0` : `M${(w + 1) * cellSize},0V${d * cellSize}H${w * cellSize}` }V${weekDays * cellSize}`; } const svg = d3 .create('svg') .attr('width', width) .attr('height', height * years.length) .attr('viewBox', [0, 0, width, height * years.length]) .attr('style', 'max-width: 100%; height: auto; height: intrinsic;') .attr('font-family', 'sans-serif') .attr('font-size', 10); const year = svg .selectAll('g') .data(years) .join('g') .attr( 'transform', (d, i) => `translate(40.5,${height * i + cellSize * 1.5})` ); year .append('text') .attr('x', -5) .attr('y', -5) .attr('font-weight', 'bold') .attr('text-anchor', 'end') .text(([key]) => key); year .append('g') .attr('text-anchor', 'end') .selectAll('text') .data(weekday === 'weekday' ? d3.range(1, 6) : d3.range(7)) .join('text') .attr('x', -5) .attr('y', (i) => (countDay(i) + 0.5) * cellSize) .attr('dy', '0.31em') .text(formatDay); const cell = year .append('g') .selectAll('rect') .data( weekday === 'weekday' ? ([, I]) => I.filter((i) => { const x = new Date(X[i]); return ![0, 6].includes(x.getUTCDay()); }) : ([, I]) => I ) .join('rect') .attr('width', cellSize - 1) .attr('height', cellSize - 1) .attr('x', (i) => timeWeek.count(d3.utcYear(X[i]), X[i]) * cellSize + 0.5) .attr('y', (i) => { const x = new Date(X[i]); return countDay(x.getUTCDay()) * cellSize + 0.5; }) .attr('fill', (i) => color(Y[i])); if (title) cell.append('title').text(title); const month = year .append('g') .selectAll('g') .data(([, I]) => d3.utcMonths(d3.utcMonth(X[I[0]]), X[I[I.length - 1]])) .join('g'); month .filter((d, i) => i) .append('path') .attr('fill', 'none') .attr('stroke', '#fff') .attr('stroke-width', 3) .attr('d', pathMonth); month .append('text') .attr( 'x', (d) => timeWeek.count(d3.utcYear(d), timeWeek.ceil(d)) * cellSize + 2 ) .attr('y', -5) .text(formatMonth); return Object.assign(svg.node(), { scales: { color } }); }
Advertisement
Answer
Adding an Element
ObservableHQ posts operate differently from conventional JavaScript. If an expression returns a DOM element, it automatically shows up in the post. Outside of that environment, in regular JavaScript, you have to add them to your DOM explicitly.
In your case, add document.body.appendChild(chart);
to the bottom of index.js
.
Fixing the Parsing
Once you do this, you’ll notice that only one column of dates shows up. The calendar seems to expect date objects, not date strings. In that case, you can change the x parsing function in index.js
from:
x: (d) => d.Date,
to
x: (d) => new Date(d.Date),