Dashed line segments in D3

by epiph0ne

HTML

<body></body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>

CSS

body {
  font: 10px sans-serif;
}

.axis path,
.axis line {
  fill: none;
  stroke: #000;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}

.line {
  fill: none;
  stroke: steelblue;
  stroke-width: 1.5px;
}

Babel + JSX

/* global _, d3 */
const MARGIN = { top: 20, right: 20, bottom: 30, left: 50 }
const WIDTH = 550 - MARGIN.left - MARGIN.right
const HEIGHT = 300 - MARGIN.top - MARGIN.bottom
const DASH_LENGTH = 2
const DASH_SEPARATOR_LENGTH = 2


// Generate some random data to populate the chart:
const lineData = getRandomData()


// Draw the chart as usual:
const scales = {
  x: d3.time.scale()
    .domain(d3.extent(lineData, d => d.date))
    .range([0, WIDTH]),
  y: d3.scale.linear()
    .domain(d3.extent(lineData, d => d.value))
    .range([HEIGHT, 0]),
}

const xAxis = d3.svg.axis()
  .scale(scales.x)
  .orient('bottom')

const yAxis = d3.svg.axis()
  .scale(scales.y)
  .orient('left')

const line = d3.svg.line()
  .x(d => scales.x(d.date))
  .y(d => scales.y(d.value))
  .interpolate('basis')

const svg = d3.select('body').append('svg')
  .attr('width', WIDTH + MARGIN.left + MARGIN.right)
  .attr('height', HEIGHT + MARGIN.top + MARGIN.bottom)
  .append('g')
  .attr('transform', `translate(${MARGIN.left}, ${MARGIN.top})`)

svg.append('g')
  .attr('class', 'x axis')
  .attr('transform', `translate(0, ${HEIGHT})`)
  .call(xAxis)

svg.append('g')
  .attr('class', 'y axis')
  .call(yAxis)

svg.append('path')
  .datum(lineData)
  .attr('class', 'line')
  .attr('d', line)
  .attr('stroke-dasharray', function(d) { return getDashArray(d, this) })


// Dashing-related stuff begins here:

function getDashArray(data, path) {
  const dashedRanges = getDashedRanges(data)
  if (dashedRanges.length === 0) return null

  const lengths = data.map(d => getPathLengthAtX(path, scales.x(d.date)))
  return buildDashArray(dashedRanges, lengths)
}

function getDashedRanges(data) {
  const hasOpenRange = (arr) => _.last(arr) && !('end' in _.last(arr))
  const lastIndex = data.length - 1

  return _.reduce(data, (res, d, i) => {
    const isRangeStart = !hasOpenRange(res) && isDashed(d)
    if (isRangeStart) res.push({ start: Math.max(0, i - 1) })

    const isRangeEnd = hasOpenRange(res) &&...