Prevent Templates From Discarding DOM Elements - d3.js

I am attempting to integrate a D3.js visualization into a Meteor app. After the page has loaded, a D3 function injects DOM elements into a <div> in my template, based on available data.
Whenever there is a reactive update anywhere on the page, however, Meteor dumps the contents of the template that were injected by my D3.js function. I can re-insert the elements, but this leads to an undesirable flicker and decrease in performance.
Any idea on how to suppress this dropping of externally injected elements? I gather that since these elements were not part of the template originally, they are discarded as part of Meteor's 'cleanup' process.

With the introduction of the Spark templating engine in version 0.4.0, they've introduced the {{#constant}} block helpers to address this issue.
http://meteor.com/blog/2012/08/31/introducing-spark-a-new-live-page-update-engine
Your html template should look something like this....
<template name="samplePageTemplate">
<div id="samplePage" class="page">
{{#constant}}
<div id="sampleGraph"></div>
{{/constant}}
</div>
</template>
And the javascript should look something like this...
Template.samplePageTemplate.destroyed = function () {
this.handle && this.handle.stop();
};
Template.samplePageTemplate.rendered = function () {
self.node = self.find("svg");
if (!self.handle) {
self.handle = Meteor.autorun(function(){
$('#sampleGraph').html('');
renderChart();
});
};
};
function renderChart(){
// lots of d3 chart specific stuff
var vis = d3.select("#sampleGraph").append("svg:svg")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight)
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
// more d3 chart specific stuff
});
};
I've sometimes had to use self.node instead of self.handle, but otherwise it should be fairly straight forward.

