I'm trying to drag a group of shapes on a clipped path. For the first time, It works fine, but as soon as I started dragging, clipping does not work at all.
Here is my working code;
var svg = d3.select("svg");
// draw a circle
svg.append("clipPath") // define a clip path
.attr("id", "clip") // give the clipPath an ID
.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
var g = svg.append("g")
.datum({x:0, y:0})
.attr("transform", function(d) { return 'translate(' + d.x + ' '+ d.y + ')'; })
.attr("clip-path","url(#clip)")
.call(d3.drag()
.on("start", function(d){
d3.select(this).raise().classed("active", true);
})
.on("drag", function(d){
d3.select(this).attr("transform","translate(" + (d3.event.x) + "," + (d3.event.y) + ")" );
})
.on("end", function(d){
d3.select(this).classed("active", false);
}));
g.append("rect")
.attr("x",100)
.attr("y",80)
.attr("height",100)
.attr("width",200)
g.append("line")
.attr("x1", 100)
.attr("y1", 80)
.attr("x2", 200)
.attr("y2", 80)
.style("stroke", "purple")
.style("stroke-width", 12)
.svgClass{
border:2px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="500" height="300" class="svgClass"></svg>
You can see on dragging, first time clipped shape is moving all the way. No further clipping is there.
To make it easy, I redraw the outer circle again. Check this code;
var svg = d3.select("svg");
// draw a circle
svg.append("clipPath") // define a clip path
.attr("id", "clip") // give the clipPath an ID
.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
// redraw circle to make it easy
svg.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
var g = svg.append("g")
.datum({x:0, y:0})
.attr("transform", function(d) { return 'translate(' + d.x + ' '+ d.y + ')'; })
.attr("clip-path","url(#clip)")
.call(d3.drag()
.on("start", function(d){
d3.select(this).raise().classed("active", true);
})
.on("drag", function(d){
d3.select(this).attr("transform","translate(" + (d3.event.x) + "," + (d3.event.y) + ")" );
})
.on("end", function(d){
d3.select(this).classed("active", false);
}));
g.append("rect")
.attr("x",100)
.attr("y",80)
.attr("height",100)
.attr("width",200)
g.append("line")
.attr("x1", 100)
.attr("y1", 80)
.attr("x2", 200)
.attr("y2", 80)
.style("stroke", "purple")
.style("stroke-width", 12)
.svgClass{
border:2px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="500" height="300" class="svgClass"></svg>
Here You can see clipping is not working at all. I want to bound this dragging within circle and if moves out of clipping boundaries, it should clip it accordingly.
Can anyone help me out with this requirement? Or let me know where I'm
doing it wrong.
The drag callback is transforming the same g element that the clip path has been applied to. This means that the g element's clip path is also being transformed, which is why the clipped shape is moving around as you drag your shape.
The snippet below uses a grey rectangle to show the clip path definition, and a pink rectangle to show the area of the transformed g element. The circle is retaining the original clip shape because the g element's clip path is being translated along with the rest of the element.
<svg width="300" height="300">
<clipPath id="cut">
<rect width="100" height="100" x="100" y="50"></rect>
</clipPath>
<rect x="100" y="50" width="100" height="100" fill="#eee"></rect>
<g clip-path="url(#cut)" transform="translate(50, 50)">
<rect x="100" y="50" width="100" height="100" fill="pink"></rect>
<circle
class="consumption"
cx="100"
cy="100"
r="50">
</circle>
</g>
</svg>
In the snippet below, a clip path is applied to an outer g element (which is not translated and has the same co-ordinates as the original clip path definition), while the transformation is applied to an inner g element.
<svg width="300" height="300">
<clipPath id="cut">
<rect width="100" height="100" x="100" y="50"></rect>
</clipPath>
<rect x="100" y="50" width="100" height="100" fill="#eee"></rect>
<g clip-path="url(#cut)">
<rect x="100" y="50" width="100" height="100" fill="pink"></rect>
<g transform="translate(100, 50)">
<circle
class="consumption"
cx="100"
cy="100"
r="50">
</circle>
</g>
</g>
</svg>
So, as shown in the example you should apply the clip path to an outer g element, while transforming an inner g element.
Related
I am learning D3.js. I wrote following code which moves the circle on click. I want to move this circle forward (+100px) from its current position on every click.
I want to do it without CSS
Following is my code
var svgContainer = d3.select("body").append("svg").attr("id", "root")
.attr("width", 500)
.attr("height", 500).append("g")
//Draw the Circle
var circle = svgContainer.append("circle")
.attr("id", "Zubi")
.attr("cx", 100)
.attr("cy", 30)
.attr("r", 20)
.on("click", function() {
d3.select(this)
.transition()
.duration(3000)
.style("fill", "red")
.attr("cx", 100)
})
//.attr("transform", "translate(200,200)")})
The easiest way to achieve what you want is using a getter (selection.attr("cx")) to get the current position and increasing it. You can also use a translate, but since you're a D3 learner using the circle's cx attribute is probably easier to understand.
Here is a demo, each click increases 50px:
const circle = d3.select("circle")
.on("click", function() {
d3.select(this)
.transition()
.attr("cx", +d3.select(this).attr("cx") + 50)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100">
<circle cx="50" cy="50" r="30" fill="teal"></circle>
</svg>
Have in mind that the getter always returns a string, so you have to coerce it to a number.
Also, since you have just one element in the selection and it has a name, you don't need d3.select(this):
const circle = d3.select("circle")
.on("click", function() {
circle.transition()
.attr("cx", +circle.attr("cx") + 50)
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100">
<circle cx="50" cy="50" r="30" fill="teal"></circle>
</svg>
I'm a newbie to D3 and ReactJS, and trying to clip circle with a straight line, but could not get how it works. I have this HTML code to draw image;
<div >
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" className="clip-path" ref={this.attachCircle.bind(this)}>
</svg>
</div>
here is my function to draw image;
attachCircle = (svgRef:SVGElement) => {
const svg = d3.select(svgRef);
// draw a circle
svg.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "SteelBlue")
}
and it results in this way;
but as soon as I add clipPath to circle;
svg.append("clipPath") // define a clip path
.attr("id", "clip") // give the clipPath an ID
.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "SteelBlue")
it shows nothing on the screen. It is not drawing any circle or anything.
Can anyone explain why is it so?? or What am I missing?
You need to use use
<use clip-path="url(#clip)" xlink:href="#clip" fill="red" />
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath
I have this HTML structure
<g class="type type-project" id="g-nsmart_city_lab" transform="translate(954.9537424482861,460.65694411587845)">
<circle class="highlighter-circles" fill-opacity="0" r="70" fill="rgb(150,150,150)" id="hc-nsmart_city_lab"></circle>
<circle class="node" r="50" fill="#768b83" id="nsmart_city_lab" filter="url(#blur)"></circle>
<text font-family="Comic Sans MS" font-size="18px" fill="black" class="nodetext" id="t-nsmart_city_lab" style="text-anchor: middle;" x="0" y="0">SMART CITY LAB</text>
<image href="./icons/project.svg" width="30" height="30" id="i-nsmart_city_lab" class="nodeimg"></image>
<image href="./icons/expand2.svg" width="30" height="30" for-node="nsmart_city_lab" x="25" y="-45" id="ne-nsmart_city_lab" class="nodeexp" style="visibility: hidden;" data-expandable="false"></image>
<circle class="inv_node" r="50" fill="red" fill-opacity="0" id="inv_nsmart_city_lab"></circle>
</g>
and I want to to something with the g elements that fulfill certain condition. But when doing,
d3.selectAll("g.type").filter(g_element => g_element.class !== "whatever");
the filter does not work as expected (at least for me). g_element.class is undefined. After debugging, for some reason the filtering is returning <circle class="node" r="50" fill="#768b83" id="nsmart_city_lab" filter="url(#blur)"></circle> instead of a g object to access its attributes and do the filtering.
How could this be done then ?
Here you have a jsfiddle which always returns undefined, https://jsfiddle.net/k6Ldxtof/40/
In your snippet...
d3.selectAll("g.type").filter(g_element => g_element.class !== "whatever");
... the first argument, which you named g_element, is the datum bound to that element. As there is no data bound here, that's obviously undefined.
To get the element instead, you have to use this. However, since you're using a arrow function here, you need to use the second and third arguments combined:
d3.selectAll("g.type")
.filter((_,i,n) => console.log(n[i]))
Then to get the classes, you can simply use a getter...
d3.selectAll("g.type")
.filter((_,i,n) => console.log(d3.select(n[i]).attr("class")))
Or, even simpler, using classList:
d3.selectAll("g.type")
.filter((_, i, n) => console.log(n[i].classList))
Here is the demo:
function create() {
let g = d3.select("body")
.append("svg")
.attr("height", "500")
.attr("width", "500")
.append("g");
g.append("g")
.attr("class", "type type-red")
.attr("data-color", "red")
.append("circle")
.attr("r", 50)
.attr("fill", "red")
.attr("cx", 50)
.attr("cy", 50);
g.append("g")
.attr("class", "type type-green")
.attr("data-color", "green")
.append("circle")
.attr("r", 50)
.attr("fill", "green")
.attr("cx", 200)
.attr("cy", 50);
g.append("g")
.attr("class", "type type-blue")
.attr("data-color", "blue")
.append("circle")
.attr("r", 50)
.attr("fill", "blue")
.attr("cx", 100)
.attr("cy", 150);
filter_out();
}
/***************** USING THE SELECTOR ********************/
function filter_out() {
d3.selectAll("g.type")
.filter((_, i, n) => console.log(n[i].classList))
.attr("opacity", 0.5);
}
create();
<script src="https://d3js.org/d3.v4.js"></script>
In my previous question I asked how could I generate unique IDs for generated rectangles here
Now, I've been trying to draw small rectangles inside those rectangles. However, id="rectup0" can only have 4 small rect with unique xlink:href attribute
the rest of them can have 2 rectangles inside.
<body>
<div id="chart">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.3.0/d3.js"></script>
<script type="text/javascript">
var width = 1000,
height = 250,
margin = 7,
nRect = 11,
rectWidth = (width - (nRect - 1) * margin) / nRect,
svgContainer = d3.select('#chart').append('svg')
.attr('width', width)
.attr('height', height),
svgWalkway = d3.select('#chart').append('svg')
.attr('width', '1000')
.attr('height', '150'),
svgContainer2 = d3.select('#chart').append('svg')
.attr('width', width)
.attr('height', height);
// Rectangles scaling
var firstRow = d3.range(nRect),
posxScale = d3.scaleLinear()
.domain(d3.extent(firstRow))
.range([0, width - rectWidth]);
var middleRow = d3.range(1),
posxScale3 = d3.scaleLinear()
.domain(d3.extent(middleRow))
.range([0, width - rectWidth]);
var secondRow = d3.range(nRect),
posxScale2 = d3.scaleLinear()
.domain(d3.extent(secondRow))
.range([0, width - rectWidth]);
//Drawing the actual objects HTML rects
svgContainer.selectAll('rect.up')
.data(firstRow)
.enter()
.append('rect')
.attr("id", function(d,i){ return "rectup" + i })
.attr('x', posxScale)
.attr('width', rectWidth)
.attr('height', height)
.attr('class','up');
svgWalkway.selectAll('rect.middle')
.data(middleRow)
.enter()
.append('rect')
.attr('x', posxScale3)
.attr('width','1000')
.attr('height','150')
.attr('class', 'middle');
svgContainer2.selectAll('rect.down')
.data(secondRow)
.enter()
.append('rect')
.attr('id', function (d,i) { return "rectdown" + i })
.attr('x', posxScale2)
.attr('width', rectWidth)
.attr('height', height)
.attr('class', 'down')
.attr('y', 0);
</script>
</div>
</body>
I've tried drawing similar rectangles like the previous ones (using .append()) but so far that just put them next to svgContainer. I've been reading some posts regarding using grouping, tried it but didn't work. Thanks in advance
UPDATE
so basically, the whole question can be resumed as adding a rect tag right after rect id="rectup0" so I can have
<rect id="rectup0"></rect>
<rect id="redrect" x="0" width="20" height="40"></rect>
I know JQuery lets you add class attributes to other classes, but not sure about D3 , this will be something like adding a new rect tag programmatically. is that even possible?
UPDATE 2
I guess it is possible with d3.insert, with the following:
d3.select('#rectup0').insert('rect')
.attr('id', 'redrect')
.attr('x', 0)
.attr('width', '20')
.attr('height', '40');
however it gets it in the middle of the beginning and closing rect tag like this
<rect id="rectup0">
<rect id="redrect" x="0" width="20" height="40"></rect>
</rect>
Needless to say this doesn't work because of the closing rect tag
You can do the trick with each wrapper for selected data. It accepts 3 arguments: current data d, current index i, and whole array of data arr. So if you need some kind of checks, should you append the small rect, or not, you can create a helper function:
var testIndex = function(d, i, arr) {
return
// test index
i == 5 ||
// test current data
d == 10 ||
// test other array members, which would be an EnterNode here
(i > 0 && arr[i - 1]['__data__'] == 7);
}
// do checks in one line:
var testIndex = function (d, i, arr) { return i == 4 || i == 10 || i == 7; }
var data = svgContainer.selectAll('rect.up')
.data(firstRow);
data
.enter()
.each(function (d, i, arr) {
// select DOM element to add the rects
t = d3.select(this);
t
.append('rect')
// note that here we use empty the parameters list
.attr('id', 'rectup' + i)
.attr('x', posxScale)
.attr('width', rectWidth)
.attr('height', height)
.attr('class','up');
if (!testIndex(d, i, arr)) {
return;
}
t
.append('rect')
.attr('id', 'redrect' + i)
// note that we need to define x-coord similar to first rect
.attr('x', posxScale)
.attr('class', 'redrect');
});
// remove your data before redraw
data
.exit()
.remove();
Output:
<svg width="1000" height="250">
<rect id="rectup0" x="0" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup1" x="91.54545454545456" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup2" x="183.09090909090912" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup3" x="274.6363636363636" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup4" x="366.18181818181824" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup5" x="457.72727272727275" width="84.54545454545455" height="250" class="up"></rect>
<rect id="redrect5" x="457.72727272727275" class="redrect"></rect>
<rect id="rectup6" x="549.2727272727273" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup7" x="640.8181818181818" width="84.54545454545455" height="250" class="up"></rect>
<rect id="redrect7" x="732.3636363636365" class="redrect"></rect>
<rect id="rectup8" x="732.3636363636365" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup9" x="823.909090909091" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup10" x="915.4545454545455" width="84.54545454545455" height="250" class="up"></rect>
<rect id="redrect10" x="915.4545454545455" class="redrect"></rect>
</svg>
The downside of such method is that you can't provide a attr parameter as JSON:
// wouldn't work
.attr({
id: 'redrect',
x: posxScale,
width: '20',
height: '40'
});
My goal is to add an image into an existing circle with d3. The circle will render and is interactive with mouseover method, but only when I use "fill", "color", and not something more sophisticated like .append("image").
g.append("circle")
.attr("class", "logo")
.attr("cx", 700)
.attr("cy", 300)
.attr("r", 10)
.attr("fill", "black") // this code works OK
.attr("stroke", "white") // displays small black dot
.attr("stroke-width", 0.25)
.on("mouseover", function(){ // when I use .style("fill", "red") here, it works
d3.select(this)
.append("svg:image")
.attr("xlink:href", "/assets/images/logo.jpeg")
.attr("cx", 700)
.attr("cy", 300)
.attr("height", 10)
.attr("width", 10);
});
The image doesn't show after I mouse over. Using Ruby on Rails app, where my image "logo.jpeg" is stored in the assets/images/ directory. Any help for getting my logo to show within the circle? Thanks.
As Lars says you need to use pattern, once you do that it becomes pretty straightforward. Here's a link to a conversation in d3 google groups about this. I've set up a fiddle here using the image of a pint from that conversation and your code above.
To set up the pattern:
<svg id="mySvg" width="80" height="80">
<defs id="mdef">
<pattern id="image" x="0" y="0" height="40" width="40">
<image x="0" y="0" width="40" height="40" xlink:href="http://www.e-pint.com/epint.jpg"></image>
</pattern>
</defs>
</svg>
Then the d3 where we only change the fill:
svg.append("circle")
.attr("class", "logo")
.attr("cx", 225)
.attr("cy", 225)
.attr("r", 20)
.style("fill", "transparent")
.style("stroke", "black")
.style("stroke-width", 0.25)
.on("mouseover", function(){
d3.select(this)
.style("fill", "url(#image)");
})
.on("mouseout", function(){
d3.select(this)
.style("fill", "transparent");
});
nodeEnter.append("svg:image")
.attr('x',-9)
.attr('y',-12)
.attr('width', 20)
.attr('height', 24)
.attr("xlink:href", function(d) {
if(d.type=="root"){
return "resources/images/test.png";}
else if(d.type.toLowerCase()=="A"){
return "resources/icon01/test1.png";}
else if(d.type=="B"){
return "resources/icon01/test2.png";}
})
.append("svg:title")
.text(function(d){
if(d.type=="root"){
return "Root Name:"+d.name;}
else if(d.type=="test"){
return "Name:"+d.name;}
});