I have the following d3 logic for rendering individual objects:
svg.selectAll("path")
.data(hugePathDataset)
.enter().append("path")
.attr("class", (d) => d.properties.cls )
.attr("id", (d) => d.properties.name )
.each(... canvas render logic ...)
For performance reasons, the svg element above is set to display: none, the real rendering happens on canvas via d3's projection logic. The svg element is still needed, however, for later updates to canvas (such as changing color of each path individually).
My dataset includes over 60,000 paths and above code takes around 30 seconds to run. Testing it in Chrome's profiler I noticed that 90% of this time is spent in reflow. This made no sense to me since canvas doesn't reflow and the SVG with display: none shouldn't reflow the DOM. As I continued looking into it, I realized that the reflow is not triggered by appending elements to invisible SVG, but by setting class and id attributes on these elements. Sure enough, if I remove lines 4 and 5, the reflow slowdown completely disappears. Setting other attributes (i.e. data-something) does not cause a slowdown/reflow.
The problem is that I'm then unable to manipulate these paths individually later, as described above. My questions are:
Why does adding class or id to an element with parent set to display: none trigger a reflow?
How can I work around this? How can I get these properties set without the slowdown?
Reading D3 documentation I realized that selection.append("path") is equivalent to selection.append(() => document.createElement("path")). Since document.createElement does not yet attach the element to the DOM, it should be safe to set properties on it without a reflow. I rewrote the above logic differently and the issue went away:
svg.selectAll("path")
.data(hugePathDataset)
.enter().append((d) => {
let element = document.createElement("path");
element.id = d.properties.name;
element.className = d.properties.cls;
return element;
})
.each(... canvas render logic ...)
I still don't understand why class/id change on invisible element causes a reflow, however, but I'm no longer blocked by this.
Related
Note: I asked this question about interrupting transitions during a scroll, but am trying a different technique now that is resulting in a similar issue that doesn't get resolved with the accepted (and working) answer.
This time, rather than initializing all the graphs with 0 opacity and having a separate function to change the opacity that gets called on each step, I'd like to use selection.remove() in each drawing function. I want to do this so that out-of-view graphs don't get in the way of any mouseover interactions that I might want on the current graph.
For example, I have functions that clear the existing graphs and then draw the current one with some transition:
var makeCircle0 = function() {
d3.selectAll(".nancygraphs").interrupt().remove()
g.append("circle")
.attr("cx", 50)
.attr("cy", 100)
.attr("r", 20)
.attr("fill", "red")
.attr("id", "red")
.attr("opacity", 0)
.transition()
.duration(1000)
.attr("opacity", 1)
.attr("class", "nancygraphs")
}
These functions are put in a list
var activateFunctions = [];
activateFunctions[0] = makeCircle0;
activateFunctions[1] = makeCircle1;
activateFunctions[2] = makeCircle2;
activateFunctions[3] = makeCircle3;
And depending on the step, the function gets called to draw the correct graph
function handleStepEnter(response) {
console.log(response)
step.classed('is-active', function(d, i) {
return i === response.index;
})
figure.select('p').text(response.index);
figure.call(activateFunctions[response.index]) // HERE!
}
Here is a jsfiddle to illustrate. Basically, if you scroll back-and-forth quickly then old graphs don't get cleared and you'll notice several graphics in view simultaneously. Why isn't d3.selectAll(".nancygraphs").interrupt().remove() doing the job?
Three observations regarding your approach:
First, according to the d3 manual on
transitions:
remove: remove the selected elements when the transition ends.
The remove will not interrupt already running transitions - it only removes when all transitions have stopped. More specifically it seems to act when __transition__.count of an element reaches 0. You could consider using a non-d3 remove implementation here, e. g. jQuery.
Second, from the same manual:
Interrupting a transition on an element has no effect on any transitions on any descendant elements. (...) you must therefore interrupt the descendants: selection.selectAll("*")
You should call interrupt on both by doing d3.selectAll(".nancygraphs").interrupt().selectAll("*").interrupt().
Third, it is never a good idea to directly couple mouse or scroll input to your logic (when you directly couple input events to e. g. attaching a transition, you might be doing so many thousands of times): did you use a debounce function? The lodash implementation is highly recommended.
After trying these modifications I would assume your current problem is solved. If it is not, a further way of debugging would be to log / overwrite the __transition__.count attribute of your elements.
I'm using a third library that is adding 'transform: translate3d(200,0,0);' to a parent element. So the elements within the svg tend to move to the right. For drag&drop events, setting the origin is enough, but I'm having troubles to get corrected the d3.mouse(this).
I have created a jsfiddle of this example: http://bl.ocks.org/hlucasfranca/f133da4493553963e710
jsFiddle: https://jsfiddle.net/rahpuser/5jp6123u/
So, when clicking without applying the transform to the parent, everything is ok, but when the parent has the transform, d3.mouse returns a wrong number
var coords = d3.mouse(this);
coords[0] === 100; //true without transform..
coords[0] === 300; // true when the transform to the parent is applied.
// the values are not precise but you can verify the behavior in the jsfiddle
Why d3.mouse(this) is not able to get the correct value ?
my understanding is that d3.mouse should get the coords based on the container this ( svg ).
What should I do to fix the behavior keeping the parent with the new transform?
Thanks.
UPDATE:
Not working in Firefox 46 for ubuntu
Working well in chrome and opera
As you've discovered, this is a known FireFox bug. Even if you did fix FireFox, it's going to take some time to propagate, and it'll never be backwards-fixed. So you still need a workaround.
One thing you can do — as long as the SVG's top-left is always at the 0,0 coordinate of its containing div (as is the case with your jsFiddle) — is replace the call to d3.mouse(this) with:
d3.mouse(this.parentNode)
That'll get the mouse coordinate from the parent of the SVG, which is a normal div, so apparently it's not affected by this bug and will get you the value you want on all platforms.
This does not address the root problem (for example getClientBoundingRect would still return the wrong values), but it works around your specific problem.
svg.append("defs")
.append("filter").attr("id", "blur")
.attr("x", "-50%").attr("y", "-50%")
.attr("height", "200%").attr("width", "200%")
.append("feGaussianBlur").attr("in", "SourceGraphic").attr("stdDeviation", 10);
I saw these code in javascript. I try to google, but still can not understand what is a "defs" in js.
BTW, can anyone explain what is a "filter"?
Many thanks!
Looks to me like your code is some implementation of a Gaussian blur filter using SVG. So defs and filter there are really SVG concepts, not so much JavaScript.
defs
SVG allows graphical objects to be defined for later reuse. It is
recommended that, wherever possible, referenced elements be defined
inside of a defs element. Defining these elements inside of a defs
element promotes understandability of the SVG content and thus
promotes accessibility. Graphical elements defined in a defs will not
be directly rendered. You can use a element to render those
elements wherever you want on the viewport.
filter
The filter element serves as container for atomic filter operations.
It is never rendered directly. A filter is referenced by using the
filter attribute on the target SVG element.
feGaussianBlur
The filter blurs the input image by the amount specified in
stdDeviation, which defines the bell-curve.
Documentation from Mozilla
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/filter
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feGaussianBlur
I'm using Raphael.js to produce an SVG drawing from Javascript (actually Coffeescript)
However, I'd like to be able to include subdrawings, automatically scaled, by putting them inside a second <svg> tag nested inside the first. Unfortunately the Raphael Paper object which allows me to add rects and paths etc. doesn't have an option for adding svgs.
I've tried to add the tag directly to the DOM in javascript with the following code :
res = document.createElement("svg")
res.setAttribute("x",x)
res.setAttribute("y",y)
res.setAttribute("width",width)
res.setAttribute("height",height)
res.setAttribute("viewBox","0 0 100 100")
res.innerHTML = someInnerSVG
#paper.canvas.appendChild(res)
This seems to update the DOM as expected, adding my new SVG tag as a child of the main outer SVG. However, nothing in this inner actually appears on the screen. (My inner SVG path is scaled within 0 0 100 100 so is within the viewBox.)
The rest of the drawing in the outer SVG, as produced by Raphael, is visible. But nothing of the inner one is.
Should what I'm trying be possible? Any idea what I'm doing wrong?
You should place these sub elements into a Raphael set. From there, you will be able to apply transformations to the set. To transform the set, you would call the transform() method and use the s attribute:
set.transform('s[sx],[sy]');
Where sx is the horizontal scale amount and sy is the vertical scale amount. Remove the brackets when you insert the numbers.
Please refer to the documentation here: http://raphaeljs.com/reference.html#Element.transform
The methods for elements apply to sets because sets are pseudo elements.
I'm working on an application that uses Raphael to draw primitive shapes (rectangles, ellipses, triangles etc) and lines but allows the user to move/resize these objects as well. One of the main requirements is that the face of shapes can have formatted text. The actual text is a subset of Markdown (simple things like bolding, italics, lists) and is rendered as HTML.
FWIW - I'm using Backbone.js views to modularize the shape logic.
Approach 1
My initial thought was to use a combination of foreignObject for SVG and direct HTML with VML for IE. However, IE9 doesn't support foreignObject, and therefore this approach had to be abandoned.
Approach 2
With the beside the canvas object, add divs that contain the actual HTML. Then, position them over the actual shape with a transparent background. I've created a shape view that has references to both the actual Raphael shape and the "overlay" div. There are a couple of problems with this approach:
Using overlay that aren't children of the SVG/VML container feels wrong. Does having this overlay element cause other issues with rendering down the road?
Events that are normally trapped by Raphael (drag, click etc) need to be forwarded from the overlay to the Raphael object. For browsers that support pointer-events, this is easily done:
div.shape-text-overlay {
position: absolute;
background: none;
pointer-events: none;
}
However, other browsers (like IE8 and below) need forwarding of the events:
var forwardedEvents = 'mousemove mousedown mouseup click dblclick mouseover mouseout';
this.$elText.on(forwardedEvents, function(e) {
var original = e.originalEvent;
var event;
if (document.createEvent) {
event = document.createEvent('HTMLEvents');
event.initEvent(e.type, true, true);
}
else {
event = document.createEventObject();
event.eventType = e.type;
}
// FYI - This is the most simplistic approach to event forwarding.
// My real implementation is much larger and uses MouseEvents and UIEvents separately.
event.eventName = e.type;
_.extend(event, original);
if (document.createEvent) {
that.el.node.dispatchEvent(event);
}
else {
that.el.node.fireEvent('on' + event.eventType, event);
}
});
Overlapping shapes cause the text to be overlapped because the text/shapes are on different layers. Although overlapping shapes won't be common, this looks bad:
This approach is what I'm currently using but it just feels like a huge hack.
Approach 3
Almost like Approach 1, but this would involve writing text nodes/VML nodes directly. The biggest problem with this is the amount of manual conversion necessary. Breaking outside of Raphael's API seems like it could cause stability issues with the other Raphael objects.
Question
Has anyone else had to do something similar (rendering HTML inside of SVG/VML)? If so, how did you solve this problem? Were events taken into account?
I built this project (no longer live) using Raphael. What I did is actually abandoned the idea of using HTML inside of SVG because it was just too messy. Instead, I absolutely positioned an HTML layer on top of the SVG layer and moved them around together. When I wanted the HTML to show, I merely faded it in and faded the corresponding SVG object out. If timed and lined up correctly, it's not really noticeable to the untrained eye.
While this may not necessarily be what you're looking for, maybe it will get your mind thinking of new ways to approach your problem! Feel free to look at the JS on that page, as it is unminified ;)
PS, the project is a lead-gathering application. If you just want to see how it works, select "Friend" in the first dropdown and you don't have to provide any info.
Unless another answer can be provided and trump my solution, I have continued with the extra div layer. Forwarding events was the most difficult part (if anyone requires the forwarding events code, I can post it here). Again, the largest downside to this solution is that overlapping shapes will cause their text to overlap above the actual drawn shape.