I am trying to "clip" this spinning wheel: https://bl.ocks.org/mpmckenna8/7f1f0adbf7d9ed7520b3950103e8094c
I want to only make the top-half of the wheel visible. When I try to do this with "clip-path" I end up having a half-wheel rotating. (see: https://codepen.io/icklerly/pen/JMBdGX)
svg.append("clipPath") // define a clip path
.attr("id", "ellipse-clip") // give the clipPath an ID
.append("rect")
.attr("x", -100)
.attr("y", 0)
.attr("width", 200)
.attr("height", 200);
But I want the wheel to rotate and the clip window always on the same position top.
Any suggestions?
The issue is that you are rotating the g element on where you applied the clip-path. Instead you can add another g on where you apply the clip-path and keep the rotation on another g inside.
So intead of this :
var hub = svg.append('g')
.attr('transform', function(){
return "translate(" + width/2 + "," + height/2 + ")"
})
.attr('class', 'hub')
.attr("clip-path", "url(#rect-clip)")
Do this :
var hub = svg.append('g').attr("clip-path", "url(#rect-clip)") /* append first g with clip-path */
.append('g') /* then create the inside g with the remaining properties*/
.attr('transform', function(){
return "translate(" + width/2 + "," + height/2 + ")"
})
.attr('class', 'hub')
You can also adjust the clip-path and simply make its size half the wheel to avoid using negative value for x/y.
Full Code:
var svg = d3.select('svg')
var margin = 20;
var width = 200, // margin,
height = 200 // margin;
svg.append("clipPath") // define a clip path
.attr("id", "rect-clip") // give the clipPath an ID
.append("rect")
.attr("x", 0) // position the x-centre
.attr("y", 0) // position the y-centre
.attr("width", 200) // set the x radius
.attr("height", 100);
var hub = svg.append('g').attr("clip-path", "url(#rect-clip)").append('g')
.attr('transform', function() {
return "translate(" + width / 2 + "," + height / 2 + ")"
})
.attr('class', 'hub')
hub.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 10)
.attr('fill', 'pink')
hub.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 90)
.attr('stroke', 'red')
.attr('stroke-width', 5)
.attr('fill', 'none')
var linelen = [0, 90];
var line = d3.line().x(function(d) {
return (0)
})
.y(function(d) {
return (d)
})
const numberSpokes = 10;
for (let i = 0; i < numberSpokes; i++) {
var rotation = (360 / numberSpokes) * i;
var spoke = hub
.append('path')
.datum(linelen)
.attr('d', line)
.attr('stroke', 'blue')
.attr('transform', 'rotate(' + rotation + ')')
.attr('class', 'spoke')
}
const alreadyTransformed = hub.attr('transform')
rotateIt(false)
function rotateIt(much) {
//console.log(alreadyTransformed)
hub.transition().duration(4000)
.attr('transform', alreadyTransformed + ' rotate(' + (much ? 0 : 180) + ')')
.on('end', function() {
rotateIt(!much)
})
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg id="svger" width="200px" height="200px"></svg>
Related
In my transition, an axis rotates 90 degree and then the labels rotate in the opposition direction in order to remain upright. Below is a minimal example of what I want, except the transition is not as smooth as it could be. If you watch closely, you can see the labels shift (translate) up before rotating into place. How can I get rid of this shift? I've fiddled with rotate and translate to no avail.
(If you think this isn't too bad, I agree, but the shift is actually significantly more noticeable in my actual plot for some reason.)
Update. The culprit is the text-anchor property's getting switched back and forth between middle and start. Since these are discrete values, I can't think of a simple way to transition between them.
var width = 170;
var scale = d3.scaleLinear().domain([0, 5])
.range([0, width]);
var axis = d3.axisBottom()
.scale(scale)
.ticks(6);
var graph = d3.select('svg').append('g')
.attr('transform', 'translate(10,10)');
graph.append('g')
.attr('transform', 'translate(0,' + width + ')')
.call(axis);
var tickLabels = d3.selectAll('text');
var toggle = false;
d3.select('button').on('click', function() {
toggle = !toggle;
if (toggle) {
graph.transition().duration(1000)
// .attr('transform','rotate(-90)');
.attr('transform', 'rotate(-90 ' + (width / 2 + 10) + ' ' + (width / 2 + 10) + ')');
tickLabels.transition().duration(1500).delay(1000)
.attr("y", 0)
.attr("x", 9)
.attr("dy", ".3em")
.attr("transform", "rotate(90)")
.style("text-anchor", "start");
} else {
graph.transition().duration(1000)
.attr('transform', 'rotate(0) translate(10,10)');
tickLabels.transition().duration(1500).delay(1000)
.attr('y', 9)
.attr('x', 0.5)
.attr('dy', '0.71em')
.attr('transform', 'rotate(0)')
.style('text-anchor', null);
}
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width='200' height='200'>
</svg>
<div>
<button>Rotate</button>
</div>
Found the solution, which is actually fairly simple. The key is to alter the x attribute to offset the text-anchor shift before rotating the labels. The result is actually quite nice.
var width = 170;
var scale = d3.scaleLinear().domain([0, 5])
.range([0, width]);
var axis = d3.axisBottom()
.scale(scale)
.ticks(6);
var graph = d3.select('svg').append('g')
.attr('transform', 'translate(10,10)');
graph.append('g')
.attr('transform', 'translate(0,' + width + ')')
.call(axis);
var tickLabels = d3.selectAll('text');
var toggle = false;
d3.select('button').on('click', function() {
toggle = !toggle;
if (toggle) {
graph.transition().duration(1000)
// .attr('transform','rotate(-90)');
.attr('transform', 'rotate(-90 ' + (width / 2 + 10) + ' ' + (width / 2 + 10) + ')');
tickLabels.transition().duration(0).delay(1000)
.attr('x', -3)
.style("text-anchor", "start")
.transition().duration(1000)
.attr("y", 0)
.attr("x", 9)
.attr("dy", ".3em")
.attr("transform", "rotate(90)");
} else {
graph.transition().duration(1000)
.attr('transform', 'rotate(0) translate(10,10)');
tickLabels.transition().duration(0).delay(1000)
.attr('x', 12)
.style('text-anchor', null)
.transition().duration(1000)
.attr('y', 9)
.attr('x', 0.5)
.attr('dy', '0.71em')
.attr('transform', 'rotate(0)');
}
});
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width='200' height='200'>
</svg>
<div>
<button>Rotate</button>
</div>
I've tried to create a legend using inspiration from http://zeroviscosity.com/d3-js-step-by-step/step-3-adding-a-legend. However, despite having almost the exact same code, the legend isn't visualized. Here's the jsfiddle and the code: http://jsfiddle.net/u5hd25qs/
var width = $(window).width();
var height = $(window).height() / 2;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var legendRectSize = 36; // 18
var legendSpacing = 8; // 4
var recordTypes = []
recordTypes.push({
text : "call",
color : "#438DCA"
});
recordTypes.push({
text : "text",
color : "#70C05A"
});
var legend = svg.selectAll('.legend')
.data(recordTypes)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function (d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * recordTypes.length / 2;
var horz = -2 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', function (d) {
return d.color
})
.style('stroke', function (d) {
return d.color
});
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function (d) {
return d.text;
});
Your code works okay, but this is what you generate:
<g class="legend" transform="translate(-72,-44)">...
Because your translate rule has negative values in it, the legend is positioned outside the screen (it is simply not visible).
Now, the example you're basing your work on has a pie chart that has already been translated to the center of the screen, so negative values are not an issue.
You need to change your math or wrap the legend in some container which you can position in the same way as the pie chart example:
legendContainer
.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')');
Fiddle Example
How can I get the tooltip to show up closer to the hovered slice? I can't get d3.pageXOffset and d3.pageYOffset (like this example) to work. What is the way to get the hovered pie slice position for the tooltip?
Not working:
path.on('mouseover', function(d) {
tooltip.style("top", d3.pageYOffset + "px").style("left", d3.pageXOffset + "px")
});
Full code:
var dataset =
[
{item:"Boy",result:12},
{item:"Girl",result:24},
{item:"Woman",result:60},
{item:"Man",result:10}
]
var width = 280;
var height = 280;
var radius = Math.min(width, height) / 2;
var color = d3.scale.category10();
var svg = d3.select('#area')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var arc = d3.svg.arc()
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(d) { return d.result; })
.sort(null);
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.item);
});
var tooltip = d3.select('#area').append('div').attr('class', 'tooltip');
path.on('mouseover', function(d) {
var matrix = this.getScreenCTM()
.translate(+this.getAttribute("cx"),
+this.getAttribute("cy"));
var total = d3.sum(dataset.map(function(d) {
return d.result;
}));
var percent = Math.round(1000 * d.data.result / total) / 10;
tooltip.style("top", d3.pageYOffset + "px")
.style("left",d3.pageXOffset + "px")
.style('display', 'block')
.style("opacity", 1)
.append('div')
.attr('class', 'label')
.append('div')
.attr('class', 'count')
.append('div')
.attr('class', 'percent');
tooltip.select('.label').html(d.data.item);
tooltip.select('.count').html(d.data.result);
tooltip.select('.percent').html(percent + '%');
});
path.on('mouseout', function() {
tooltip.style('display', 'none');
});
Use d3.event:
tooltip.style("top", d3.event.y + "px").style("left", d3.event.x + "px");
I would also put it into a mousemove handler for better results.
Updated fiddle.
Jsfiddle: http://jsfiddle.net/6NBy2/
Code:
var in_editor_drag = d3.behavior.drag()
.origin(function() {
var g = this.parentNode;
return {x: d3.transform(g.getAttribute("transform")).translate[0],
y: d3.transform(g.getAttribute("transform")).translate[1]};
})
.on("drag", function(d,i) {
g = this.parentNode;
translate = d3.transform(g.getAttribute("transform")).translate;
x = d3.event.dx + translate[0],
y = d3.event.dy + translate[1];
d3.select(g).attr("transform", "translate(" + x + "," + y + ")");
d3.event.sourceEvent.stopPropagation();
});
svg = d3.select("svg");
d = {x: 20, y: 20 };
groups = svg
.append("g")
.attr("transform", "translate(20, 20)");
groups
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.style("fill", "green")
.call(in_editor_drag)
.style("opacity", 0.4);
I'm trying to drag a group by using one of it's children as a handle. Simply, what i'm trying to do is, when a groups child is dragged:
Get translation transformation of group
Get drag distance from d3.event.dx, d3.event.dy
Apply difference to group's transform attribute
When child dragged, group does not move as expected. It moves less than the dragged distance, and it begins to jump here and there.
What am I doing wrong here?
Edit:
Updated jsfiddle: http://jsfiddle.net/6NBy2/2/
I'm trying to drag the whole group by using one or more of it's children as dragging handles.
This is an old question, but not really answered. I had exactly the same problem and wanted to drag the group by only one child (not all child elements of the <g>).
The problem is, that the d3.event.dx/y is calculated relatively to the position of the <g>. And as soon as the <g> is moved by .attr(“transform”, “translate(x, y)”), the d3.event.dx/dy is adjusted to the new (smaller) value. This results in a jerky movement with approx. the half of the speed of the cursor. I found two possible solutions for this:
First (finally I ended up with this approach):
Append the drag handle rect directly to the svg and not to the <g>. So it is positioned relatively to the <svg> and not to the <g>. Then move both (the <rect> and the <g>) within the on drag function.
var svg = d3.select("svg");
var group = svg
.append("g").attr("id", "group")
.attr("transform", "translate(0, 0)");
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.style("fill", "green")
.style("opacity", 0.4);
group
.append("text")
.attr("x", 10)
.attr("y", 5)
.attr("dominant-baseline", "hanging")
.text("drag me");
handle = svg
.append("rect")
.data([{
// Position of the rectangle
x: 0,
y: 0
}])
.attr("class", "draghandle")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 20)
.style("fill", "blue")
.style("opacity", 0.4)
.attr("cursor", "move")
.call(d3.drag().on("drag", function (d) {
console.log("yep");
d.x += d3.event.dx;
d.y += d3.event.dy;
// Move handle rect
d3.select(this)
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
});
// Move Group
d3.select("#group").attr("transform", "translate(" + [d.x, d.y] + ")");
}));
<body>
<svg width="400" height="400"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
</body>
Second:
Check on which element the cursor was during the drag event with d3.event.sourceEvent.path[0] and run the drag function only if the handle <rect> was clicked. With this approach, all elements can be grouped within one <g> (no need for an additional <rect> outside the group). The downside of this method is, that the drag is also executed, if the cursor is moved over the drag handle with mouse down.
var svg = d3.select("svg");
var group = svg
.append("g")
.data([{
// Position of the rectangle
x: 0,
y: 0
}])
.attr("id", "group")
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")"
})
.call(d3.drag().on("drag", function (d) {
if (d3.event.sourceEvent.target.classList.value === "draghandle") {
console.log("yep");
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function (d) {
return "translate(" + [d.x, d.y] + ")"
})
} else {
console.log("nope");
return;
}
}));
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.style("fill", "green")
.style("opacity", 0.4);
group
.append("text")
.attr("x", 10)
.attr("y", 5)
.attr("dominant-baseline", "hanging")
.text("drag me");
handle = group
.append("rect")
.attr("class", "draghandle")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 20)
.style("fill", "blue")
.style("opacity", 0.4)
.attr("cursor", "move");
<body>
<svg width="400" height="400"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
</body>
use g = this; instead of g = this.parentNode;
Use drag.container() to set the container accessor.
See the D3 docs.
I have this D3 radial tree graph and what I need is the images to not being rotated, but appear straight together it's corresponding blue circle. Here is the code working in Codepen and I copy it here:
var radius = 960 / 2;
var tree = d3.layout.tree()
.size([360, radius - 120])
.separation(function (a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
var diagonal = d3.svg.diagonal.radial()
.projection(function (d) { return [d.y, d.x / 180 * Math.PI]; })
var vis = d3.select('#graph').append('svg:svg')
.attr('width', radius * 2)
.attr('height', radius * 2 - 150)
.append('g')
.attr('transform', 'translate(' + radius + ',' + radius + ')');
d3.json('flare2.json', function (json) {
var nodes = tree.nodes(json);
var link = vis.selectAll('path.link')
.data(tree.links(nodes))
.enter().append('path')
.attr('class', 'link')
.attr('d', diagonal);
var node = vis.selectAll('g.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; });
node.append('circle')
.attr('r', 4.5);
node.append('image')
.attr('xlink:href', 'img/avatar.40.gif')
.attr('width', 40)
.attr('height', 40)
.attr('x', 10);
node.append('image')
.attr('xlink:href', 'img/avatar.41.gif')
.attr('width', 40)
.attr('height', 40)
.attr('x', 50 );
});
Your nodes are rotated
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
And your images are appended to your nodes
node.append('image')
So you need to rotate the images back
.attr("transform", function(d) { return "rotate(" + (90 - d.x) + ")"; })
I'm not sure exactly how you want to position them, but you need to translate them on both x and y.
See a working example: http://codepen.io/anon/pen/qiCeG