Quick update - the code in this article was used as the starting point for components in the d3fc project. It’s a bit more advanced than this article, but you might find it interesting to see what this code evolved into!
In my last article I put together an interactive chart that could be panned and zoomed, but there was one obvious omission - crosshairs. In this article I’m going to create a D3 crosshairs component, and show you how to add it to a chart.
Here’s the chart we’re going to build:
You can see that, when you move your mouse over the chart, crosshairs appear on the chart to give you information about the data point the mouse is nearest to. This should also work if you’re on a touch screen device - you’d just tap on a data point to bring up the crosshairs, or tap off the chart to clear them - but I have to admit I haven’t tested this.
I’m going to approach this project in the same way I approached the line annotation and Bollinger Bands components - that is, by breaking the work down into three sections:
- First we write the component (this is obviously where the majority of the work goes)
- Then we style the component
- Finally the add the component to a chart
Let’s get cracking!
The Crosshairs Component
Here’s the full code for the crosshairs component - I’ve left it in one block so it’s easier for you to copy & paste. Underneath, we’ll go through the code and explain what’s going on.
OK, now let’s go through this and talk about what’s going on.
Declarations
At the top of the component we set up the various user-configurable properties we require. They are:
target
: the area we’re going to be adding our crosshairs elements toseries
: the data seriesxScale
andyScale
: the X and Y scales, which we need to determine positioningyValue
: the name of the field on the data model that we’re going to use for the Y-value (we’ll assume that the X-value field is ‘date’)formatH
andformatV
: callbacks that we’re going to use to format the text for the horizontal line and the vertical line respectively
We also declare variables to hold the five SVG elements that our crosshairs component will comprise:
- a horizontal line
- a vertical line
- a circle (at the intersection of the horizontal and vertical lines)
- a value callout for the horizontal line
- a value callout for the vertical line
We then declare a variable called highlight
, which will hold the currently highlighted data point.
Component function
In the component function we initialise the SVG elements as follows:
- for the
lineH
element, we set the X co-ordinates so that the line reaches all the way across the target area - similarly, we set the Y co-ordinates for the
lineV
element so that it takes up the whole height of the target area - we set the
r
(radius) property on thecircle
element - for
calloutH
we set thex
co-ordinate so that the text appears at the right hand side of the chart, and we set thetext-anchor
style attribute toend
so that the text is right-aligned - for
calloutV
we set they
co-ordinate so that the text appears just below the top of the chart, and we set thetext-anchor
style attribute as above
Each of the elements is also given a unique set of CSS classes, and we set display=none
on each element so that they are initially not shown.
Property accessors
Jumping ahead a little, let’s look at the property accessor I’ve included in the code above, for the target
property. Accessors are implemented for the other properties but I’ve left them out of the code block above because they’re all standard get/set accessors, whereas this one’s slightly different.
To save you scrolling back up, here’s the relevant code again…
… but in essence the only extra thing we’re doing here is adding handlers for the mouseover
and mouseout
events when we set the property - and clearing those event handlers if we reset the property.
Event handlers
Let’s look at those event handlers now. The first one, mousemove
, is the more programmatically interesting one:
What’s happening here is pretty straightforward though. We use xScale.invert()
to get the date value of the location of the mouse, and then we call findNearest()
(see below) to find the data point closest to that date.
If we have a data point (and it’s not the one that’s currently highlighted) then we spring into action:
- We get the X and Y co-ordinates for the point we’re going to highlight
- We set these co-ordinates on our SVG elements, moving them all to the correct places on the chart
- We set the text of our
calloutH
andcalloutV
elements by delegating to theformatH
andformatV
callbacks respectively - We show the SVG elements by setting
display=inherit
on each one
The second event handler, mouseout
, is much simpler:
All we’re doing here is clearing the highlight
field and hiding all the SVG elements when the mouse leaves the target area.
The last bit of code to look at is the findNearest()
function:
This is pretty self-explanatory - we’re just iterating through the data points in series
, comparing their date
fields to xMouse
so that we find the data point which is the closest, temporally, to it.
Styling
I’m using three CSS rules for this component - nice and simple. One is for the horizontal and vertical line elements, one is for the circle element, and the last is for the two text elements. If you look at the CSS you can see that the rule for the lines and the rule for the circle is identical - I’m really just setting the colour. For the text elements I’m copying the style of the axis labels (10pt sans serif).
The important thing to note here is that the user could style each element differently if they wanted to, without having to get into the JavaScript, as each element has a unique set of CSS classes - this component is very stylable.
Adding it to the chart
The final step is to add the crosshairs component to a chart.
Rather than create a chart from scratch, I’m starting with the OHLC chart that Tom developed in his article on OHLC and candlestick components.
To add the crosshairs component, I’m firstly using a trick I learned in my previous article - adding an invisible overlay onto the chart area. If we don’t do this, the mouseover()
event will only be fired when we mouse over a data point, but adding this we get the events fired when the mouse moves anywhere on the chart area.
Once that’s created, we initialise the crosshairs component, providing values for all the properties we defined.
Finally we add the overlay to the chart area, and call the crosshairs component on it.
The following bit of CSS makes our overlay invisible:
And with that, we’re done! We’ve created the interactive chart you see above.
Enhancements
As we’ve learned from my previous article, when you add multiple modes of interactivity to a chart, most of your time is spent making sure everything is synchronised correctly. If we added this crosshairs component to the interactive chart we developed in that article, we’d soon run into a problem - when you zoomed the chart, the crosshairs would stay in the same place because they only respond to the mouse moving over the chart (panning the chart probably wouldn’t be a problem, because you need to move the mouse to do that).
Luckily there’s an easy fix for this issue - all we’d have to do would be to refactor the drawing code from mousemove()
into its own method, maybe called update()
, and then call crosshairs.update()
whenever a zoom would cause the chart to be redrawn.
Conclusion
I wanted to create a D3 component to add crosshairs to a chart. The component is comprised of five SVG elements, which are updated on mousemove
and mouseout
events. The component can be styled in whatever way the user needs.