Skip to content

Area + Bar Overlay

A background area trend with foreground bar highlights — the area shape layer and reusable BarsChart composed on the same axes. Shows how transform() adapts the data shape for BarsChart while the area layer reads the original fields.

Live Preview

Full Source

js
import { D3Blueprint } from 'd3-blueprint';
import { scaleBand, scaleLinear } from 'd3-scale';
import { max } from 'd3-array';
import { area, line, curveCatmullRom } from 'd3-shape';
import { AxisChart } from './charts/AxisChart.js';
import { BarsChart } from './charts/BarsChart.js';

const MARGIN = { top: 20, right: 20, bottom: 30, left: 40 };

class AreaBarOverlay extends D3Blueprint {
  // Map highlight → value so BarsChart can consume the data
  transform(data) {
    return data.map((d) => ({ ...d, value: d.highlight }));
  }

  initialize() {
    this.chart = this.base
      .append('g')
      .attr('transform', `translate(${MARGIN.left},${MARGIN.top})`);

    this.attach('axes', AxisChart, this.chart);

    this.areaFn = area().curve(curveCatmullRom);
    this.lineFn = line().curve(curveCatmullRom);

    // Area layer (background)
    const areaGroup = this.chart.append('g').attr('class', 'area');
    this.layer('area', areaGroup, {
      dataBind: (sel, data) => sel.selectAll('path').data([data]),
      insert: (sel) => sel.append('path'),
      events: {
        enter: (sel) => {
          sel.attr('fill', 'var(--vp-c-brand-1)')
            .attr('fill-opacity', 0.1)
            .attr('d', (d) => this.areaFn(d));
        },
        'merge:transition': (t) => t.duration(750).attr('d', (d) => this.areaFn(d)),
      },
    });

    // Area top-edge stroke
    const areaLineGroup = this.chart.append('g').attr('class', 'area-line');
    this.layer('area-line', areaLineGroup, {
      dataBind: (sel, data) => sel.selectAll('path').data([data]),
      insert: (sel) => sel.append('path'),
      events: {
        enter: (sel) => {
          sel.attr('fill', 'none')
            .attr('stroke', 'var(--vp-c-brand-1)')
            .attr('stroke-width', 1.5)
            .attr('d', (d) => this.lineFn(d));
        },
        'merge:transition': (t) => t.duration(750).attr('d', (d) => this.lineFn(d)),
      },
    });

    // Bars layer (foreground highlights)
    this.attach('bars', BarsChart, this.chart.append('g').classed('bars', true));
  }

  preDraw(data) {
    const innerWidth = 500 - MARGIN.left - MARGIN.right;
    const innerHeight = 320 - MARGIN.top - MARGIN.bottom;

    this.xScale = scaleBand()
      .domain(data.map((d) => d.label))
      .range([0, innerWidth])
      .padding(0.15);

    this.yScale = scaleLinear()
      .domain([0, (max(data, (d) => d.total) ?? 0) * 1.1])
      .range([innerHeight, 0]);

    this.areaFn
      .x((d) => this.xScale(d.label) + this.xScale.bandwidth() / 2)
      .y0(innerHeight)
      .y1((d) => this.yScale(d.total));

    this.lineFn
      .x((d) => this.xScale(d.label) + this.xScale.bandwidth() / 2)
      .y((d) => this.yScale(d.total));

    this.attached.axes.config({
      xScale: this.xScale, yScale: this.yScale,
      innerWidth, innerHeight, duration: 750, yTickCount: 5,
    });

    this.attached.bars.config({
      xScale: this.xScale, yScale: this.yScale,
      innerHeight, fill: '#e45858', duration: 750, rx: 2,
    });
  }
}

Usage

js
chart.draw([
  { label: 'Jan', total: 120, highlight: 45 },
  { label: 'Feb', total: 150, highlight: 70 },
  { label: 'Mar', total: 100, highlight: 30 },
  { label: 'Apr', total: 180, highlight: 90 },
]);

Key Takeaways

  • Area + BarsChart compositionattach() plugs in the reusable BarsChart while the area is an inline layer, all sharing one AxisChart.
  • transform() adds the value field that BarsChart expects — a one-line data adapter.
  • Layer order ensures the area renders behind the bars for proper visual stacking.