Skip to content

Instantly share code, notes, and snippets.

@mikehadlow
Last active July 25, 2023 08:55
Show Gist options
  • Save mikehadlow/93b471e569e31af07cd3 to your computer and use it in GitHub Desktop.
Save mikehadlow/93b471e569e31af07cd3 to your computer and use it in GitHub Desktop.
D3js Graph with X and Y crosshairs, and a threshold line.

Example of a D3js time series graph with X,Y crosshairs and a threshold line. Just copy the drawLineGraph function and call it with your data. The data shoud be an array of two element arrays. Something like:

var data = [
    [new Date(2014, 01, 10), 404],
    [new Date(2014, 01, 11), 123],
    [new Date(2014, 01, 12), 666]
    ];
    
var warnLine = { lineValue: 200, label: 'my important threshold' };

drawLineGraph(400, 800, data, 'awesomenes', warnLine);
<!DOCTYPE html>
<html xmlns:xlink="http://www.w3.org/1999/xlink">
<head>
<title>SVG Test</title>
<style>
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: 2px;
}
.circle {
fill: white;
stroke: steelblue;
stroke-width: 2px;
}
.area {
fill: steelblue;
stroke: none;
opacity: 0.1;
}
.zeroline {
fill: none;
stroke: red;
stroke-width: 0.5px;
stroke-dasharray: 5 5;
}
.zerolinetext {
fill: red;
}
.overlay {
fill: none;
stroke: none;
pointer-events: all;
}
.focusLine {
fill: none;
stroke: steelblue;
stroke-width: 0.5px;
}
.focusCircle {
fill: red;
}
</style>
</head>
<body>
</body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
// generate some awesomeness data
var data = [];
var currentValue = 100;
var random = d3.random.normal(0, 20.0);
for(var i=0; i<100; i++) {
var currentDate = new Date();
currentDate.setDate(currentDate.getDate() + i);
data.push([currentDate, currentValue]);
currentValue = currentValue + random();
}
var drawLineGraph = function(containerHeight, containerWidth, data, yLabel, warnLine) {
var svg = d3.select("body").append("svg")
.attr("width", containerWidth)
.attr("height", containerHeight);
var margin = { top: 50, left: 50, right: 50, bottom: 50 };
var height = containerHeight - margin.top - margin.bottom;
var width = containerWidth - margin.left - margin.right;
var xDomain = d3.extent(data, function(d) { return d[0]; })
var yDomain = d3.extent(data, function(d) { return d[1]; });
var xScale = d3.time.scale().range([0, width]).domain(xDomain);
var yScale = d3.scale.linear().range([height, 0]).domain(yDomain);
var xAxis = d3.svg.axis().scale(xScale).orient('bottom');
var yAxis = d3.svg.axis().scale(yScale).orient('left');
var line = d3.svg.line()
.x(function(d) { return xScale(d[0]); })
.y(function(d) { return yScale(d[1]); });
var area = d3.svg.area()
.x(function(d) { return xScale(d[0]); })
.y0(function(d) { return yScale(d[1]); })
.y1(height);
var g = svg.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
g.append('path')
.datum(data)
.attr('class', 'area')
.attr('d', area);
g.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis);
g.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.attr('text-anchor', 'end')
.text(yLabel);
g.append('path')
.datum(data)
.attr('class', 'line')
.attr('d', line);
g.selectAll('circle').data(data).enter().append('circle')
.attr('cx', function(d) { return xScale(d[0]); })
.attr('cy', function(d) { return yScale(d[1]); })
.attr('r', 5)
.attr('class', 'circle');
// focus tracking
var focus = g.append('g').style('display', 'none');
focus.append('line')
.attr('id', 'focusLineX')
.attr('class', 'focusLine');
focus.append('line')
.attr('id', 'focusLineY')
.attr('class', 'focusLine');
focus.append('circle')
.attr('id', 'focusCircle')
.attr('r', 5)
.attr('class', 'circle focusCircle');
var bisectDate = d3.bisector(function(d) { return d[0]; }).left;
g.append('rect')
.attr('class', 'overlay')
.attr('width', width)
.attr('height', height)
.on('mouseover', function() { focus.style('display', null); })
.on('mouseout', function() { focus.style('display', 'none'); })
.on('mousemove', function() {
var mouse = d3.mouse(this);
var mouseDate = xScale.invert(mouse[0]);
var i = bisectDate(data, mouseDate); // returns the index to the current data item
var d0 = data[i - 1]
var d1 = data[i];
// work out which date value is closest to the mouse
var d = mouseDate - d0[0] > d1[0] - mouseDate ? d1 : d0;
var x = xScale(d[0]);
var y = yScale(d[1]);
focus.select('#focusCircle')
.attr('cx', x)
.attr('cy', y);
focus.select('#focusLineX')
.attr('x1', x).attr('y1', yScale(yDomain[0]))
.attr('x2', x).attr('y2', yScale(yDomain[1]));
focus.select('#focusLineY')
.attr('x1', xScale(xDomain[0])).attr('y1', y)
.attr('x2', xScale(xDomain[1])).attr('y2', y);
});
// warn line
if(warnLine && yDomain[0] < warnLine.lineValue && yDomain[1] > warnLine.lineValue) {
g.append('line')
.attr('x1', xScale(xDomain[0]))
.attr('y1', yScale(warnLine.lineValue))
.attr('x2', xScale(xDomain[1]))
.attr('y2', yScale(warnLine.lineValue))
.attr('class', 'zeroline');
g.append('text')
.attr('x', xScale(xDomain[1]))
.attr('y', yScale(warnLine.lineValue))
.attr('dy', '1em')
.attr('text-anchor', 'end')
.text(warnLine.label)
.attr('class', 'zerolinetext');
}
};
drawLineGraph(400, 800, data, "Intensity", { lineValue: 200, label: "OMG!" });
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment