d3 dates | | d3 calendar | Search

This code sets up the foundational structure for a D3.js swimlane chart, defining scales, axes, and the basic SVG elements, but lacks the code to render the actual chart elements.

Run example

npm run import -- "d3 swimlane"

d3 swimlane

var D3Node = require('d3-node');

var margin = {top: 30, right: 15, bottom: 35, left: 100},
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;

var styles = `<style>
.chart {
	shape-rendering: crispEdges;
}

.mini text {
	font: 9px sans-serif;	
}

.main text {
	font: 12px sans-serif;	
}

.month text {
	text-anchor: start;
}

.todayLine {
	stroke: blue;
	stroke-width: 1.5;
}

.axis line, .axis path {
	stroke: black;
}

.miniItem {
	stroke-width: 12;	
}

.future {
	stroke: gray;
}

.past {
	stroke-opacity: .6;
	fill-opacity: .6;
}

.brush .extent {
	stroke: gray;
	fill: blue;
	fill-opacity: .165;
}
</style>`


var now = new Date();
function d3Swimlane(events) {
    var d3n = new D3Node(); // initializes D3 with container element 
    var d3 = d3n.d3;
    var data = events
        , lanes = data.lanes
        , items = data.items;

    var fill = d3.scaleOrdinal(d3.schemeCategory20);

    var miniHeight = lanes.length * 12 + 50
        , mainHeight = height - miniHeight - 50;

    var x = d3.scaleTime()
        .domain([d3.timeSunday(d3.min(items, function (d) {
            return d.start;
        })),
            d3.max(items, function (d) {
                return d.end;
            })])
        .range([0, width]);
    var x1 = d3.scaleTime().range([0, width]);

    var ext = d3.extent(lanes, function (d) {
        return d.id;
    });
    var y1 = d3.scaleLinear()
        .domain([ext[0], ext[1] + 1])
        .range([0, mainHeight]);
    var y2 = d3.scaleLinear()
        .domain([ext[0], ext[1] + 1])
        .range([0, miniHeight]);

    var chart = d3n.createSVG(
        width + margin.right + margin.left,
        height + margin.top + margin.bottom)
        .attr('class', 'chart');

    chart.append('defs').append('clipPath')
        .attr('id', 'clip')
        .append('rect')
        .attr('width', width)
        .attr('height', mainHeight);

    var main = chart.append('g')
        .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
        .attr('width', width)
        .attr('height', mainHeight)
        .attr('class', 'main');

    var mini = chart.append('g')
        .attr('transform', 'translate(' + margin.left + ',' + (mainHeight + 70) + ')')
        .attr('width', width)
        .attr('height', miniHeight)
        .attr('class', 'mini');

    // draw the lanes for the main chart
    main.append('g').selectAll('.laneLines')
        .data(lanes)
        .enter().append('line')
        .attr('x1', 0)
        .attr('y1', function (d) {
            return Math.round(y1(d.id)) + 0.5;
        })
        .attr('x2', width)
        .attr('y2', function (d) {
            return Math.round(y1(d.id)) + 0.5;
        })
        .attr('stroke', function (d) {
            return d.label === '' ? 'white' : 'lightgray'
        });

    main.append('g').selectAll('.laneText')
        .data(lanes)
        .enter().append('text')
        .text(function (d) {
            return d.label;
        })
        .attr('x', -10)
        .attr('y', function (d) {
            return y1(d.id + .5);
        })
        .attr('dy', '0.5ex')
        .attr('text-anchor', 'end')
        .attr('class', 'laneText');

    // draw the lanes for the mini chart
    mini.append('g').selectAll('.laneLines')
        .data(lanes)
        .enter().append('line')
        .attr('x1', 0)
        .attr('y1', function (d) {
            return Math.round(y2(d.id)) + 0.5;
        })
        .attr('x2', width)
        .attr('y2', function (d) {
            return Math.round(y2(d.id)) + 0.5;
        })
        .attr('stroke', function (d) {
            return d.label === '' ? 'white' : 'lightgray'
        });

    mini.append('g').selectAll('.laneText')
        .data(lanes)
        .enter().append('text')
        .text(function (d) {
            return d.label;
        })
        .attr('x', -10)
        .attr('y', function (d) {
            return y2(d.id + .5);
        })
        .attr('dy', '0.5ex')
        .attr('text-anchor', 'end')
        .attr('class', 'laneText');

    // draw the x axis
    var xDateAxis = d3.axisBottom(x)
        .tickArguments(d3.timeMondays, (x.domain()[1] - x.domain()[0]) > 15552e6 ? 2 : 1)
        .tickFormat(d3.timeFormat('%d'))
        .tickSize(6, 0, 0);

    var x1DateAxis = d3.axisBottom(x1)
        .tickArguments(d3.timeDays, 1)
        .tickFormat(d3.timeFormat('%a %d'))
        .tickSize(6, 0, 0);

    var xMonthAxis = d3.axisTop(x)
        .tickArguments(d3.timeMonths, 1)
        .tickFormat(d3.timeFormat('%b %Y'))
        .tickSize(15, 0, 0);

    var x1MonthAxis = d3.axisTop(x1)
        .tickArguments(d3.timeMondays, 1)
        .tickFormat(d3.timeFormat('%b - Week %W'))
        .tickSize(15, 0, 0);

    main.append('g')
        .attr('transform', 'translate(0,' + mainHeight + ')')
        .attr('class', 'main axis date')
        .call(x1DateAxis);

    main.append('g')
        .attr('transform', 'translate(0,0.5)')
        .attr('class', 'main axis month')
        .call(x1MonthAxis)
        .selectAll('text')
        .attr('dx', 5)
        .attr('dy', 12);

    mini.append('g')
        .attr('transform', 'translate(0,' + miniHeight + ')')
        .attr('class', 'axis date')
        .call(xDateAxis);

    mini.append('g')
        .attr('transform', 'translate(0,0.5)')
        .attr('class', 'axis month')
        .call(xMonthAxis)
        .selectAll('text')
        .attr('dx', 5)
        .attr('dy', 12);

    // draw a line representing today's date
    main.append('line')
        .attr('y1', 0)
        .attr('y2', mainHeight)
        .attr('class', 'main todayLine')
        .attr('clip-path', 'url(#clip)');

    mini.append('line')
        .attr('x1', x(now) + 0.5)
        .attr('y1', 0)
        .attr('x2', x(now) + 0.5)
        .attr('y2', miniHeight)
        .attr('class', 'todayLine');

    // draw the items
    var itemRects = main.append('g')
        .attr('clip-path', 'url(#clip)');

    mini.append('g').selectAll('miniItems')
        .data(getPaths(items))
        .enter().append('path')
        .attr('class', function (d) {
            return 'miniItem ' + d.class;
        })
        .style('stroke', function (d) {
            return fill(d.lane);
        })
        .style('fill', function (d) {
            return fill(d.lane);
        })
        .attr('d', function (d) {
            return d.path;
        });

    // draw the selection area
    var brush = d3.brushX(x)
        .extent([d3.timeMonday(now), d3.timeSaturday.ceil(now)]);

    mini.append('g')
        .attr('class', 'x brush')
        .call(brush)
        .selectAll('rect')
        .attr('y', 1)
        .attr('height', miniHeight - 1);

    mini.selectAll('rect.background').remove();

    function display() {
        var rects, labels
            , minExtent = d3.timeMonday(now)
            , maxExtent = d3.timeSaturday.ceil(now)
            , visItems = items.filter(function (d) {
            return d.start < maxExtent && d.end > minExtent
        });

        mini.select('.brush').call(brush.extent([minExtent, maxExtent]));

        x1.domain([minExtent, maxExtent]);

        if ((maxExtent - minExtent) > 1468800000) {
            x1DateAxis
                .tickArguments([d3.timeMondays.every(1)])
                .tickFormat(d3.timeFormat('%a %d'))
            x1MonthAxis
                .tickArguments([d3.timeMonday.every(1)])
                .tickFormat(d3.timeFormat('%b - Week %W'))
        }
        else if ((maxExtent - minExtent) > 172800000) {
            x1DateAxis
                .tickArguments([d3.timeDay.every(1)])
                .tickFormat(d3.timeFormat('%a %d'))
            x1MonthAxis
                .tickArguments([d3.timeMonday.every(1)])
                .tickFormat(d3.timeFormat('%b - Week %W'))
        }
        else {
            x1DateAxis
                .tickArguments([d3.timeHour.every(4)])
                .tickFormat(d3.timeFormat('%I %p'))
            x1MonthAxis
                .tickArguments([d3.timeDays.every(1)])
                .tickFormat(d3.timeFormat('%b %e'))
        }


        //x1Offset.range([0, x1(d3.time.day.ceil(now) - x1(d3.time.day.floor(now)))]);

        // shift the today line
        main.select('.main.todayLine')
            .attr('x1', x1(now) + 0.5)
            .attr('x2', x1(now) + 0.5);

        // update the axis
        main.select('.main.axis.date').call(x1DateAxis);
        main.select('.main.axis.month').call(x1MonthAxis)
            .selectAll('text')
            .attr('dx', 5)
            .attr('dy', 12);

        // upate the item rects
        rects = itemRects.selectAll('rect')
            .data(visItems, function (d) {
                return d.id;
            })
            .attr('x', function (d) {
                return x1(d.start);
            })
            .attr('width', function (d) {
                return x1(d.end) - x1(d.start);
            })

        rects.enter().append('rect')
            .attr('x', function (d) {
                return x1(d.start);
            })
            .attr('y', function (d) {
                return y1(d.lane);
            })
            .attr('width', function (d) {
                return x1(d.end) - x1(d.start);
            })
            .attr('height', function (d) {
                return y1(1);
            })
            .attr('class', function (d) {
                return 'mainItem ' + d.class;
            })
            .style('stroke', function (d) {
                return fill(d.lane);
            })
            .style('fill', function (d) {
                return fill(d.lane);
            })

        rects.exit().remove();

        // update the item labels
        labels = itemRects.selectAll('text')
            .data(visItems, function (d) {
                return d.id;
            })
            .attr('x', function (d) {
                return x1(Math.max(d.start, minExtent)) + 2;
            });

        labels.enter().append('text')
            .text((d) => d.desc)
            .attr('x', function (d) {
                return x1(Math.max(d.start, minExtent)) + 2;
            })
            .attr('y', function (d) {
                return y1(d.lane) + .4 * y1(1) + 0.5;
            })
            .attr('text-anchor', 'start')
            .attr('class', 'itemLabel');

        labels.exit().remove();

        return styles + d3n.svgString();
    }

    function moveBrush(origin) {
        var point = x.invert(origin[0])
            , halfExtent = (brush.extent()[1].getTime() - brush.extent()[0].getTime()) / 2
            , start = new Date(point.getTime() - halfExtent)
            , end = new Date(point.getTime() + halfExtent);

        brush.extent([start, end]);
    }

    // generates a single path for each item class in the mini display
    // ugly - but draws mini 2x faster than append lines or line generator
    // is there a better way to do a bunch of lines as a single path with d3?
    function getPaths(items) {
        var paths = {}, d, offset = .5 * y2(1) + 0.5, result = [];
        for (var i = 0; i < items.length; i++) {
            d = items[i];
            const key = d.class + ' ' + d.class + '-' + d.lane;
            if (!paths[key]) paths[key] = {class: key, path: '', lane: d.lane};
            paths[key].path += ['M', x(d.start), (y2(d.lane) + offset), 'H', x(d.end)].join(' ');
        }
        return Object.keys(paths).map(k => paths[k]);
    }

    return display();
}

