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) &&...