d3.js Change attributes of external svg injected through d3.xml - d3.js

I am working on this pie-chart. This is what I would like to happen as the user clicks on a wedge of the pie:
1) An external svg is loaded and inserted in the div on the right (DONE)
1.1) The svg has the following structure
<svg id="Ebene_1">
<circle></circle>
<path></path>
<svg>
2) Change circle's fill according to a color() function which assign a color depending on the wedge's index
2) Change path's fill to white
This is the relevant code I have so far:
descrImg = d3.select("#descrImg"); //select the target div
descrImg.select("svg").remove(); //removes previous image
descrImgURI = d.data.descrImg; //extract svg's URI from data
d3.xml(descrImgURI, "image/svg+xml", function(error, xml) {
if (error) throw error;
var svg= xml.documentElement;
descrImg.node().appendChild(svg); //append svg
var a = document.getElementById("Ebene_1"); //select svg
var b = a.getElementsByTagName("cicle"); //select circle
var c = a.getElementsByTagName("path"); //select path
d3.select(c).attr("fill", "white"); //change path's fill attribute
});
When I click on a wedge, the svg is loaded correctly but the path's fill is not changed. I get the following error (same happens if I use style instead of attr):
TypeError: this.setAttribute is not a function
Other options:
//Option 1
var c = d3.select(document.getElementById("#Ebene_1").contentDocument).selectAll("path");
d3.select(c).attr("fill", "yellow");
//Option 2
var c = d3.select(document.getElementById("Ebene_1").contentDocument).selectAll("path");
d3.select(c).attr("fill", "yellow");
//Both return the error
TypeError: document.getElementById(...) is null
//while Option 3
var c = document.getElementById("Ebene_1").contentDocument.getElementsByTagName("path");
d3.select(c).attr("fill", "yellow");
//Returns
TypeError: document.getElementById(...).contentDocument is undefined
I do not know if this matters, but when I inspect circle or fill I see their attributes are "locked" (a bit hard to see but there is a lock next to attributes)
Any ideas?

I have managed to solve the problem. This is the code that works:
d3.xml(descrImgURI, "image/svg+xml", function(error, xml) {
if (error) throw error;
var svg= xml.documentElement;
descrImg.node().appendChild(svg);
var a = document.getElementById("Ebene_1");
var b = a.getElementsByTagName("circle")[0];
var c = a.getElementsByTagName("path");
// d3.select(b).attr("fill", function (d, i) {
// var scale = colorMap[d.data.categoria];
// if (scale) return scale(d.data.catIndex);
// })
d3.select(b).attr("fill", fillColor);
d3.selectAll(c).attr("fill", "white");
});
getElementsByTagName returns a collection of items, regardless of how many items will be in the collection.
Therefore, in the case of b (i.e. the circle) I had to select the first element of the connection (through index [0]).
The path(s) in c, on the other hand, can be collectively modified by using d3.selectAll. If I use the same method as with b, then only the first set of paths would be modified and, at least in my case, this would leave some drawings partly uncolored.
Hope this helps!

Related

How to dragover in this algorithm using d3.js drag.behaviour()?

I am trying to design an association miminap using d3.js. My goal is to position different items according to data and associate them using a drag function.
I try to calculate distance to different elements and if distance is lower than total radius, I consider this condition as a dragover. However drag function keeps selecting the node I drag instead of the node I drag it to.
var drag = d3.behavior.drag()
.on('dragstart', function() {})
.on('drag', function(d) {
nodes=d3.select(this.parentNode).selectAll("circle")[0];
nodes.pop(this);
var minDist=1000;
for (i=0;i<nodes.length;i++) {
var currentNode=d3.select(nodes[i]);
var r1=parseInt(this.style.r);
var r2=parseInt(currentNode.style("r"));
var dx=d3.event.x-parseInt(currentNode.style("cx"));
var dy=d3.event.y-parseInt(currentNode.style("cy"));
var dist=Math.sqrt(dx*dx+dy*dy);
if(dist<(r1+r2)) {
d.con=currentNode.attr("id");
currentNode.style("fill","red");
console.log(currentNode[0][0]);
}
}
d3.select(this)
.style("cx",d3.event.x)
.style("cy",d3.event.y);
})
Why does my futile attempt to remove the node I drag by entering nodes.pop(this) does not work ?
I have added an editable codepen version :
https://codepen.io/TeaCult/pen/ezJVyz?editors=1111
Thank you for reading.

