d3js: zooming when there are two y axes - javascript

reference: https://github.com/mbostock/d3/wiki/Zoom-Behavior
//make zoom
var zoomFirst = d3.behavior.zoom()
.x(x)
.y(y1)
.scaleExtent([0, 3])
.size([w, h])
//.center([w/2+200, h/2-200])
.on("zoom", zoomedFirst);
function zoomedFirst() {
svgContainer.select(".x.axis").call(xAxis);
svgContainer.selectAll(".y.axis.axisLeft").call(yAxisLeft);
//set y2's scale manually
svgContainer.select(".price")
.attr("d", line1(priceData))
.attr("class", "price");
svgContainer.select(".difficulty")
.attr("d", line2(difficultyData))
.attr("class", "difficulty");
}
d3.behavior.zoom() supports autoscaling of x and y axes. However, I have to scale two y axes at the same time. When zoom() is triggered, I can get the current scale and translate info from d3.event.scale and d3.event.translate, but I cant figure out how to make appropriate scale for the second y axis(y2) with them.
I am also looking at https://github.com/mbostock/d3/wiki/Quantitative-Scales.
Since y1's range is automatically adjusted by zoom, if there is a way to get y1's current range, I can get its min and max and set y2's range based on them. However, the document doesn't specify a way to get range given a scale object.

Calling y1.range() (without any arguments) will return you the [min, max] of the scale.
From the docs:
If values is not specified, returns the scale's current output range.
Most accessor functions in D3 work like this, they return you (get) the value if you call them without any arguments and set the value if you call them with arguments and return the this object for easy chaining:
d3Object.propertyName = function (_) {
if (!arguments.length) return propertyName;
propertyName = _;
return this;
}
However, the zoom behaviour alters the domain and not the range of the scales.
From the docs:
Specifies an x-scale whose domain should be automatically adjusted when zooming.
Hence, you do do not need to get/set the range, but instead the domain of the scales y1 and y2: y2.domain(y1.domain()).

Since the zoom function already manages all the ratios, a more abbreviated answer would be:
var zoomFirst = d3.behavior.zoom()
.x(x)
.y(y1)
.scaleExtent([0, 3])
.size([w, h])
.on("zoom", function() {
zoomSecond.scale(zoom.scale());
zoomSecond.translate(zoom.translate());
// Update visual. Both y domains will now be updated
});
// Create copy for y2 scale
var zoomSecond = d3.behavior.zoom()
.x(x)
.y(y2) // <-- second scale
.scaleExtent([0, 3]) // <-- extent
This assumes you have called only zoomFirst to your visual.

Related

Adding an auto brush to the d3 timeline chart

