Setting the SVG basic shape on a node-by-node basis - javascript

(D3 beginner here)
I have the following snippet:
// myShape (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
myShape = myShape.data(nodes, function(d) { return d.nodeId; });
// update existing nodes (reflexive & selected visual states)
myShape.selectAll('circle')
.style('fill', function(d) { return (d === selected_node) ? d3.rgb(colors(d.nodeType)).brighter().toString() : colors(d.nodeType); })
.classed('reflexive', function(d) { return d.reflexive; });
// add new nodes
var g = myShape.enter().append('svg:g');
g.append('svg:circle')
.attr('r', 12)
But I would like to make this more flexible: instead of using only circles, I would like to use circles and polygons. This will be selected in a property in d:
var d = [
{ nodeId: 1, nodeType : 'type1' , shape: 'circle' },
{ nodeId: 2, nodeType : 'type2' , shape: 'triangle' },
];
Which means that, depending on d.shape. I must set 'svg:circle' or 'svg:polygon', and then set the radius (for the circle) or the points (for the polygons). I have tried to set the svg shape like this:
g.append(function (d) {
if (d.shape === 'circle' ) { return 'svg:circle'; }
else { return 'svg:polygon'; } } )
But this is not working: I am getting a:
Uncaught Error: NotFoundError: DOM Exception 8
It seems append does not accept a function? How can I set the svg shape on a node-by-node basis?
EDIT
I have prepared this jsbin to show what I want to do.

In recent versions of D3, you can append elements that are the results of function calls. That is, instead of passing a static name, you can pass a function that evaluates to the element to add.
It doesn't quite work the way you're using it -- it's not enough to return the name of the element from the function. Instead of
g.append(function (d) { return svgShape(d); })
where svgShape returns a string, you need to do something like
g.append(function(d) {
return document.createElementNS("http://www.w3.org/2000/svg", svgShape(d));
})
and the corresponding shape will be created. I've updated your jsbin here to demonstrate.
In your case, it might be easier to always append a path and vary the line generator, i.e. the d attribute value. That is, for a circle you would pass in a function that returns a circular path, for a polygon a function that returns the particular polygon path etc.

Related

plotting graph symbols using two-level nested data in d3.js

I am trying to replicate this example of a multiline chart with dots. My data is basically the same, where I have an object with name and values in the first level, and then a couple of values in the second level, inside values. The length of the arrays inside values is 40.
Now, one requirement is that all the dots for all the paths are inside the same g group within the DOM. This is giving me a lot of trouble because I can't seem to figure out how to join the circles with the appropriate portion of the nested data.
The last thing I've tried is this:
var symbolsb = d3.select("#plot-b") // plot-b is the graph area group within the svg
.append("g")
.attr("id", "symbols-b");
symbolsb.selectAll("circle")
.data(games, function(d) {console.log(d.values) // games is my data object
return d.values})
.enter()
.append("circle")
.attr("class", "symbolsb")
.attr("cx", function(d,i) {console.log(d)
return x(d.values.date);})
.attr("cy", function(d,i) {return y_count(d.count);})
.attr("r", function(d,i) {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
console.log(parent)
if (i%3 === 1 && included_names.includes(datum[i].name)) {
return 8;}
else {return null;}})
.style("fill", function(d,i) {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
{return color(datum.name);}});
As I (incorrectly) understand the data() function, I thought that by returning d.values, the functions in cx, cy, and r would just see the array(s) that is inside d.values, but when log d to the console within the functions to define cx, cy, etc. I see again the full object games. Again, I though I should only get the values portion of the object.
I have been able to get a plot that looks like the result I want by loading the data and appending a g when defining symbolsb, but this creates a group for each set of circles.
I think the problem comes from my confusion of how nested objects are accessed by the data() function. So any help explaining that would be greatly appreciated.
It would be great if you could provide a live reproduction, for example in an Observable or VizHub notebook.
This line looks suspect
.data(games, function(d) {console.log(d.values) // games is my data object
return d.values})
The second argument to *selection*.data should be a 'key function', a function that returns a unique string identifier for each datum. Here you are giving an object (d.values) which will get stringified to [object Object] for each data point. This also explains why you're seeing the full games object when logging. I think it's safe here to just remove the second argument to .data():
.data(games)
This also doesn't look right
.attr("r", function(d,i) {
let parent = this.parentNode;
let datum = d3.select(parent).datum();
console.log(parent)
if (i%3 === 1 && included_names.includes(datum[i].name)) {
return 8;}
else {
return null;
*emphasized text*}})
I'm not entirely sure what you're trying to do here. If you're trying to access the name of the data point you can just access it on the data point itself using .attr("r", function(d,i) { if (included_names.includes(d.name)) { return 8 } else { return 0} )

