D3js brush area on zoom not centered - d3.js

I'm working on zooming and brushing on a barchart in D3js. I'm following this example: https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172.
I'm having issue's when zooming with the brush area. It's not centered correctly (most of the times). When it's not centering correct it's centered too much centered to the left (see screenshot).
This is the code for brushing and zooming
// Zoom
var zoom = d3.zoom()
.scaleExtent([.9, 20])
.translateExtent([[0, 0], [graph.width(), graph.height()]])
.on('zoom', zoomUpdateChart);
svg.call(zoom);
function zoomUpdateChart() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type == "brush") return;
// recover the new scale
var transform = d3.event.transform;
var zoomLevel = d3.event.transform.k;
graph._xScale.domain(transform.rescaleX(graph._xScaleZoom).domain());
resetZoomBtn.attr("opacity", 1)
.on("click", resetZoom);
graph.TooltipRemove(tooltip);
// regenerating the box axis
axis.remove();
axis = graph.RenderBoxAxis(g, graph._xScale);
bars.attr("x", function(d) { return graph._xScale(d.x); })
.attr("width", barWidth * zoomLevel );
zoomSection.select(".brush").call(brush.move, graph._xScale.range().map(transform.invertX, transform));
}
function resetZoom() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity)
.on("end", () => {
resetZoomBtn.attr("opacity", 0.2)
.on("click", null);
});
graph.TooltipRemove(tooltip);
}
// Brush
var brush = d3.brushX()
.extent([[0, 0], [graph._width, graph._heightZoom]])
.on("brush", brushed);
zoomSection.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, graph._xScale.range());
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type == "zoom") return;
const selection = d3.event.selection || graph._xScaleZoom.range();
graph._xScale.domain(selection.map(graph._xScaleZoom.invert, graph._xScaleZoom));
const selectionWidth = selection[1] - selection[0];
const scale = (selectionWidth / graph._width);
const zoomLevel = 1 / scale;
resetZoomBtn.attr("opacity", 1)
.on("click", resetZoom);
graph.TooltipRemove(tooltip);
// regenerating the box axis
axis.remove();
axis = graph.RenderBoxAxis(g, graph._xScale);
bars.attr("x", function(d) { return graph._xScale(d.x) })
.attr("width", barWidth * zoomLevel)
svg.call(zoom.transform, d3.zoomIdentity
.scale(graph._width / selectionWidth)
.translate(-selection[0], 0));
}
I've done some research myself. The problem is within setting the range for the new brush position.
zoomSection.select(".brush").call(brush.move, graph._xScale.range().map(transform.invertX, transform));
It's set to [-166.07655066619142, 298.5037864592652]. The first index should be a positive number but is not calculated correctly. I've looked over it a couple of hours but haven't found the solution.
Some more details: The page loads more graphs of different types like areachart and line charts.

After much searching I found the anwser. I wasn't setting .extent() on zoom. The correct code is:
var zoom = d3.zoom()
.scaleExtent([0.9, 20])
.extent([[0, 0], [graph._width, graph._height]])
.translateExtent([[0, 0], [graph._width, graph._height]])
.on('zoom', zoomUpdateChart);

Related

Updating zoom behavior from v3 to v5