I am working on a d3 timeline chart -- but on load - I want the brush to be automatically deployed -- with the option of fine-tuning on a particular set of in/out dates
https://jsfiddle.net/nu1z4d3r/
https://jsfiddle.net/2y8gkas3/8/ -- latest example --
I've tried adding the -- draw brush logic to the bottom of the code base
https://bl.ocks.org/micahstubbs/3cda05ca68cba260cb81
what would be the correct values to make this work -- should xTop be x2?
function drawBrush(a, b) {
// define our brush extent
// define our brush extent
// note that x0 and x1 refer to the lower and upper bound of the brush extent
// while x2 refers to the scale for the second x-axis, for the context or brush area.
// unfortunate variable naming :-/
var x0 = xTop.invert(a*w)
var x1 = xTop.invert(b*w)
console.log("x0", x0)
console.log("x1", x1)
brush.extent([x0, x1])
// now draw the brush to match our extent
// use transition to slow it down so we can see what is happening
// set transition duration to 0 to draw right away
//brush(d3.select(".brush").transition().duration(500));
// now fire the brushstart, brushmove, and brushend events
// set transition the delay and duration to 0 to draw right away
//brush.event(d3.select(".brush").transition().delay(1000).duration(500));
}
// call drawBrush once on load with the default value
//var zoomA = d3.select("input#a")[0][0].value;
//var zoomB = d3.select("input#b")[0][0].value;
var zoomA = 0;
var zoomB = -1;
drawBrush(zoomA, zoomB);
/*
// update the extent and call drawBrush again
window.setTimeout(function() {
d3.select("input#a")[0][0].value = .2;
d3.select("input#b")[0][0].value = .7;
var zoomA = d3.select("input#a")[0][0].value;
var zoomB = d3.select("input#b")[0][0].value;
drawBrush(zoomA, zoomB)
}, 2500);
*/
With the brush -- there were some modifications I had to make
https://jsfiddle.net/m6ueL79o/3/
where the brush is called -- we append a variable to the artefact. We make a 2nd call with "brush.move, x1.range()" -- this loads the scrubber
var brush = d3.brushX()
.extent([[0, 0], [w, miniHeight]])
.on("brush", brushed);
var gBrush = mini.append("g")
.attr("class", "x brush")
.call(brush)
.call(brush.move, x1.range());
otherwise -- to load just the chart first -- do not have the .call(brush.move... and at the base add "drawBrush(timeBegin, timeEnd);"

D3 bar chart sorting - axis label sorting logic explanation needed

I've build bar chart with sorting on click: https://codepen.io/wawraf/pen/gvpXWm. It's based on Mike Bostock's chart https://bl.ocks.org/mbostock/3885705.
It works fine, but when I tried to build it from scratch I realized there is something i do not fully understand: Line 72 contains following function:
var x0 = scaleX
.domain(data.sort(sort(direction))
.map(function(d) { return d[0]; }));
So it's using variable scaleX defined before (Line 16), but when instead of "scaleX" variable I want to use raw d3 reference (which is actually the same as scaleX):
var x0 = d3.scaleBand().rangeRound([0, width - margin * 2])
.domain(data.sort(sort(direction))
.map(function(d) { return d[0]; }));
axis sorting ("g" elements) doesn't work.
I would be glad if anyone could explain why it doesn't actually work.
When you do...
var x0 = scaleX.domain(data.sort(sort(direction)).map(function(d) {
return d[0];
}));
... you are not only setting a new variable x0, but changing the scaleX domain as well. As the axis is based on scaleX, not x0, it won't do the transition in your second case, which only sets x0 (without changing scaleX).
You can certainly do:
var x0 = d3.scaleBand()
.rangeRound([0, width - margin * 2])
.domain(data.sort(sort(direction))
.map(function(d) {
return d[0];
}));
As long as you change the axis' scale:
xAxis.scale(x0);
here is the updated CodePen with those changes: https://codepen.io/anon/pen/VQveBy?editors=0010

Placing D3 tooltip in cursor location

I'm using d3-tip in my visualisation. I now want to add tooltips to elements that are very wide and may extend out of the visible canvas. By default, the tooltip is shown in the horizontal center of an object, which means in my case that the tooltip might not be in the visible area. What I need is the tooltip showing up in the horizontal position of the cursor but I don't know how to change the tooltip position correctly. I can set an offset and I can get the coordinates of the cursor, but what I can't get is the initial position of the tooltip so that I can compute the right offset. Nor can I set an absolute position:
.on("mouseover",function(d){
var coordinates = [0, 0];
coordinates = d3.mouse(this);
var x = coordinates[0];
var y = coordinates[1];
tip.offset([-20,20]); // this works
tip.attr("x",40); // this doesn't
tip.show(d);
})
If you want to use offset, you can get the initial position of the tooltip after tip.show(d):
tip.style('top');
tip.style('left');
Similarly, to set the absolute position:
.on('mouseover', function(d){
var x = d3.event.x,
y = d3.event.y;
tip.show(d);
tip.style('top', y);
tip.style('left', x);
})
The previously stated answer did not work for me (and cannot be modified as "suggested edit queue is full.."), but with some minor adjustments, it is working fine:
.on('mouseover', function(d){
var x = d3.event.x,
y = d3.event.y;
tip.show(d);
tip.style('top', y-10 + 'px'); // edited
tip.style('left', x+'px'); // edited
})

d3.js rewriting zoom example in version4

Drag and Drop Example
I am trying to rewrite part of this example above to use in my code, specifically this piece:
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
However I am getting stuck since the v4 package has changed quite a bit. I wrote my zoomListener function to be
var zoomListener = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
function zoomed() {
transform = d3.event.transform;
console.log(d3.event);
svg.attr("transform", transform);
}
function centerNode(source){
t = transform;
console.log(t);
x = t.x*t.k; //I only want things to be centered vertically
y = (t.y + -source.x0)*t.k + (viewerHeight)/2 ;
svg.transition()
.duration(duration)
.attr("transform","translate(" + x + "," + y +")scale(" + t.k + ")");
transform.scale(t.k); //DOES NOT WORK
transform.translate([x, y]); //DOES NOT WORK
}
and I know that according to the doc things have changed and info are no longer are stored on what would be my zoomListener
D3 V4 release note on zoom I guess I am just confused on how I am suppose to do it with the new version. The last few lines of my centerNode function don't work which has for effect that when I center the node the zooming and panning reset...
Any suggestion?
So after much digging and trial and error I cam up with an answer that works pretty well for my purposes. Note that this code below is only the relevant part of my code not the whole code, certain variable were self explanatory so did not include them. ALSO THIS IS IN VERSION 4 of d3.js.
var zoom = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight);
var zoomer = svg.append("rect")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
var g = svg.append("g");
zoomer.call(zoom.transform, d3.zoomIdentity.translate(150,0)); //This is to pad my svg by a 150px on the left hand side
function zoomed() {
g.attr("transform", d3.event.transform);//The zoom and panning is affecting my G element which is a child of SVG
}
function centerNode(source){
t = d3.zoomTransform(zoomer.node());
console.log(t);
x = t.x;
y = source.x0;
y = -y *t.k + viewerHeight / 2;
g.transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + t.k + ")")
.on("end", function(){ zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))});
}
As per the examples for v4 on the d3.js page, I used a rectangle to apply the zoom to
The zoom behavior is applied to an invisible rect overlaying the SVG
element; this ensures that it receives input, and that the pointer
coordinates are not affected by the zoom behavior’s transform. Pan & Zoom Example
In the Center node function I am using d3.zoomTransform(zoomer.node()); to get the current transform applied to the page.
The purpose of this function is only to center the collapsible tree vertically not horizontally, so I am keeping the current transform.x (here t.x) the same.
The coordinate in my svg are flip hence why y= source.x0, source is a what node was clicked in my collapsible tree. ("Look to the example referenced to the top of this thread to understand what I am trying to convert to version 4)
I am apply the transformation to my G element and then I want to commit those changes to the zoom transform, to do so I use the .on("end", function(){}) otherwise it was doing weird behavior with the transition, by doing that all it does is setting the current state of the transform.
zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))
This line above is applying a translation of x and y and a scale -- that is equal to what the current state -- to the identiy matrix has to get a new transform for G, i then apply it to zoomer which is the element I called zoom on earlier.
This worked like a charm for me!
Calling transform.scale and transform.translate returns a new transform, and modifies nothing. Therefore:
transform = transform.translate([x, y]).scale(k)
svg.call(zoomListener.transform, newTransform)
(At this point zoomListener is a pretty inaccurate name for this, but regardless...)
k, x, and y can be derived from source, maybe as you show, but I'm not sure, because I don't know what source is. But to me, t.x*t.k looks suspicious, because it's multiplying the existing transforms x by its scale. Seems like it would cause a feedback loop.
For more into about the zoom in v4, check out this related StackOverflow post, or this example by mbostock demonstrating programmatic control over the zoom transform of an element (canvas in this case) and includes transitions.