Have you tried giving unique ids to the D3 injected elements (or at least a parent element)? From the doc (http://docs.meteor.com/#livehtml):
Just make sure that each of your focusable elements either has a unique id, or has a name that is unique within the closest parent that has an id. Meteor will preserve these elements even when their enclosing template is rerendered, but will still update their children and copy over any attribute changes.

Related

dc.js - dynamically change valueAccessor of a stacked layer in a lineChart and redraw it

I am trying to realize a dashboard to display basic data.
I am actually completely stuck on an issue. Strangely enough, I couldn't find anything even similar to it online, so I don't have many leads on how to move forward.
I have mainly two charts:
a lineChart called "stackChart" that
displays consumption as a base layer with its valueAccessor function
dispalys production as a stacked layer with its value Accessor function
a barChart called "volumeChart" that is simply the rangeChart for the lineChart
I use radio buttons to select whether to aggregate the grouped data by sum or by average (using the same approach as this example) and then I just use:
stackChart.valueAccessor(/*function with new value (avg or sum)*/);
dc.redrawAll();
to refresh the base layer (consumption).
What I don't manage to do is to refresh the "stacked layer" by updating its valueAccessor! I can't find any way to access its valueAccessor (or, worst case, just completely remove the stacked layer and then add a new refreshed stacked layer using just ".stack(...)").
Here is the respective part of my code where the chart is built:
// Charts customization #js
stackChart
.renderArea(true)
.height(350)
.transitionDuration(1500)
.dimension(dateDim)
.group(powByTime, "Consumption")
// BASE LAYER valueAccessor HERE
.valueAccessor(function(d) { return d.value.conSum; })
.x(d3.time.scale().domain([minDate, maxDate]))
.xUnits(d3.time.days)
.elasticY(true)
.renderHorizontalGridLines(true)
.legend(dc.legend().x(80).y(0).itemHeight(13).gap(5))
.brushOn(false)
// STACKED LAYER HERE
.stack(powByTime, "Production", function(d) { return d.value.prodSum; })
.rangeChart(volumeChart)
.controlsUseVisibility(true)
;
And here is where I look for changes in the radio buttons and re-draw the layers:
// Listen for changes
d3.selectAll('#select-operation input')
.on('click', function() {
var aggrMode = this.value; // fetch "avg" or "sum" from buttons
// UPDATE BASE LAYER HERE:
stackChart.valueAccessor(function(d) { var sel = accessors[aggrMode]['consPow']; return d.value[sel]; });
// ???HOW TO UPDATE STACKED LAYER valueAccessor function???
//stackChart.stack.valueAccessor(function(d) { var sel = accessors[aggrMode]['prodPow']; return d.value[sel]; });
dc.redrawAll();
});
If you need more details on what I am trying to do and full code you can check here.
As a reference, here is what it looks like:
I don't really know dc.js, but it may be possible that you can't change an accessor once it's been set. Try writing a single function for your accessor that will return either the sum or the average, depending on the state of some variable that you can set.
#Ryan's solution will probably work fine (and may be a better design), but here's the lowdown on the dc.js API with respect to stacking, in case you need it.
As described in this issue the group and stack API is pretty weird. It grew organically, in a backward-compatible way, so both the stacks and the value accessors on top of the stacks sort of branch out in a beautiful fractal of... well, no it's pretty messy.
But the issue also suggests the solution for your problem. Since chart.group() resets the set of stacks, just go ahead and build them all from scratch in your event handler:
stackChart.group(powByTime, "Consumption") // this resets the stacks
.valueAccessor(function(d) { var sel = accessors[aggrMode]['consPow']; return d.value[sel]; })
.stack(powByTime, "Production", function(d) { var sel = accessors[aggrMode]['prodPow']; return d.value[sel]; });
Internally it's just emptying an array of layers/stacks and then populating it with some references.
This is quite efficient since dc.js doesn't store your data except where it is bound to the DOM elements. So it is the same amount of work to redraw using the old group and value accessor as it is to redraw using new ones.

Get id of dragged element in d3.js

I am probably having some kind of brain damage atm because something like this should be trivial.
I got a bunch of SVG circles rendered manually (via React). I am then attaching d3 drag behavior to all of them. The drag behavior is applied, and the drag function is being executed, but when I drag one of these circles I am not able to respond accordingly because I do not know which one of them was moved. Where can I get the ID of dragged element?
I have checked a few other questions and found just some crazy filter solution... that cannot be it.
I have also peeked at docs and found the subject property.. however that one is null everywhere I tried it.
My code:
componentWillUpdate() {
let nodes = d3.selectAll("circle");
const dragFn = (d,i) => {
d3.event.sourceEvent.stopPropagation();
this.props.onNodeDrag(I_NEED_AN_ID_HERE);
}
const dragBehavior = d3.behavior.drag();
dragBehavior.on('drag', dragFn);
dragBehavior.on('dragstart', () => {
d3.event.sourceEvent.stopPropagation();
});
nodes.call(dragBehavior);
}
I don't know what your "this" is inside the function but in plain js you can get any attribute of the html element with:
d3.select(this).attr("id"); //or class etc.
or if it's wrapped
d3.select(this).select("circle").attr("id");
Here's an example: http://jsfiddle.net/a2QpA/343/

D3JS Circle on top of each other [duplicate]

What is an effective way to bring an SVG element to the top of the z-order, using the D3 library?
My specific scenario is a pie chart which highlights (by adding a stroke to the path) when the mouse is over a given piece. The code block for generating my chart is below:
svg.selectAll("path")
.data(d)
.enter().append("path")
.attr("d", arc)
.attr("class", "arc")
.attr("fill", function(d) { return color(d.name); })
.attr("stroke", "#fff")
.attr("stroke-width", 0)
.on("mouseover", function(d) {
d3.select(this)
.attr("stroke-width", 2)
.classed("top", true);
//.style("z-index", 1);
})
.on("mouseout", function(d) {
d3.select(this)
.attr("stroke-width", 0)
.classed("top", false);
//.style("z-index", -1);
});
I've tried a few options, but no luck so far. Using style("z-index") and calling classed both did not work.
The "top" class is defined as follows in my CSS:
.top {
fill: red;
z-index: 100;
}
The fill statement is there to make sure I knew it was turning on/off correctly. It is.
I've heard using sort is an option, but I'm unclear on how it would be implemented for bringing the "selected" element to the top.
UPDATE:
I fixed my particular situation with the following code, which adds a new arc to the SVG on the mouseover event to show a highlight.
svg.selectAll("path")
.data(d)
.enter().append("path")
.attr("d", arc)
.attr("class", "arc")
.style("fill", function(d) { return color(d.name); })
.style("stroke", "#fff")
.style("stroke-width", 0)
.on("mouseover", function(d) {
svg.append("path")
.attr("d", d3.select(this).attr("d"))
.attr("id", "arcSelection")
.style("fill", "none")
.style("stroke", "#fff")
.style("stroke-width", 2);
})
.on("mouseout", function(d) {
d3.select("#arcSelection").remove();
});
As explained in the other answers, SVG does not have a notion of a z-index. Instead, the order of elements in the document determines the order in the drawing.
Apart from reordering the elements manually, there is another way for certain situations:
Working with D3 you often have certain types of elements that should always be drawn on top of other types of elements.
For example, when laying out graphs, links should always be placed below nodes. More generally, some background elements usually need to be placed below everything else, while some highlights and overlays should be placed above.
If you have this kind of situation, I found that creating parent group elements for those groups of elements is the best way to go. In SVG, you can use the g element for that. For example, if you have links that should be always placed below nodes, do the following:
svg.append("g").attr("id", "links")
svg.append("g").attr("id", "nodes")
Now, when you paint your links and nodes, select as follows (the selectors starting with # reference the element id):
svg.select("#links").selectAll(".link")
// add data, attach elements and so on
svg.select("#nodes").selectAll(".node")
// add data, attach elements and so on
Now, all links will always be appended structurally before all node elements. Thus, the SVG will show all links below all nodes, no matter how often and in what order you add or remove elements. Of course, all elements of the same type (i.e. within the same container) will still be subject to the order in which they were added.
One of the solutions presented by the developer is: "use D3's sort operator to reorder the elements." (see https://github.com/mbostock/d3/issues/252)
In this light, one might sort the elements by comparing their data, or positions if they were dataless elements:
.on("mouseover", function(d) {
svg.selectAll("path").sort(function (a, b) { // select the parent and sort the path's
if (a.id != d.id) return -1; // a is not the hovered element, send "a" to the back
else return 1; // a is the hovered element, bring "a" to the front
});
})
Since SVG doesn't have Z-index but use the order of the DOM elements, you can bring it to front by:
this.parentNode.appendChild(this);
You can then e.g. make use of insertBefore to put it back on mouseout. This however requires you to be able to target the sibling-node your element should be inserted before.
DEMO: Take a look at this JSFiddle
The simple answer is to use d3 ordering methods. In addition to d3.select('g').order(), there is .lower() and .raise() in version 4. This changes how your elements appear. Please consult the docs for more information - https://github.com/d3/d3/blob/master/API.md#selections-d3-selection
SVG doesn't do z-index. Z-order is dictated by the order of the SVG DOM elements in their container.
As far as I could tell (and I've tried this a couple of times in the past), D3 doesn't provide methods for detaching and reattaching a single element in order to bring it to the front or whatnot.
There is an .order() method, which reshuffles the nodes to match the order they appear in the selection. In your case, you need to bring a single element to the front. So, technically, you could resort the selection with the desired element in front (or at the end, can't remember which is topmost), and then call order() on it.
Or, you could skip d3 for this task and use plain JS (or jQuery) to re-insert that single DOM element.
I implemented futurend's solution in my code and it worked, but with the large number of elements I was using, it was very slow. Here's the alternative method using jQuery that worked faster for my particular visualization. It relies on the svgs you want on top having a class in common (in my example the class is noted in my data set as d.key). In my code there is a <g> with the class "locations" that contains all of the SVGs I'm re-organizing.
.on("mouseover", function(d) {
var pts = $("." + d.key).detach();
$(".locations").append(pts);
});
So when you hover on a particular data point, the code finds all the other data points with SVG DOM elements with that particular class. Then it detaches and re-inserts the SVG DOM elements associated with those data points.
Wanted to expand on what #notan3xit answered rather than write out an entire new answer (but I don't have enough reputation).
Another way to solve the element order problem is to use 'insert' rather than 'append' when drawing . That way the paths will always be placed together before the other svg elements(this assumes your code already does the enter() for links before the enter() for the other svg elements).
d3 insert api: https://github.com/mbostock/d3/wiki/Selections#insert
It took me ages to find how to tweak the Z-order in an existing SVG. I really needed it in the context of d3.brush with tooltip behavior. In order to have the two features work nicely together (http://wrobstory.github.io/2013/11/D3-brush-and-tooltip.html), you need the d3.brush to be the first in Z-order (1st to be drawn on the canvas, then covered by the rest of the SVG elements) and it will capture all mouse events, no matter what is on top of it (with higher Z indices).
Most forum comments say that you should add the d3.brush first in your code, then your SVG "drawing" code. But for me it was not possible as I loaded an external SVG file. You can easily add the brush at any time and alter the Z-order later on with:
d3.select("svg").insert("g", ":first-child");
In the context of a d3.brush setup it will look like:
brush = d3.svg.brush()
.x(d3.scale.identity().domain([1, width-1]))
.y(d3.scale.identity().domain([1, height-1]))
.clamp([true,true])
.on("brush", function() {
var extent = d3.event.target.extent();
...
});
d3.select("svg").insert("g", ":first-child");
.attr("class", "brush")
.call(brush);
d3.js insert() function API: https://github.com/mbostock/d3/wiki/Selections#insert
Hope this helps!
You can Do like this On Mouse Over You can Pull it to top.
d3.selection.prototype.bringElementAsTopLayer = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.pushElementAsBackLayer = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
nodes.on("mouseover",function(){
d3.select(this).bringElementAsTopLayer();
});
If You want To Push To Back
nodes.on("mouseout",function(){
d3.select(this).pushElementAsBackLayer();
});
Version 1
In theory, the following should work fine.
The CSS code :
path:hover {
stroke: #fff;
stroke-width : 2;
}
This CSS code will add a stroke to the selected path.
The JS code :
svg.selectAll("path").on("mouseover", function(d) {
this.parentNode.appendChild(this);
});
This JS code first removes the path from the DOM tree and then adds it as the last child of its parent. This makes sure the path is drawn on top of all other children of the same parent.
In practice, this code works fine in Chrome but breaks in some other browsers. I tried it in Firefox 20 on my Linux Mint machine and couldn't get it to work. Somehow, Firefox fails to trigger the :hover styles and I haven't found a way to fix this.
Version 2
So I came up with an alternative. It may be a bit 'dirty', but at least it works and it doesn't require looping over all elements (as some of the other answers).
The CSS code :
path.hover {
stroke: #fff;
stroke-width : 2;
}
Instead of using the :hover pseudoselector, I use a .hover class
The JS code :
svg.selectAll(".path")
.on("mouseover", function(d) {
d3.select(this).classed('hover', true);
this.parentNode.appendChild(this);
})
.on("mouseout", function(d) {
d3.select(this).classed('hover', false);
})
On mouseover, I add the .hover class to my path. On mouseout, I remove it.
As in the first case, the code also removes the path from the DOM tree and then adds it as the last child of its parent.
I solved it by using the raise function.
const raise = (d) => {
d3.select(d).raise()
}
And in the component that you need to raise on hover (along with all its children, just place this.
.on("mouseover", (d) => raise(d.srcElement.parentNode))
Depending on your structure, maybe parentNode is not needed. In this example they used "this" but that didn't work in React. https://codepen.io/_cselig/pen/KKgOppo

How to append child divs to parent divs by selecting on ids from a data file

I have html in this structure:
<div id="typeA"></div>
<div id="typeB"></div>
and a table of data such as:
[["#typeA","someValue"],
["#typeA","anotherValue"],
["#typeB","thirdValue"]]
and I think it's possible to use d3.js to append the values as text inside child div of each of the present divs like so:
<div id="typeA">
<div>someValue</div>
<div>anotherValue</div>
</div>
<div id="typeB">
<div>thirdValue</div>
</div>
Would this need a nest()?
fyi this didn't work for reasons that are now obvious to me:
var body = d3.select("body")
.data(matrix)
.enter()
.select(function(d) {return d[0]}).
.data(this)
.enter()
.append("div")
.text(function(d) {return d[1]});
D3 isn't ideal in this case because your document is not entirely data-driven -- you already have elements that need to be matched to the data in a non-D3 fashion. You can however emulate what D3 does and "bind" the data to the existing elements manually:
data.forEach(function(d) {
var elem = d3.select(d[0]).node();
if(elem.__data__ === undefined) elem.__data__ = [];
elem.__data__.push(d[1]);
});
This will bind the array of corresponding data items to each DOM element. Now you can use D3's nested selections to create the nested elements:
d3.selectAll("div").selectAll("div")
.data(function(d) { return d; })
.enter().append("div")
.html(function(d) { return d; });
Note the double .selectAll("div"); the first to select the existing elements and the second to select the (yet not existent) nested elements.
Complete demo here.
use jquery istead it's much more supported and performant.
in Jquery you just have a look to traversing in
this pretty good reference good reference :

Using D3 in SAPUI5 to load an external svg

I want to load an exteranal svg file in SAPUI5. I was thinking of using D3 JS library to do this
Is it possible and advisable ?
If yes ,then are there any other framework based on D3 library which can be utilized to do the integration as SAP UI5 is a object oriented framework and writing D3 code inside the view is not working?
It's possible to load an external SVG with D3, but it's not built-in. Here's an example (the footballs are external SVG icons):
http://bl.ocks.org/emeeks/a347eed5c50a7f1cf08a
However, this is not what D3 is built for. Rather, D3 is built for creating complex SVG graphics based on data. You're better off either using native Javascript (which is mostly what is done in that example, anyway, loading the element on a documentFragment and then cloning the node) or another library. You might want to look at snap.svg, since it's more focused on traditional SVG manipulation.
find my solution below. The idea with the onAfterRendering is taken from SCN.
Alternatively you may consider to use a sap.m.Image. According to CSS Tricks, an <img> tag should be sufficient to display SVG from an external source. However, in my case it didn't work; I suspect the reason was that my service URL doesn't return an adequate MIME type (is: application/xml, should: image/svg+xml???)
return Control.extend("com.example.SvgDisplay", {
metadata : {
properties: {
svgID: "string",
mySvg: "object"
}
},
setSvgID: function(sSvgID) {
var that = this,
sParams;
this.setProperty("svgID", sSvgID, true);
if (sSvgID) {
sParams = $.sap.encodeURLParameters({ id: sSvgID });
d3.xml("/my/svg/service?" + sParams, function (oError, oDocument) {
var oSvgNode;
if (oError) {
// Do error handling - for example evaluate oError.responseText
} else if (oDocument) {
oSvgNode = oDocument.lastChild;
}
that.setMySvg(oSvgNode); // may still be undefined; force re-rendering
});
}
},
// "static" function, renders the basic <div> tag. The rest will be done by method onAfterRendering
renderer : function(oRm, oControl) {
var sId = oControl.getId(),
oMySvg = oControl.getMySvg();
oRm.write("<div"); // Control - DIV
oRm.writeControlData(oControl); // writes the Control ID and enables event handling - important!
oRm.writeClasses();
oRm.write('><div id="' + sId + '-svgContainer"></div>');
oRm.write("</div>"); // end Control - DIV
},
onAfterRendering: function(){
var oMySvg = this.getMySvg(), sDomID, oVis;
if (oMySvg) {
sDomID = this.getId();
oVis = d3.select("#" + sDomID + "-svgContainer").node();
oVis.appendChild(oMySvg);
}
}
});

Resources