Can I append a literal SVG element with d3? - javascript

I'd like to append a literal SVG element with d3.
So instead of writing
svg.selectAll("circle")
.data(data)
.enter()
.append("circle") // etc etc
I'd like to do:
svg.selectAll("circle")
.data(data)
.enter()
.append('<circle cx="158.9344262295082" cy="200" r="16" fill="red"></circle>')
so that I could create a complex template elsewhere (for example with handlebars), and then compile it with data and append it.

You can do this, although not via the selection.append() function. Instead you'd need to use the selection.html() function.
This would make it quite difficult to use in the context of data-joins, but not impossible. This is probably the best you could do, which involves adding an additional svg group to the DOM which may not be a bad thing anyway:
var svg = d3.selectAll("svg");
svg.selectAll("circle")
.data([50, 100, 150])
.enter()
.append("g")
.attr("transform", function(d) { return "translate(" + [d, d] + ")"; })
.html('<circle r="16" fill="red"></circle>');
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="500" height="500"></svg>
I guess taking this answer a bit further, you could actually embed the result that you wish to render directly into your data object. So you've add some code that looked like:
.html(function(d) { return d.templatedHTML; });
At this point however stop and ask yourself the question: "What am I using D3 for?". D3 is described as
Data Driven Documents
If you're using something like handlebars, then you're taking away one of the core responsibilities that D3 was designed for (building some DOM from some data) and giving it to some other library.
I'm not stating you shouldn't do that (as you did mention complex templates) but do just ask yourself the question to make sure that it's a path you wish to go down.

No, you can't. Don't believe me? Check their docs HERE
What you must do is call .append(), followed by several calls of .attr(attr_name, attr_value) to set each attribute's value. D3 does not work like jQuery.

D3 doesn't provide this functionality, and it does not make much sense (when you think about manipulating elements based on data).
But, as a side note, you can implement your own function to append a literal SVG element.
This is a function created by Chris Viau, named appendSVG:
d3.selection.prototype.appendSVG =
d3.selection.enter.prototype.appendSVG = function(SVGString) {
return this.select(function() {
return this.appendChild(document.importNode(new DOMParser()
.parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + SVGString +
'</svg>', 'application/xml').documentElement.firstChild, true));
});
};
After extending the prototype, you can use it:
selection.appendSVG("<some literal SVG element>");
Here is a demo. First, we set the data:
var data = [{x:30,y:50},{x:420,y:100},{x:160,y:150},{x:260,y:30}];
Then, we append our literal SVG element in the usual way:
var myLiteral = svg.selectAll(".literal")
.data(data)
.enter()
.appendSVG("<literal SVG here>");
And finally we set the positions using translate:
.attr("transform", function(d){
return "translate(" + d.x + "," + d.y + ")";
});
Check the snippet:
d3.selection.prototype.appendSVG = d3.selection.enter.prototype.appendSVG = function(SVGString) {return this.select(function() { return this.appendChild(document.importNode(new DOMParser().parseFromString('<svg xmlns="http://www.w3.org/2000/svg">' + SVGString + '</svg>', 'application/xml').documentElement.firstChild, true));});};
var svg = d3.select("body").append("svg").attr("width", 500).attr("height", 300);
var data = [{x:30,y:50},{x:420,y:100},{x:160,y:150},{x:260,y:30}];
var myLiteral = svg.selectAll(".literal")
.data(data)
.enter()
.appendSVG("<path d='M 22.889471,25.607172 C 22.589713,24.605127 24.092318,24.708731 24.554936,25.108955 C 25.808602,26.193538 25.053398,28.14136 23.885905,28.938102 C 21.797533,30.363287 19.018523,29.16303 17.893076,27.101823 C 16.241437,24.076919 17.936475,20.36976 20.896603,18.945312 C 24.841988,17.046747 29.504523,19.25402 31.216796,23.116087 C 33.371517,27.976105 30.644503,33.605344 25.878773,35.599962 C 20.106834,38.015712 13.505062,34.765112 11.231216,29.094691 C 8.551568,22.412295 12.327973,14.834577 18.903736,12.283452 C 26.495714,9.3380778 35.051552,13.641683 37.878656,21.12322 C 41.09099,29.624218 36.259254,39.159651 27.87164,42.261821 C 18.462006,45.741988 7.9459296,40.381466 4.5693566,31.087558 C 0.82072068,20.769559 6.7105029,9.2720694 16.910868,5.6215926' style='fill:none;fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1'/>")
.attr("transform", function(d){ return "translate(" + d.x + "," + d.y + ")"});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Related

