How to get the result of zoom.translate() without altering the sensibility of panning? - d3.js

I have a D3.js draw in a svg and I want to be able to pan and zoom on it. I also want a rectangle to be drawn around the initial window. I want the rectangle to become smaller as I zoom out, larger as I zoom in and to move normally as I pan around. Usual stuff.
I also want to be able to redraw the rectangle to make it fit again the actual window (so it will be smaller than the first if I zoomed in between the redraw for example). I didn't included this part in the code example but it explains why I need a way to get the zoom properties.
To properly trace the rectangle I found the zoom.translate() and zoom.scale() property, supposed to give me the parameters I need to calculate the coordinates of the rectangle's parts. However since I added this part to my code the panning sensibility became to shift as I zoom in and out: The more I zoom in, the less sensible is the panning, and the more I zoom out the more sensible it becomes.
In my mind, zoom.translate() and zoom.scale() were only supposed to fetch the parameters, not to change the way zooming and panning work, how can I fix that?
I also have inexplicably a rectangle that doesn't fit the window: it is a bit larger and shorter.
Here my piece of code:
var svg;
var map;
var zoom;
var graph = d3.select("#graph");
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.on("zoom", function () {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
});
svg = graph.append("svg")
.attr("width",window.innerWidth)
.attr("height",window.innerHeight)
.call(zoom);
map = svg.append("g");
Here is the acquisition of the rectangle's coordinates:
var w_b = {
x_min: (0 - zoom.translate()[0])/zoom.scale(),
x_max: (svg.attr("width") - zoom.translate()[0])/zoom.scale(),
y_min: (0 - zoom.translate()[1])/zoom.scale(),
y_max: (svg.attr("height") - zoom.translate()[1])/zoom.scale()
And here is the drawing of the rectangle:
map.selectAll("line").remove();
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_max']).attr("y1", w_b['y_min']).attr("y2", w_b['y_min'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_max']).attr("y1", w_b['y_max']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_min']).attr("y1", w_b['y_min']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_max']).attr("x2", w_b['x_max']).attr("y1", w_b['y_min']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
As a bonus question, this doesn't work at all on Chrome, any idea about what it could be ?

To work correctly, the zoom must be defined like this:
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.on("zoom", function () {
map.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
});
Notice the "map.attr" instead of "svg.attr".
However it doesn't solve the issues about the size of the window and the fact it's not working on Chrome.

Related

Synchronize an image with the rest of the svg with D3js

I am trying to plot some points on a map. My map is a svg generated separately (in the PATH_PARAM file). It is supposed to act as a background, with details that must be at specific coordinates in the final plot. Even if it's not exactly that you can imagine the map is a picture of a face I traced myself (so I know its size and where are each elements in the map's referential) and I want to add points precisely in the middle of the eyes after importing it on my svg with D3.js.
For now I simply append it to the svg, like so: (this javascript is called during the loading of the page)
var SIZE_PARAM = 200;
var DATA_PARAM = [{position: [0,1]}, {position: [4,5]}] //actually it's a far more bigger set of points.
var graph = d3.select("#graph");
svg = graph.append("svg")
.attr("width",window.innerWidth)
.attr("height",window.innerHeight)
.call(d3.behavior.zoom().on("zoom",
function () {svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")")}
))
.append("g");
background = svg.selectAll("image")
.data([0])
.enter()
.append("svg:image");
background.attr("xlink:href", PATH_PARAM)
.attr("width",SIZE_PARAM)
.attr("height",SIZE_PARAM);
scale = d3.scale.linear()
.domain([dom_x_min, dom_x_max])
.rangeRound([ 0, SIZE_PARAM ])
.clamp(true);
circles = svg.selectAll("circle")
.data(DATA_PARAM)
.enter()
.append("circle");
circles.attr("class", "point")
.attr("cx", function(d,i) {return scale(d.position[0]);})
.attr("cy", function(d,i) {return scale(d.position[1]);})
.attr("r", function(d,i) {return 1;})
The problem I got is that the map and the points are not synchronized with the map. The relative position between points is good, and I managed to have the same scale for the map and the points by setting a unique SIZE_PARAM parameter, but there is a translation between the points and the map. This translation is not visible for a small number of points (like the two of the example) but becomes more and more noticeable as this number increases.
Following some advice I added this to the CSS:
#graph * {
position: absolute;
top: 0px;
left: 0px;
}
But it doesn't solve my problem.
What can I do to synchronize the map with the points ?

