Take a look at this jsfiddle. Magnifying on hover works well for text and images, but I would like to have the same effect for the chart.
I would like the solution, if possible, to be applicable to any D3 SVG-based chart.
The example uses jquery plugin AnythingZoomer, it looked to me as a good starting point, however, you don't need to stick to it, you can use anything else if you wish.
I am aware of D3 fisheye pluging, this is related, but not quite what I want.
You can do this by not explicitly declaring width and height in the SVG (which is overwritten by CSS anyway), using the viewBox attribute, and then allowing AnythingZoom to clone the content of your original chart.
Demo (Fragile): http://jsfiddle.net/H9psX/ http://jsfiddle.net/H9psX/38/
Changes
var svg = d3.select("#small-chart").append("svg")
// .attr("width", diameter + 300)
// .attr("height", diameter)
.attr('viewBox', "0 0 " + 225 + " " + 225);
// ...
$("#zoom3").anythingZoomer({
clone: true
});
Separation of concerns
Since you are drawing in SVG using D3 (where you need to know the width and height for the pack layout) and using a jquery plugin which zooms by setting classes and absolute positioning, you have to share the coordinates (the 225px magic number) in CSS and in JS.
Ideally, you would want to keep the magic number at only one place. To do that you can declare the value only in CSS and then read them in your JS after creating your SVG element.
Related
I've made a simple Zoom & Pan notebook in ObservableHQ.com with D3.js I have two questions:
https://observablehq.com/d/c63434913a56fbb2
If the curson is on the black square (i.e. SVG), it dosen't click and drag anymore! How to allow drag at all time?
How to disable zoom function when mouse is scroling and keep only panning?
Thank you very much!
I'tried different code snnipets, but the simpler one I found don't behave like I would like.
You’re using d3.drag and d3.zoom, which both include functionality to let you drag stuff around. d3.drag lets you drag individual elements with finer control (like rearranging circles); d3.zoom only lets you drag the whole scene. They can be combined, but you only need one or the other here.
In your notebook, the d3.zoom piece is working, but doing more than you want it to (zooming and panning when you only want panning); the d3.drag piece is broken.
You call d3.zoom on the whole SVG, and d3.drag on just the group g. The group contains the bigger black-stroked empty square and the smaller black-filled square; if you click anywhere on that, the d3.drag code runs before the d3.zoom. (It's not just on the black-filled square; if you zoom in, you can see that dragging directly on the outer stroke also doesn't work.)
But the d3.drag code is throwing an error (as you can see in the browser console), because it's trying to set d.x and d.y when d doesn't exist, so nothing happens when you drag. And it wouldn't work anyway, because it's trying to set the cx and cy attributes, which the SVG group element doesn't have; it was probably originally written for an SVG circle element. For a group element, the dragged event should be setting a transform on the group instead, which your d3.zoom code is already doing.
Using d3.zoom
In this approach, you can click and drag anywhere on the SVG. If you don’t want it to zoom when you scroll, you can make scaleExtent only allow a scale of 1:
svg.call(
d3.zoom()
.extent([[0, 0], [w, w]])
.scaleExtent([1, 1])
.on("zoom", function(event) {
g.attr("transform", event.transform);
})
);
Here's a suggestion you can merge into your notebook to use the d3.zoom approach (you should only merge one or the other!): https://observablehq.com/compare/c63434913a56fbb2...a3c5334fa206bb61
Using d3.drag
In this approach, you can only click and drag on the group (where it has stroke or fill). You can give the group a datum with x and y values of 0 to start with, and use a transform instead of setting cx and cy:
const g = svg.append("g")
.datum({x: 0, y: 0})
.call(d3.drag().on("drag", function(event, d) {
d3.select(this)
.attr("transform", `translate(${d.x = event.x}, ${d.y = event.y})`);
}));
If you wanted to be able to click and drag anywhere, you could add an invisible rectangle for pointer capture, as described here.
Here's a suggestion you can merge into your notebook to use the d3.drag approach (you should only merge one or the other!): https://observablehq.com/compare/c63434913a56fbb2...3650eb69db864a42
This question relates to NVD3.js multiChart x-axis labels is aligned to lines, but not bars
I am using NVD3.js multiChart to show multiple lines and multiple bars in the chart. All is working fine, but the x-axis labels is aligned only to the line points, not bars. I want to correctly align labels directly below the bars as it should. But I get this:
As you can see - x-axis (example, 2014-Feb) is not aligned to Bars.
1) How to align x-axis labels to bars and lines at the same time?
2) I need this solution for NVD3.js or how to properly integrate.
I made jsFiddle: http://jsfiddle.net/n2hfN/28/
Thanks!
The problem here is that nv.models.multiChart uses a linear scale for its x-axis, and then when it draws the bars it calls nv.models.multiBar, which uses an ordinal scale with .rangeBands().
You can follow this mess through the source code:
First lets look at multiChart.js
HERE is where it sets the x-scale to be a linear scale.
HERE it calls the nv.models.multiBar model to create the bars.
If we jump over to have a look at multiBar.js
HERE it creates an ordinal scale, and HERE it sets the range of the scale using .rangeBands()
The result is that the ordinal scale used for placing the bars, and the linear scale used for the chart's axis do not align. Here's what the two scales look like on their own if plotted on an axis:
The solution would be to force the chart to render the line graphs and the x-axis in terms of the ordinal scale used by the bars. This would work in your case because the bars and the lines all use the same data for the x-axis. This is very simple to do if you are making your own chart and not relying on nvd3, as I showed in my answer to your previous question HERE. This is extraordinarily complicated to do if you're trying to work within nvd3, and many others have tried and failed to switch out the default scales used by nvd3 charts. Have a look at this issue on the nvd3 github page that has been open since January, 2013 for example.
I've tried a number of approaches myself to reuse the bars' ordinal scale, but with little success. If you want to poke around and try to brute-force it yourself, I can tell you that from my experiments I came closest when using chart.bars1.xScale().copy() to make a copy of the bars' scale, and set its domain and rangeBands. Unfortunately, since the chart's width is computed at render time, and I can't seem to create a hook into the chart.update function, it is impossible to set the rangeBands' extent to the correct values.
In short, if you can't live with the labels being offset, you're probably going to need to code up your own chart without nvd3, or else find a different type of layout for your visualization.
After playing around with the NVD3 v1.7.1 source code with the immensely helpful guidance offered by jshanley's answer, I think I've managed to come up with an answer (perhaps more of a kludge than a good solution).
What I did was to have the x-axis labels align with the bars, and have the line data points align with the bars.
1.1. To align the x-axis label, I shifted the x-axis to the right so that the first label appears underneath the middle of the first bar. I then shifted the last label to the left, so that it appears underneath the middle of the last bar. See code here. The amount to shift by is computed at drawing time using .rangeBand() and saved in a rbcOffset variable (I had to modify multiBar.js for this to work).
1.2. To align the line data points with the bars, a similar shift is also required. Luckily, this part is easy because scatter.js (which is used by line chart) comes with a padData boolean variable that does what we want already. So basically, I just set padData to true and the lines shift to align with the bars, see here.
In order to properly integrate with NVD3 and make everything look good, some additional changes are required. I've forked NVD3 on GitHub so you can see the complete solution there. Of course, contributions are welcome.
I use last solution and it runs. So, you can specify
lines1.padData(true)
in order to align lines too.
Same here, I used the last solution,it worked for me as well. Find the following line in multiChart.js
if(dataLines1.length){
lines1.scatter.padData(true); // add this code to make the line in sync with the bar
d3.transition(lines1Wrap).call(lines1);
}
I encountered the same problem and fixed it with below code:
at lines 7832 and 7878 replace
.attr('transform', function(d,i) { return 'translate(' + x(getX(d,i)) + ',0)'; })
with :
var w = (x.rangeBand() / (stacked && !data[j].nonStackable ? 1 : data.length));
var sectionWidth = availableWidth/(bars.enter()[0].length - 1);
if(bars.enter().length == 2)
return 'translate(' + ((i-1)*w + i*w + (i*(sectionWidth - 2*w))) + ',0)';
else
return 'translate(' + ((i-0.5)*w + i*(sectionWidth - w)) + ',0)';
The first case handles multiple bars case while the second one handles single bar case.
lawry's solution works. Also if using interactive guidelines, you need to shift the interactive line to match the new scale. Modify:
if(useInteractiveGuideline){
interactiveLayer
.width(availableWidth)
.height(availableHeight)
.margin({left:margin.left, top:margin.top})
.svgContainer(container)
.xScale(x);
wrap.select(".nv-interactive").call(interactiveLayer);
//ADD THIS LINE
wrap.select(".nv-interactiveGuideLine")
.attr('transform', 'translate(' + rbcOffset +', ' + 0 + ')' +
'scale(' + ((availableWidth - rbcOffset*2)/availableWidth) + ', 1)');
}
in multiChart.js.
I'm using a D3 tree. Similar to: http://bl.ocks.org/mbostock/4063570
However, this particular diagram is bigger than the browser window.
Given that i know exactly which one of the JSON element's that i want to reveal.
How could i scroll that element into view within the svg canvas?
The easiest way to achieve this is probably to wrap what you want to appear in a g element and use a transition on the translation.
groupToShow.attr("transform", "translate(1000,1000)")
.transition()
.duration(1000)
.attr("transform", "translate(0,0)");
Adapt the values for translation and duration as needed.
I am implementing the Zoomable Treemap in D3 (http://bost.ocks.org/mike/treemap/), however I have modified it so the leaf rects will go to URLs when clicked. It also adds an ellipsis if the title is too long.
I'd like to implement word wrapping on leaf rects instead but cannot get it to work. I wanted to add a set of tspans to the text but am struggling with the execution order too much to understand where to put it.
Code: https://secure.polisci.ohio-state.edu/faq/d3/zoomabletreemap.htm
Data: https://secure.polisci.ohio-state.edu/faq/d3/zoomabletreemap.json
I've researched that I either need tspans breaking it up or a div with the text inside but don't know how to do either. There are examples of text wrap for the regular D3 Treemap but I've found none for the Zoomable Treemap and the code is significantly different.
The easiest thing to do is probably to replace the SVG text elements with divs inside foreignObject. To do that, you would replace
g.append("text") //was text
.attr("dy", ".75em")
...
with
g.append("foreignObject")
.attr("dy", ".75em")
...
.append("xhtml:div")
...
It might also be advisable to set the width and height attributes of the foreignObject (using code similar to what you're using to determine whether the text is too long) so that the text flows correctly.
I want to display a series of images as an animation -- I'd use a GIF, except I want to manipulate specific frames.
I've displayed the image on an SVG canvas and now I want to change xlink:href dynamically.
function startAnimation(){
c += 1;
d3.select(this)
.transition()
.attr("xlink:href", "images/image-" + c.toString() +".png")
.each("end", startAnimation);
};
This approach works fine if I try to change an attribute like image height, but it doesn't seem to work with the attribute xlink:href.
Is there a way to dynamically update images like this?
You need to preload and cache the images before you start the animation sequence. Otherwise, the images will be loaded lazily—and it's not unreasonable for an image to take 250ms to load, which means that by the time the image is loaded, you're on to the next frame.
For HTML elements, you can preload by creating Image objects (new Image), setting the src attribute, and then listening for load events (onload) to see when all the images are ready. Browsers use the cached in-memory image when you set the src of a displayed HTML img element from a preloaded Image.
This technique may or may not work for SVG images, though. An even better technique is to use a sprite sheet, where you combine all the frames of your animation into a single image, which you then crop on the client. In HTML, you commonly use the background-position style for this; in SVG, you can do the same thing by clipping or overflow: hidden.
Yet another option is to create multiple svg:image elements and then hide all but one (e.g., via opacity or positioning the elements off-screen).
(Also, since you are not interpolating attributes, it might be simpler to use setInterval for your animation rather than d3.transition.)
nodeEnter.append("svg:image")
.attr('x',-9)
.attr('y',-12)
.attr('width', 15)
.attr('height', 24)
.attr("xlink:href", function(d) {
if(d.Type=="name"){
return "/images/icon-1.png";}
else{
return "/images/icon-2.png";}
});