getting and using data from CSV file d3 v5

I am trying to create a map visualization using d3.
I have gotten the map to work and am trying to add points on the map.
Depending on some other data in the CSV file (inspection results) I want certain points to have a different color.
I can add the points using this code, but I cannot get the colors to come out correctly (there should be red ones, but all of them are green). I think it is a problem with how I'm trying to get the data out, or just a problem with my JavaScript in general. Any help would be appreciated. Thank you!
function colorr(d) {
if (d == 0) {
return "red";
} else {
return "green";
}
}
var dataset = []
// load data about food
// https://stackoverflow.com/questions/47821332/plot-points-in-map-d3-javascript
// https://groups.google.com/forum/#!topic/d3-js/AVEa7nPCFAk
// https://stackoverflow.com/questions/10805184/show-data-on-mouseover-of-circle
d3.csv('data6.csv').then( function(data) {
// don't know if this actually does anything...
dataset=data.map(function(d) { return [+d["InspectionScore"],+d["Longitude"],+d["Latitude"]];});
g.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr("cx",function(d) { return projection([d.Longitude,d.Latitude])[0]; }).merge(g)
.attr("cy",function(d) { return projection([d.Longitude,d.Latitude])[1]; }).merge(g)
.attr("r", .4)
.attr("fill", d3.color(colorr( function(d) { return d.InspectionScore } ) ));
});
This can be resolved by changing the last line to:
.attr("fill", d => d3.color(colorr(d.InspectionScore))));
The reason this works is that d3's attr allows you to take the attribute value from a function. In this case you want to transform the data element to either red or blue. This is what the arrow function does in the above code. It is equivalent to :
.attr("fill", function(d) {
return d3.color(colorr(d.InspectionScore));
})
To get a deeper understanding of how D3 works, you can check the tutorials.

D3: How to conditionally bind SVG objects to data?

I have here an array of objects that I'm visualising using D3. I bind each object to a group element and append to that an SVG graphic that depends on some object property, roughly like this:
var iconGroups = zoomArea.selectAll("g.icons")
.data(resources)
.enter()
.append("g")
var icons = iconGroups.append(function(d){
if(d.type == "Apple"){
return appleIcon;
}else if(d.type == "Orange"){
return orangeIcon;
})
etc. Now I'd like to extend some of those icons with an additional line. I could add a line element for each data point and set them visible only where applicable, but since I want to add them only for say one out of a hundred data points, that seems inefficient. Is there a way to bind SVG lines to only those objects where d.type == "Apple"?
I would create separate selections for icons and lines, this way:
var iconGroups = zoomArea.selectAll('g.icons')
.data(resources);
iconGroups
.enter()
.append('g')
.classed('icons', true);
iconGroups.exit().remove();
var icons = iconGroups.selectAll('.icon').data(function(d) {return [d];});
icons
.enter()
.append(function(d) {
if(d.type === 'Apple'){
return appleIcon;
}else if(d.type === 'Orange'){
return orangeIcon;
}
}).classed('icon', true);
icons.exit().remove();
var lines = iconGroups.selectAll('.line').data(function(d) {
return d.type === 'Apple' ? [d] : [];
});
lines
.enter()
.append('line')
.classed('line', true);
lines.exit().remove();
.exit().remove() is added just because I add it always to be sure that updates work better. :)
Maybe the code is longer than .filter() but I use the following structure all the time and it's easier to scale it.
edit: apropos comment - If you need to pass indexes, you should pass them in binded data:
var iconGroups = zoomArea.selectAll('g.icons')
.data(resources.map(function(resource, index) {
return Object.create(resource, {index: index})
}));
(Object.create() was used just to not mutate the data, you can use _.clone, Object.assign() or just mutate it if it does not bother you)
then you can access it like:
lines.attr("x1", function(d){ console.log(d.index);})
You could add a class to the icons to be selected (e.g. appleIcon), and use that class in a selector to add the lines.
Use d3 filter.
selection.filter(selector)
Filters the selection, returning a new selection that contains only the elements for which the specified selector is true.
Reference: https://github.com/mbostock/d3/wiki/Selections#filter
Demo: http://bl.ocks.org/d3noob/8dc93bce7e7200ab487d

