I'm building a dashboard with a couple of d3 visualizations, which looks like:
The way I built this is by making a few div's on the page, and call the .js script inside these divs. This worked perfectly fine so far. Unfortunately I run into a problem when calling my .js file with a line graph on the right top div on my page. When I call the graph.js file, this happens:
I'm not entirely sure what's happening, but I think both the visualizations are using "path" elements, and therefore they interfere with eachother. This is the main code for the map of Europe:
//Load in GeoJSON data
d3.json("ne_50m_admin_0_countries_simplified.json", function(json) {
//Bind data and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.filter(function(d) { return d.properties.continent == 'Europe'}) //we only want Europe to be shown ánd loaded.
.append("path")
.attr("class", "border")
.attr("d", path)
.attr("stroke", "black")
.attr("fill", "#ADD8E6")
.attr("countryName", function(d){return d.properties.sovereignt})
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div .html(d.properties.sovereignt + ', ' + getCountryTempByYear(dataset, d.properties.sovereignt, document.getElementById("selectYear").value))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
})
.on("click", function(d){
//add/delete country to/from selected countries
updateSelectedCountries(d.properties.sovereignt);
//draw the temperature visualizations again
redrawTemp(getCountryTempByYear(dataset, selectedCountries[0], document.getElementById("selectYear").value), getCountryTempByYear(dataset, selectedCountries[1], document.getElementById("selectYear").value));
//update header from the webpage
d3.select("#header").text("Climate Change: 1840-2013. Currently viewing: " + selectedCountries + ", " + document.getElementById("selectYear").value + '.');
console.log(selectedCountries);
});
});
And this is the code for the line graph:
// set the dimensions and margins of the grap
var array = [[1850, 11.1], [1851, 11.7], [1852, 12.2], [1853, 11.1], [1854, 11.7], [1855, 12.2], [1856, 13.4]]
var array2 = [[1850, 14.1], [1851, 17.7], [1852, 22.2], [1853, 13.1], [1854, 24.7], [1855, 19.2], [1856, 13.4]]
// set the ranges
var x = d3.scaleLinear().range([0, 200]);
var y = d3.scaleLinear().range([200, 0]);
// define the line
var valueline = d3.line()
.x(function(d, i) { return x(array[i][0]); })
.y(function(d, i) { return y(array[i][1]); })
.curve(d3.curveMonotoneX); //smooth line
var valueline2 = d3.line()
.x(function(d, i) { return x(array2[i][0]); })
.y(function(d, i) { return y(array2[i][1]); })
.curve(d3.curveMonotoneX); //smooth line
var svg = d3.select("#div2")
.append("svg")
.attr("width", 200)
.attr("height", 200)
.attr("id", "graph");
x.domain([1850, 1856]);
y.domain([10, 25]);
array.forEach(function(data, i){
svg.append("path")
.data([array])
.attr("class", "line")
.attr("d", valueline);
})
array2.forEach(function(data, i){
svg.append("path")
.data([array2])
.attr("class", "line")
.attr("d", valueline2);
})
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + 200 + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));
As I said, I'm not entirely sure it's because of the graphs both using the "path" element, it could be something different too. Now what I'm wondering is: what is interfering with eachother? And how can I prevent this?
I can imagine something like a filter needs to be applied, but I'm not sure how to apply this to the right "path" elements.
Any help is highly appreciated!
When you create a new chart you should create a new variable to associate with it.
In your case, this might be happening due to the reuse of the variable var svg =....
Create one variable for each chart should be enough.
Related
I have a map and a matching legend on my website. As the user selects different values from a select list, the map is updated and in the same function, the legend should be updated with new values. As the map actualization works properly, the values of the legend stay the same even in the console are logged the right values if I log the variables.
This is the function that draws the legend:
color_domain = [wert1, wert2, wert3, wert4, wert5];
ext_color_domain = [0, wert1, wert2, wert3, wert4, wert5];
console.log(ext_color_domain);
legend_labels = ["< "+wert1, ""+wert1, ""+wert2, ""+wert3, ""+wert4, "> "+wert5];
color = d3.scale.threshold()
.domain(color_domain)
.range(["#85db46", "#ffe800", "#ffba00", "#ff7d73", "#ff4e40", "#ff1300"]);
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
var ls_w = 20, ls_h = 20;
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.append("text")
.attr("x", 50)
.attr("y", function(d, i){ return height - (i*ls_h) - ls_h - 4;})
.text(function(d, i){ return legend_labels[i]; });
console.log(legend_labels); //gives the right legend_labels but doesn't display them correctly
};
Sadly even the map is updated with new colors they're colored with the old thresholds. This is the way the map is colored:
svg.append("g")
.attr("class", "id")
.selectAll("path")
.data(topojson.feature(map, map.objects.immoscout).features)
.enter().append("path")
.attr("d", path)
.style("fill", function(d) {
return color(rateById[d.id]);
})
This is tough to answer without a complete, working code sample but...
You are not handling the enter, update, exit pattern correctly. You never really update existing elements, you are only re-binding data and entering new ones.
Say you've called your legend function once already, now you have new data and you do:
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
This re-binds the data and computes an enter selection. It says, hey d3, what data elements are new? For those new ones, you then append a g. Further:
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
Again, this is operating on those newly entered elements only. The ones that already existed on the page aren't touched at all.
Untested code, but hopefully it points you in the right direction:
// selection of all enter, update, exit
var legend = svg.selectAll("g.legend")
.data(ext_color_domain); //<-- a key function would be awesome here
legend.exit().remove(); //<-- did the data go away? remove the g bound to it
// ok, what data is coming in? create new elements;
var legendEnter = legend.enter().append("g")
.attr("class", "legend");
legendEnter.append("rect");
legendEnter.append("text");
// ok, now handle our updates...
legend.selectAll("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.selectall("text")
...
There's some really great tutorials on this; and it's confusing as hell, but it's the foundation of d3.
An example that helps you get started with updating d3 (d3, v4):
const line = svg.selectAll('line').data(d3Data); // binds things
line.exit().remove(); // removes old data
line.enter()
.append('line') // add new lines for new items on enter
.merge(line) // <--- this will make the updates to the lines
.attr('fill', 'none')
.attr('stroke', 'red');
I am trying to do the obvious thing of getting the arrowhead colors of my directed graph's links to match the edge colors. Surprisingly I have not found a complete solution for doing this, although this older post seems like an excellent starting point. I would be fine with adapting that solution to work as outlined below, or if there is a superior method for creating arrowheads that achieves this effect I would be most thankful.
First, I have a linear gradient color function to color my edges by property like this:
var gradientColor = d3.scale.linear().domain([0,1]).range(["#08519c","#bdd7e7"]);
Then, like that previous post I have a function for adding markers:
function marker (color) {
var reference;
svg.append("svg:defs").selectAll("marker")
.data([reference])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15) // This sets how far back it sits, kinda
.attr("refY", 0)
.attr("markerWidth", 9)
.attr("markerHeight", 9)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.style("fill", color);
return "url(#" + reference + ")"; };
And then the links definition I have is this one based on the Curved Links example.
var link = svg.selectAll(".link")
.data(bilinks)
.enter().append("path")
.attr("class", "link")
.style("fill", "none")
.style("opacity", "0.5")
.style("stroke-width", "2")
.style("stroke", function(d) { return gradientColor(d[3]); } )
.attr("marker-end", marker( "#FFCC33" ) );
This DOES NOT work as written; the browser gives me an "Uncaught TypeError: Cannot read property '5' of undefined" (where 'd[5]' refers to the fifth property in a list of properties that the links have). The problem is clearly passing the data function to the marker function in this case. If I feed in a static color like "#FFCC33" then the arrowheads DO change color (now). Unfortunately the person who posted this "marker function" solution 1.5 years ago didn't include the bit about passing the color to the marker function at all.
I don't know how to feed in the link's color properly. Ideally I would be able to use a reference to the color of the link that the arrowhead is attached to rather than inputting the same color function (because eventually I'm going to be coloring the links via different schemes based on button presses).
I've created a JS Fiddle that includes all the necessary bits to see and solve the problem. Currently I'm passing a static color to the markers, but it should be whatever is the color of the link it is attached to. I've also included features for another question on properly positioning the arrowheads and edge tails.
I don't believe you're able to define a single SVG marker and change it's colour. Instead you need to define the marker many times (1 for each colour that you need to use). There's a nice example that recently popped up onto the D3 website.
The way this works, is by having lots if different markers, each defining the colour of the marker. Here's a screenshot of all the markers that are defined:
Then this particular example, cycles the CSS classes on the paths. The particular colored marker that each path is using is defined within the CSS class that's being applied to a path at any given time.
I've modified your example to add a new marker per path (and changed the colors slightly in the gradient to prove that it's working). Here's what I've got:
var width = 960,
height = 500;
var color = d3.scale.category20();
var gradientColor = d3.scale.linear().domain([0, 15]).range(["#ff0000", "#0000ff"]);
var force = d3.layout.force()
.linkDistance(10)
.linkStrength(2)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var defs = svg.append("svg:defs");
d3.json("http://bost.ocks.org/mike/miserables/miserables.json", function (error, graph) {
if (error) throw error;
function marker(color) {
defs.append("svg:marker")
.attr("id", color.replace("#", ""))
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15) // This sets how far back it sits, kinda
.attr("refY", 0)
.attr("markerWidth", 9)
.attr("markerHeight", 9)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.style("fill", color);
return "url(" + color + ")";
};
var nodes = graph.nodes.slice(),
links = [],
bilinks = [];
graph.links.forEach(function (link) {
var s = nodes[link.source],
t = nodes[link.target],
i = {}, // intermediate node
linkValue = link.value // for transfering value from the links to the bilinks
;
nodes.push(i);
links.push({
source: s,
target: i
}, {
source: i,
target: t
});
bilinks.push([s, i, t, linkValue]);
});
force.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll(".link")
.data(bilinks).enter().append("path")
.attr("class", "link")
.style("fill", "none")
.style("opacity", "0.5")
.style("stroke-width", "2")
.each(function(d) {
var color = gradientColor(d[3]);
console.log(d[3]);
d3.select(this).style("stroke", color)
.attr("marker-end", marker(color));
});
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", function (d) {
return 2 + d.group;
})
.style("opacity", 0.5)
.style("fill", function (d) {
return color(d.group);
});
node.append("title")
.text(function (d) {
return d.name;
});
force.on("tick", function () {
link.attr("d", function (d) {
return "M" + d[0].x + "," + d[0].y + "S" + d[1].x + "," + d[1].y + " " + d[2].x + "," + d[2].y;
});
node.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>
I'm trying to create a basic d3 pie chart with a legend. I'm following the examples in two different tutorials and somehow code from one example isn't playing well with the other. What I'm trying to do is set an ordinal scale's domain so I can use that to create a legend.
On the following line, I set the domain. If I step through the code, I can see that immediately after I get ["HEURISTIC", "ADWARE", "COMPANY_BLACK_LIST", "PUP", "SUSPECTED_MALWARE", "KNOWN_MALWARE"]. This is exactly what I want.
color.domain(labels)
However, if I keep stepping through, once I reach the following line, the domain changes to ["HEURISTIC", "ADWARE", "COMPANY_BLACK_LIST", "PUP", "SUSPECTED_MALWARE", "KNOWN_MALWARE", 0, 1, 2, 3, 4, 5]
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } )
.attr("d", arc);
QUESTION: What is causing those six extra items to be inserted into the domain?
Code (http://jsfiddle.net/tonicboy/2urZY/5/):
var w = 150,
h = 100,
r = 50,
color = d3.scale.category20c(),
dataset = [{"name":"HEURISTIC","value":65},{"name":"ADWARE","value":75},{"name":"COMPANY_BLACK_LIST","value":9},{"name":"PUP","value":34},{"name":"SUSPECTED_MALWARE","value":14},{"name":"KNOWN_MALWARE","value":156}],
labels = _.pluck(dataset, "name");
color.domain(labels);
var chart = d3.select("#pie_chart")
.append("svg:svg")
.data([dataset])
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", "0 0 " + w + " " + h)
.attr("preserveAspectRatio", "xMinYMin meet");
var vis = chart.append("g")
.attr("transform", "translate(" + (w - r) + "," + r + ")");
var arc = d3.svg.arc()
.outerRadius(r);
var pie = d3.layout.pie()
.value(function(d) { return d.value; });
var arcs = vis.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice");
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } )
.attr("d", arc);
var legend = chart.append("g")
.attr("class", "pie-legend")
.selectAll("g")
.data(color.domain())
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 7 + ")"; });
legend.append("rect")
.attr("width", 5)
.attr("height",5)
.style("fill", color);
legend.append("text")
.attr("x", 8)
.attr("y", 9)
.text(function(d) { return d; });
Here is what the chart looks like so far:
You're setting your ordinal scale domain with strings, but then calling it with index numbers. If you ask an ordinal scale for a value that isn't currently in its domain, it will add it to the domain and assign it the next value in the range (or recycle the range values if it runs out).
Original code:
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } )
.attr("d", arc);
Should be
arcs.append("svg:path")
.attr("fill", function(d, i) {return color( d.data.name); } )
.attr("d", arc);
The d value is the object created by the pie chart function; it stores the original data object as d.data. The name from that data is one of the values used in the color scale domain.
Updated fiddle: http://jsfiddle.net/2urZY/6/
I've used arc.Centroid to try to plot my circles on the arcs with labels. However, the labels do not stay with it?
force.on("tick", function() {
text.attr("x", function(d) { return d.x + 6; })
.attr("y", function(d) { return d.y + 4; });
node.attr("transform", function(d,i) {
return "translate(" + arc[i].centroid(d) + ")"; })
});
I have attempted to put centroid & arc[i] instead of the x & y. How can I put my circles with text? http://jsfiddle.net/xwZjN/20/
Also say if I were to have more json data, would I be able to restrict the plots only going into each section e.g. each section being a category?
Any help would be great. I think the solution may be similar to this - http://jsfiddle.net/nrabinowitz/GQDUS/
It seems that the force layout is not the right choice for your application. Try to group your symbol and text in a g element and place them at the calculated coordinates. See updated fiddle without force layout: http://jsfiddle.net/xwZjN/26/
var node = svg.selectAll("g.node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d,i) {
return "translate(" + arc[i].centroid() + ")";
});
node.append("path")
.attr("d", d3.svg.symbol().type(function(d) { return d.type; }))
// change (0,0) for exact symbol placement
.attr("transform", "translate(0,0)")
.style("fill", "blue" );
node.append("text")
.text(function(d) { return d.Name; })
// shift text in nice position
.attr("x", 10)
.attr("y", 5);
Given a tree diagram like the Dendrogram example (source), how would one put labels on the edges? The JavaScript code to draw the edges looks like the next lines:
var link = vis.selectAll("path.link")
.data(cluster.links(nodes))
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal);
Mike Bostock, the author of D3, very graciously helped with the following solution. Define a style for g.link; I just copied the style for g.node. Then I replaced the "var link =...." code with the following. The x and y functions place the label in the center of the path.
var linkg = vis.selectAll("g.link")
.data(cluster.links(nodes))
.enter().append("g")
.attr("class", "link");
linkg.append("path")
.attr("class", "link")
.attr("d", diagonal);
linkg.append("text")
.attr("x", function(d) { return (d.source.y + d.target.y) / 2; })
.attr("y", function(d) { return (d.source.x + d.target.x) / 2; })
.attr("text-anchor", "middle")
.text(function(d) {
return "edgeLabel";
});
The text function should ideally provide a label specifically for each edge. I populated an object with the names of my edges while preparing my data, so my text function looks like this:
.text(function(d) {
var key = d.source.name + ":" + d.target.name;
return edgeNames[key];
});