Starting linear scale from 1 instead of 0 (D3.js)

I'm trying to create a line graph with D3.js and I want my X axis to start from 1 instead of 0.
The code looks as follows:
var temp = [36.5, 37.2, 37.8, 38.2, 36.8, 36.5, 37.3, 38.2, 38.3, 37];
var x = d3.scale.linear().domain([0,temp.length]).range([0, w]);
var xAxis = d3.svg.axis().scale(x).tickSize(-h).tickSubdivide(false);
graph.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
When I change this to:
var x = d3.scale.linear().domain([1,temp.length]).range([0, w]);
the scale get's edited but the graph starts outside of the graph itself.
I tried to use tickvalues but I can't get this to work.
How can I let my scale start from 1?
I'm assuming you are labeling your axis by using the index of the data.
When you set your domain starting at 1, you're actually just telling your chart to make the left-most part of your graph be the x-coordinate of the second datum (index 1).
When you actually create the chart, there is a datum (index 0, value 36.5) that is outside of your defined domain, and d3 uses linear extrapolation to determine where it should be placed, making it end up to the left of the start of your chart.
What you really want to do is start your domain at 0, so that the first datum is in your domain, but to reformat your tick labels so that they show the index incremented by 1.
You can use axis.tickFormat() to do this.
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(-h)
.tickFormat(function(d) { return d + 1; })
Side note: you shouldn't specify .tickSubdivide(false), since:
That function expects a number, not a boolean, and false will be coerced to 0.
The default value is 0 anyways.
axis.tickSubdivide is deprecated and does nothing as of version 3.3.0

Categories

Resources