Disable pan on mousewheel when zooming, but retain it when dragging

I'm trying to use a zoom feature in d3. It's the simple one, with the following code
var zoom = d3.behavior.zoom()
.scaleExtent([0.5, 4])
.on("zoom", function() {
this.svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}.bind(this));
It's working more or less fine, so it zoom on mousewheel and pans on drag. I want that. But when zooming with mousewheel, it also pans. That, I don't want.
It should only zoom, when mousewheel and pan when dragged. Tried few things already, including separate drag functions and disabling part of the mousewheel (which I failed miserably), but to no avail.
Could You please help me?
By default, your code will zoom centered to the mouse position.
If you want to zoom to the center of the screen, you might have to explicitly set center() on your zoom behavior like so:
var zoom = d3.behavior.zoom()
.scaleExtent([0.5, 4])
.center([width / 2, height / 2])
.on("zoom", function() {
this.svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}.bind(this));
If at any stage you need to reset the default zoom center behaviour (to zoom to the current mouse position), you can do so with:
zoom.center(null);
Src: Zoom Center

Modifiying dimple layout grouped chart

I am using the same chart as below. I want to push the x-axis headers i.e. Regular, Premium, Budget little bit below i.e. top padding or margin. Give some styling to it like give background color and change text color. I tried using fill and it does not work as desired. I would like to hide Price Tier/Channel also
http://dimplejs.org/examples_viewer.html?id=bars_vertical_grouped
These are SVG text elements so there is no top-padding or margin. You can move them down a bit by increasing the y property though, running the following after you call the chart.draw method will move the labels down 5 pixels:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) {
// Get the y property of the current shape and add 5 pixels
return parseFloat(d3.select(this).attr("y")) + 5;
});
To change the text colour you need to use the fill property (again that's an svg text thing):
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.style("fill", "red");
To colour the background of the text is a little less trivial, there actually isn't a thing for that in SVG, however you can insert a rectangle behind the text and do what you like with it:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
// Iterate each shape matching the selector above (all the x axis labels)
.each(function () {
// Select the shape in the current iteration
var shape = d3.select(this);
// Get the bounds of the text (accounting for font-size, alignment etc)
var bounds = shape.node().getBBox();
// Get the parent group (this the target for the rectangle to make sure all its transformations etc are applied)
var parent = d3.select(this.parentNode);
// This is just the number of extra pixels to add around each edge as the bounding box is tight fitting.
var padding = 2;
// Insert a rectangle before the text element in the DOM (SVG z-position is entirely determined by DOM position)
parent.insert("rect", ".dimple-custom-axis-label")
// Set the bounds using the bounding box +- padding
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
// Do whatever styling you want - or set a class and use CSS.
.style("fill", "pink");
});
These three statements can all be chained together so the final code will look a bit like this:
d3.selectAll(".dimple-axis-x .dimple-custom-axis-label")
.attr("y", function (d) { return parseFloat(d3.select(this).attr("y")) + 5; })
.style("fill", "red")
.each(function () {
var shape = d3.select(this);
var bounds = shape.node().getBBox();
var parent = d3.select(this.parentNode);
var padding = 2;
parent.insert("rect", ".dimple-custom-axis-label")
.attr("x", bounds.x - padding)
.attr("y", bounds.y - padding)
.attr("width", bounds.width + 2 * padding)
.attr("height", bounds.height + 2 * padding)
.style("fill", "pink");
});
FYI the dimple-custom-axis-label class was added in a recent release of dimple so please make sure you are using the latest version. Otherwise you'll have to find an alternative selector

set d3 zoom scaleExtent to positive values

Well, first of all, here is a jsfiddle to illustrate what I would like to ask
So I am trying to implement a zoom behaviour. I managed to limit the zooming by setting a translate to the zoom function, (which I don't really quite understand, Just got it from another SO question), and a scaleExtent([1,10]) on the zoom behaviour
So this is the zoom behavior:
zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([1, 10])
.on("zoom", zoomed)
and this is the zoomed function:
zoomed = ->
t = zoom.translate()
s = zoom.scale()
tx = t[0]
ty = t[1]
tx = Math.min(0, Math.max(w * (1-s), t[0]))
ty = Math.min(0, Math.max((h-padding) * (1-s), t[1]))
zoom.translate([tx, ty])
svgContainer.select("g.x.axis").call xAxis
svgContainer.select("g.y.axis").call yAxis
svgContainer.selectAll("line.matched_peak")
.attr("x1", (d) -> return xScale(d.m_mz) )
.attr("y1", h - padding)
.attr("x2", (d) -> return xScale(d.m_mz) )
.attr("y2", (d) -> return yScale(d.m_intensity) )
However, setting the scaleExtent to
.scaleExtent([1, 10]) ,
makes that one particular point with a value of 1 in the y scale is never going to be displayed (first one on the left in the jsfiddle).
But setting
.scaleExtent([0, 10])
disables the limit to zoom, and the user may zoom and pan below 0 in the y axis.
Also tried .scaleExtent([0.1, 10]) but this also allows zoom and pan below 0.
So How could I allow zoom only to positive values from 0 (including a value of 1) ?
And what function can be used to avoid the lines show beyond the axis? Is not that implicit when I use the translate function in the zoom?
The scale proceeds in a geometric series -- that is, the scale step before 1 is not 0, but 0.5. Limiting your scale extent to
.scaleExtent([0.5, 10])
should do what you want. I've also added .nice() to your scale definitions to get round numbers at the ends.
As for the clipping, no, this is not implicit in the translation. You need to clip explicitly using a clip path, which in your case can be defined as follows.
cp = svgContainer.append("defs").append("clipPath").attr("id", "cp")
.append("rect")
.attr("x", padding)
.attr("y", padding)
.attr("width", w - 2 * padding)
.attr("height", h - 2 * padding)
All you then need to do is use it when adding the elements:
msBars = svgContainer.selectAll('line.matched_peak')
.data(jsonFragmentIons)
.enter()
.append("line")
.attr("clip-path", "url(#cp)")
Complete example here.

How to limit the text of polygons in Voronoi diagram with D3.js?

I've see the Example of D3.js-Voronoi Tessellation.But I want to put some text in each of polygons instead of a circle,Here is my js code:
var width = 600, height = 400;
var vertices = d3.range(20).map(function(d){
return [Math.random() * width, Math.random() * height]
});
var voronoi = d3.geom.voronoi();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
path = svg.append("g").selectAll("path");
svg.selectAll("info")
.data(vertices.slice(1))
.enter().append("text")
.attr("transform", function(d) {
return "translate(" + d + ")";
})
.text("someText")
.attr("shape-rendering","crispEdges")
.style("text-anchor","middle");
redraw();
function redraw(){
path = path
.data(voronoi(vertices), polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) {return "q" + (i % 9) + "-9";})
.attr("d", polygon);
path.order();
}
function polygon(d){
return "M" + d.join("L") + "Z";
}
I have a JSFiddle for that basic example here:
my voronoi code
now, I want each of the polygons' text in the center of the polygon, and don't cross with the polygon's border. If the polygon have not enough space to contain the all text, just contain the first part of it!
Let me know if there is anything I can do to solve this issue, thank you!
PS:I'm so sorry to my English, yes, it's so poor! :)
Have a look at this example http://bl.ocks.org/mbostock/6909318 , you probably want to place the text at the polygon centroid and not the seed (point) used to determine the voronoi tessellation.
That should fix the majority of your layout issues.
Automatically scaling the text to fit is a little bit harder, if you are willing to scale and rotate the text you can use a technique similar to the following to determine the length of the line at that point:
https://mathoverflow.net/questions/116418/find-longest-segment-through-centroid-of-2d-convex-polygon
Then you need to determine the angle of the line. I have a plugin that should help with that:
http://bl.ocks.org/stephen101/7640188/3ffe0c5dbb040f785b91687640a893bae07e36c3
Lastly you need to scale and rotate the text to fit. To determine the width of the text use getBBox() on the text element:
var text = svg.append("svg:text")
.attr("x", 480)
.attr("y", 250)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("font", "300 128px Helvetica Neue")
.text("Hello, getBBox!");
var bbox = text.node().getBBox();
Then you use the angle you calculated earlier to scale and rotate your text:
text.attr("transform", "rotate(40) scale(7)")
I would love to give a complete example but this is quite a bit of work to get it right.
There are other options to achieve the same effect but none of them are simple (ie you could anneal the layout similar to the way d3 does the Sankey layout)

Resources