How to create SVG shapes based on data? - svg

I have structure with data and shapes definition:
var data = [
{
"id": "first",
"shapes": [
{
"shape": "polygon",
"points": [["8","64"],["8","356"],["98","356"],["98","64"]]
}
]
},
{
"id": "second",
"shapes": [
{
"shape": "ellipse",
"cx": "63", "cy": "306", "rx": "27","ry": "18"
}, {
"shape": "polygon",
"points": [["174","262"],["171","252"],["167","262"]]
}
]
}
]; // in the data may be stored any SVG shape
I would like to create SVG:
<svg width="218" height="400">
<g transform="translate(0,400) scale(1,-1)">
<g>
<polygon points="8,64 8,356 98,356 98,64"/>
</g>
<g>
<ellipse cx="63" cy="306" rx="27" ry="18"/>
<polygon points="174,262 171,252 167,262"/>
</g>
</g>
</svg>
For each data element I'm appending <g>:
var groups = svg.selectAll("g").data(data, function (d) {
return d.id;
});
groups.enter().append("g");
Now I'm binding data for group shapes:
var shapes = groups.selectAll(".shape").data(function (d) {
return d.shapes;
}, function(d,i){return [d.shape,i].join('-');});
So far it was as expected. Now I want to for each entering DOM node dispatch drawing function with proper shape but apparently shapes.enter().each() is not working in this context (not defined). I suppose it works rather on each DOM node than on each data to be bound. And this is working:
shapes.enter().append("g").each(function(draw, i) {
var shape = draw.shape;
d3.select(this).call(drawer[shape]);
});
But painful side-effect is that SVG has two levels of <g>:
<svg width="218" height="400">
<g transform="translate(0,400) scale(1,-1)">
<g>
<g>
<polygon points="8,64 8,356 98,356 98,64"/>
</g>
</g>
...
</svg>
How to get rid of that? How to build data based shapes correctly?

Two ways to add basic shapes based on data provided by AmeliaBR are ok but slightly outside d3.js API. After long consideration I've decided to add answer my own question.
Inspiration for searching other solution was comment of Lars Kotthoff under question suggesting using paths instead of primitive SVG shapes. In this approach instead of adding second level of <g> there should be added <path>:
shapes.enter().append("path").attr("d", function(d) {
return drawer[d.shape](d);
});
Original drawer object generating basic shapes would return value for attribute d of <path>.
var drawer = {
polygon: function (d) {
return [
"M", d.points[0].join(','),
"L", d.points
.slice(1, d.points.length)
.map(function (e) {
return e.join(",")
}), "Z"].join(" ");
},
ellipse: function (d) {
return [
'M', [d.cx, d.cy].join(','),
'm', [-d.rx, 0].join(','),
'a', [d.rx, d.ry].join(','),
0, "1,0", [2 * d.rx, 0].join(','),
'a', [d.rx, d.ry].join(','),
0, "1,0", [-2 * d.rx, 0].join(',')].join(' ');
},
circle: function (d) {
return this.ellipse({
cx: d.cx,
cy: d.cy,
rx: d.r,
ry: d.r
});
},
rect: function (d) {
return this.polygon({
points: [
[d.x, d.y],
[d.x + d.width, d.y],
[d.x + d.width, d.y + d.height],
[d.x, d.y + d.height]
]
});
},
path: function (d) {
return d.d;
}
};
Working example you can check at http://fiddle.jshell.net/daKkJ/4/

Tricky question. Shame that you can't call each on an enter() selection. Neither can you use a function(d){} in an append statement.(See comments)
But I got it working, using a Javascript forEach() call on the data array itself. It calls your function with the array entry, index, and array itself as parameters, and you can specify a this context -- I just passed in the desired parent element as a selection.
The fabulous fiddle: http://fiddle.jshell.net/994XM/1/
(simpler than your case, but should be able to adapt easily)
It's a bit confusing since the D3 code inside the forEach is all per element, with the d and i values already available for you, so you don't need any internal functions in your D3 method calls. But once you figure that out, it all works.