module.exports = d3Swimlane;
d3Swimlane;

What the code could have been:

const D3Node = require('d3-node');

class D3Swimlane {
  constructor(events) {
    this.events = events;
  }

  static initD3() {
    const d3n = new D3Node();
    return d3n.d3;
  }

  getD3() {
    return D3Swimlane.initD3();
  }

  getStyles() {
    return `
      <style>
       .chart {
          shape-rendering: crispEdges;
        }
       .mini text {
          font: 9px sans-serif;	
        }
       .main text {
          font: 12px sans-serif;	
        }
       .month text {
          text-anchor: start;
        }
       .todayLine {
          stroke: blue;
          stroke-width: 1.5;
        }
       .axis line,.axis path {
          stroke: black;
        }
       .miniItem {
          stroke-width: 12;	
        }
       .future {
          stroke: gray;
        }
       .past {
          stroke-opacity:.6;
          fill-opacity:.6;
        }
       .brush.extent {
          stroke: gray;
          fill: blue;
          fill-opacity:.165;
        }
      </style>
    `;
  }

  getNow() {
    return new Date();
  }

  getPaths(items) {
    const paths = {};
    items.forEach((item, index) => {
      const key = item.class +'' + item.class + '-' + item.lane;
      if (!paths[key]) paths[key] = { class: key, path: '', lane: item.lane };
      const x1 = this.getD3().scaleTime().range([0, this.width]);
      const y1 = this.getD3().scaleLinear().range([0, this.miniHeight]);
      paths[key].path += `M${x1(item.start)} ${y1(item.lane) + 0.5}${'H'}${x1(item.end)} `;
    });
    return Object.keys(paths).map(key => paths[key]);
  }