The problem i am facing is that i am not able to write the function following in my code because of version mismatch.
Code is
var zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1, 10])
.on("zoom", zoomed);
I have tried by this way
const zoom = d3.zoom()
.scaleExtent([1, 4])
.x(this.xScale)
.on('zoom', () => {})
But it does not work for me.
How to write same function in d3 version 5? I want to make line chart scrollable in x axis with y axis as fixed position using d3 version 5
This is my implementation Basic Code
private createLineChart() {
this.width = 2000 - this.margin.left - this.margin.right;
this.height = 600 - this.margin.top - this.margin.bottom;
// X AXIS
this.xScale = d3.scaleBand()
.domain(this.dataset[0].fluencyData.map((data) => {
return new Date(data.date);
}))
.range([0, this.width]);
// Y AXIS
this.yScale = d3.scaleLinear()
.domain([0, 110])
.range([this.height, 0]);
// Line Generator
this.line = d3.line()
.x((data) => this.xScale(new Date(data.date)))
.y((data) => this.yScale(data.wcpm));
// .curve(d3.curveMonotoneX);
// Add SVG to Div
this.svg = d3.select('#displayChart').append('svg')
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr(
'viewBox',
'0 0 ' +
(this.width + this.margin.left + this.margin.right) +
' ' +
(this.height + this.margin.top + this.margin.bottom))
// .attr('width', this.width + this.margin.left + this.margin.right)
// .attr('height', this.height + this.margin.top + this.margin.bottom)
.append('g')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
// Define the div for the tooltip
this.toolTipDiv = d3.select('#displayChart').append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
// Append XAXIS to the SVG
this.svg.append('g')
.attr('class', 'xAxis')
.attr('transform', 'translate(0,' + this.height + ')')
.call(d3.axisBottom(this.xScale).tickSizeOuter(0).tickFormat(d3.timeFormat('%b %d')));
const zoom = d3.zoom()
.scaleExtent([1, 4])
.extent([100, 100], [this.width - 100, this.height - 100])
.x(this.xScale)
.on('zoom', () => {
console.log(d3.event.transform);
// this.svg.select('#displayChart').attr('d', this.line);
});
this.svg.call(zoom);
// Append YAXIS to SVG
this.svg.append('g')
.attr('class', 'yAxis')
.call(d3.axisLeft(this.yScale).tickSize(-this.width)
);
// Make a Path for Dataset
this.svg.append('path')
.datum(this.dataset[0].fluencyData)
.attr('class', 'line')
.attr('d', this.line)
.attr('transform', 'translate(' + this.margin.left + ',0)');
// Text Heading of DATE in chart
this.svg.append('text')
.attr('transform', 'translate(' + (-20) + ',' + (this.height + 13) + ')')
.attr('dy', '.35em')
.attr('class', ' xAxis')
.text('Date');
}
}
Error I am getting is
LineChartComponent_Host.ngfactory.js? [sm]:1 ERROR TypeError: d3__WEBPACK_IMPORTED_MODULE_2__.zoom(...).scaleExtent(...).x is not a function
at LineChartComponent.push../src/app/line-chart/line-chart.component.ts
With d3v3 and before, the zoom could track a scale's state. From the documentation, scale.x(): "Specifies an x-scale whose domain should be automatically adjusted when zooming." (docs). This modifies the original scale.
D3v4+ does not have zoom.x or zoom.y methods.
With d3v4+, the zoom does not track or modifiy a d3 scale's state. Infact, for d3v4+, the zoom behavior doesn't even track the current zoom state: "Zoom behaviors no longer store the active zoom transform (i.e., the visible region; the scale and translate) internally. The zoom transform is now stored on any elements to which the zoom behavior has been applied.(change log)".
As part of this, and more importantly, "Zoom behaviors are no longer dependent on scales, but you can use transform.rescaleX, transform.rescaleY, transform.invertX or transform.invertY to transform a scale’s domain(change log)".
So rather than have the zoom update the d3 scale, we need to do this ourselves. The most common way this is done is through a reference scale, which remains unchanged, and a scale to which we apply the zoom transform:
var zoom = d3.zoom()
.on("zoom",zoomed)
var x = d3.scaleLinear().... // working scale
var x2 = x.copy(); // reference scale.
function zoomed() {
x = d3.event.transform.rescaleX(x2) // update the working scale.
// do something...
}
So, something like this:
var x = d3.scale.linear()
.domain([0,1])
.range([0,500]);
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.svg.axis()
.orient("bottom")
.scale(x);
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<svg width="500" height="200"></svg>
Becomes something like that:
var x = d3.scaleLinear()
.domain([0,1])
.range([0,500]);
var x2 = x.copy(); // reference.
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.axisBottom().scale(x)
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
x = d3.event.transform.rescaleX(x2)
axis.scale(x);
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="200"></svg>
Note that d3.event.transform.rescaleX is for continuous scales - you have an ordinal band scale, so we'll need to use a slightly modified approach for band and/or point scales:
var x = d3.scaleBand()
.domain(d3.range(10).map(function(d) { return d/10; }))
.range([0,500]);
var x2 = x.copy(); // reference.
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var svg = d3.select("svg")
.call(zoom);
var axis = d3.axisBottom().scale(x)
var axisG = svg.append("g")
.attr("transform", "translate(0,30)")
.call(axis);
function zoomed() {
// Rescale the range of x using the reference range of x2.
x.range(x2.range().map(function(d) {
return d3.event.transform.applyX(d);
}))
axisG.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="200"></svg>
This is band/point scale solution is based on this issue and Bostock's proposed solution to it

reversing order of d3.zoom scale and translate

If you click the red button in this example:
https://bl.ocks.org/interwebjill/fe782e6f195b17f6fe6798a24c390d90
you can see that the chart translates so that the circle is in the center and then zooms in to a specified level (reclicking on the button zooms back out). Translating and then zooming in this way leaves a gap on the left that I would rather not have. How might I change the code so that the chart zooms first and then translates to center so that I don't have this gap in the chart?
I have tried reversing the order of the scale and translate in both the zoom definition and the zoomToExtent function but there is no different in effect.
The ultimate source of the problem is d3.interpolateZoom. This interpolator has scale interpolate faster than translate - even though they mostly both are transitioning at the same time. The pattern implemented with d3.interpolateZoom is based on this paper.
Because scale and translate both interpolate differently in d3.interpolateZoom, you get a gap in the side of your chart as the scale decreases/increases more rapidly than the translate values.
d3.interpolateZoom is used when you call the zoom on a transition.
However, if you apply a transform directly on a transition using .attr(), the d3 transition will use d3.interpolateString, which will search the start and end strings for corresponding numbers and use d3.interpolateNumber on those. This will apply the same interpolation to both scale and translate.
Using both methods we can compare the discrepancy between d3.interpolateZoom and d3.interpolateString. Below the black rectangle uses d3.interpolateString while the orange rectangle uses d3.interpolateZoom. Click on a rectangle to start the transition:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 300);
var g1 = svg.append("g"), g2 = svg.append("g");
var zoom1 = d3.zoom().on("zoom", function() {
g1.attr("transform", d3.event.transform);
});
var zoom2 = d3.zoom().on("zoom", function() {
g2.attr("transform", d3.event.transform);
});
g1.call(zoom1.transform, d3.zoomIdentity
.translate(150, 100)
.scale(2));
g2.call(zoom2.transform, d3.zoomIdentity
.translate(150,100)
.scale(2));
g1.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 50)
.attr("height", 50);
g2.append("rect")
.attr("x", 22)
.attr("y", 22)
.attr("width", 46)
.attr("height",46)
.attr("fill","orange");
d3.selectAll("rect").on("click", function() {
g1.transition()
.duration(6000)
.attr("transform", d3.zoomIdentity)
.on("end", function() {
d3.select(this).call(zoom1.transform, d3.zoomIdentity);
})
g2.transition()
.duration(6000)
.call(zoom2.transform, d3.zoomIdentity)
});
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
Where the first rectangle transitions the transform with .attr(), we need to call the zoom afterwards to ensure the zoom has the current transform, we don't need to in this example, but if you wanted to use the zoom after the transform you need to do this
Comparing these two we get:
(Y axis indicates percentage remaining in transition from start attribute to end attribute)
You want scale and translate to move simultaneously at the same rate when transitioning. We can do this if we use a tweening function. Unlike above we can't just use transition().attr("transform",newTransfrom) because you are also drawing canvas and updating the axis. So we'll need to create our own tweening function that can use the current transform and scale, apply it to the axis, canvas, and markers.
For example, rather than calling the zoom (which will use d3.interpolateZoom):
function zoomToExtent(d0, d1) {
zoomRect.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.translate(-xSVG(d0), 0)
.scale(width / (xSVG(d1) - xSVG(d0))));
}
Instead, we can use a tweening function which controls the element's transform and applies the same interpolator to scale and translate:
function zoomToExtent(d0, d1) {
//get transition start and end values:
var startScale = d3.zoomTransform(zoomRect.node()).k;
var startTranslate = d3.zoomTransform(zoomRect.node()).x;
var endTranslate = -xSVG(d0);
var endScale = width / (xSVG(d1) - xSVG(d0));
zoomRect.call(zoom).transition()
.duration(1500)
.tween("transform", function() {
var interpolateScale = d3.interpolateNumber(startScale,endScale);
var interpolateTranslate = d3.interpolateNumber(startTranslate,endTranslate);
return function(t) {
var t = d3.zoomIdentity.translate(interpolateTranslate(t),0).scale(interpolateScale(t));
zoomed(t);
}
})
.on("end", function() { // update the zoom identity on end:
d3.select(this).call(zoom.transform, d3.zoomIdentity
.translate(endTranslate, 0)
.scale(endScale));
})
}
You may notice I'm passing a transform value to the zoomed function, since there is no d3.event.transform for this, we need to modify the zoomed function to use the passed parameter if available, otherwise to fall back on the event transform:
function zoomed(transform) {
var t = transform || d3.event.transform;
...
Altogether, that might look something like this.
For another comparison between the two transitioning methods, I've created a gridded comparison that can be toggled between the two zoom identities:
var svg = d3.select("body").append("svg")
.attr("width", 510)
.attr("height", 310);
var g1 = svg.append("g");
var g2 = svg.append("g");
var rectangles1 = g1.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","#ccc")
.attr("stroke","white")
.attr("stroke-width", 2);
var rectangles2 = g2.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","none")
.attr("stroke","#444")
.attr("stroke-width", 1);
var startZoom = d3.zoomIdentity
.translate(-250,-200)
.scale(4);
var endZoom = d3.zoomIdentity
.translate(-100,-100)
.scale(5);
var zoom1 = d3.zoom().on("zoom", function() { g1.attr("transform", d3.event.transform); });
var zoom2 = d3.zoom().on("zoom", function() { g2.attr("transform", d3.event.transform); });
g1.call(zoom1.transform, startZoom);
g2.call(zoom2.transform, startZoom);
var toggle = true;
svg.on("click", function() {
toggle = !toggle;
g1.transition()
.duration(5000)
.call(zoom1.transform, toggle ? startZoom: endZoom)
g2.transition()
.duration(5000)
.attr("transform", toggle ? startZoom: endZoom)
.on("end", function() {
d3.select(this).call(zoom2.transform, toggle ? startZoom: endZoom);
})
})
rect {
opacity: 0.5;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>

Add Zoom to a Grouped Bar Chart that uses a scaleBand

I'm trying to use rescaleX on a scaleBand to add zoom functionality to a Grouped Bar Chart.
var x0 = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1);
var x1 = d3.scaleBand()
.padding(0.05);
var zoom = d3.zoom()
.scaleExtent([1, 8])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", () => { zoomed() });
...
function zoomed() {
var t = d3.event.transform;
var x0t = t.rescaleX(this.x0);
var x1t = t.rescaleX(this.x1);
...
}
But t.rescaleX(this.x0) doesn't work on a scaleBand, how can I apply zoom functionallity to a scaleBand?
Here is a full codepen Grouped Bar Chart
I was able to achieve this by changing the zoomed function to:
function zoomed() {
var t = d3.event.transform;
// redefine the x0 domain range with the event transform scale (k)
x0.range([0, width * t.k]);
// transform .barGroup using redefined domain range and event transform params
g.selectAll(".barGroup")
.attr("transform", function(d) { return "translate(" + (x0(d.State) + t.x) + ",0)scale(" + t.k + ",1)"; });
// apply transform to .axis--x and call xAxis
g.select(".axis--x")
.attr("transform", "translate(" + t.x + "," + (height) + ")")
.call(xAxis);
}
Full codepen here
Update:
A better method of zooming a scaleBand bar chart by Mike Bostock (non-grouped)
https://beta.observablehq.com/#mbostock/d3-zoomable-bar-chart
Update 2:
Updated Grouped bar chart zoom function:
function zoomed() {
x0.range([0, width].map(d => d3.event.transform.applyX(d)));
x1.rangeRound([0, x0.bandwidth()]);
g.selectAll(".barGroup").attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; });
g.selectAll(".bar").attr("x", function(d) { return x1(d.key); }).attr("width", x1.bandwidth());
g.select(".axis--x").call(xAxis);
}
Full Codepen: here