I found this question through Google, and the jsfiddles linked to above have ceased to work. You now have to call document.createElementNS, giving it the SVG namespace, otherwise the shapes don't show up.
I created an updated fiddle. I also cleaned out a lot of the unnecessary bits, so it should be easier to see what's going on.

Related

JointJS baseline-shift

I'm using JointJS 3.2.0, and I need to have texts like PN=xxx.
How baseline-shift is supposed to be used in JointJS? I tried
markup: '<g><text>\
<tspan class="left"></tspan>\
<tspan class="sub"></tspan>\
<tspan class="right"></tspan>\
</text></g>',
attrs: {
'.sub': {
'baseline-shift': 'sub',
},
but it creates another tspan inside each tspan which all have dy=0.
Similar approach with didn't make the correct tspan have the baseline-shift attribute.
I tried another approach where I changed textVerticalAnchor to 'top', but in that case I can't control the spacing between each tspan, and the letters are spaced further apart than just inside one tspan.
Using Text Annotations might help here.
element.attr('label', {
text: 'Pn = xxx',
annotations: [{ start: 1, end: 2, attrs: { 'baseline-shift': 'sub' }}]
});
Here's a JSFiddle.

How to extract one element from SVG with its reference element?

here is an example SVG:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="78" y1="269.543" x2="237" y2="269.543">......</linearGradient>
<symbol id="test" viewBox="-16.126 -14.41 32.251 28.819">...</symbol>
<rect x="78" y="203.043" style="fill:url(#SVGID_1_);" width="159" height="133"/>
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
<g>
<use xlink:href="#test" width="32.251" height="28.819" x="-16.126" y="-14.41" transform="matrix(1 0 0 -1 402.9284 846.39)" style="overflow:visible;"></use>
</g>
</svg>
I want to get extract three subelements: rect, circle, g, however, you know, rect refers linearGradient and g refers symbol, how to extract one element along with its referenct elements?
I actually once did an implementation for a node.js library. See svg-icon-toolbox for the complete source. It does not use svg.js, but cheerio, a jQuery-like library to parse and manipulate XML sources.
The principle it works by is like that of a garbage collector: everything not marked as a valid reference is swept up and removed.
Identify the element to be preserved with its id.
var cheerio = require('cheerio');
// elements always removed
const remove = [
'animate',
'animateColor',
'animateMotion',
'animateTransform',
'cursor',
'script',
'set'
].join(',');
// elements always preserved
const preserve = [
'color-profile',
'font'
].join(',');
// elements whose links are ignored
const ignore = [
'a',
'altGlyph'
].join(',');
// removes everything not needed for a single icon export
function reduceTo ($copy, id) {
var reflist = new Set();
//mark elements for preservation
function mark (ref) {
var $target = $copy.find(ref);
//avoid doubles
if (!$target.parents(preserve).length && !reflist.has(ref)) {
reflist.add(ref);
//mark for preservation
$target.prop('refby', id);
//mark as having preserved children
$target.parentsUntil('svg').prop('passedby', id);
}
//find links
$target.find('*').addBack() // descendents and self
.add($target.parents()) // parents
.not([remove, ignore, preserve].join(','))
.each((i, el) => {
var $elem = $(el);
//unpack links and recurse
var link = $elem.attr('xlink:href');
if(link) {
mark(link);
}
funcProps.forEach((prop) => {
var value = $elem.css(prop) || $elem.attr(prop);
link = funcRegex.exec(value);
if (link) {
mark(link[1]);
}
});
});
}
//remove elements not needed
function sweep ($inspect) {
//filter out elements generally preserved
$inspect.children().not(preserve).each((i, el) => {
var $child = $(el);
//elements with children to be preserved: recurse
if ($child.prop('passedby') === id && $child.prop('refby') !== id) {
sweep($child);
//elements without mark: remove
} else if ($child.is(remove) || $child.prop('refby') !== id) {
$child.remove();
}
});
}
mark('#' + id);
sweep($copy);
return $copy;
}
var $ = cheerio.load(svgString, { xmlMode: true });
var reduced = reduceTo ($, id);
var serialized = $.xml();

SVG Text Path in Power BI Visuals - Failing in Firefox/Edge - Can It Work?

After some cross-browser testing i'm finding that SVG text paths seem to break in Power BI custom visuals, at least in recent versions of Firefox or Edge. They work fine in recent and not so recent Chrome, as well as in IE11.
The code in the JSFiddle below works correctly in Firefox, displaying "hello" along the path and proving the browser handles the functionality, however close to equivalent code which can be compiled directly in Power BI DevTools shows the text sat at 0,0, disconnected from the path. In Chrome though, it appears as it does in the JSFiddle.
JSFiddle to demonstrate issue
IVisual demo code:
module powerbi.visuals {
export class newVisual implements IVisual {
public static capabilities: VisualCapabilities = {
dataRoles: [
{
name: "Category",
kind: VisualDataRoleKind.Grouping
},
{
name: "Y",
kind: VisualDataRoleKind.Measure
}
],
dataViewMappings: [{
categorical: {
categories: {
for: { in: "Category" }
}
}
}]
};
private root: D3.Selection;
private gCont: D3.Selection;
public init(options: VisualInitOptions): void {
options.viewport.width=500;
options.viewport.height=500;
this.root = d3.select(options.element.get(0))
.append('svg')
.attr("width", options.viewport.width)
.attr("height", options.viewport.height);
this.gCont = this.root
.append("g")
.attr("transform", "translate(50,50)");
var line = d3.svg.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
}).interpolate("linear");
this.gCont
.append("path")
.style("stroke", "black")
.attr("id", "pathid")
.attr("d", function() {
return line([{"x": 100, "y": 100}, {"x": 200, "y": 150}]);
});
this.gCont
.append("text")
.append("textPath")
.text("hello")
.attr("xlink:href", "#pathid");
}
public update(options: VisualUpdateOptions) {}
public destroy() {}
}
}
Have I forgotten to set something that's causing this problem, or is this a genuine incompatibility that cannot currently be overcome?
Note - I'm aware of the bug that requires the xlink:href to be constantly re-set during a transition - that exists in my more complex code with animation, and does it's job in Chrome etc - it does not help with this issue.
Output in Firefox (cut down - the whole thing is quite large even in DevTools):
<div class="visualContainer visual">
<svg height="500" width="500">
<g transform="translate(50,50)">
<path d="M100,100L200,150" id="pathid" style="stroke: black;"></path>
<text><textPath xlink:href="#pathid">hello</textPath></text>
</g>
</svg>
</div>