d3 update number - count up/down instead of replacing number immediately

Hei,
I'm updating a bar chart when user presses a button. That works fine with the .transition- property. However, if I do that on text, it replaces the text immediately. Instead what I'd like to happen is that it would count from the old to the new number (while the label moves with the bar). So as an example: a bar is updated from value 1453 to 1102. Instead of replacing 1453 immediately when the user clicks it should count up from 1102 to 1453 over the specified transition time.
Can I achieve that? Is there any d3 function for that?
I uploaded a quick example of text interpolation on bl.ocks. The relevant parts are the custom interpolator:
function interpolateText(a, b) {
var regex = /^(\d+), (\d+)$/;
var matchA = regex.exec(a);
var matchB = regex.exec(b);
if (matchA && matchB) {
var x = d3.interpolateRound(+matchA[1], +matchB[1]);
var y = d3.interpolateRound(+matchA[2], +matchB[2]);
return function(t) {
var result = [x(t), y(t)].join(", ");
return result;
};
}
}
d3.interpolators.push(interpolateText);
And using d3.transition.tween:
.on("dragend", function(d, i) {
var prev = [d.x, d.y].join(", ");
d.x = d.origin[0];
d.y = d.origin[1];
var next = [d.x, d.y].join(", ");
var selection = d3.select(this)
.transition()
.duration(1000)
.call(draw);
selection
.select("text")
.tween("textTween", function() {
var i = d3.interpolate(prev, next);
return function(t) {
this.textContent = i(t);
}
});
});
In my case, I am listening for a drag start/end but you can hook it up to a button press very easily.
The reason the above code works is because .tween will get the same animation "ticks" that the standard interpolators use. This causes the inner t parameter to match the progress of the animation and when you set this.textContent it will update the inner value of the DOM element.
The example I use is interpolating between two points which is fairly trivial but if all you want is to update text containing exactly one number it is even easier.

geoJSON projection with d3.js and dc.js for South Africa and provinces

