Updating a clip-path in d3.js? - javascript

In my recent project, I am interested in creating a clip-path which moves with my mousemove. My initial idea was simply to select and re-position the ellipsis with its attributes cx and cy using the mousemove coordinates, then selecting the rectangle and re-initializing its clip-path attribute.
This, however, does not seem to work. The only workable solution I have found so far is to delete the rectangle and the clip-path, then re-initializing them at the new coordinates. This works fine for the simple test-case below, but in my actual experiment, the object I'll try to clip is an externally loaded svg, and having to re-load it every mouseover tick might be prohibitively expensive.
Do you have any suggestions on how to achieve the same effect as I have shown below without re-initializing everything?
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/mathjs/lib/browser/math.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
</style>
</head>
<!-- Create a div where the graph will take place -->
<div id="my_datavisualization">
<svg id="click" xmlns="http://www.w3.org/2000/svg">
<defs>
<g id="pointer" transform="scale(0.5)">
<circle cx="0" cy="0" r="20" id="dragcircle" />
</g>
</defs>
</svg>
</div>
<body style='overflow:hidden'>
<script>
// Get the viewport height and width
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
// Fit to viewport
var height = vw*0.7;
var width = vw;
// Create the canvas. We will use only part of it for the main plot
var svg = d3.select("#click") // This selects the div
.attr("width", width) // This defines the canvas' width
.attr("height", height) // This defines the canvas' height
// define the clipPath
svg.append("clipPath") // define a clip path
.attr("id", "ellipse-clip") // give the clipPath an ID
.append("ellipse") // shape it as an ellipse
.attr("cx", 175) // position the x-centre
.attr("cy", 100) // position the y-centre
.attr("rx", 100) // set the x radius
.attr("ry", 50); // set the y radius
// draw clipped path on the screen
svg.append("rect") // attach a rectangle
.attr("id","cliprect")
.attr("x", 125) // position the left of the rectangle
.attr("y", 75) // position the top of the rectangle
.attr("clip-path", "url(#ellipse-clip)") // clip the rectangle
.style("fill", "lightgrey") // fill the clipped path with grey
.attr("height", 100) // set the height
.attr("width", 200); // set the width
// Shift the marker around on mouseover; restrict it to the contour
var movex
var movey
svg
.on("mousemove", function () {
// Get the current mouseover coordinates
movex = d3.event.x;
movey = d3.event.y;
// The only way I get this to work right now is by removing the previous clipped shape, then re-adding it
d3.select("#cliprect").remove()
d3.select("#ellipse-clip").remove()
// define the clipPath
svg.append("clipPath") // define a clip path
.attr("id", "ellipse-clip") // give the clipPath an ID
.append("ellipse") // shape it as an ellipse
.attr("cx", movex) // position the x-centre
.attr("cy", movey) // position the y-centre
.attr("rx", 100) // set the x radius
.attr("ry", 50); // set the y radius
// draw clipped path on the screen
svg.append("rect") // attach a rectangle
.attr("id","cliprect")
.attr("x", 125) // position the left of the rectangle
.attr("y", 75) // position the top of the rectangle
.attr("clip-path", "url(#ellipse-clip)") // clip the rectangle
.style("fill", "lightgrey") // fill the clipped path with grey
.attr("height", 100) // set the height
.attr("width", 200); // set the width
});
</script>
</body>
</html>

Use this in place of your mousemove callback
function() {
// Get the current mouseover coordinates
movex = d3.event.x;
movey = d3.event.y;
// move the clipPath
d3.select("#ellipse-clip") // selects the clipPath
.select("ellipse") // selects the ellipse
.attr("cx", movex) // position the x-centre
.attr("cy", movey) // position the y-centre
// move clipped path on the screen
svg.select("rect") // attach a rectangle
.attr("x", movex) // position the left of the rectangle
.attr("y", movey) // position the top of the rectangle
}

Related

How to create variable number of image or circle elements in an SVG element using a loop in D3 js(v3)