Maintain orientation of some elements in dynamic rotation?

I'm animating rotations of groups of SVG elements using d3.js. However, I want to preserve the orientation of some elements. For example, in this fiddle (code below), the blue dot is the center of the blue circle. The blue dot is displaced vertically from the black dot, which rotates around the yellow center. I want to maintain the vertical relationship between these two dots.
In the fiddle, I maintain the vertical orientation by rotating the "shift" <g> group backwards the same amount that its enclosing group is rotating forwards. This is the same method given in cmonkey's answer here. That works, but I'm wondering whether there are other methods. Is there any way to preserve orientation without an inverse rotation?
Why?
The inverse rotation strategy means that one has to carefully keep the inverse rotations in sync with changes to rotations of outer groups. In full-fledged versions of this code, I use rotations within rotations (within rotations), as in this example. That means summing up all of the outer groups' rotations in order to determine what the inverse rotation should be. I also want to add text labels to SVG elements. Since different text labels will fall within different numbers of rotation groups, each text label will need its own customized rotation if I want to keep the text upright.
Feel free to suggest more D3. I only hand-coded the SVG in these versions in order to get clarity about how I would dynamically generate the SVG with D3 in a later version.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style>
.cycle {
stroke : #000000;
fill : none;
}
.movingPointOnCycle {
stroke : #000000;
fill : #000000;
}
#pointB {
stroke : #0000FF;
fill : #0000FF;
}
#epicycle2 {
stroke : #0000FF;
}
.centerOfUniverse {
stroke : #000000;
fill : #000000;
}
.sun {
stroke : #000000;
fill : #F0F000;
}
.mars {
stroke : #000000;
fill : #A00000;
}
.earth {
stroke : #000000;
fill : #00A000;
}
</style>
</head>
<body>
<svg width="400" height="400">
<g transform="translate(200,200) scale(1.3)">
<circle class="sun" r="10"></circle>
<g class="cycle"speed="0.01">
<circle id="deferent" class="cycle" r="60"></circle>
<g class="epicycleCenter" transform="translate(60,0)">
<circle id="pointD" class="movingPointOnCycle" r="2"></circle>
<g class="shift" speed="-0.01" displacement="-25">
<circle id="pointB" class="movingPointOnCycle" r="2"></circle>
<line x1="0" y1="0" x2="0" y2="25" stroke-dasharray="1,2"></line>
<g class="cycle"speed="0.01">
<circle id="epicycle2" class="cycle" r="75"></circle>
</g>
</g>
</g>
</g>
</g>
</svg>
<script type="text/javascript">
var t0 = Date.now();
var svg = d3.select("svg");
d3.timer(function() {
var delta = (Date.now() - t0);
svg.selectAll(".cycle").attr("transform", function(d) {
return "rotate(" + delta * d3.select(this).attr("speed") + ")";
});
svg.selectAll(".shift").attr("transform", function(d) {
return "rotate(" + delta * d3.select(this).attr("speed") + ")"
+
"translate(0," + d3.select(this).attr("displacement") + ")"
;
});
});
</script>
</body>
</html>

