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/
Related
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.
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.
I have a plunker here https://plnkr.co/edit/hBWoIIyzcHELGyewOyZE?p=preview
I'm using this as starting point to create a stacked bar chart
https://bl.ocks.org/d3indepth/30a7091e97b03eeba2a6a3ca1067ca92
I need the chart to be vertical and to have axis
In my example I have the the chart vertical but I'm stuck getting it to start from the base and go upwards.
I know it's because the y attr but I'm stuck getting it to work.
.attr('y', function(d) {
return d[0];
})
try basing the y attribute as the difference between height and d[1]:
.attr('y', function(d) {
return height - d[1];
})
your plunker works for me (i think as desired?) with this correction.
explanation: these coordinates are relative to point 0, 0 (upper left hand corner) so for the y-axis of a graph like this you always have to flip it around... you captured it correctly in your axis scales (e.g. for the x: [0, width] and y: [height, 0])
I'd like to either:
dynamically adjust the tickFormat of an D3 timeaxis depending on current zoom, like first showing years -> zoom in -> show months -> zoom in -> show days and so on up to seconds.
I've tried a bit. See this fiddle.
var w = 700,
h = 50,
xY = d3.time.scale().range([0, w]),
xAxisY = d3.svg.axis()
.scale(xY)
.orient("bottom")
.ticks(10)
.tickSize(10, 1)
var svgY = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g");
svgY.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + 10 + ")");
svgY.append("svg:rect")
.attr("class", "pane")
.attr("width", w)
.attr("height", h)
.call(d3.behavior.zoom().on("zoom", zoom));
xY.domain([new Date(2000, 0, 1), new Date(2014, 0, 0)]);
xY.ticks(d3.time.minute, 1);
draw();
function draw() {
console.log("drawing");
svgY.select("g.x.axis").call(xAxisY);
}
function zoom() {
console.log("zooming");
d3.event.transform(xY); // TODO d3.behavior.zoom should support extents
draw();
}
It would be nice if the label format could be adjusted. If I zoom into minutely interval, th complete time shold be displayed ( 12:01 ) instead of sth like 12:00..............10......20......30....
or ( preferred, if possible ) :
add multiple x-axis with different formats below each other. In this case, the labels should disappear on overlap, eg:
-------2012--------------------------------------------------------------2013-----------------
--12----01----02----03----04----05----06----07----08----09----10----11----12----01----02------
Below the monthly axis there should appear a daily and so on. If I zoom into an detailled interval, the overlapping labels should disappear.
For this case I tried playing around with the above fiddle, simply adding futher axis / svgs but they react independently, eg: I have a yearly axis and a monthly axis. Zooming on the first also zooms the second and vice versa, but when I first change from first to second, both axis do a "jump". I think the second one moves back to its initial state on first moving.
Any ways to accomblish this?
In both cases, I'm surprised why the tick-lines have gone away?
In many examples the axis look like:
|2012 |2011 |2010
but the vertical lines are gone in the fiddled example!?
Im running a simple bar chart using d3.scale.linear() and hardcoding he domain range for this example
I can see in firebug that when aplying attr of width to my div, w_bar appears to be NaN.
Why is that?
var w_bar = d3.scale.linear()
.domain([0, 107525233]) //harcoded
.range(["0px", "290px"]);
var theList = d3.select("#list").selectAll("div")
.data(myJSON);
theList.enter().append("div")
.text(function (d) { return d.v; })
.transition()
.duration(1000)
.attr("width", w_bar); // Why NaN?
theList.exit()
.remove();
Here's the jsfiddle:
http://jsfiddle.net/NAhD9/5/
Well, w_bar is Not a Number, and the width attribute needs to be a number. Hence, NaN.
If you want your width attribute to scale based on the v attribute in your myJSON object, you should say
.attr("width",function (d) {return w_bar(d.v)}).
This is how scales work in d3, they are a function which takes in an argument (some value within the domain of the scale) and returns that value transformed to fit into the range of your scale.
Updated jsFiddle here.
You need to use a function to tell d3 how you want your data to interact with to scale to create an array:
.attr("width", function(d){ return w_bar(d.v); })
This will take all the v attributes from objects making up the myJSON array, scale them with w_bar, and set the width of their corresponding rectangles equal to that value.