Using a range slider to change the radius of an SVG circle - javascript

So basically this script uses the output from a range slider to change the radius of an SVG circle. There are two circles however only one of the circles changes in size when the slider is adjusted.
var slider = document.getElementById("myRange");
var output = document.getElementById("circle_radius");
output.innerHTML = slider.value;
slider.oninput = function() {
output.innerHTML = this.value;
update(this.value)
}
var c1 = d3.select("circle")
var c2 = d3.select("circle1")
function update(update_radius) {
c1.transition()
.ease(d3.easeLinear)
.duration(2000)
.delay(100)
.attr("r", update_radius)
c2.transition()
.ease(d3.easeLinear)
.duration(2000)
.attr("r",update_radius)
}

Your issue is with d3.select. You're attempting to select an element like <circle1> rather than an id or class.
You should add id attributes to your circles and select them by id instead.
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle id="circle1" cx="50" cy="50" r="50"/>
</svg>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle id="circle2" cx="50" cy="50" r="50"/>
</svg>
Then you can do
var c1 = d3.select("#circle1")
var c2 = d3.select("#circle2")

Since you're doing a delay only for the second circle, here's a more general solution. Group the relevant circles with a class:
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle class="slider-circle" cx="50" cy="50" r="50"/>
<circle class="slider-circle" cx="50" cy="50" r="50"/>
</svg>
Then, using a selection with all the circles, transition them with a delay based on the index:
var circles = d3.selectAll(".slider-circle")
function update(update_radius) {
circles.transition()
.ease(d3.easeLinear)
.duration(2000)
.delay((d, i) => i * 100) // i = 0, delay = 0; i = 1, delay = 100
.attr("r", update_radius)
}

Related

D3 SVG Move zoom to rect inside <g> having different angle