I have been struggling for a few days trying to use dc.js with d3.js projection to draw a map of South Africa and the provinces. I have exhausted my search as most results incorporate the path used for SVG when not using dc.js and I can't seem to find a suitable example for correcting a projection in dc.js.
I can't seem to find the map thats being drawing and I don't know how to correct my projection.
I really really don't know what i'm missing, and anyone that can assist will be appreciated.
UPDATE: I have geoJson that ive tested in mapshaper and it works so the geojson is fine. I am just struggling with the projection.
zaMap = zaMapString
//new array
var zaMapData = [];
for(var p in zaMap["features"])
{
console.log("ndx2 province data " + zaMap["features"][p]["properties"]["name"]);
zaMapData.push({
province: zaMap["features"][p]["properties"]["name"],
donation: 1000
})
};
//crossfilter instance
var ndx2 = crossfilter(zaMapData);
//dimensions and group for dc/d3
var provinceDim = ndx2.dimension(function(d) {console.log("province d " + d["province"]); return d["province"];});
var donationsByProvince = provinceDim.group().reduceSum(function(d) {
return d["donation"];
});
//geoChoroplethChart
var zaChart = dc.geoChoroplethChart("#map");
//start of chart code using d3 and dc
zaChart.dimension(provinceDim)
.group(donationsByProvince)
.width(1000)
.height(330)
.colors(["#E2F2FF", "#C4E4FF", "#9ED2FF", "#81C5FF", "#6BBAFF", "#51AEFF", "#36A2FF", "#1E96FF", "#0089FF", "#0061B5"])
.projection(d3.geo.mercator()
.scale(26778)
.translate([8227, 3207]))
.overlayGeoJson(zaMap["features"], "name", function (d) {
return d.properties.name;
});
dc.renderAll();
$("#progress").css({"display": "none"});
})
UPDATE 2: I switched from fiddle to codepen so I could upload the geoJson file as a asset. The geoJson takes a while to load but using code from an existing stackoverflow question, I have gotten the map to draw and projection to correct itself automatically. The d3.js function is not wrapping the dc.js to tie in with crossfilter.js as yet but I am working on that. But this is progress :)
In http://jsfiddle.net/Jimmypoo/f67xo5ry/1/, you are trying to use JSON.parse to parse an zaMapString, which is already a JS object. You don't need to parse it, it's not a string.
Secondly, d3.json is meant for passing in a remote URL, which d3 grabs for you. You are trying to pass in a JS object, which already exists. So you can remove that function, and simply use .overlayGeoJson(zaMap["features"], "name", function (d) { inside.
You also forgot to include jQuery, yet you use it in $("#progress").css({"display": "none"});. You'll need to wrap the entire JS section in a $(document).ready as well.
Also, you are including the scripts multiple times, in both minified and unminified forms.You only need one instance of each library.
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/dc/1.7.0/dc.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/dc/1.7.0/dc.min.js"></script>
You are also trying to include dc's CSS as JavaScript.
<script src="http://cdnjs.cloudflare.com/ajax/libs/dc/1.7.0/dc.css"></script>
It should be added in JsFiddle's left-hand side resource panel instead.
I also don't think assigning #map directly to the body of your document is going to make things easier for you either..would recommend including something interior of that like <div id="map" style="width:100%;height:300px"></div>
These suggestions don't solve all your problems but get you most of the way along.You still have projection issues. Here is an forked fiddle to move from - http://jsfiddle.net/uggtjem6/
I have gotten the geoJson to work with d3.js, dc.js and crossfiler.
var width = 300;
var height = 400;
var zaMapData = [];
//geoChoroplethChart
var zaChart = dc.geoChoroplethChart("#map");
d3.json("https://s3-us-west-2.amazonaws.com/s.cdpn.io/384835/layer1.json", function(json) {
var zaMap = JSON.stringify(json);
console.log(zaMap);
for (var i = 0; i < json.features.length; i++) {
console.log("ndx2 province data " + json["features"][i]["properties"]["PROVINCE"]);
zaMapData.push({
province: json["features"][i]["properties"]["PROVINCE"],
donation: i*1000
})
};
//crossfilter instance
var ndx2 = crossfilter(zaMapData);
//dimensions and group for dc/d3
var provinceDim = ndx2.dimension(function(d) {console.log("province d " + d["province"]); return d["province"];});
var donationsByProvince = provinceDim.group().reduceSum(function(d) {
return d["donation"];
});
var max_province = donationsByProvince.top(1)[0].value;
// create a first guess for the projection
var center = d3.geo.centroid(json)
var scale = 150;
var offset = [width/2, height/2];
var projection = d3.geo.mercator().scale(scale).center(center)
.translate(offset);
// create the path
var path = d3.geo.path().projection(projection);
// using the path determine the bounds of the current map and use
// these to determine better values for the scale and translation
var bounds = path.bounds(json);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
var scale = (hscale < vscale) ? hscale : vscale;
var offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate(offset);
path = path.projection(projection);
//create dc.js chart
zaChart.dimension(provinceDim)
.group(donationsByProvince)
.width(width)
.height(height)
.colors(["#E2F2FF", "#C4E4FF", "#9ED2FF", "#81C5FF", "#6BBAFF", "#51AEFF", "#36A2FF", "#1E96FF", "#0089FF", "#0061B5"])
.colorDomain([0, max_province])
.projection(d3.geo.mercator()
.center(center)
.scale(scale)
.translate(offset))
.overlayGeoJson(json["features"], "PROVINCE", function (d) {
return d.properties.PROVINCE;
})
.title(function (p) {
return "Province: " + p["key"]
+ "\n"
+ "Total Donations: R " + Math.round(p["value"])
});
dc.renderAll();
});
My codepen here.

Create Unique Path Within Each SVG Group Element Using D3

I'm trying to create 50 SVG groups with each group containing a path that draws a particular US state. However, I can't figure out how to access the state data from the SVG group when creating the state path for that group. Can someone put me on the right track? Thanks!
var coords = [
{ 'id':'HI', 'class':'state hi', 'd': 'm 233.08751,519.30948 1.93993, ...' },
{ 'id':'AK', 'class':'state ak', 'd': 'm 158.07671,453.67502 -0.32332, ...'}
...
];
// Select the SVG
var svgContainer = d3.select('svg');
// Create groups with state data
var groups = svgContainer.selectAll('g')
.data(coords)
.enter()
.append('g');
// Create state path for each group
groups.each(function(g){
g.select('path')
.data(___NEED TO RETURN THE PATH DATA HERE___)
.enter()
.append('path');
});
Uncaught TypeError: g.select is not a function
Assuming your data is valid and you only want to handle new elements (no updates), this should work:
// Select the SVG
var svgContainer = d3.select('svg');
// Create groups with state data
var groups = svgContainer.selectAll('g')
.data(coords)
.enter()
.append('g')
.append('path')
.attr('d', function (d) { return d.d; });
I have created a pretty simplified fiddle with an example.
If you still want to use the .each function, you can proceed as follows:
groups.each(function (d, i){
// `this` (in this context) is bound to the current DOM element in the enter selection
d3.select(this).append('path').attr('d', d.d);
});
For further details, you can check the API documentation.
Hope it helps.

change axis color dimple

I have a very simple line chart in dimple. I want to change the x and y axis colour to white.
var svg = dimple.newSvg(".line_chart_container", 400, 300),dataset;
var chart = new dimple.chart(svg, dataset);
var x = chart.addCategoryAxis("x", "Attempts");
//x.style ("fill","red")
var y = chart.addMeasureAxis("y", "Value");
y.showGridlines = true;
x.showGridlines = true;
var s = chart.addSeries(["Metric", "Value"], dimple.plot.bubble);
var lines = chart.addSeries("Metric", dimple.plot.line);
lines.lineWeight = 2;
lines.lineMarkers = true;
chart.assignColor("Metric", "#30D630");
chart.draw();
s.shapes.style("opacity", function (d) {
return (d.yValue === 0 ? 0 : 0.8);
});
I've checked dimple.axis documentation in GitHub but couldn't find any thing. There is a dimple.axis.colors attribute, but it changes the color of the data and not the axis. Does dimple even support this?
I've also tried to add style attribute(like in D3):
x.style ("fill","red")
but caught an error: Uncaught TypeError: x.style is not a function
Any idea?
x is not a d3 selection, it is a dimple.axis. You can access the inner d3 selection with the shapes property (which is standard for any dimple object). There is an example of that here.
Depending on if you want to change the line color, text color, or everything, you would do
x.shapes.selectAll("*").style("fill", "white")
where * could also be "text" or "line".
One note : the individual tick marks are <line> nodes, and to change their color you need to use 'stroke', not 'fill'.
Also, the actual axis line itself is not a <line> element, it's a <path> element. So to change that color would be :
x.shapes.select("path.dimple-custom-axis-line").style("stroke", "white");
You can also just manually write a css rule to override the style for a chart :
g.dimple-axis > g.tick > line, g.dimple-axis path.dimple-custom-axis-line {
stroke:white;
}
For X-axis
d3.selectAll("path.domain")[0][0].style.stroke = "red";
For Y-axis
d3.selectAll("path.domain")[0][1].style.stroke = "yellow";

Resources