  createChart() {
    const margin = { top: 30, right: 15, bottom: 35, left: 100 };
    const width = 960 - margin.left - margin.right;
    const height = 500 - margin.top - margin.bottom;
    const d3 = D3Swimlane.initD3();
    const data = this.events;
    const lanes = data.lanes;
    const items = data.items;

    const fill = d3.scaleOrdinal(d3.schemeCategory20);

    const miniHeight = lanes.length * 12 + 50;
    const mainHeight = height - miniHeight - 50;

    const x = d3.scaleTime().domain(d3.extent(data.items, (d) => d.start)).range([0, width]);
    const x1 = d3.scaleTime().range([0, width]);
    const y1 = d3.scaleLinear().domain(d3.extent(lanes, (d) => d.id)).range([0, mainHeight]);
    const y2 = d3.scaleLinear().domain(d3.extent(lanes, (d) => d.id)).range([0, miniHeight]);

    const chart = d3.createSVG(
      width + margin.right + margin.left,
      height + margin.top + margin.bottom
    ).attr('class', 'chart');

    const main = chart.append('g').attr('transform', `translate(${margin.left}, ${margin.top})`);
    const mini = chart.append('g').attr('transform', `translate(${margin.left}, ${mainHeight + 70})`);

    const laneLines = main.selectAll('.laneLines').data(lanes).enter().append('line').attr('x1', 0).attr('y1', (d) => Math.round(y1(d.id)) + 0.5).attr('x2', width).attr('y2', (d) => Math.round(y1(d.id)) + 0.5).attr('stroke', (d) => d.label === ''? 'white' : 'lightgray');

    const laneText = main.selectAll('.laneText').data(lanes).enter().append('text').text((d) => d.label).attr('x', -10).attr('y', (d) => y1(d.id +.5)).attr('dy', '0.5ex').attr('text-anchor', 'end').attr('class', 'laneText');

    const miniLaneLines = mini.selectAll('.laneLines').data(lanes).enter().append('line').attr('x1', 0).attr('y1', (d) => Math.round(y2(d.id)) + 0.5).attr('x2', width).attr('y2', (d) => Math.round(y2(d.id)) + 0.5).attr('stroke', (d) => d.label === ''? 'white' : 'lightgray');

    const miniLaneText = mini.selectAll('.laneText').data(lanes).enter().append('text').text((d) => d.label).attr('x', -10).attr('y', (d) => y2(d.id +.5)).attr('dy', '0.5ex').attr('text-anchor', 'end').attr('class', 'laneText');

    const xDateAxis = d3.axisBottom(x).tickArguments(d3.timeMondays, (x.domain()[1] - x.domain()[0]) > 15552e6? 2 : 1).tickFormat(d3.timeFormat('%d')).tickSize(6, 0, 0);

    const x1DateAxis = d3.axisBottom(x1).tickArguments(d3.timeDays, 1).tickFormat(d3.timeFormat('%a %d')).tickSize(6, 0, 0);

    const xMonthAxis = d3.axisTop(x).tickArguments(d3.timeMonths, 1).tickFormat(d3.timeFormat('%b %Y')).tickSize(15, 0, 0);

    const clip = chart.append('defs').append('clipPath').attr('id', 'clip').append('rect').attr('width', width).attr('height', mainHeight);

    main.append('g').attr('class','main axis date').call(x1DateAxis).attr('transform', `translate(0,${mainHeight})`);

    main.append('g').attr('class','main axis month').call(x1MonthAxis).attr('transform', `translate(0,0.5)`).selectAll('text').attr('dx', 5).attr('dy', 12);

    mini.append('g').attr('class', 'axis date').call(xDateAxis).attr('transform', `translate(0,${miniHeight})`);

    mini.append('g').attr('class', 'axis month').call(xMonthAxis).attr('transform', `translate(0,0.5)`).selectAll('text').attr('dx', 5).attr('dy', 12);

    main.append('line').attr('y1', 0).attr('y2', mainHeight).attr('class', 'todayLine').attr('clip-path', 'url(#clip)');

    mini.append('line').attr('y1', 0).attr('y2', miniHeight).attr('x1', x(this.getNow()) + 0.5).attr('x2', x(this.getNow()) + 0.5).attr('class', 'todayLine');

    const itemRects = main.append('g').attr('clip-path', 'url(#clip)');
    mini.selectAll('miniItems').data(this.getPaths(items)).enter().append('path').attr('class', (d) => `miniItem ${d.class}`).style('stroke', (d) => fill(d.lane)).style('fill', (d) => fill(d.lane)).attr('d', (d) => d.path);

    const brush = d3.brushX(x).extent([d3.timeMonday(this.getNow()), d3.timeSaturday.ceil(this.getNow())]);

    mini.append('g').attr('class', 'x brush').call(brush).selectAll('rect').attr('y', 1).attr('height', miniHeight - 1);

    mini.selectAll('rect.background').remove();

    return this.display();
  }

