Adding a data-* attribute to charts with r2d3.js - internet-explorer-8

I am trying to add data-link attributes within charts created with d3.js, the issue I'm having is once the charts go through r2d3.js for ie8 support the data-link attribute "gets lost" somehow, I'm not sure what happens. Here is my code:
var path = group.selectAll("path")
.data(pie(tempObj))
.enter().append("path")
.attr("data-link", function () { return obj.Name.Target; })
.attr("fill", function (d, i) { return color(i) })
.attr("d", arc)
.on('click', function (d) {
alert('click');
});

The ultimate goal of making the VML shape elements, produced by r2d3.js in the process of converting charts for ie8, interactive breaks jQuery and therefore not worth the trouble. If anyone else is reading this I suggest trying another way of making interactive charts with d3.js and r2d3.js. I have used anchor tags and text within charts to accomplish this.
http://bugs.jquery.com/ticket/7071
http://blogs.microsoft.co.il/blogs/rotemb/archive/2011/04/10/asp-net-mvc-jquery-unobtrusive-ajax-failed.aspx
https://github.com/jquery/jquery/pull/13

Related

Show/hide D3 data points dynamically

I'm using Svelte and D3 to create a scatter-point graph.
I need it to dynamically show and hide data points depending on the length of an array of years.
If the array is empty, I want to show all of the points.
If the array is populated, I want to show only the data points which occur on years which are present in the array.
The array is coming from a parent component, I've logged it with a reactive statement and the array is correctly updated in the child component as it needs to be.
My component is currently set up as follows:
onMount(() => {
drawGraph();
plotPoints();
});
$: filterYears.length,
destroyPoints();
plotPoints();
function drawGraph() {
logic to render the graph with axis and titles relevant to the data input.
this works fine.
}
function plotPoints() {
svg.selectAll("dot")
.data(dataArray2)
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return x(d.year); })
.attr("cy", function(d) { return yRight(d.value); })
.style("stroke", "#f8ff2a")
.style('stroke-width', '1px')
.style("fill", "#f8ff2a55")
.style("display", d => (filterYears.length > 0 && !filterYears.includes(d.year)) ? "none" : "inline")
.on('mouseover', function (event, datum) {
d3.select(this).transition()
.duration('100')
.attr("r", 7);
div2.transition()
.duration(100)
.style("opacity", 1)
div2.html((datum.value).toFixed(2))
.style("left", (event.offsetX + 25) + "px")
.style("top", (event.offsetY - 10) + "px")
})
.on('mouseout', function (event, datum) {
d3.select(this).transition()
.duration('200')
.attr("r", 3);
div2.transition()
.duration('200')
.style("opacity", 0);
});
console.log('plot point');
}
I have 2 sets of data being rendered, the above is one of them.
function destroyPoints() {
console.log('destroying points');
svg.selectAll("dot").remove()
}
All variables are correctly scoped and all functions have access to what they need.
The reactive statement seems to be called initially on component render and was triggering destroyPoints() which was trying to access svg before initialisation, causing an error, I have gotten around this with a really hacky setTimeout. If somebody could shed some light on why this happens and a better workaround I would love to hear it.
The real issue is that on change of filterYears, the points are not updating in the graph.
Could somebody give me a hand here?
Thanks!
Regular component code, including reactive statements, is executed before anything it mounted at which point DOM elements will not exist yet. It is common to just add a condition to reactive statements that depend on DOM bindings:
$: if (svg) {
filterYears;
destroyPoints();
plotPoints();
}
There seems to be another error here which is obfuscated by deceptive formatting, the correct format of the original code is:
$: filterYears.length,
destroyPoints();
plotPoints();
plotPoints() is not part of the reactive statement due to the ;.
(Also, in case you modify filterYears using functions like push: An assignment has to be used to trigger reactivity.)
I am new to d3.js, but wouldnt a pre-refinement of the inputdata be more straight forward?
svg.selectAll("dot")
.remove()
.data(filteredDataArray.any() ? filteredDataArray : dataArray2)
.enter()
.append("circle")
or maybe even more outside. just filter the dataArray

d3js force layout set node symbol based on d