How to add links from CSV file to SVG elements generated with D3?

I am in a bit of a bind and need some with help with linking my svg elements with URL's contained in an CSV file. I have a symbols map with over 100 symbols. The symbols are based on coordinates pulled from longitude and latitude in a CSV file which also contains the links that I would like each unique symbol to link to. I know there is an easy way to do this, pretty sure I'm overlooking the solution.
My CSV file is as follows:
name,longitude,latitude,city,state,url
College of Charleston,803,342,Charleston,SC,http://sitename.com/colleges/college-of-charleston/
etc...
My symbols are generated using D3 and placed on top of my SVG map. I am also using D3 to wrap the symbols in anchor tags. I simply want these anchor tags to link to the appropriate url that correlates with the latitude and longitudes of that particular symbol.
/* Start SVG */
var width = 960,
height = 640.4,
positions = [],
centered;
var bodyNode = d3.select('#Map').node();
var list = $('.school-list').each(function(i){});
var svg = d3.select("#Map");
var contain = d3.select("#map-contain");
var circles = svg.append("svg:g")
.attr("id", "circles");
var g = d3.selectAll("g");
// var locationBySchools = {};
d3.csv("http://sitename.com/wp-content/themes/vibe/homepage/schools.csv",function(schools){
schools = schools.filter(function(schools){
var location = [+schools.longitude, +schools.latitude];
// locationBySchools[schools.loc] = location;
positions.push(location);
return true;
});
circles.selectAll("circles")
.data(schools)
.enter().append("svg:a")
.attr("xlink:href", function(d) { })
.append("svg:circle")
.attr("cx", function(d,i) {return positions[i][0]})
.attr("cy", function(d,i) {return positions[i][1]})
.attr("r", function(d,i) {return 6})
.attr("i", function(d,i) {return i})
.attr("class", "symbol")
Really stuck with this one...Any ideas?
The short answer is that you should simply return the url property from your data when you are assigning the xlink:href attribute:
.attr("xlink:href", function(d) { return d.url; })
However, there are a couple other issues with the code you posted.
Issue 1. circles.selectAll('circles')
This starts with a the selection of your g element, and within it, selects all elements with the tag-name circles. The problem is that circles is not a valid svg tag. This just creates an empty selection, which is okay in this case because the selection is only being used to create new elements. But, it's a bad habit to make dummy selections like this, and it can be confusing to others trying to understand your code. Instead, you should decide on a class name to give to each of the new link elements, and use that class name to make your selection. For example, if you decide to give them a class of link you would want to do the following:
First create a selection for all of the elements with class="link":
circles.selectAll('.link')
This selection will initially be empty, but when you use .data() to bind your data to it, it will be given an enter selection which you can use to create the new elements. Then you can add the class of link to the newly created elements:
.data(schools).enter().append('svg:a')
.attr('class', 'link')
Issue 2. .attr("i", function(d,i) {return i})
This one's pretty straightforward, there is no such attribute as i on svg elements. If you want to store arbitrary data on an element to be able to access it later, you can use a data attribute. In this case you might want to use something nice and semantic like data-index.
Issue 3. positions.push(location)
This is a big one. I would not recommend that you make a separate array to store the altered values from your dataset. You can use an accessor function in your d3.csv() function call, and clean up the incoming data that way. It will save you from having to maintain consistent data across two separate arrays. The accessor function will iterate over the dataset, taking as input the current datum, and should return an object representing the adjusted datum that will be used. This is a good spot to use your unary operator to coerce your latitude and longitude:
function accessor(d) {
return {
name: d.name,
longitude: +d.longitude,
latitude: +d.latitude,
city: d.city,
state: d.state,
url: d.url
};
}
There are two different ways to hook the accessor function into your d3.csv() call:
Method 1: Add a middle parameter to d3.csv() so that the parameters are (<url>, <accessor>, <callback>):
d3.csv('path/to/schools.csv', accessor, function(schools) {
// ...etc...
});
Method 2: Use the .row() method of d3.csv()
d3.csv('path/to/schools.csv')
.row(accessor)
.get(function(schools) {
// ...etc...
});
Now when you want to access the latitude and longitude in your preferred format, you can get them right from the bound data, instead of from an outside source. This keeps everything clean and consistent.
Putting all of this together, you get the following:
d3.csv('http://sitename.com/wp-content/themes/vibe/homepage/schools.csv')
// provide the accessor function
.row(function accessor(d) {
return {
name: d.name,
longitude: +d.longitude,
latitude: +d.latitude,
city: d.city,
state: d.state,
url: d.url
};
})
// provide a callback function
.get(function callback(schools) {
circles.selectAll('.link')
.data(schools)
.enter().append('svg:a')
.attr('class', 'link')
// point the link to the url from the data
.attr('xlink:href', function(d) { return d.url; })
.append('svg:circle')
.attr('class', 'symbol')
// now we can just use longitude and latitude
// since we cleaned them up in the accessor fn
.attr('cx', function(d) { return d.longitude; })
.attr('cy', function(d) { return d.latitude; })
// constants can be assigned directly
.attr('r', 6)
.attr('data-index', function(d,i) { return i; });
});

Is it possible to set custom attributes of SVG objects as number and not as string?

I am assigning artificial attributes to a SVG-G element (SVG group object). I move the group with its content with SVG transformations and I store the x/y coordinates of the group and its width/height in those attributes.
I am using the D3 Javascript library and the call :
embeddedElemContainer = nodeBoxContainer.append('svg:g')
.attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height)
results in following object :
<g transform="translate(13.585786437626904,31.585786437626904)" x="13.585786437626904" y="31.585786437626904" width="43.00000000000001" height="0"></g>
This is Ok, the only ting bothering me is the fact, that the attribute values are stored as string. If I want to use them for some computation, I am forced to cast.
parseInt(#embeddedElemContainer.attr('x'))
Is there a way to store those values directly as integer/double ?
The normal approach in D3 is to have lists of data that are bound to nodes. See the data portion of the Selection API. D3 puts this in the __data__ property of the DOM nodes that it creates/modifies. Internally D3 pulls out that property and passes it as a parameter to various functions, but you can certainly access it yourself directly.
It's also possible to associate an arbitrary data structure to a single node via the Datum method.
Without the rest of the context it's hard to say, but below is a modified version of what I think you are trying to do:
var vis = d3.select("body").append("svg").attr("width", 400).attr("height", 300);
var groupData = {x: 100, y:100, height: 50, width: 50, theanswer : 42, thecolor: "blue", somedouble: 45.1651654 };
var embeddedElemContainer = vis.append('svg:g')
.datum( groupData )
.attr( 'id', 'mygroup' )
.attr( 'x', function(d) { return d.x; } )
.attr( 'y', function(d) { return d.y; } )
.attr( 'height', function(d) { return d.height; } )
.attr( 'width', function(d) { return d.width; } )
// the regular DOM way:
console.log(document.getElementById('mygroup').__data__)
// the D3 way:
console.log( d3.select('#mygroup').datum() );
Both console.log statements output:
height: 50
somedouble: 45.1651654
theanswer: 42
thecolor: "blue"
width: 50
x: 100
y: 100
You could override d3's attr function to sniff out numbers and do the parseInt for you. That might present compatibility problems later, so maybe it would be better to create a new attrInt function, for example:
d3.selection.prototype.attrInt = function(name, value) {
if(arguments.length == 1) {
return parseInt(this.attr(name));
} else {
return this.attr(name, value);
}
};
Disclaimer: I don't have experience with d3 so I'm not sure if that is the correct prototype to attach to; I just picked that up from glancing at the source. :)

Categories