I have generated a line chart graph using D3. I am aware that I could use biject functionality to get exact values for x and y co-ordinates that I have specified in my data but I am interested in finding approximate x and y values for any given x or y value that falls with in the min-max range of my data set. Is that possible to do so with D3.js?
The method path.getPointAtLength can be used to get an x and y coordinate on path, and this can be recursively called to get a y coordinate that's equal, or very close to equal, to a specified x coordinate.
In the example below, I've pre-calculated all the y coordinates for each x in the chart's width. This improves the mouse hover performance, by just looking up the y value each time the mouse moves over the chart (the hover over is captured using a invisible rect element).
const height = 500
const width = 500
const margin = { "top": 20, "bottom": 20, "left": 20, "right": 20 }
const data = [2, 5, 6, 7, 3, 8, 3, 4]
let xScale = d3.scaleLinear()
.domain([0, data.length - 1])
.range([0, width])
let yScale = d3.scaleLinear()
.domain([0, 10])
.range([height, 0])
let xAxis = d3.axisBottom(xScale)
let yAxis = d3.axisLeft(yScale)
let curve = d3.curveCatmullRom.alpha(0.5)
let line = d3.line()
.x(function (d, i) { return xScale(i) })
.y(function (d) { return yScale(d) })
.curve(curve);
let svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
let g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
g.append("g").call(yAxis)
let path = g.append("path")
.datum(data)
.attr("d", line)
let pathNode = path.node()
let pathNodeLength = pathNode.getTotalLength()
//for every x coordinate, get the y coordinates for the line
//and store for use later on
let allCoordinates = []
let x = 0;
for (x; x < width; x++) {
let obj = {}
obj.y = findY(pathNode, pathNodeLength, x, width)
allCoordinates.push(obj)
}
let dots = g.selectAll(".dot")
.data([1]) //create one circle for later use
.enter()
.append("g")
.style("opacity", 0)
dots.append("circle")
.attr("r", 8)
dotsBgdText = dots.append("text")
.attr("class", "text-bgd")
.attr("x", 0)
dotsText = dots.append("text")
.attr("class", "text-fgd")
.attr("x", 0)
//Add a rect to handle mouse events
let rect = g.append("rect")
.attr("width", width - 1) // minus 1 so that it doesn't return an x = width, as the coordinates is 0 based.
.attr("height", height)
.style("opacity", 0)
.on("mousemove", function (d) {
let mouseX = d3.mouse(this)[0]
let dotsData = [
{ "cx": mouseX, "cy": allCoordinates[mouseX].y}
]
dots.data(dotsData)
.attr("transform", function (d) { return "translate(" + d.cx + "," + d.cy + ")" })
.style("opacity", 1)
dotsText.data(dotsData)
.text(function (d) {
return roundNumber(yScale.invert(d.cy));
})
.attr("y", function (d) {
let maxY = d3.max(dotsData, function (e) { return e.cx == d.cx ? e.cy : 0 })
return d.cy == maxY ? 27 : -15;
})
dotsBgdText.data(dotsData)
.text(function (d) {
return roundNumber(yScale.invert(d.cy));
})
.attr("y", function (d) {
let maxY = d3.max(dotsData, function (e) { return e.cx == d.cx ? e.cy : 0 })
return d.cy == maxY ? 27 : -15;
})
})
//iteratively search a path to get a point close to a desired x coordinate
function findY(path, pathLength, x, width) {
const accuracy = 1 //px
const iterations = Math.ceil(Math.log10(accuracy/width) / Math.log10(0.5)); //for width (w), get the # iterations to get to the desired accuracy, generally 1px
let i = 0;
let nextLengthChange = pathLength / 2;
let nextLength = pathLength / 2;
let y = 0;
for (i; i < iterations; i++) {
let pos = path.getPointAtLength(nextLength)
y = pos.y
nextLength = x < pos.x ? nextLength - nextLengthChange : nextLength + nextLengthChange
nextLengthChange = nextLengthChange / 2
}
return y
}
function roundNumber(n) {
return Math.round(n * 100) / 100
}
path {
stroke: black;
fill: none;
}
.text-bgd {
stroke: white;
fill: white;
stroke-width: 3;
text-anchor: middle;
}
.text-fgd {
stroke: none;
text-anchor: middle;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>