I'm using D3 to draw a custom graphs with a border rectangle around it. On window resize, I recalculate the SVG size and want to redraw this border. I'm currently doing this in a drawGraph() function which gets called on resize / new data:
...
svg.selectAll('rect')
.data([true])
.attr('width', w)
.attr('height', h)
.enter()
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', w)
.attr('height', h)
.style('fill', 'none')
...
i.e. - I am binding a single value in an array ([true]) to the selection so that I can either draw the box, or just change its size if it's already been drawn once. It certainly works, but it feels slightly odd to bind data which is guaranteed to be ignored, and possibly a slightly hacky way of doing things.
Is this a well-used convention, or is there another standard way of doing this, such as:
A D3 method which I've not come across
Just using svg.append('rect') and then removing + redrawing it on resize
svg.selectAll('*').remove() and redrawing everything from scratch?
Anything else
Removing and redrawing elements is what I call lazy coding or lazy update in D3.
That being said, why don't you simply assign the rectangle to a variable and change it on resize? Something like this (click "full page" and resize the window):
var div = document.getElementById("div");
var svg = d3.select("svg")
var rect = svg.append("rect")//here you define your rectangle
.attr("width", 300)
.attr("height", 150)
.attr("stroke-width", 3)
.attr("stroke", "black")
.attr("fill", "none");
function resize() {
var width = div.clientWidth;
svg.attr("width", width)
.attr("height", width / 2);
//here you just change its width and height
rect.attr("width", width)
.attr("height", width / 2);
}
window.addEventListener("resize", resize);
svg {
background-color: tan;
}
<script src="//d3js.org/d3.v4.min.js"></script>
<div id="div">
<svg></svg>
</div>
Related
So I've made a React component that will return a D3 SVG element that takes in an image and then displays it with some text wrapped around the image. However, when I use the component with multiple images, it seems that it sets all the images to be the first in the list. Example (the actual picture needed to be displayed is on bottom, while the SVG only shows the first).
What might be going on here? Would it happen to be something to do with how I'm selecting? My render function is as follows:
render() {
return (
<div ref="arc" style={{ minHeight: 225}}></div>
)
}
And my d3.select is as follows:
context = d3.select(this.refs.arc).append('svg')
.attr("preserveAspectRatio", "xMaxYMin slice")
.attr('viewBox', '0 0 '+width+' '+height)
.classed("svg-content", true)
.append('g').attr('class', 'wrapper')
.attr('transform', `translate(${width / 2}, ${height / 2})`)
setImage(context, id){
var width, height;
width = height = this.props.innerRadius*2;
return context
// .selectAll('.images')
// .data(this.props.img).enter()
.append('defs')
.attr('class', 'images')
.append('pattern')
.attr('id', id)
.attr('width', width)
.attr('height', height)
.attr("patternUnits", "userSpaceOnUse")
.attr("x", -width/2)
.attr("y", -height/2)
.append('image')
// .attr('xlink:href', function(d) {return d})
.attr('xlink:href', this.props.img)
.attr('width', width)
.attr('height', height)
;
}
drawImage(context, size, id){
const { height, width } = this.props;
return context
.append('circle')
.attr('r', size)
.attr('id', id)
.style('fill', 'url(#'+id+')')
;
}
Can anyone tell me what might be going on? I'll post any relevant information upon request.
Update: I decided to try passing text in as well, and THAT seems to update. Not sure whats going on.
I figured it out. I was an idiot and hadn't set an unique ID for each of the images, and they were all being set as the same ID. Thus the .style('fill', 'url(#'+id+')') was always only filling using the ID of the very first image.
I'm trying to have an image fill my donut chart, then rotate the image 60 degrees from its center.
I've had success filling a simple shape as a pattern with this method, but the pattern image gets all screwy and repeats itself when applied to a donut chart. The image is 300px x 300px - same size as the svg. The final result should look like this.
Here's my fiddle.
imgPath = "http://www.mikeespo.com/statDonkey/inner.png";
w = 300;
h = 300;
passingPercent = 60;
rotateStartPosition = 50;
var myScale = d3.scale.linear().domain([0, 100]).range([0, 2 * Math.PI]);
// MAKES SVG
var svg = d3.select("body")
.append("svg")
.attr("id", "svg_donut")
.attr("width", w)
.attr("height", h);
// MAKE DEFS
var defs = d3.select("#svg_donut")
.append("defs");
// MAKES PATTERN
defs.append('pattern')
.attr('id', 'pic1')
.attr('width', 300)
.attr('height', 300)
.attr('patternUnits', 'userSpaceOnUse')
.append('svg:image')
.attr('xlink:href', imgPath)
.attr("width", 300)
.attr("height", 300)
.attr("transform", "rotate(60 150 150)")
.attr("x", 0)
.attr("y", 0);
// CREATES VARIABLE *VIS* TO SVG
var vis = d3.select("#svg_donut");
// DEFINES DONUT GRAPH
var arc = d3.svg.arc()
.innerRadius(95)
.outerRadius(140)
.startAngle((myScale(0 + rotateStartPosition)))
.endAngle((myScale(passingPercent + rotateStartPosition)));
// APPENDS *VIS* TO SVG
vis.append("path")
.attr("id", "passing")
.attr("d", arc)
.attr("fill", "white")
.attr("transform", "translate(150,150)")
.attr("fill", "url(#pic1)");
I'm not exactly sure why this works to be honest but when I changed the width and height of the pattern element and removed the patternUnits attribute, I was able to achieve the desired look:
defs.append('pattern')
.attr('id', 'pic1')
.attr('width', 1)
.attr('height', 1)
.append('svg:image')
.attr('xlink:href', imgPath)
.attr("width", 300)
.attr("height", 300)
.attr("transform", "rotate(00 150 150)")
.attr("x", 0)
.attr("y", 0);
I don't understand it completely but it has something to do with the coordinate system and the way in which the pattern scales to the object you're applying it to. The width and height aren't defining the size of the image as you might initially think, but the way in which the pattern will map to the new coordinate system of the donut. A width and height of 1 indicates that the pattern will just scale to the width and height of the donut.
Getting my info from here and admittedly not fully grasping it all yet but hopefully this will help: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Patterns
D3.js panning appears to be slower and more choppy than zooming when the svg has many elements. I made an example on JSFiddle http://jsfiddle.net/cornhundred/cfeu1ws2/10/ and the code is also shown below
var num_rect = 3000;
var zoom = d3.behavior.zoom()
.on("zoom", zoomed);
function zoomed() {
console.log('zooming or panning');
d3.select('svg')
.select('#rect_group')
.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
svg = d3.select("body")
.append("svg:svg").attr("width", 800).attr("height", 800)
.call(zoom);
var rect_group = d3.select('svg')
.append('g')
.attr('id', 'rect_group');
var data = _.range(num_rect);
var color = d3.scale.linear().domain([0, num_rect]).range(['red', 'blue']);
rect_group.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', 175)
.attr('width', 5)
.attr('x', function (d) {return 50 + d / (0.003 * num_rect);})
.attr('y', 50)
.style('fill', function (d) {return color(d);});
In this example I'm appending num_rects rectangles onto the svg side-by-side and adding some coloring so its clear there are many rectangles. I'm also making a console log whenever the zoomed function is called.
Increasing num_rect above ~3000 caused panning to be choppy while zooming remained smooth. This can ben seen in the behavior of the visualization or from the frequency of the console logs - 'zooming or panning'. This is odd since I would expect panning to be as cpu intensive than zooming.
This behavior also appears to be browser-specific - I'm only seeing this in chrome (which is also odd since Chrome is usually the best at rendering D3.js visualizations).
I am implementing a geometric zoom behaviour as seen in this example
The problem is that if the cursor is on a white spot outside the green overlay rect or any other SVG element (line, circle etc.) the mousewheel event gets intercepted by the browser and scrolls down the page.
I would like to be able to freely zoom independently of where I am on the visualisation.
Here is a simplified jsFiddle recreating the problem.
var width = 300,
height = 300;
var randomX = d3.random.normal(width / 2, 80),
randomY = d3.random.normal(height / 2, 80);
var data = d3.range(2000).map(function() {
return [
randomX(),
randomY()
];
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.call(d3.behavior.zoom().scaleExtent([-8, 8]).on("zoom", zoom))
.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) { return "translate(" + d + ")"; });
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
Hope this isn't too late, I missed this question the first time around.
The reason it isn't working under Chrome is because Chrome hasn't yet implemented the standard CSS transform on html elements -- and as strange as it is to understand, the outermost <svg> tag on an SVG element embedded in a webpage is treated as an HTML element for layout purposes.
You have two options:
Use Chrome's custom transform syntax, -webkit-transform in addition to the regular transform syntax:
http://jsfiddle.net/aW9xC/5/
Kind of jumpy, since you are transforming the entire SVG and readjusting the page layout accordingly. For reasons I don't understand neither the CSS/webkit transform nor the SVG attribute transform work when applied to the "innerSVG" element.
Replace the nested SVG structure with an SVG <g> group element, which Chrome has no problem transforming:
http://jsfiddle.net/aW9xC/4/
Stick a transparent rect in front of everything so the mouse event has something to latch on to. In SVG events are only sent to rendered elements such as rects and not to the general unrendered background.
svg.append("rect")
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("width", "100%")
.attr("height", "100%");
In order to make this work properly the SVG would have to cover the whole area so to get the same look as your original fiddle you'd want to clip to the original area which can be done either by setting a clipPath or (as I've done in the fiddle) by creating an innser <svg> element which will clip.
var svg = d3.select("body").append("svg")
.attr("width", "100%")
.attr("height", "100%")
.call(d3.behavior.zoom().scaleExtent([-8, 8]).on("zoom", zoom));
svg = svg.append("svg")
.attr("width", width)
.attr("height", height)
So altogether it looks like this...
I looked at some d3 drag questions and examples, but I haven't been able to find a way to permanently attach an element to the mouse.
For example, let's say I make some lines in d3:
var height = $(document).height() - 20;
var width = $(document).width() - 20;
var svgSelection =
d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var lines = 100;
for (var i = 0; i < lines; i++){
var myLine = svgSelection
.append("line")
.attr("x1", Math.random() * width)
.attr("y1", Math.random() * height)
.attr("x2", width/2)
.attr("y2", height/2)
.style("stroke", "black")
.style("stroke-width", 5)
.style("visiblity", "visible");
}
How would I make it so the "x1" and "y1" .attrs are attached to the mouse location indefinitely? This would mean I could move the "x2" and "y2" .attrs at any time while keeping the other ends of the lines attached to the mouse. Is there an easy way to do this?
(This would just be in any old <svg> block, of course.)
The way to do this is to attach a mouse handler to your entire SVG and in the handler function get the mouse position and update the coordinates you want. You can see what I'm talking about here (click on a node and move the mouse while holding the click).