D3.js: How to keep marker and text on same position when zooming in and out

I have a D3.js map based on this: https://bl.ocks.org/mbostock/2374239.
I have added a custom marker and a text on the feature that a user zooms in to a county. However, the marker and text do not stay on the same position as the county when I zoom in or out. My zoom function is as follows:
function zoomToCounty(stname, cntyname) {
d3.json("/topo/us-wgs84.json", function (us) {
var t = projection.translate(); // the projection's default translation
var s = projection.scale() // the projection's default scale
//initialize marker
d3.selectAll(".mark").remove();
d3.selectAll("text").remove();
//reset active to inactive size and color
d3.select("#svgMap2").select("g").select(".active")
.style("stroke-width", "0.5px")
.style("stroke", "#808080");
d3.selectAll(".county")
.classed("active", function (d) {
if (d.properties.StateName === stname && d.properties.County === cntyname) {
var zoom = d3.zoom()
.scaleExtent([1, 6])
.on("zoom", zoomed3);
svg.select("rect")
.call(zoom);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = 0.9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
//get centroid
var center = path.centroid(d);
//create marker
d3.select("#svgMap2").select("g")
.append("image")
.attr("xlink:href", "/images/marker2.png")
.attr("width", 14)
.attr("height", 20)
.attr("class", "mark")
.attr("transform", function (d) {
return "translate(" + center + ")";
});
//add text
d3.select("#svgMap2").select("g")
.append("text")
.style("fill", "#000")
.attr("x", x)
.attr("y", y)
.attr("dy", ".05em") //set offset y position
.attr("text-anchor", "middle") //set anchor y justification
.text(cntyname);
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
return true;
}
})
}); //end d3.json
Working website can be found at: http://realtimeceap.brc.tamus.edu/
Thanks in advance.
01-28-2018 Status: I'm still unable to fix this one. I just need help on how to keep my image marker and text on the same coordinates as the selected feature when I zoom in/out using the mouse wheel. Initial zoom is at the middle of the svg with scale = 8. How do I make the marker "STICK" to specified coordinates once a user moves the wheel? HELP!
Solved this problem by:
1. I called the zoom function on the "g" element then
2. I created a function for when user moves the wheel; called this function from the "rect" element.
View the working codes at: http://realtimeceap.brc.tamus.edu

D3 scatter plot - visible points outside the chart after zoom

I'm new to D3 and I've a question related to adding zoom into a chart.
I built a scatter plot with zoom/pan and everything is working except for the fact that when I use the zoom on the chart, I can see some points outside the chart "area" and I really want to avoid that.
The zoom behavior looks like this:
var zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([1, 10])
.on("zoom", zoomed);
And the zoomed function like this:
function zoomed() {
var panX = d3.event.translate[0];
var panY = d3.event.translate[1];
var scale = d3.event.scale;
panX = panX > 10 ? 10 : panX;
var maxX = -(scale-1)*width-10;
panX = panX < maxX ? maxX : panX;
panY = panY > 10 ? 10 : panY;
var maxY = -(scale-1)*height-10;
panY = panY < maxY ? maxY : panY;
zoom.translate([panX, panY]);
main.select(".x.axis").call(xAxis);
main.select(".y.axis").call(yAxis);
main.selectAll("circle")
.attr("cx", function (d,i) { return xScale(d[0]); } )
.attr("cy", function (d) { return yScale(d[1]); } )
.attr("r", 5);
}
You can see a working example (and all the code) here
Is there a way I can define the "area" of scope for the zoom, so any point outside this area is not visible? or can I add something into the zoomed function to fix this?
Thank you very much.
One option would be to restrict the g region of the element in which your points are drawn using a clip path
See http://jsfiddle.net/henbox/duwyay3y/1/ for the code.
First define your clip path:
var clip = main.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr('width', width)
.attr('height', height);
then add to the g element
main.append("g").attr("clip-path", "url(#clip)")
.selectAll("circle")
...

Resources