Skip to content
Advertisement

D3 element not showing up in DOM

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),
User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement