I recently just found d3js and this block of example code when looking for a good way to zoom on elements: the example. I'm trying to use the same method of zooming, but instead of using json to populate the svg and body, I want to just zoom on specific html elements.I'm new to using d3 so I'm not sure what exactly I'm doing wrong, but heres my code so far:
JS:
window.onload=function(){
var width = 400,
height = 400,
centered;
var projection = d3.geo.albersUsa()
.scale(1070)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.on("click", clicked);
var circle = 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)
.attr("d", path)
.on("click", clicked)
.on("mouseout", function(){
d3.select(this)
.style("fill", "transparent");
});
function clicked(d) {
console.log("clicked");
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
circle.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });
circle.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
}
}
HTML:
<script src="d3.min.js"></script>
<script src="index.js"></script>
</head>
<body>
<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="image1.png"></image>
</pattern>
</defs>
</body>
Here is a fiddle of what I have so far. As you can see it calls the click function and styles the stroke width, but it doesn't zoom on the object.
Related
I am trying to fit d3.js hexbin visualization into the selected svg element.
However, at least the outer hexagons are cut into half by the element border. I tried applying a smaller width and height to the hexbin scaling but it does seem to have any effect.
Maybe there is something wrong with the viewBox?
Can anyone help me?
Thanks in advance.
var rect = document.querySelector('#dash'),
width = rect.offsetWidth,
height = rect.offsetHeight;
// set the dimensions and margins of the graph
var margin = {top: 50, right: 30, bottom: 30, left: 60}
var svg = d3.select("div#dash")
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 " + String(2000) + " " + String(2000) )
.classed("svg-content", true)
.append("g")
var randomX = d3.randomNormal(width, 80)
var randomY = d3.randomNormal(height, 80)
points = d3.range(200).map(function() { return [randomX(), randomY()]; });
var color = d3.scaleSequential(d3.interpolateLab("white", "blue")).domain([0, 20]);
var graph_width = width - width/10
var graph_height = height - height/10
var hexbin = d3.hexbin()
.size([graph_width, graph_height])
.radius(20);
svg.append("g")
.selectAll(".hexagon")
.data(hexbin(points))
.enter().append("path")
.attr("class", "hexagon")
.attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.style("fill", function(d) { return color(d.length); });
<html>
<head>
</head>
<body>
<div id="dash" class="svg-container"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
</body>
</html>
Set svg's width and height explicitly
Remove viewBox
Use simple randomization
const rect = document.querySelector('#dash');
const width = rect.offsetWidth;
const height = rect.offsetHeight;
const radius = 20;
const svg = d3.select("div#dash")
.append("svg")
.attr("width", width)
.attr("height", height)
.classed("svg-content", true)
.append("g")
points = d3.range(200)
.map(function() { return [Math.random() * width, Math.random() * height]; })
.filter(point => point[0] > radius && point[0] < width - radius * 2 && point[1] > radius && point[1] < height - radius);
const color = d3.scaleSequential(d3.interpolateLab("white", "blue")).domain([0, 20]);
var hexbin = d3.hexbin()
.size([width, height])
.radius(radius);
svg.append("g")
.selectAll(".hexagon")
.data(hexbin(points))
.enter().append("path")
.attr("class", "hexagon")
.attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.style("fill", function(d) { return color(d.length); });
#dash {
width: 300px;
height: 200px;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://d3js.org/d3-hexbin.v0.2.min.js"></script>
<div id="dash" class="svg-container"></div>
I am trying to modify the the tooltip in zalando's tech radar.
The relevant code is:
function showBubble(d) {
if (d.active || config.print_layout) {
var tooltip = d3.select("#bubble text")
.text(d.label);
var bbox = tooltip.node().getBBox();
d3.select("#bubble")
.attr("transform", translate(d.x - bbox.width / 2, d.y - 16))
.style("opacity", 0.8);
d3.select("#bubble rect")
.attr("x", -5)
.attr("y", -bbox.height)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 4);
d3.select("#bubble path")
.attr("transform", translate(bbox.width / 2 - 5, 3));
}
}
In order to extend the tooltip I tried doing the following based on the solution described here.
My modified code:
function showBubble(d) {
if (d.active || config.print_layout) {
var tooltip = d3.select("#bubble text");
tooltip.html("foo"); // this works!
//tooltip.html(function(d) { d.label}) // d is undefinded here ...
tooltip.append("div").attr("id", "foo");
d3.select("#foo").html("This is not shown").attr("style", "block");
var bbox = tooltip.node().getBBox();
d3.select("#bubble")
.attr("transform", translate(d.x - bbox.width / 2, d.y - 16))
.style("opacity", 0.8);
d3.select("#bubble rect")
.attr("x", -5)
.attr("y", -bbox.height)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 4);
d3.select("#bubble path")
.attr("transform", translate(bbox.width / 2 - 5, 3));
}
}
Can someone give me a hint how to show this extra text?
update
the complete code https://github.com/zalando/tech-radar
Multiline text in svg works a little different than HTML. You can't just append <div> & <br> tags because they don't mean anything in SVG.
So your options are to:
use a foreignObject to render HTML within SVG
var tooltip = d3.select("#bubble")
var fo = tooltip.append('foreignObject').attr('width', '100%').attr('height', '100%')
var foDiv = fo.append("xhtml:body").append("xhtml:div").attr('class', 'fe-div').style('background', '#ccc').html("foo <br>2nd line <br>3rd line")
html,
body,
svg {
height: 100%
}
.fe-div {
color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
<g id="bubble">
</g>
</svg>
or use positioned tspan elements to break up text like so:
var tooltip = d3.select("#bubble text");
tooltip.html("foo"); // this works!
// Create a tspan for the 2nd line
var tspan1 = tooltip.append("tspan");
tspan1.html("2nd line");
tspan1.attr('x', 0).attr('dy', '1em')
// Create a tspan for the 3rd line
var tspan2 = tooltip.append("tspan");
tspan2.html("3rd line");
tspan2.attr('x', 0).attr('dy', '1em')
html,
body,
svg {
height: 100%
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
<g id="bubble">
<text y="35"></text>
</g>
</svg>
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>
In a browser window I have an svg containing an image.
I also put some circles in this page.
When I resize the window, the image resizes correct but the circles just stay on their absolute position.
What is the best way to set this up?
If possible, the circles should not resize but change their position.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<style>
html,body{padding:0px; margin:0px; height:100%; width:100%;}
</style>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<title>Test</title>
<script type='text/javascript'>//<![CDATA[
window.onload=function()
{
function click()
{
// Ignore the click event if it was suppressed
if (d3.event.defaultPrevented) return;
// Extract the click location
var point = d3.mouse(this)
, p = {x: point[0], y: point[1] };
//Append the group
var newGroup = d3.select("svg").append("g")
.attr("transform", "translate(" + p.x + "," + p.y + ")")
.attr("drgg", "")
.style("cursor", "pointer")
.on("mouseup", selremove)
.call(drag);
//Append the circle
var newCircle = newGroup.append("circle")
.attr("r", "25")
.attr("class", "dot")
.style("stroke", "#999999")
.style("fill", "#66B132")
.attr("opacity", 0.8);
//Append the text
var newText = newGroup.append("text")
.text("43")
.style("fill", "#FFFFFF")
.style("font-family", "Arial")
.style("font-size", "24px")
.style("text-anchor", "middle")
.style("alignment-baseline", "central")
.style("readonly", "true");
}
//Create the SVG
var svg = d3.select("body").append("svg")
.attr("width", "100%")
.attr("height", "100%")
.on("click", click);
//Add a background to the SVG
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.style("stroke", "#999999")
.style("fill", "#F6F6F6")
//Add a Background-Picture
var pPic = d3.select("body").select("svg").append("image")
.attr("opacity", 1.0)
.attr("width", "100%")
.attr("height", "100%")
.attr("preserveAspectRatio", "xMidyMid")
.attr("xlink:href", "https://m.bmw.de/content/dam/bmw/common/all-models/m-series/x6m/2014/model-card/X6-M-F86_ModelCard.png")
//Move or delete
function selremove() {
if (d3.select(this).attr("drgg") == "")
{
d3.select(this).remove();
}
else
{
d3.select(this).attr("drgg", "");
}
}
function showinfo() {
//d3.select(this).attr("fill", "#000000");
var point = d3.mouse(this)
, p = {x: point[0], y: point[1] };
var newRect = svg.append("rectangle")
.attr("transform", "translate(" + p.x + "," + p.y + ")")
.attr("width", "25")
.attr("height", "25")
.style("stroke", "#999999")
.style("fill", "#FFFA83")
.attr("opacity", 1.0);
}
// Define drag beavior
var drag = d3.behavior.drag()
.on("drag", dragmove);
function dragmove()
{
var x = d3.event.x;
var y = d3.event.y;
d3.select(this)
.attr("transform", "translate(" + x + "," + y + ")")
.attr("drgg", "1");
}
}//]]>
</script>
</head>
<body>
<script>
// tell the embed parent frame the height of the content
if (window.parent && window.parent.parent){
window.parent.parent.postMessage(["resultsFrame", {
height: document.body.getBoundingClientRect().height,
slug: "None"
}], "*")
}
</script>
</body>
</html>
I first thought this could be achieved with relative units, but the changing aspect ratio of the SVG gets you into hot waters. So the best approach seems to come with clamping the SVG viewBox to the original image dimensions. These need to be known beforehand, as SVGImageElement is not able to extract them from the image source itself.
The price to pay for this is that the overlay circles have to be resized every time the window is resized.
This example does not concern itself with the drag functionality.
//an event counter
var counter = 0;
//image metadata
var pData = {
url: "https://m.bmw.de/content/dam/bmw/common/all-models/m-series/x6m/2014/model-card/X6-M-F86_ModelCard.png",
width: 890,
height: 501
}
//Create the SVG with viewBox at native image size
var svg = d3.select("body").append("svg")
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
.attr("width", "100%")
.attr("height", "100%")
.attr('viewBox', "0 0 " + pData.width + " " + pData. height)
.attr("preserveAspectRatio", "xMidyMid")
.on("click", click);
var defs = svg.append("defs");
//Add a Background-Picture
var pPic = d3.select("body").select("svg").append("image")
.attr("width", "100%")
.attr("height", "100%")
.attr("xlink:href", pData.url)
function click() {
// Ignore the click event if it was suppressed
if (d3.event.defaultPrevented) return;
// Extract the click location relative to SVG
var point = d3.mouse(this);
// get SVG scaling
var ctm = svg.node().getScreenCTM(),
scale = "scale(" + (1 / ctm.a) + "," + (1 / ctm.d) + ")";
// Unique id
var id = "dot" + counter++;
//Append the group offscreen
var newGroup = defs.append("g")
.attr("id", id)
.attr("transform", scale);
//Append the circle
var newCircle = newGroup.append("circle")
.attr("r", "25")
.attr("class", "dot")
.style("stroke", "#999999")
.style("fill", "#66B132")
.attr("opacity", 0.8);
//Append the text
var newText = newGroup.append("text")
.text("43")
.style("fill", "#FFFFFF")
.style("font-family", "Arial")
.style("font-size", "24px")
.style("text-anchor", "middle")
.style("alignment-baseline", "central")
.style("readonly", "true");
// indirect rendering with a new viewport
svg.append("use")
.attr("xlink:href", "#" + id)
.attr("x", point[0])
.attr("y", point[1]);
}
// adjust group sizes on window resize
var resize;
window.addEventListener("resize", function() {
clearTimeout(resize);
resize = setTimeout(function () {
var ctm = svg.node().getScreenCTM();
// select all groups before they are repositioned
defs.selectAll('g').attr("transform", "scale(" + (1 / ctm.a) + "," + (1 / ctm.d) + ")");
}, 100);
});
I want to use d3 to add smiley (or frowny) faces to an existing SVG, containing many circle elements.
So far, I have been able to achieve this by appending elements directly to the SVG root. It works, but only because their coordinates happen to be set in the correct way.
I would like to extend it to be able to add a smiley face to any number of circles, wherever they are.
I have tried selecting circles, and appending to them, but it does not work.
Here is what I have achieved so far:
let svg = d3.select("#mySvg");
let appendedTo = svg;
//let appendedTo = svg.select(".mainCircle");
appendedTo.append("circle")
.attr("cx",13)
.attr("cy",15)
.attr("r",5);
appendedTo.append("circle")
.attr("cx",37)
.attr("cy",15)
.attr("r",5);
var arc = d3.svg.arc()
.innerRadius(10)
.outerRadius(11)
.startAngle(3*(Math.PI/2)) //converting from degs to radians
.endAngle(5 * (Math.PI/2)) //just radians
appendedTo.append("path")
.attr("d", arc)
.attr("transform", "translate(25,40)");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width = 50 height = 50 id="mySvg">
<circle class="mainCircle" cx=25 cy=25 r=25 fill="red"></circle>
</svg>
The issue is that is the position of the circle changes on the HTML page, the smiley would not be positionned correctly.
Could you give me some pointers, to 'anchor' the smiley to the circle element?
Edit:
An example of SVG:
<svg width = 500 height = 500 id="mySvg">
<circle class="mainCircle" cx=25 cy=25 r=25 fill="red"></circle>
<circle class="mainCircle" cx=125 cy=65 r=50 fill="red"></circle>
<circle class="mainCircle" cx=200 cy=12 r=10 fill="red"></circle>
<circle class="mainCircle" cx=210 cy=300 r=90 fill="red"></circle>
<circle class="mainCircle" cx=320 cy=25 r=5 fill="red"></circle>
<circle class="mainCircle" cx=400 cy=120 r=50 fill="red"></circle>
<circle class="mainCircle" cx=410 cy=230 r=25 fill="red"></circle>
</svg>
I reckon that a good solution here would be creating a function to which you can pass the circles' attributes (cx, cy and r), which would create the smileys based only on those values.
Creating the circles yourself
So, for instance, suppose that our circle's data has x, y, and r as those attributes. We can create a function, here named makeSmileys, that draws the circles and the path in the container group:
function makeSmileys(group, xPos, yPos, radius) {
//left eye
group.append("circle")
.attr("cx", xPos - radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
//right eye
group.append("circle")
.attr("cx", xPos + radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
arc.innerRadius(radius / 2)
.outerRadius(radius / 2.2);
//mouth
group.append("path")
.attr("d", arc)
.attr("transform", "translate(" + xPos + "," + yPos + ")");
}
As you can see, the position of the two eyes (circles) and the mouth (path) is based on the arguments only. You can tweak those positions the way you want.
For this function to work, we have to create the container groups and then call it on those respective selections:
circlesGroup.each(function(d) {
d3.select(this).call(makeSmileys, d.x, d.y, d.r)
})
Because I'm using selection.call, the first argument (which is group) is the selection itself. As an alternative, if you don't want to use selection.call, just call the function as a normal JavaScript function, passing the container to it.
Here is a demo, with 10 randomly generated circles:
const svg = d3.select("svg");
const data = d3.range(10).map(function(d) {
return {
x: 50 + Math.random() * 500,
y: 50 + Math.random() * 300,
r: Math.random() * 50
}
});
const arc = d3.arc()
.startAngle(1 * (Math.PI / 2))
.endAngle(3 * (Math.PI / 2));
const circlesGroup = svg.selectAll(null)
.data(data)
.enter()
.append("g");
circlesGroup.each(function(d) {
d3.select(this).append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.r)
.style("fill", "yellow")
.style("stroke", "black")
})
circlesGroup.each(function(d) {
d3.select(this).call(makeSmileys, d.x, d.y, d.r)
})
function makeSmileys(group, xPos, yPos, radius) {
group.append("circle")
.attr("cx", xPos - radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
group.append("circle")
.attr("cx", xPos + radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
arc.innerRadius(radius / 2)
.outerRadius(radius / 2.2);
group.append("path")
.attr("d", arc)
.attr("transform", "translate(" + xPos + "," + yPos + ")");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="600" height="400"></svg>
Using pre-existing circles
If you have an existing SVG (as you made clear in the edited question), you can select all circles with a selector...
const circles = svg.selectAll("circle");
...then get their attributes and finally call the function:
circles.each(function() {
const x = +d3.select(this).attr("cx");
const y = +d3.select(this).attr("cy");
const r = +d3.select(this).attr("r");
makeSmileys(x, y, r)
});
Mind the unary plus here, because the getters return strings for those attributes.
Here is the demo:
const svg = d3.select("svg");
const arc = d3.arc()
.startAngle(1 * (Math.PI / 2))
.endAngle(3 * (Math.PI / 2));
const circles = svg.selectAll("circle");
circles.each(function() {
const x = +d3.select(this).attr("cx");
const y = +d3.select(this).attr("cy");
const r = +d3.select(this).attr("r");
makeSmileys(x, y, r)
})
function makeSmileys(xPos, yPos, radius) {
svg.append("circle")
.attr("cx", xPos - radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
svg.append("circle")
.attr("cx", xPos + radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
arc.innerRadius(radius / 2)
.outerRadius(radius / 2.2);
svg.append("path")
.attr("d", arc)
.attr("transform", "translate(" + xPos + "," + yPos + ")");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="500" id="mySvg">
<circle class="mainCircle" cx=25 cy=25 r=25 fill="yellow"></circle>
<circle class="mainCircle" cx=125 cy=65 r=50 fill="yellow"></circle>
<circle class="mainCircle" cx=200 cy=12 r=10 fill="yellow"></circle>
<circle class="mainCircle" cx=210 cy=300 r=90 fill="yellow"></circle>
<circle class="mainCircle" cx=320 cy=25 r=5 fill="yellow"></circle>
<circle class="mainCircle" cx=400 cy=120 r=50 fill="yellow"></circle>
<circle class="mainCircle" cx=410 cy=230 r=25 fill="yellow"></circle>
</svg>
Instead of using a unique identifier on your SVG elements, use a class instead like this:
<svg width="50" height="50" class="face">
Then in your D3 you can reference all the instances of this class like so:
let svg = d3.selectAll(".face");