I am using SVG to draw text and shapes in D3.js, and want to draw shapes inline with text and with similar dimensions to the text. The only way I can think of doing this is draw a rect around each tspan then draw the shape in relative position to the tspan rect. The result being:
This is a rectangle [] this is a circle ()
Where the brackets above represent the SVG shapes. Current code is below.
js:
function setupSVG(){
d3.select("div#chartId")
.append("div")
.classed("svg-container", true)
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 200 200")
.attr("id", "svg_area_id")
}
function renderSVGText(){
var svgArea = d3.select("svg#svg_area_id");
svgArea.append("rect")
.attr("x", 100)
.attr("y", 0)
.attr("height", 10)
.attr("width", 10)
.attr("id", "shape");
var group = svgArea.append("g")
.attr("width", "100%")
.attr("height", "100%")
.style("stroke", "red") //I only want to draw rect stroke
.style("fill", "none");
var text = group.append("text")
.attr("y", "0")
.attr("font-size",52)
.attr("dy", "1em")
.style('fill', 'black')
var tspan1 = text.append('tspan')
tspan1.text("This is a square");
var tspan2 = text.append('tspan')
tspan2.text("and this is a triangle");
var boundingRect = group.append("rect")
//see http://phrogz.net/SVG/tspan_bounding_box.xhtml
var bbox = tspan1.getBoundingClientRect();
var pt = svg.createSVGPoint();
pt.x = bbox.left;
pt.y = bbox.top;
var pt2 = pt.matrixTransform(xform);
rect.setAttribute('x',pt2.x);
rect.setAttribute('y',pt2.y);
pt.x = bbox.right;
pt.y = bbox.bottom;
pt = pt.matrixTransform(xform);
boundingRect.attr('width', pt.x-pt2.x);
boundingRect.attr('height',pt.y-pt2.y);
/* this draws a rect around all text
var textSize = text.node().getBBox();
boundingRect.attr("width", textSize.width)
.attr("height", textSize.height);
*/
}
html:
<div class="svg-container" id="chartId"></div>
css:
.svg-container {
display: inline-block;
width: 512px;
height: 512px;
padding-top: 50px;
padding-bottom: 100%; /* aspect ratio */
vertical-align: top;
overflow: hidden;
border: 1px solid black;
}
Any ideas on how to do this? Any easier ways than the track I am following?
I tried to get tspan dimensions using tspan.node().getComputedTextLength() but this returned an error, I presume because it hadn't been rendered at the call time. I just used text.node().getBBox() to get each text blocks dimensions instead:
function renderSVGText(){
var svgArea = d3.select("svg#svg_area_id");
var group = svgArea.append("g")
.attr("width", "100%")
.attr("height", "100%")
.style("stroke", "red")
.style("fill", "none");
var text = group.append("text")
.attr("y", "0")
.attr("font-size",52)
.attr("dy", "1em")
.attr("id", "text_id")
.style('fill', 'black');
var tspan1 = text.append('tspan')
.attr("id", "tspan1_id")
tspan1.text("This is a square");
var boundingRect = svgArea.append("rect")
.style("stroke", "pink")
.style("fill", "none");
var textSize = text.node().getBBox();
boundingRect.attr("width", textSize.width)
.attr("height", textSize.height);
svgArea.append("rect")
.attr("x", textSize.width+10)
.attr("y", 0)
.attr("height", textSize.height)
.attr("width", textSize.height)
.attr("id", "shape");
var text2 = group.append("text")
.attr("x", textSize.width+textSize.height+20)
.attr("y", "0")
.attr("font-size",52)
.attr("dy", "1em")
.attr("id", "text2_id")
.style('fill', 'black');
var tspan2 = text2.append('tspan')
tspan2.text("and this is a triangle");
}
Related
I want to add 2 icons next to a svg text ( image attached, the node text/name will have diff length ), i know we could use an foreignObject but when using foreignObject i'm not able to get the node values
var addSvgCnt = nodeEnter.append("g")
.attr("class", "txt-swap-parent");
addSvgCnt.append("foreignObject")
.attr("width", 1)
.attr("height", 30)
.append("xhtml:div")
.attr("class", "svg-node")
.html("<img src='https://cdn.onlinewebfonts.com/svg/img_356964.png' height='10' width='10'/>
<span>BRANCH1.2</span>
<img src='https://cdn.onlinewebfonts.com/svg/img_356964.png' height='10' width='10'/>");
here instead of BRANCH1.2 i need the node text, i tried psuedo elements but it's not working either. what is the best solution to achieve this
Your icon looks so much like this unicode character, you might even replace it and just use tspan:
Try clicking the nodes to see that clicks are registered correctly.
const someText = "Hi from branch 1";
const circledPlusUnicode = "\u2295";
const x = 50, y = 100;
const text = d3.select("svg")
.append("text")
.attr("x", x)
.attr("y", y);
text.append("tspan")
.attr("class", "circle")
.text(circledPlusUnicode)
.on("click", function() {
console.log("click circle 1");
});
text.append("tspan")
.attr("dy", 2)
.attr("dx", 2)
.text(someText);
text.append("tspan")
.attr("class", "circle")
.attr("dy", -2)
.attr("dx", 2)
.text(circledPlusUnicode)
.on("click", function() {
console.log("click circle 2");
});
.circle {
fill: darkgrey;
font-size: 14px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
Otherwise, you use getBBox, which returns a bounding box of the element it's called on, and you can use that to just position the image right next to the text:
const someText = "Hi from branch 1";
const circledPlusImg = "https://cdn.onlinewebfonts.com/svg/img_356964.png";
const x = 50,
y = 100;
const textGroup = d3.select("svg")
.append("g")
.attr("transform", "translate" + [x, y] + ")");
textGroup.append("image")
.attr("class", "circle")
.attr("href", circledPlusImg)
.attr("height", 14)
.attr("width", 14)
.on("click", function() {
console.log("click circle 1");
});
const text = textGroup.append("text")
.attr("dy", 12)
.attr("dx", 16)
.text(someText);
textGroup.append("image")
.attr("class", "circle")
.attr("href", circledPlusImg)
.attr("height", 14)
.attr("width", 14)
.attr("x", function() {
const bbox = text.node().getBBox();
return bbox.x + bbox.width;
})
.on("click", function() {
console.log("click circle 2");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
How could I scale a rectangle in svg to fit the text using d3.js.
When you run it for the first time the yellowgreen rect covers the text nicely, but if you will resize the screen the text size and position will be changed, while the rectangle stays the same.
Below is the code, here is the fiddle:
debugger;
var svg = d3.select("#drawRegion")
.append("svg")
.attr("width", "100%")
.attr("height", "100%");
svg.append("rect")
.attr("x", "0")
.attr("y", "0")
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", "yellow");
const rectAroundText = svg
.append("rect");
const textMiddleX = 50;
const textMiddleY = 50;
const testText = svg
.append("text");
testText
.attr("x", textMiddleX + "%")
.attr("y", textMiddleY + "50%")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "central")
.attr("x", "50%")
.attr("y", "50%")
.attr("fill", "#000")
.classed("scalingText", true)
.text("svdfv");
const textBox = testText.node().getBBox();
rectAroundText
.attr("x", textBox.x)
.attr("y", textBox.y)
.attr("width", textBox.width)
.attr("height", textBox.height)
.attr("fill", "yellowgreen");
<div id="drawRegion">
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
I would like the rectangle to scale up and move along with the text. Is it possible?
I am working on dragging and dropping svg using d3js. There are two problems and I think they are related to each other.
When the circle is dropped it has to detect that it was dropped into the rectangle. Some of the examples that I have looked at uses x and y coordinates of the mouse, but I don't fully understand it.
Another problem is that the circle appears behind the rectangle. Is there a way to bring it to the front when the circle is moving around without changing the order of where the circle and rectangle are created i.e(create circle first and then rectangle).
var width = window.innerWidth,
height = window.innerHeight;
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
//create circle and space evenly
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var circle = d3.select("svg")
.append("circle")
.attr("cx", 50)
.attr("cy", 30)
.attr("r", 15)
.attr("transform", "translate(0,0)")
.style("stroke", "black")
.call(drag);
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
}
function dragged(d) {
d3.select(this).attr("transform", "translate(" + [d3.event.x, d3.event.y] + ")");
}
function dragended(d) {
d3.event.sourceEvent.stopPropagation();
// here would be some way to detect if the circle is dropped inside the rect.
}
var ellipse = svg.append("rect")
.attr("x", 150)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green");
<script src="https://d3js.org/d3.v3.min.js"></script>
Any help is appreciated.
Updated to still include the bounding client rectangle, but iterate through any number of rectangles that exist. New Fiddle here.
Here's my solution to the problem. I used a great little "moveToBack" helper function seen here to move the rect to the back without changing the order in which it appears.
To get the positions of the circle and rectangle, I made heavy use of the vanilla js getBoundingClientRect() method. You can see all this together in this JS Fiddle.
var width = window.innerWidth,
height = window.innerHeight;
var drag = d3.behavior.drag()
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
//create circle and space evenly
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var circle = d3.select("svg")
.append("circle")
.attr("r", 15)
.attr("transform", "translate(50,30)")
.style("stroke", "black")
.attr("id", "circle")
.call(drag);
d3.selection.prototype.moveToBack = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
var rect = svg.append("rect")
.attr("x", 150)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green")
.attr("id", "rect")
.moveToBack();
var rect2 = svg.append("rect")
.attr("x", 350)
.attr("y", 50)
.attr("width", 50)
.attr("height", 140)
.attr("fill", "green")
.attr("id", "rect")
.moveToBack();
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
}
function dragged(d) {
d3.select(this).attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
}
function dragended(d) {
// Define boundary
var rects = document.querySelectorAll("rect");
for (var i = 0; i < rects.length; i++) {
var rectDimensions = rects[i].getBoundingClientRect();
var xmin = rectDimensions.x;
var ymin = rectDimensions.y;
var xmax = rectDimensions.x + rectDimensions.width;
var ymax = rectDimensions.y + rectDimensions.height;
// Get circle position
var circlePos = document.getElementById("circle").getBoundingClientRect();
var x1 = circlePos.x;
var y1 = circlePos.y;
var x2 = circlePos.x + circlePos.width;
var y2 = circlePos.y + circlePos.height;
if(x2 >= xmin && x1 <= xmax && y2 >= ymin && y1 <= ymax) {
rects[i].setAttribute("fill", "red");
} else {
rects[i].setAttribute("fill", "green");
}
}
d3.event.sourceEvent.stopPropagation();
}
I am new to java script and I am trying to draw a radar chart. Everything works well except title. Can u please tell me what is wrong? I attached the code below. First I create text var and call it to show.
<style>
body {
overflow: hidden;
margin: 0;
font-size: 14px;
font-family: "Helvetica Neue", Helvetica;
}
#chart {
position: absolute;
top: 60px;
left: 20px;
}
</style>
<script type="text/javascript" src="<c:url value='/js/radar.js'/>"></script>
<div id="body">
<div id="chart"></div>
</div>
<script>
var w = 200;
var h = 200;
var colorscale = d3.scale.category10();
//Legend, titles
var LegendOptions = ['Try Count','Succcess Count', 'Success Rate'];
////////////////////////////////////////////
/////////// Initiate legend ////////////////
////////////////////////////////////////////
var svg = d3.select('#body')
.selectAll('svg')
.append('svg')
.attr("width", w+300)
.attr("height", h)
//Create the title for the legend
var text = svg.append("text")
.attr("class", "title")
.attr('transform', 'translate(90,0)')
.attr("x", w - 70)
.attr("y", 10)
.attr("font-size", "12px")
.attr("fill", "#404040")
.text("What % of owners use a specific service in a week");
//Initiate Legend
var legend = svg.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 200)
.attr('transform', 'translate(90,20)')
;
//Create colour squares
legend.selectAll('rect')
.data(LegendOptions)
.enter()
.append("rect")
.attr("x", w - 65)
.attr("y", function(d, i){ return i * 20;})
.attr("width", 10)
.attr("height", 10)
.style("fill", function(d, i){ return colorscale(i);})
;
//Create text next to squares
legend.selectAll('text')
.data(LegendOptions)
.enter()
.append("text")
.attr("x", w - 52)
.attr("y", function(d, i){ return i * 20 + 9;})
.attr("font-size", "11px")
.attr("fill", "#737373")
.text(function(d) { return d; })
;
//Options for the Radar chart, other than default
var mycfg = {
w: w,
h: h,
maxValue: 0.6,
levels: 6,
ExtraWidthX: 300
}
Try changing:
var svg = d3.select('#body')
.selectAll('svg')
.append('svg')....
To:
var svg = d3.select('#body')
.append('svg')....
As you are only appending one svg, you do not need the selectAll() portion. The following takes your code and makes that one change:
var w = 200;
var h = 200;
var svg = d3.select('#body')
.append('svg')
.attr("width", w+300)
.attr("height", h)
var text = svg.append('g').append("text")
.attr("class", "title")
.attr('transform', 'translate(90,0)')
.attr("x", w - 70)
.attr("y", 10)
.attr("font-size", "12px")
.attr("fill", "#404040")
.text("What % of owners use a specific service in a week");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="body"></div>
I have a few hundred svg containers on a page. Each container has a thick line, and each line has a different gradient applied to it. (I've done this dynamically with d3.js.)
But when I zoom in on the image, all the gradients change to the gradient on the first svg container.
Why is this happening, and more importantly how can I stop this from happening?
Thanks!
Edit: Here's the code:
<style id="css">
.line {
fill: none;
stroke: url(#line-gradient);
stroke-width: 10px;
}
</style>
<script>
var myData = <%= #myData %>
for (i = 0; i < myData.length; i++) {
var margin = {top: 20, right: 20, bottom: 30, left: 50};
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
//Make an SVG Container
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 20);
//Set the gradient
svgContainer.append("linearGradient")
.attr("id", "line-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0).attr("y1", 0)
.attr("x2", 200).attr("y2", 0)
.selectAll("stop")
.data(myData[i])
.enter().append("stop")
.attr("offset", function(d) { return d.offset; })
.attr("stop-color", function(d) { return d.color; })
.attr("stop-opacity", function(d) { return d.opacity; });
//Draw the line
var circle = svgContainer.append("line")
.attr("class", "line")
.attr("x1", 5)
.attr("y1", 5)
.attr("x2", 200)
.attr("y2", 5)
.attr("stroke-width", 2)
.attr("stroke", "black");
}
</script>
Edit2: Using the Robert's answer, I was able to get the results I wanted. I needed to learn how to create style elements dynamically using javascript, which I found here:
http://www.phpied.com/dynamic-script-and-style-elements-in-ie/
and here
How to create a <style> tag with Javascript
The code I used:
Inside the for-loop:
...
var ss1 = document.createElement('style');
var def = '.line'+i+' {\
fill: none;\
stroke: url(#line-gradient'+i+');\
stroke-width: 10px;\
}';
ss1.setAttribute("id", "css");
var hh1 = document.getElementsByTagName('header')[0];
hh1.appendChild(ss1);
var tt1 = document.createTextNode(def);
ss1.appendChild(tt1);
...
svgContainer.append("linearGradient")
.attr("id", "line-gradient" + i)
...
var circle = svgContainer.append("line")
.attr("class", "line"+i)
...
All id attributes on a page must be unique. Yours aren't, you have multiple elements with the attribute line-gradient.