I have a demo here
It's a simple D3 chart in an Angular app.
I would like to have four horizontal grid lines across the chart and have them proportionally space so a line at 25%, 50%, 75% and 100% the height of the chart.
I'm not concerned about the scale on the y-axis I just ned them proportionally space on the height on the chart.
I sort of have it working here but using some dodge math
const lines = chart.append('g')
.classed('lines-group', true);
lines.selectAll('line')
.data([0,1.33,2,4])
.enter()
.append('line')
.classed('hor-line', true)
.attr("y1", (d) => {
return height/d
})
.attr("y2", (d) => {
return height/d
})
.attr("x1", 0)
.attr("x2", width)
Is there a better way to do this or a proper D3 way to space the lines
Use your y scale. If you want to keep the data as percentages, all you need is:
lines.selectAll('line')
.data([25, 50, 75, 100])
.enter()
.append('line')
.attr("y1", (d) => {
return y(y.domain()[1] * (d / 100))
})
.attr("y2", (d) => {
return y(y.domain()[1] * (d / 100))
})
//etc...
As you can see we're just multiplying the maximum value in the y scale domain, which is y.domain()[1], by any value you want (in this case the percentage, represented by d / 100).
Here is the forked StackBlitz: https://stackblitz.com/edit/d3-start-above-zero-9b389s
You can customize a d3.axisRight to get this to work. Instead of adding the custom lines, try adding something like this after you add your axisBottom:
const maxVal = d3.max( graphData, (d) => d.value )
yAxis.call(
d3.axisRight(y).tickSize(width).tickValues([0.25*maxVal, 0.5*maxVal, 0.75*maxVal, maxVal])
).call(
g => g.select(".domain").remove()
).call(
g => g.selectAll(".tick text").remove()
)
Note that I am passing in the exact tick values you want using .tickValues, which allows this customization. See this post for more details on customization.
Related
I have created a scatter chart in D3 using images and now need to add a horizonal line to the chart based on the value of the "yave" field from my dataset which equals 5
I have added the following script that I know has worked in previous assignments but now I can't seem to align it to the y-axis:
//y-average-line
var yaveline = function(d) {
return data[0].yave;
}
var line = svg.append("line")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", yaveline)
.attr("y2", yaveline)
.attr("stroke-width", 1)
.attr("stroke", "black")
.attr("stroke-dasharray", "3,3");
A line is created but it appears near the top of the y-axis rather than at y = 5
Any ideas would be greatly appreciated. My fiddle is here
It appears at the top because the 5 is interpreted by the SVG as pixel coordinates, so it is at pixel y=5 (SVG counts from top to bottom). In your case, the 5 is not in pixel space, but in data space. To convert from the data space to the pixel space you have to use the yScale, the same way you do with the circles on the scatterplot:
var yaveline = function(d) {
return yScale(data[0].yave);
}
This will convert data[0].yave from data to pixel, that is, it will return the pixel that is associated with the data value.
I am working on a d3 scatter plot where an area of the chart will be circled (a Youden Plot). Based on available samples, I have been able to add zoom to both my data points and my axis. However, I am unable to get the circle to zoom correctly.
I suspect that I need to set up some kind of scale (scaleSqrt, possibly), but I am struggling to find documentation on this that is written at a beginner level.
My current circle code is very straightforward
var circle = drawCircle();
function drawCircle() {
return svg
.append('g')
.attr('class', 'scatter-group')
.append('circle')
.attr("r", 75 )
.attr('cx', 200 + margin.left) //suspect this needs to be related to a scale
.attr('cy', 200 + margin.top) //suspect this needs to be related to
.attr('r', 75)//suspect this needs to be related to a scale
.attr('stroke', 'red')
.attr('stroke-width', 3)
.style('fill', 'none')
}
As is the zoomed function
function zoomed() {
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
// update axes
gX.call(xAxis.scale(new_xScale));
gY.call(yAxis.scale(new_yScale));
//redraw data ppints
points.data(data)
.attr('cx', function(d) {return new_xScale(d.x)})
.attr('cy', function(d) {return new_yScale(d.y)});
//redraw circle
}
My work in progress is available in this fiddle . Can someone possible point me in the right direction?
I believe this will get you most of the way there. You need to update your circle attributes in the zoomed function along with the other elements:
function zoomed() {
var new_xScale = d3.event.transform.rescaleX(xScale);
var new_yScale = d3.event.transform.rescaleY(yScale);
// update axes
gX.call(xAxis.scale(new_xScale));
gY.call(yAxis.scale(new_yScale));
//redraw data ppints
points.data(data)
.attr('cx', function(d) {return new_xScale(d.x)})
.attr('cy', function(d) {return new_yScale(d.y)});
// The new part:
// the transform
let trans = d3.event.transform
// the approximate domain value of the circle 'cx' for converting later
let cx_domain = xScale.invert(200 + margin.left)
// the approximate domain value of the circle 'cy' for converting later
let cy_domain = yScale.invert(200 + margin.top)
// the circle
let circ = d3.select('.scatter-group circle')
// the radius
let rad = 75
// reset the circle 'cx' and 'cy' according to the transform
circ
.attr('cx',function(d) { return new_xScale(cx_domain)})
.attr('cy',function(d) { return new_yScale(cy_domain)})
// reset the radius by the scaling factor
.attr('r', function(d) { return rad*trans.k })
}
See this fiddle
You'll notice the circle does not scale or move at quite the same rate as the scatter dots. This is possibly because of the use of the invert function, because the conversion from range to domain and back to range is imperfect. This issue is documented
For a valid value y in the range, continuous(continuous.invert(y)) approximately equals y; similarly, for a valid value x in the domain, continuous.invert(continuous(x)) approximately equals x. The scale and its inverse may not be exact due to the limitations of floating point precision.
Your original idea to assign dynamic values to cx, cy and r will likely compensate for this, because you can then avoid the inversion.
This animation tries to illustrate balls following a curved line "falling" into a bucket:
(1) https://bl.ocks.org/max-l/ddfef6f8415675878baba32080d6a874/bae06bead60551cdae7488faccaa0d9c5624455c
For a reason that I can't understand, in (1), the balls get "teleported" outside the rectangle, it's as if the display suddenly had changed coordinate system.
The following code illustrates what should happen at the end of the transition: the balls should bounce in the rectangle that represents a bucket:
(2) https://bl.ocks.org/max-l/cda07bafcf7970e724b3aa00aefe9a02/8230c5db14e666efcb833c6c41c3c941f836729f
Why do the circles get "teleported" on the display, while the x,y coordinate shows no such "teleportation" ?
function redraw(data){
var circle = svg.selectAll("circle")
.data(data)
circle.enter().append("circle")
.attr("r", radius)
.transition()
.ease(d3.easeQuad)
.delay(rndDelay)
.duration(2000)
.attrTween("transform", translateAlong(path.node()))
.on("end", d => {
const lastP = faucet[2]
d.state = 1
d.x = lastP[0]
d.y = lastP[1]
console.log("a1",[d.x,d.y])
})
circle.filter(d => d.state == 1)
.attr("r", radius)
.attr("cx", d => d.x)
.attr("cy", d => {
console.log("a2",[d.x,d.y])
return d.y
})
}
After the transition is complete, you are both transforming with translate and positioning with cx/cy, which results in the position being off.
During the transition you set the transform for each circle:
.attrTween("transform", translateAlong(path.node()))
Afterwards you position by:
.attr("cx", d => d.x)
.attr("cy", d => d.y)
But this is added to the end transition point/translation (the end of the faucet). This is why everything appears normal except off by a fixed amount.
Just reset the transform after the transition.
Example
Or alternatively, update the translate with the new x/y values rather than using cx/cy.
I need to add a line with multiple values along the x axis of a multiBarChart. I cannot use another nvd3 control like multiChart or linePlusBarChart due to existing functionality breaking if i switch.
The following code and fiddle show what i have so far.
var limits = [[60,166990904656],[300,154990904656],[500,142990904656]];
var lineFunction = d3.svg.line()
.x(function(d) { return d[0] })
.y(function(d) {
d = d[1] / 1000000000;
return d
}).interpolate('step-after');
//The line SVG Path we draw
d3.select("#chart svg")
.append("path")
.attr("d", lineFunction(limits))
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("fill", "none");
https://jsfiddle.net/s2vemzht/11/
I am facing 3 issues: First is the x axis placement of the line. At the moment i have hardcoded this value into the limits array because i am unsure how to position it dynamically based on where the next bar starts.
The second issue is the limit value in relation to the other values in the data array. It does not seem accurately positioned.
The third issue is with the line not drawing over the 3rd bar even though there are 3 values in the limits array. I tried changing the interpolate property but it's still an issue.
I am a beginner with D3 so apologies for all the questions :-)
I managed to figure it out by drawing lines over the bars at particular points and positioning based on the y and x axis scales
var yValueScale = chart.yAxis.scale(),
yValue = 0,
xValueScale = chart.xAxis.scale(),
g = d3.select("#chart svg .nvd3");
for(var i=0;i<limits.length;i++){
g.append("line")
.attr("class","limit-line")
.attr("x1", xValueScale(d3.time.format('%b-%d')(new Date(limits[i][0]))))
.attr("y1", yValueScale(limits[i][1]/ 1000000000))
.attr("x2", xValueScale(d3.time.format('%b-%d')(new Date(limits[i][0]))) + parseInt(d3.select("#chart svg .nv-bar").attr("width")))
.attr("y2", yValueScale(limits[i][1]/ 1000000000))
;
}
https://jsfiddle.net/s2vemzht/25/
I'm trying to wrap my head around the log scales provided by D3.js. It should be noted that as of yesterday, I had no idea what a logarithmic scale was.
For practice, I made a column chart displaying a dataset with four values: [100, 200, 300, 500]. I used a log scale to determine their height.
var y = d3.scale.log()
.domain([1, 500])
.range([height, 1]);
This scale doesn't work (at least not when applied to the y-axis as well). The bar representing the value 500 does not reach the top of the svg container as it should. If I change the domain to [100, 500] that bar does reach the top but the axis ticks does not correspond to the proper values of the bars. Because 4e+2 is 4*10^2, right?
What am I not getting here? Here is a fiddle.
Your scale already reverses the range to account for the SVG y-coordinates starting at the top of the screen -- ie, you have domain([min, max]) and range([max, min]). This means your calcs for the y position and height should be reversed because your scale already calculated y directly:
bars.append("rect")
.attr("x", function (d, i) { return i * 20 + 20; })
.attr("y", function (d) { return y(d); })
.attr("width", 15)
.attr("height", function (d) { return height - y(d); });
Here's an updated Fiddle: http://jsfiddle.net/findango/VeNYj/2/