Datum-Data difference in map behavior in d3

I'm pretty new to d3js and trying to understand the difference between using data and datum to attach data to elements. I've done a fair bit of reading the material online and I think I theoretically understand what's going on but I still lack an intuitive understanding. Specifically, I have a case where I'm creating a map using topojson. I'm using d3js v7.
In the first instance, I have the following code to create the map within a div (assume height, width, projection etc. setup correctly):
var svg = d3.select("div#map").append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath()
.projection(projection);
var mapGroup = svg.append("g");
d3.json("json/world-110m.json").then(function(world){
console.log(topojson.feature(world, world.objects.land))
mapGroup.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
});
The console log for the topojson feature looks like this:
And the map comes out fine (with styling specified in a css file):
But if I change datum to data, the map disappears. I'm trying to improve my understanding of how this is working and I'm struggling a little bit after having read what I can find online. Can someone explain the difference between data and datum as used in this case and why one works and the other doesn't?
Thanks for your help!
There are several differences between data() and datum(), but for the scope of your question the main difference is that data() accepts only 3 things:
An array;
A function;
Nothing (in that case, it's a getter);
As you can see, topojson.feature(world, world.objects.land) is an object. Thus, all you'd need to use data() here (again, not the idiomatic D3, I'm just addressing your specific question) is wrapping it with an array:
.data([topojson.feature(world, world.objects.land)])
Here is your code using data():
var svg = d3.select("div#map").append("svg")
.attr("width", 500)
.attr("height", 300)
.attr("transform", "translate(" + 15 + "," + 0 + ")");
var path = d3.geoPath();
var mapGroup = svg.append("g");
d3.json("https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json").then(function(world) {
const projection = d3.geoEqualEarth()
.fitExtent([
[0, 0],
[500, 300]
], topojson.feature(world, world.objects.land));
path.projection(projection);
mapGroup.append("path")
.data([topojson.feature(world, world.objects.land)])
.attr("class", "land")
.attr("d", path);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson#3"></script>
<div id="map"></div>

D3.js: Context + Focus zoom on multiple graphs

I was looking into the following example by Mike Bostock on focus + context zooming. https://bl.ocks.org/mbostock/1667367.
I was wondering if we can link multiple charts with a main graph. Something similar to the attached image. I am quiet new to d3.js so i might have missed something but i am unable to find any links on how to go about it. All of the graphs have equal data points.
Thanks!
It depends obviously on how your data is organized. I imagine you have a data array as follows:
[ {date : ... /*x-coordinate*/
price1 : .../*first y-coordinate*/
price2 : .../*second y-coordinate*/
/* ...and as many as you like*/
},
{date : ...
price1 : ...
price2 : ... }
]
You also have an array containing the names of the fields you want to fetch as y-axis:
fields = ["price1", "price2", ...]
So first thing to do is a way to extract the data for each separate curve. This can be done as follows:
function singleCurveData(fieldId) {
return data.map(function (d) {
return {date: d.date, price: d[fieldId]};
});
}
If your data is organized differently, only this function needs to change. Basically, it receives the id of the graph you want to draw, and outputs the data for this specific graph in a standard fashion.
Now to the drawing section. You need as many focus parts as different fields you want to show, each one containing a single area; and a single context part containing many area2s.
var focus = svg.selectAll(".focus")
.data(fields) //associate one field id to each focus block
.enter()
.append("g")
.attr("class", "focus")
.attr("transform", function(d,i) {return "translate(" + margin.left + "," + (margin.top - i*focusHeight) + ")"});
//the above line takes care of the positioning, you need to know the target height of a focus block.
var context= ...//as before
And now we move on to drawing the curves:
focus.append("path") //append one path per focus element
.datum(function(fieldId) {return singleCurveData(fieldId)}) //only this line changes
.attr("class", "area")
.attr("d", area);
context.selectAll("path")
.data(fields) // add a "path" for each field
.enter()
.append("path") //here the datum for the path is the field Id
.datum(function(fieldId) {return singleCurveData(fieldId)})
//now the datum is the path data for the corresponding field
.attr("class", "area")
.attr("d", area2);
This should be all there is to it. Good luck!

Making a d3 chart update with new data

I have an example of a chart on jsFiddle which has multiple groups of multiple lines. It draws successfully, but I would like to be able to transition to new sets of data.
The example should update with new data after 4 seconds. Although the chart gets called (and outputs something in the console), the lines aren't updated.
I've tried lots of variations based on existing, simpler examples, with no luck. I suspect its the nested data based on this SO answer that's confusing me even more than usual.
SO insists I have some code in the answer, so here's where I assume I need to add/change something:
svg.transition().attr({ width: width, height: height });
g.attr('transform', 'translate(' + margin.left +','+ margin.right + ')');
var lines = g.selectAll("g.lines").data(function(d) { return d; });
lines.enter().append("g").attr("class", "lines");
lines.selectAll("path.line.imports")
.data(function(d) { return [d.values]; })
.enter().append("path").attr('class', 'line imports');
lines.selectAll('path.line.imports')
.data(function(d) { return [d.values]; })
.transition()
.attr("d", function(d) { return imports_line(d); });
// repeated for path.line.exports with exports_line(d).
The problem is the way you're determining the top-level g element that everything is appended to:
var g = svg.enter().append('svg').append('g');
This will only be set if there is no SVG already, as you're only handling the enter selection. If it exists, g will be empty and therefore nothing will happen. To fix, select the g explicitly afterwards:
var g = svg.enter().append('svg').append('g').attr("class", "main");
g = svg.select("g.main");
Complete demo here.

D3: move circle between two different g elements in force layout

I have two g elements each containing circles. Circles are organized using force.layout. The g elements are transitioning.
You can see here: demo. Reduced code:
var dots = svg.selectAll(".dots")
.data(data_groups)
.enter().append("g")
.attr("class", "dots")
.attr("id", function (d) {
return d.name;
})
...
.each(addCircles);
dots.transition()
.duration(30000)
.ease("linear")
.attr("transform", function (d, i) {
return "translate(" + (150 + i * 100) + ", " + 450 + ")";
});
function addCircles(d) {
d3.select(this).selectAll('circle')
.data(data_circles.filter(function (D) {
return D.name == d.name
}))
.enter()
.append('circle')
.attr("class", "dot")
.attr("id", function (d) {
return d.id;
})
...
.call(forcing);
}
function forcing(E) {
function move_towards(alpha) {
...
}
var force = d3.layout.force()
.nodes(E.data())
.gravity(-0.01)
.charge(-1.9)
.friction(0.9)
.on("tick", function (e) {
...
});
force.start();
}
I need to move circle (for example id=1) from the first g element to the second one using transition.
Any suggestions are appreciated.
It can be done.
What I did was:
1) Use jquery to append the point to the target group
2) Use a transformation (no transition) to move the point back to its original location
3) Transition the point to its new location
The jQuery was used for the appendTo method. It can be removed and replaced with some pure Javascript stuff, but it's quite convenient.
I've got a partially working fiddle here. The green points work right, but something is going wrong with the blue ones. Not sure why.
In my view, transitions work on a single element. If an element changes its position in the DOM tree, from below one g to another, I can't think of a way to make that as one smooth transition because it's basically a binary split: Now there's an element under one g, now it's gone but there's another one somewhere else.
What I'd do in order to achieve what I think you want to do: Group everything under the same ´g´, assign color and translation individually, then change color and translation for that single element you want to change.
But don't take that as a reliable statement that you can't do it the way you originally wanted.