I have <svg> with background images and different rectangle on that svg that are in proper position.
My rectangles are grouped by <g> with translation values and each rectangle has specific rotation values. Now I want my SVG to focus (show in center of page) on specific rectangle without disturbing background image and svg translate there.. Main issue is that these rectangles have different angles too.
So translate is not working properly....
SVG is like as below.
Can anyone provide me with guidelines?
<svg width="4500" height="3610" style="background: url("/images/001.jpg") no-repeat;">
<g id="228" transform="translate(2001,620)rotate(216) translate(0 -216)">
<g>
<rect x="0" y="0" height="72" width="36" stroke="black" style="fill: transparent;">
</rect><text x="0" y="36" text-anchor="start" style="fill: black;">228</text>
</g>
<g>
<rect x="36" y="0" height="72" width="36" stroke="black" style="fill: transparent;">
</rect><text x="36" y="36" text-anchor="start" style="fill: black;">229</text>
</g>
</g>
</svg>
This should be easier in SVG, and it might be, but here's my solution:
First, we need to get the bounding box of the rectangle we want to zoom to in it's coordinate space:
// Bounds in local coordinates space:
var bounds = rectElement.getBBox();
Second we get four corners out of this bounding box:
// Corners of rect in local coordinate space:
var corners = [
{x: bounds.x,y: bounds.y},
{x: bounds.x + bounds.width, y: bounds.y},
{x: bounds.x + bounds.width, y: bounds.y + bounds.height },
{x: bounds.x, y: bounds.y + bounds.height }
];
Third, we get the transformation matrix between the rectangle and the SVG:
// relevant transform:
var t = rectElement.getCTM();
Now, we can convert the corners we have to global SVG units:
// Convert the points to global SVG coordainte space:
for(var i = 0; i < corners.length; i++) {
var point = svg.node().createSVGPoint();
point.x = corners[i].x;
point.y = corners[i].y;
corners[i] = point.matrixTransform(t);
}
Ok, at this point we have the corners of the rotated rectangle in usable coordinates. We want to find the width and the height of these coordinates:
// get extents for x,y in global SVG space:
var x = d3.extent(corners, function(d) { return d.x; });
var y = d3.extent(corners, function(d) { return d.y; });
var w = x[1] - x[0];
var h = y[1] - y[0];
This gives us the rotated width and height, which we can now use to scale the SVG:. I'm using a small margin in my scaling function here (10% margin on each side):
// For getting scale factor:
var scale = function(elementWidth,elementHeight,SVGWidth,SVGHeight) {
return Math.min(SVGWidth/elementWidth*0.8,SVGHeight/elementHeight*0.8);
}
Which I use as so:
var k = scale(w,h,SVGwidth,SVGheight);
At this point I can zoom to the rectangle with a translate of -x[0]*k, -y[0]*k and a scale of k. However, this will result in the shape in the top left corner of the SVG, I want to account for the margin and whatever of rectangle width/height is smaller relative to SVG width/height. For this I calculate an offset for each of x, y:
// Offset to center feature:
var ox = (SVGwidth - w*k)/2;
var oy = (SVGheight- h*k)/2;
And now I can create my transform to center a given rectangle in the SVG:
var newTransform = "translate("+[-x[0]*k+ox,-y[0]*k+oy]+")scale("+k+")";
These snippets use a parent g to hold everything in the SVG, this makes it easier to manage the transforms. Also, this g has it's transform wiped when zooming to a rectangle because we don't want the relative transform from the current zoom state/transform, but one to the original zoom state/transform, otherwise we'll see odd behavior.
Here's an example with a zoom behavior allowing you to zoom/pan manually, but with the option to click to zoom to a rectangle:
var svg = d3.select("svg");
var contents = svg.html();
svg.selectAll("*").remove();
var g = svg.append("g")
.attr("transform", "translate(0,0)scale(1)")
.html(contents);
var SVGwidth = 400;
var SVGheight = 180;
// For getting scale factor:
var scale = function(elementWidth,elementHeight,SVGWidth,SVGHeight) {
return Math.min(SVGWidth/elementWidth*0.8,SVGHeight/elementHeight*0.8);
}
var zoom = d3.zoom()
.on("zoom", function(d) {
g.attr("transform",d3.event.transform);
})
svg.call(zoom);
function zoomToRect(rect) {
// Bounds in local coordinates space:
var bounds = rect.getBBox();
// Corners of rect in local coordinate space:
var corners = [
{x: bounds.x,y: bounds.y},
{x: bounds.x + bounds.width, y: bounds.y},
{x: bounds.x + bounds.width, y: bounds.y + bounds.height },
{x: bounds.x, y: bounds.y + bounds.height }
];
// Reset transform:
g.attr("transform","");
// relevant transform:
var t = rect.getCTM();
// Convert the points to global SVG coordainte space:
for(var i = 0; i < corners.length; i++) {
var point = svg.node().createSVGPoint();
point.x = corners[i].x;
point.y = corners[i].y;
corners[i] = point.matrixTransform(t);
}
// get extents for x,y in global SVG space:
var x = d3.extent(corners, function(d) { return d.x; });
var y = d3.extent(corners, function(d) { return d.y; });
var w = x[1] - x[0];
var h = y[1] - y[0];
var k = scale(w,h,SVGwidth,SVGheight);
// Offset to center feature:
var ox = (SVGwidth - w*k)/2;
var oy = (SVGheight- h*k)/2;
var newTransform = d3.zoomIdentity
.translate(-x[0]*k+ox,-y[0]*k+oy)
.scale(k);
svg.call(zoom.transform,newTransform);
}
d3.select("button").on("click", function() { zoomToRect(d3.select("rect").node()) });
svg {
border: 1px dotted black;
}
div {
position: absolute;
top: 0;
left: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="180" style="background: url("/images/001.jpg") no-repeat;">
<g id="228" transform="translate(2001,620)rotate(216) translate(0 -216)">
<g>
<rect x="0" y="0" height="72" width="36" stroke="black" style="fill: transparent;"></rect>
<text x="0" y="36" text-anchor="start" style="fill: black;">228</text>
</g>
<g>
<rect x="36" y="0" height="72" width="36" stroke="black" style="fill: transparent;">
</rect><text x="36" y="36" text-anchor="start" style="fill: black;">229</text>
</g>
</g>
<g id="228" transform="translate(1801,550)rotate(65) translate(0 0)">
<g>
<rect x="0" y="0" height="100" width="20" stroke="black" style="fill: transparent;"></rect>
<text x="0" y="36" text-anchor="start" style="fill: black;">454</text>
</g>
<g>
<rect x="36" y="0" height="20" width="100" stroke="black" style="fill: transparent;">
</rect><text x="36" y="36" text-anchor="start" style="fill: black;">455</text>
</g>
</g>
</svg>
<div><button>Zoom to 228</button></div>
Here's a snippet that is just zooming around without a zoom behavior, cycling between four rectangles using your SVG plus two bonus rectangles:
var svg = d3.select("svg");
var contents = svg.html();
svg.selectAll("*").remove();
var g = svg.append("g")
.attr("transform", "translate(0,0)scale(1)")
.html(contents);
var SVGwidth = 400;
var SVGheight = 180;
// For getting scale factor:
var scale = function(elementWidth,elementHeight,SVGWidth,SVGHeight) {
return Math.min(SVGWidth/elementWidth*0.8,SVGHeight/elementHeight*0.8);
}
var current = 0;
function transition() {
g.selectAll("rect").each(function(_,i,n) {
if(i != current) return;
// Bounds in local coordinates space:
var bounds = n[i].getBBox();
// Corners of rect in local coordinate space:
var corners = [
{x: bounds.x,y: bounds.y},
{x: bounds.x + bounds.width, y: bounds.y},
{x: bounds.x + bounds.width, y: bounds.y + bounds.height },
{x: bounds.x, y: bounds.y + bounds.height }
];
// Hold original transform:
var oldTransform = g.attr("transform");
// Reset transform:
g.attr("transform","");
// relevant transform:
var t = n[i].getCTM();
// Convert the points to global SVG coordainte space:
for(var i = 0; i < corners.length; i++) {
var point = svg.node().createSVGPoint();
point.x = corners[i].x;
point.y = corners[i].y;
corners[i] = point.matrixTransform(t);
}
// get extents for x,y in global SVG space:
var x = d3.extent(corners, function(d) { return d.x; });
var y = d3.extent(corners, function(d) { return d.y; });
var w = x[1] - x[0];
var h = y[1] - y[0];
var k = scale(w,h,SVGwidth,SVGheight);
// Offset to center feature:
var ox = (SVGwidth - w*k)/2;
var oy = (SVGheight- h*k)/2;
var newTransform = "translate("+[-x[0]*k+ox,-y[0]*k+oy]+")scale("+k+")"
g.attr("transform",oldTransform)
.transition()
.on("start", function() {
d3.select(n[current]).style("fill","orange");
})
.duration(2000)
.attr("transform",newTransform)
.on("end", function() {
d3.select(n[current]).style("fill","none");
current++;
current = current%g.selectAll("rect").size();
transition();
})
})
}
transition();
svg {
border: 1px dotted black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="180" style="background: url("/images/001.jpg") no-repeat;">
<g id="228" transform="translate(2001,620)rotate(216) translate(0 -216)">
<g>
<rect x="0" y="0" height="72" width="36" stroke="black" style="fill: transparent;"></rect>
<text x="0" y="36" text-anchor="start" style="fill: black;">228</text>
</g>
<g>
<rect x="36" y="0" height="72" width="36" stroke="black" style="fill: transparent;">
</rect><text x="36" y="36" text-anchor="start" style="fill: black;">229</text>
</g>
</g>
<g id="228" transform="translate(1801,550)rotate(65) translate(0 0)">
<g>
<rect x="0" y="0" height="100" width="20" stroke="black" style="fill: transparent;"></rect>
<text x="0" y="36" text-anchor="start" style="fill: black;">454</text>
</g>
<g>
<rect x="36" y="0" height="20" width="100" stroke="black" style="fill: transparent;">
</rect><text x="36" y="36" text-anchor="start" style="fill: black;">455</text>
</g>
</g>
</svg>

Change SVG fill in a loop

I have simple SVG illustration, is there is any way to change it's color constantly ? like a loop non-stop random color change.
here is my svg
<svg width="533" height="499" viewBox="0 0 533 499" fill="none" xmlns="http://www.w3.org/2000/svg">
This is how I would do it: I'm using colors hsl for the fill and I'm animating the hue of the colors using requestAnimationFrame. I hope it helps.
let p1 = document.querySelectorAll("path")[0];
let p2 = document.querySelectorAll("path")[1]
let h = 0;
function changeColor(){
window.requestAnimationFrame(changeColor);
h+=.5;
h2=210+h;
p1.setAttributeNS(null,"fill", `hsl(${~~h},100%,50%)`);
p2.setAttributeNS(null,"fill", `hsl(${~~h2},100%,50%)`);
}
changeColor()
<svg width="533" height="499" viewBox="0 0 533 499" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M258.089 59.6035C309.803 -3.94652 379.363 78.1818 407.679 127.19C352.338 67.4782 301.718 129.7 287.076 167.787C272.435 205.874 233.694 210.043 205.199 217.679C187.359 222.459 146.446 248.26 128.6 264.085C109.864 289.466 48.3081 292.846 41.8378 268.698C27.0852 213.64 95.5238 148.37 137.644 123.97C163.705 101.458 206.375 123.154 258.089 59.6035Z" fill="blue"/>
<path d="M448.323 394.788C427.389 384.661 420.75 356.279 420.047 343.354C441.009 284.421 527.63 350.762 528.167 368.218C528.703 385.674 474.491 407.447 448.323 394.788Z" fill="red"/>
</svg>
Select the element and recursively call a function that sets the fill attribute of the SVG element you want to recolor with a random hex.
const recolor = element => {
const randomColor = '#'+Math.floor(Math.random()*16777215).toString(16)
circle.setAttribute('fill', randomColor)
setTimeout(() => recolor(element), 600)
}
recolor(document.querySelector('#circle'))
svg circle {
transition: fill .5s linear;
}
<svg height="100" width="100">
<circle id="circle" cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
</svg>
fill on <svg> doesnot work. Its for its elements. You can change its background
function random_rgba() {
var o = Math.round, r = Math.random, s = 255;
return 'rgba(' + o(r()*s) + ',' + o(r()*s) + ',' + o(r()*s) + ',' + r().toFixed(1) + ')';
}
function changeColor(){
document.querySelector('svg').style.background = random_rgba();
}
setInterval(changeColor,200);
<svg width="533" height="499" viewBox="0 0 533 499" fill="none" xmlns="http://www.w3.org/2000/svg">

D3 - Drawing a path between DOM elements

I'm creating a parallel coordinates chart. Each circle is a particular brand, and every brand has one circle in each column:
When the user hovers over a circle, I would like to draw a path connecting that circle to the other three circles of the same brand. The issue is that the X position of the circles is random, so I have to draw the line using the circle's modified cx value (and not the data).
Conveniently, I have all four brand circles grouped in their own g elements:
<g class="line-group">
<circle r="5" cx="340.48700997553686" cy="0" data-brand="Brand X"></circle>
<circle r="5" cx="916.9181438059958" cy="59.347826086956466" data-brand="Brand X"></circle>
<circle r="5" cx="1589.2772695723352" cy="229.1306884480747" data-brand="Brand X"></circle>
<circle r="5" cx="2272.275506967826" cy="0" data-brand="Brand X"></circle>
</g>
I can grab the elements, and group the coordinates in a way d3.line() likes, but it makes one line that connects every point.
var line = d3.svg.line()
.interpolate('basis');
var circles = d3.selectAll('.line-group').selectAll('circle'),
circleCoords = [];
circles.forEach( function(d) {
console.log(circles);
for ( i = 0; i < d.length; i++ ) {
var cx = d3.select(d[i]).attr('cx'),
cy = d3.select(d[i]).attr('cy');
circleCoords.push([cx, cy]);
}
});
lineGroup.append('path')
.attr({
'd' : line( circleCoords )
});
How do I structure this so I can grab the cx and cy values of each circle group (the four circles of the same brand inside of g.line-group)?
I probably have to make a custom d attribute, something like this (pseudocode):
path.attr('d', function(d) {
return 'M ' + /* cx of circle 1 */ + ' ' + /* cy of circle 1 */ +
' L ' + /* cx of circle 2 */ + ' ' + /* cy of circle 2 */ +
' L ' + /* cx of circle 3 */ + ' ' + /* cy of circle 3 */ +
' L ' + /* cx of circle 4 */ + ' ' + /* cy of circle 4 */ + ' Z';
})
I believe all the pieces are there, I just can't seem to find a way to put it together correctly. If anyone has any ideas, it would be greatly appreciated!
Edit: Added line definition.
You has the wrong selection (I can't see your line definition, don't forget it):
var svg = d3.selectAll("svg")
var circles = svg.selectAll('.line-group').selectAll("circle"),
// or var circles = svg.selectAll('.line-group > circle'),
circleCoords = [];
circles.forEach( function(d) {
for ( i = 0; i < d.length; i++ ) {
var cx = d3.select(d[i]).attr('cx'),
cy = d3.select(d[i]).attr('cy');
circleCoords.push([cx, cy]);
}
console.log(circleCoords);
});
var line = d3.svg.line()
svg.append('path')
.attr({
'd' : line( circleCoords )
});
Here the working code
Don't forget styles.
You need to identify each '.line-group' or D3 will select all '.line-group' class, may adding id attribute.-
In your case you have multiple "Brands", like so:
<svg>
<g class="line-group">
<circle r="5" cx="10" cy="110" data-brand="Brand-X"></circle>
<circle r="5" cx="30" cy="120" data-brand="Brand-X"></circle>
<circle r="5" cx="150" cy="30" data-brand="Brand-X"></circle>
<circle r="5" cx="290" cy="40" data-brand="Brand-X"></circle>
</g>
<g class="line-group">
<circle r="5" cx="10" cy="10" data-brand="Brand-Y"></circle>
<circle r="5" cx="30" cy="20" data-brand="Brand-Y"></circle>
<circle r="5" cx="150" cy="130" data-brand="Brand-Y"></circle>
<circle r="5" cx="290" cy="140" data-brand="Brand-Y"></circle>
</g>
</svg>
you need to refactory your code:
var svg = d3.selectAll("svg")
var circles = svg.selectAll('.line-group').selectAll("circle"),
circleCoords = [];
var line = d3.svg.line()
// circles.length give you .line-group's count or brand's count:
for (j=0; j<circles.length; j++) {
circles[j].forEach( function(d) { // forEach brand draw a line
var cx = d.getAttribute('cx'),
cy = d.getAttribute('cy');
circleCoords.push([cx, cy]);
})
svg.append('path')
.attr({ 'd' : line( circleCoords ) })
.attr("id","id_"+j);
circleCoords = [];
};
Here's the working code

D3 - why is this sort function not updating all the elements in its selection?

I am making a D3 visualiation that sorts circle elements (each circle is a planet). There is a visualise function that creates a circle for each planet like this:
<g class="planet">
<circle class="Mercury" r="1.6264266666666667" cy="90" cx="50" fill="#333">
</g>
<g class="planet">
<circle class="Venus" r="4.034393333333334" cy="90" cx="198.5" fill="#333">
</g>
<g class="planet">
<circle class="Earth" r="4.252066666666667" cy="90" cx="347" fill="#333">
</g>
...
That same function creates labels for each planet.
I have written a second function, sort, which puts the planets/circles in descending or ascending order.
var sort = function(){
planets
.sort(function(a, b) {
if(orderOfAppearance){
return d3.descending(a["Equatorial radius (KM)"], b["Equatorial radius (KM)"]);
}
else{
return d3.ascending(a["Mean distance from Sun (AU)"], b["Mean distance from Sun (AU)"]);
}
})
.transition()
.duration(1500)
.attr("cx", function(d,i){
return ((width * 0.99) / radiuses.length) * i + 50;
});
// other stuff...
}
The sort function does order the circles correctly (i.e. it is visually correct), but when I inspect the DOM I see that after sort is called all of the circles are appended to the first g element like this, with the other 7 g elements emptied:
<g class="planet">
<circle class="Jupiter" r="47.661786666666664" cy="90" cx="50" fill="#333">
<circle class="Saturn" r="40.17809333333334" cy="90" cx="198.5" fill="#333">
<circle class="Uranus" r="17.03816666666667" cy="90" cx="347" fill="#333">
<circle class="Neptune" r="16.510906666666667" cy="90" cx="495.5" fill="#333">
...
<circle class="Mercury" r="1.6264266666666667" cy="90" cx="1089.5" fill="#333">
</g>
<g class="planet"></g>
<g class="planet"></g>
...
Why is my sort function just choosing the first g element as opposed to all g elements in the selection? planets is a selectAll("circle") selection.
I've put everything up in a gist here.
The problem here is that your planets selection is actually the array of circle elements, not g elements. To fix this, make sure the variable planets is assigned the g elements and then append the circles afterword. In your sort function, you'll need to update to sort the g elements, and then update the circle within each.
planets = solarSystem.selectAll("g.planet")
.data(planetaryData)
.enter()
.append("g")
.classed("planet", true);
planets.append("circle")
.attr({ ...
var sort = function(){
planets
.sort(function(a, b) {
if(orderOfAppearance){
return d3.descending(a["Equatorial radius (KM)"], b["Equatorial radius (KM)"]);
}
else{
return d3.ascending(a["Mean distance from Sun (AU)"], b["Mean distance from Sun (AU)"]);
}
})
.transition()
.duration(1500)
.select("circle")
.attr("cx", function(d,i){ ...

Apply updated co-ordinates from parent rotation to children

In the code below, I am rotating a selection box (parent) with two children in SVG.
It works fine, however, when the parent (selection box) is removed, the children go back to their original (pre-rotation) co-ordinates.
How can I apply the updated co-ordinates on the children once the parent is removed. I specifically need to maintain the new position of the children in X,Y co-oridinates, i.e. the rotate should be converted to translate, r.g. transform = translate (X , Y) . I only need New x,y for children so that i can 'translate' them to new location.
here is the fiddle link http://jsfiddle.net/rehankhalid/QK8L8/6/
HTML CODE:-
<button data-action="rotate" onclick="rotateMainRect()">Rotate +5 Angle</button>
<button data-action="rotate" onclick="removeRectRotation()">Remove Selection</button>
<br/>
<svg id="mainSvg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="600" height="500">
<g id="selectedRect">
<rect id="rectangle" x="135" y="135" width="110" height="35" stroke="red" stroke-width="2" fill="grey" opacity="0.4" />
<g id="button_1" transform="translate(0,0)">
<circle cx="150" cy="150" r="5" stroke="grey" stroke-width="1" fill="none" />
</g>
<g id="button_2" transform="translate(0,0)">
<circle cx="230" cy="150" r="5" stroke="grey" stroke-width="1" fill="none" />
</g>
</g>
</svg>
JAVASCRIPT CODE
var angle_incr = 5;
var angle = 0;
function rotateMainRect() {
var selectedRect = document.getElementById('selectedRect');
var rectangle = document.getElementById('rectangle');
var x = rectangle.getAttribute('x');
if (x != 0) {
var centxy = calculateCenterXY(selectedRect);
angle += angle_incr;
selectedRect.setAttributeNS(null, 'transform', 'rotate(' + angle + ',' + centxy.x + ',' + centxy.y + ')');
} else {
rectangle.setAttribute('x', '135');
rectangle.setAttribute('y', '135');
rectangle.setAttribute('width', '110');
rectangle.setAttribute('height', '35');
}
}
function calculateCenterXY(node) {
var x = node.getBBox().x + (node.getBBox().width / 2);
var y = node.getBBox().y + (node.getBBox().height / 2);
var xy_co = {
x: x,
y: y
};
return xy_co;
}
function removeRectRotation() {
var selectedRect = document.getElementById('selectedRect');
selectedRect.setAttributeNS(null, 'transform', '');
var rectangle = document.getElementById('rectangle');
rectangle.setAttribute('x', '0');
rectangle.setAttribute('y', '0');
rectangle.setAttribute('width', '0');
rectangle.setAttribute('height', '0');
angle = 0;
}
- What i Want:-
First Rotate the selection rectangle to some angle, and then press 'Remove selection'. After Removing the selection, Childrens must be placed at the New postion. (which now, move back to the original position)
If you are asking if you can read the absolute positions of the two transformed circles directly using JS, then the answer is no.
You will need to calculate their positions yourself using a bit of trigonometry.

Categories