I'm trying to make a pictogram using d3. So, I have a scaled value with a range from 1 to 10, and I want to create images or circles to represent the numbers, that is, 7 circles or images for number 7, 5 for number 5 etc. The image or circle tag should be drawn on a 'mousein' event. 'mouseout' event shouldn't remove the images. But every mousein event should redraw correct number of circles. Can someone demonstrate a simple example with the right approach?
Things I've tried:
I'm able to append n image tags to the svg element but I'm only able to remove the last element since the variable gets the value of last circle drawn.
for(i=1;i<limit;i++){ // 1<=limit<=10 --> Scaled value on each mousein event
var img = svg.append("image")
.attr("x", i*width*1/8)
.attr("y", height*1/10)
.style("opacity", 1)
.attr("width", 50)
.attr("height", 50)
.attr("xlink:href", "image.png")
.attr("class", "image");
}
When I put all the elements in a group, the image is not being displayed but the elements are present in DOM and also shows the correct path to the image.
The whole power of D3.js lies in its ability to bind data to display elements so you don't have to write loops to manually add items to the DOM.
This example demonstrates adding circles to the SVG element using the selectAll(), data(), enter() and remove() functions which are described in Mike Bostock's article Thinking in Joins.
const width = 500;
const height = 500;
let svg = d3.select('svg')
// create the rectangles to hover over
let rectangleHolders = svg.select('.rectangle-holder')
.selectAll('rect')
.data([1, 2, 3, 4, 5])
.enter()
// to add a rectangle with text, we need to add a group to hold both elements
.append('g')
// translate group to correct position
.attr('transform', (d, i) => `translate(${i*40})`)
// add rectangle to each group
rectangleHolders.append('rect')
.attr('height', 18)
.attr('width', 18)
// add event handler to rectangle to draw circles
.on('mouseover', draw)
// add text to each group
rectangleHolders.append('text')
.attr('dy','15')
.attr('dx','10')
.attr('text-anchor','middle')
.text(d => d)
// function to draw circles
function draw(number) {
console.log('draw', number)
// create some dummy data in this case an
// array of length 'count' full of nulls
const data = Array(number)
// select circle elements and bind data
let circles = svg.select('.circle-holder').selectAll('circle')
.data(data)
// enter, append and set attributes
circles.enter()
.append('circle')
.attr('cx', (d, i) => i * 20 + 10)
.attr('cy', 10)
.attr('r', 8)
.attr('value', d => d)
// add a tranisition to the opacity so the circles fade in
.attr('opacity', 0)
.transition()
.attr('opacity', 1)
// exit and remove unused elements (with transition)
circles.exit()
.transition()
.attr('opacity', 0)
.remove()
}
rect {
fill: none;
stroke: black;
}
body {
font-family:sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg height="40" width="200">
<g class="rectangle-holder"></g>
<g class="circle-holder" transform="translate(0,20)"></g>
</svg>

Rotate a rectangle on dragging event D3

Hi everybody I am trying to rotate a rectangle while the user drag it with mouse. The rectangle follow a circular curve.
Below I attach my solution that is perfect, but the mouse is always on top left corner of the rectangle. I want that the mouse would be always in the center of the rectangle during dragging. How can I control it ?
Solution:
var drag = d3.drag().on("drag", function () {
var rect = d3.select(this);
var theta = Math.atan2(d3.event.y - height/2, d3.event.x - width/2) * 180 / Math.PI
rect
.attr("x", d3.event.x)
.attr("y", d3.event.y)
.attr('transform', `rotate(${theta + 90}, ${d3.event.x}, ${d3.event.y})`)
})
Full code of my solution you can see here: https://jsfiddle.net/hsspve49/
Offset the x and y attribute in the drag handler by the size of your rectangle, e.g.:
...
.attr("x", d3.event.x - 15) // half the width
.attr("y", d3.event.y - 35) // half the height
...

Rotate a rectangle during drag D3

I am new on D3 v4 and D3 in general. I created a rectangle and I can drag it on the canvas. Now I want to dynamically rotate the rectangle basing on the circle's radius.
You can check my code here:
https://jsfiddle.net/n4m1r8nb/208/
I also tried to add rotate attribute on drag function, but if I add it, the rectangle move following the mouse as per x and y definition in the snippet you can see here below, without rotating, and an error appear ".rotate is not a function".
var drag = d3.drag().on("drag", function () {
d3.select(this)
.rotate(d3.event.x)
.attr("x", d3.event.x)
.attr("y", d3.event.y);
console.log("X: ", d3.event.x)
console.log("Y: ", d3.event.y)
})
You can see what I mean in this pic (http://imgur.com/a/APbu9). I wanna rotate the black rectangle as per the screenshot in that url.
Thanks in advance.
I have created a fiddle to demonstrate this: https://jsfiddle.net/hsspve49/
Relevant part of the code are in the drag handler:
var drag = d3.drag().on("drag", function () {
var rect = d3.select(this);
var theta = Math.atan2(d3.event.y - height / 2, d3.event.x - width / 2) * 180 / Math.PI
rect
.attr("x", d3.event.x)
.attr("y", d3.event.y)
.attr('transform', `rotate(${theta + 90}, ${d3.event.x}, ${d3.event.y})`)
})

D3 Convention for (re)drawing a single object

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>

Why radial gradient in d3 changes when the height/width of the svg container is not the same as the height/width?

I am trying to understand how the radial gradient in d3.js works. I have a circle and I filled it with the radial gradient specified in 'defs' tag. I set the 'r' coordinate of the radial gradient to be 60% and 'cx' and 'cy' to be zero. My understanding is the radial gradient will always start from 0,0 coordinate (exactly in the middle of my circle element) and the outer circle of the radial gradient will "stop" at 60% of my "r". Now when I change only the width of my svg container and leave the height unchanged the radial gradient is not the same. It seems like the outer circle of the radial gradient is pushed away. I cannot understand why is this happening. I spent hours searching online but I could not find sufficient answer. Is it possible to maintain the shape of the radial gradient when the height or width is changed?
picture 1 here
This picture explains what I actually want to achieve. I want to fill the arc with the gradient. I think I need to use "userSpaceOnUse" for gradient units because I am filling this radial gradient into individual segments as shown here picture 2 here. I tried to use objectBoundingBox instead but what I got as a result was an individual radial gradient got filled in each segment.(aka circle in each segment). When I change the height/width of the container the outer circle of the radial gradient gets pushed away so the arc colour actually loses the gradient effect. This is why I am trying to find out a way of maintaining the shape of the radial gradient when the height/width gets changed.
Thanks
This is my code:
var w = 1000, h = 1000
r = Math.min(w, h) /4 ;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" +Math.min(w,h)/2 + "," +Math.min(w,h) /2 + ")");
var radialGradient = d3.select("svg").append("defs")
.append("radialGradient")
.attr("id", "radial-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", "60%")
.attr("spreadMethod", "reflect");
radialGradient.append("stop")
.attr("offset", "15%")
.attr("stop-color", "red");
radialGradient.append("stop")
.attr("offset", "25%")
.attr("stop-color", "#fff");
radialGradient.append("stop")
.attr("offset", "35%")
.attr("stop-color", "red");
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.style("fill", "url(#radial-gradient)");
You has set the center of gradient to (cx,cy) 0,0 the origin of container and tell use container space: userSpaceOnUse.
To correct this, just change to:
.attr("gradientUnits", "objectBoundingBox") // your object limits
.attr("cx", "50%")
.attr("cy", "50%") // your object center
Hope this help

Categories