  display() {
    const now = this.getNow();
    const items = this.events.items;
    const minExtent = d3.timeMonday(now);
    const maxExtent = d3.timeSaturday.ceil(now);
    const visItems = items.filter((d) => d.start < maxExtent && d.end > minExtent);

    const range = (maxExtent - minExtent);
    const x1 = this.getD3().scaleTime().range([0, this.width]);
    x1.domain([minExtent, maxExtent]);

    const brush = d3.brushX(x1).extent([minExtent, maxExtent]);
    mini.select('.brush').call(brush);

    if (range > 1468800000) {
      const x1DateAxis = d3.axisBottom(x1).tickArguments([d3.timeMondays.every(1)]).tickFormat(d3.timeFormat('%a %d'));
      const x1MonthAxis = d3.axisTop(x1).tickArguments([d3.timeMonday.every(1)]).tickFormat(d3.timeFormat('%b - Week %W'));
      main.select('.main.axis.date').call(x1DateAxis);
      main.select('.main.axis.month').call(x1MonthAxis).selectAll('text').attr('dx', 5).attr('dy', 12);
    } else if (range > 172800000) {
      const x1DateAxis = d3.axisBottom(x1).tickArguments([d3.timeDay.every(1)]).tickFormat(d3.timeFormat('%a %d'));
      const x1MonthAxis = d3.axisTop(x1).tickArguments([d3.timeMonday.every(1)]).tickFormat(d3.timeFormat('%b - Week %W'));
      main.select('.main.axis.date').call(x1DateAxis);
      main.select('.main.axis.month').call(x1MonthAxis).selectAll('text').attr('dx', 5).attr('dy', 12);
    } else {
      const x1DateAxis = d3.axisBottom(x1).tickArguments([d3.timeHour.every(4)]).tickFormat(d3.timeFormat('%I %p'));
      const x1MonthAxis = d3.axisTop(x1).tickArguments([d3.timeDays.every(1)]).tickFormat(d3.timeFormat('%b %e'));
      main.select('.main.axis.date').call(x1DateAxis);
      main.select('.main.axis.month').call(x1MonthAxis).selectAll('text').attr('dx', 5).attr('dy', 12);
    }

    const itemRects = main.select('.main.clip');
    const rects = itemRects.selectAll('rect').data(visItems, (d) => d.id);
    rects.exit().remove();
    rects.attr('x', (d) => x1(d.start)).attr('width', (d) => x1(d.end) - x1(d.start));
    rects.enter().append('rect').attr('x', (d) => x1(d.start)).attr('y', (d) => y1(d.lane)).attr('width', (d) => x1(d.end) - x1(d.start)).attr('height', (d) => y1(1)).attr('class', (d) => `mainItem ${d.class}`).style('stroke', (d) => fill(d.lane)).style('fill', (d) => fill(d.lane));

    const labels = itemRects.selectAll('text').data(visItems, (d) => d.id);
    labels.exit().remove();
    labels.attr('x', (d) => x1(Math.max(d.start, minExtent)) + 2);
    labels.enter().append('text').text((d) => d.desc).attr('x', (d) => x1(Math.max(d.start, minExtent)) + 2).attr('y', (d) => y1(d.lane) + 0.4 * y1(1) + 0.5).attr('text-anchor','start').attr('class', 'itemLabel');

    return this.styles + chart.node().outerHTML;
  }

  styles() {
    return `
      <style>
       .chart {
          shape-rendering: crispEdges;
        }
       .mini text {
          font: 9px sans-serif;	
        }
       .main text {
          font: 12px sans-serif;	
        }
       .month text {
          text-anchor: start;
        }
       .todayLine {
          stroke: blue;
          stroke-width: 1.5;
        }
       .axis line,.axis path {
          stroke: black;
        }
       .miniItem {
          stroke-width: 12;	
        }
       .future {
          stroke: gray;
        }
       .past {
          stroke-opacity:.6;
          fill-opacity:.6;
        }
       .brush.extent {
          stroke: gray;
          fill: blue;
          fill-opacity:.165;
        }
      </style>
    `;
  }

  render() {
    const styles = this.styles();
    const chart = this.createChart();
    return styles + chart;
  }
}

module.exports = D3Swimlane;

This code generates a D3.js visualization of a swimlane chart, displaying events across time and lanes.

Here's a breakdown:

  1. Initialization:

  2. Data Preparation:

  3. Scales and Axes:

  4. Chart Creation:

  5. (Incomplete):

In essence, this code sets up the foundation for a D3.js swimlane chart, defining scales, axes, and the basic structure of the chart. It's missing the code to actually draw the lanes, items, and other visual elements.