d3.js dynamic data reload possibly related to exit().remove()

I have implemented this d3 visualization http://bl.ocks.org/4745936 , to be loaded with dynamic data instead of a .tsv
in my case, once my server passes new information to the selector, a second chart gets rendered under the first one, instead of modifying the contents of the existing graph.
I believe it has to do with this append method.
var svg = d3.select(selector).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
so I tried adding other exit().remove() methods to legend and cities variables right after they append('g'); but my javascript console says the exit() method does not exist at that point.
I feel I have the completely wrong approach, how do I update an existing graph like this? Having a second and third graph get generated alongside the previous ones is not the outcome I wanted at all
You're right the append method is adding a new svg element every time. To prevent the duplicate charts you need to check if the svg element exists already. So try something like this at the begining:
var svg = d3.select("#mycontainer > svg")
if (svg.empty())
svg = d3.select(selector).append("svg");
...
As stated in the exit() docs, This method is only defined on a selection returned by the data operator. So make sure that you're calling exit on a selection returned from .data(..).
scott's answer is one way of ensuring that the initialization happens only once.
However, I prefer a more d3-ic way of handling this:
var svg = d3.select(selector)
.selectAll('svg')
.data( [ dataFromTSV ] ); // 1 element array -> 1 svg element
// This will be empty if the `svg` element already exists.
var gEnter = svg.enter()
.append('svg')
.append('g');
gEnter.append( ... ); // Other elements to be appended only once like axis
svg.attr('width', ...)
.attr('height', ...);
// Finally, working with the elements which are surely in the DOM.
var g = svg.select("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.selectAll(...).attr(...);
This approach is exemplified in the reusable charts example's source code.
I prefer this approach because it keeps the code very declarative and true to the visualisation by hiding away the logic of initialisation and updates.
I would modify the original example: http://jsfiddle.net/8Axn7/5/ to http://jsfiddle.net/3Ztt8/
Both the legend and the graph are defined from svgElem with one single element of data:
var svgElem = d3.select("#multiLinegraph").selectAll('svg')
.data([cities]);
// ...
var svg = svgElem.select('g');
// ...
var city = svg.selectAll(".city")
.data(
function (d) { return d; },
function (d) { return d.name; } // Object consistency
);
// ...
var legend = svg.selectAll('g.legend')
.data(
function(d) { return d; },
function (d) { return d.name; } // Object consistency
);
Also, the static properties are set only once when the element is entered (or exited), while the update properties are set (transitioned) with each update:
gEnter.append("g")
.attr("class", "y multiLineaxis")
.append('text')
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Requests (#)");
svg.select('g.y.multiLineaxis').transition().call(yAxis);
The code, in my opinion, follows the cycle of enter-update-exit cleanly.
I was able to solve this problem with some jQuery and CSS voodoo
basically since my d3 graph adds an svg element to an existing selector (a div in my case), I was able to check for the name of this dynamically
var svgtest = d3.select(selector+" > svg"); getting the svg subchild element of that div. then I could use jquery to remove that element from the dom completely, and then let d3 continue running and append svg's all it wants!
var svgtest = d3.select(selector+" > svg");
if(!svgtest.empty())
{
$(selector+" > svg").remove();
}
First of all you should remove old svg, after then you can add updated charts.
For that you should add only one line before you append svg.
And its working.
var flag=d3.select("selector svg").remove();
//----your old code would be start here-------
var svg = d3.select(selector).append("svg")

Categories