I am trying to create a force layout where nodes will have different images (icons) based on some condition in the .data(). I can understand that we can set other attributes such as style, class etc easily following the standard approach:
nodeSelction.data(...)
.enter()
.append("circle")
.attr("class", function(d) { return xxx; })
Is it possible to do the same thing for deciding whether to add a svg:circle, or some image icon? I tried doing the same thing to append method, but it doesn't take a function. That is,
the following snippet doesn't work.
nodeSelction.data(...)
.enter()
.append(function(d) { return d.group==0?"circle" : "usr.png"})
.attr("class", function { return xxx; })
So, what is the correct way to achieve the above?

How to apply tooltips for dc.js chart after chart is rendered

I have multiple row charts (crossfilter + dc) and I want to customize the tooltip using d3-tip.
So basically the relevant code is:
rowtip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function (d) { return d.key + ": " + d.value; });
...my dc charts ...
... then at the bottom of my script tags ...
d3.selectAll('g.row').call(rowtip);
d3.selectAll('g.row').on('mouseover', rowtip.show);
The code seems work, but the mouseover event doesn't get triggered automatically and the tooltips don't get displayed when the page loads.
But If I run the last line (mouseover) on the console, then everything works as expected.
So my question would be how can I make sure the mouseover event gets triggered when the page loads. I tried Jquery $(document).ready(....), but that didn't work.
It has to have something to do with the order in which the elements are loaded... I guess. But I'm not an expert in javascript and much less in d3.
Thanks in advance.
Your way works fine but here is the idiomatic dc.js way to do it:
chart.on('pretransition.add-tip', function(chart) {
chart.selectAll('g.row')
.call(rowtip)
.on('mouseover', rowtip.show)
.on('mouseout', rowtip.hide);
});
The pretransition event fires after the chart is rendered or redrawn. add-tip is an event namespace to avoid interfering with anything else which may be watching this event.
chart.selectAll selects only items within the chart, to avoid accidental tooltips elsewhere in the page.
Here seems similar to your requirement, check it
http://bl.ocks.org/phil-pedruco/9032348
So I was able to solve this by using jquery after all. Seems a bit hacky, but it does what I need.
Basically I am forcing the call of the d3-tip function on mouseover of the html body.
var rowtip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d){return d.key;})
$('body').on('mouseover', function(){
d3.selectAll('g.row')
.call(rowtip)
.on('mouseover', rowtip.show)
.on('mouseout', rowtip.hide);
});

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

d3 - trigger mouseover event

I have a map of the US states and counties in a SVG graphic rendered by D3. Each path have mouseover, mouseout and click events bound to it, as well as the FIPS county code set as the path ID.
I have a jQuery Autocomplete input where the user can input the name of a state or county. Given that input, which makes the corresponding FIPS ID available, how can I trigger the mouseover event programatically?
I figured out the answer. The main problem is D3 does not have an explicit trigger function as jQuery does. However, you can simulate it.
Say you have a D3 path built via
d3.json("us-counties.json", function(json){
thisObj._svg.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", thisObj._path)
.attr("class", "states")
.attr("id", function(d){
return d.id; //<-- Sets the ID of this county to the path
})
.style("fill", "gray")
.style("stroke", "black")
.style("stroke-width", "0.5px")
.on("dblclick", mapZoom)
.on("mouseover", mapMouseOver)
.on("mouseout", mapMouseOut);
});
and a mouseover event handler that changes the fill and stroke colors
var mapMouseOver(d){
d3.selectAll($("#" + d.id))
.style("fill", "red")
.style("stroke", "blue");
}
Typically, most tutorials say to use
d3.select(this)...
but actually using the value works as well. If you have an event handler that gets you the ID of the node, and trigger it via
$("#someDropdownSelect").change(someEventHandler)
function someEventHandler(){
//get node ID value, sample
var key = $(this)
.children(":selected")
.val();
//trigger mouseover event handler
mapMouseOver({id : key});
}
will execute the mouseover event based on a dropdown selection
You can achieve this by directly dispatching the event on the desired element:
var event = document.createEvent('SVGEvents');
event.initEvent(eventName,true,true);
element.dispatchEvent(event);
See more detail in this blog post
Structure your javascript such that the the mouseover event calls a javascript function and then you can call that same function any time you want.
Steve Greatrex's solution worked for me until iOS 9, but not on iOS 10.
After debugging my code and some research it seems the issue was that the createEvent and initEvent functions are deprecated as per this documentation.
The new way of writing this is:
var event = new MouseEvent('SVGEvents', {});
element.dispatchEvent(event);
More explanation about the new way of creating and triggering events with event constructors can be found here.

Resources