How can we make SVG transparent on Canvas?

how can we achieve this?
I got the SVG in the function, how can i make it transparent on top of canvas?? Currently i have all my functions working on the canvas. But I found out that SVG can do the add and remove function. How can I go about it?
function Add() {
var id = Math.floor(Math.random()*101+1);
x = Math.random() * 550;
y = Math.random() * 250;
if (document.getElementById('amount').value < 50){
document.getElementById('amount').value++;
svg = document.getElementById("main");
// construct uniqueid for the images
uniqueid = "frog" + document.getElementById('amount').value;
//namespaces for SVG
svgNS="http://www.w3.org/2000/svg";
xlinkNS="http://www.w3.org/1999/xlink";
// create a image element
image = document.createElementNS(svgNS, 'image');
// set id and other attributes
image.setAttributeNS(null, "id", uniqueid);
image.setAttributeNS(xlinkNS, "href","jef-frog.gif");
image.setAttributeNS(null, "x", x);
image.setAttributeNS(null, "y", y);
image.setAttributeNS(null, "width", "50");
image.setAttributeNS(null, "height", "50");
// append to svg
svg.appendChild(image);
} else {
alert("we got 50");
}
}
Assuming you are asking about transparency in SVG <image> elements, I'm pleased to say that it works just fine:
Demo: http://jsfiddle.net/XBCEK/
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink">
<image xl:href="http://phrogz.net/tmp/alphaball.png"
x="20" y="30" width="128" height="128" />
<image xl:href="http://phrogz.net/tmp/hand.gif"
x="220" y="30" width="32" height="32" />
</svg>​
If you embed that SVG on a page along with the following CSS:
body { background:url(http://phrogz.net/tmp/grid.gif) }
svg { background:rgba(255,0,0,0.3) /*…*/ }
…then you will see that:
The background of the SVG is transparent by default. We can even provide a low-opacity color background that lets the background of the page (the grid) show through.
The background of both 8-bit-transparency PNG (the ball) and 1-bit transparency GIF (the hand) allow the background of the SVG/page to shine through correctly.
​

Resources