Using d3 selection.join to create a new line each time - d3.js

In the d3 examples of selection.join it seems there is an assumption that you already have a path to manipulate:
svg.selectAll("path")
.data([dataset])
.join(
enter => enter.append("path").attr("class", "line"),
update => update,
exit => exit.remove()
)
.attr("d", line);
However, I'd like to create a re-usable class component which creates a new line each time for that specific data. It should not interfere with other paths that may or may not be present in the entire graph.
const lineGenerator = d3
.line<DataPoint>()
.y((d) => yScale(d.value))
.x((d) => xScale(d.date));
mainSvg.selectAll('path')
.data([data])
.join(
enter =>
enter
.append('path')
.attr('class', 'line')
.attr('stroke', 'black'),
).attr('d', lineGenerator)
What I now see happening in a graph that already has a path somewhere is that that occurance is replaced with this new line. How would you typically solve this in d3?

Found a possible solution. Not sure if this is the way to go:
const lineGenerator = d3
.line<DataPoint>()
.y((d) => yScale(d.value))
.x((d) => xScale(d.date));
mainSvg.selectAll('#my-unique-id')
.data([data])
.join(
enter =>
enter
.append('path')
.attr('id', 'my-unique-id')
.attr('stroke', 'black'),
).attr('d', lineGenerator)
It looks like you can select something that is non-existent. If d3 can't find that occurrence it will create that instance.
Using a unique id for the new path you can make sure only the occurance is redrawn that you want to target.
I'm not sure if there are any caveats to this method, so any tips are appreciated.

Related

How to dynamically translate an SVG group in D3.js?

I'am working on a bubble-chart using d3. Now there should be an arrow as a graphical asset below the text elements. What I wan to achieve is a dynamic positioning of the arrow-group having a defined gap between itself and the last text:
I've already tried to position it with a percentage value:
arr.append("g").attr("class", "arrow").style('transform', 'translate(-1%, 5%)');
Which does not give the effect I want to.
I also tried to insert a dynamic value based on the radius of the circles which is simply not working and I don't know why:
arr.append("g")
.attr("class", "arrow")
.attr('transform', function(d, i) { return "translate(20," + d.r / 2 + ")");});
OR
arr.append("g")
.attr("class", "arrow")
.style('transform', (d, i) => 'translate(0, ${d.r/2})');
Please find my pen here.
Thank you very much in advance!
Ok.. solved it! For everyone who is interested or having the same trouble:
My last attempt was nearly correct but I was not able to transform via .style(...). I had to use .attr(...) like this:
arr.append("g")
.attr("class", "arrow")
.attr('transform', (d, i) => translate(0, ${d.r/2})');

Data join in a line chart

I'm very new to d3, so goal #1 was to show the chart. This works with the following code:
const line = d3.line()
.x(d => this.x(d.x))
.y(d => this.y(d.y));
console.log('data', JSON.stringify(data), 'color', color);
// data [{"x":"2017-07-01T04:00:00.000Z","y":81.2},{"x":"2017-08-01T04:00:00.000Z","y":79.6},{"x":"2017-09-01T04:00:00.000Z","y":79.4},{"x":"2017-10-01T04:00:00.000Z","y":80.6},{"x":"2017-11-01T04:00:00.000Z","y":80},{"x":"2017-12-01T05:00:00.000Z","y":76}] color blue
g.append('path')
.datum(data)
.attr('class', 'line')
.attr('stroke', color)
.attr('d', line);
With this code, anytime I run this method again, I get a new line, which is expected since I'm appending. I want to update only the stroke and d attributes when I have new data and/or color, I replace the code after the console.log with:
const lines = g.selectAll('.line').data(data);
lines.enter()
.append('path')
.attr('class', 'line');
lines
.attr('stroke', color)
.attr('d', line);
I don't see the line anymore, not at first, not after updates.
I'm including a codepen with the code in this question: https://codepen.io/anon/pen/BJaVNM?editors=0010
Thanks again for your help!
The proper data join would be:
// give data an array, each part of the array is an array representing a line (ie path)
let lines = g.selectAll('.line').data([data]); //<-- notice array
// you have lines entering, .merge this back to the update selection
lines = lines.enter()
.append('path')
.attr('class', 'line')
.merge(lines);
// lines variable is now enter + update
lines
.attr('stroke', color)
.attr('d', line);
Updated codepen

D3 map as black box

I've downloaded a shapefile of the Spanish provinces from the website of the Spanish Ministry of Agriculture:
http://servicios2.marm.es/sia/visualizacion/descargas/mapas.jsp
Then I've used mapshaper, with "repairing points" and exporting it as shapefile.
Next, I've put the .shp and the .shx files in a folder, with the original .dbf and I've converted it to json with ogr2ogr, exactly as Scott Murray explains in his book. http://chimera.labs.oreilly.com/books/1230000000345/ch12.html
The result is not a map, but a black box:
http://bl.ocks.org/murtra/raw/6150452a9ecca742e95c/
I can't understand why, because the GeoJSON has the same structure as other shapefiles I've used before, and if I inspect the svg, the paths are there. I've tried with another projections/scales, but nothing changes... any idea?
NOTE: changing the provincias.json by ESP_adm2.json (downloaded from http://www.gadm.org/country), the map works. All the files can be found here: https://gist.github.com/murtra/6150452a9ecca742e95c
Chances are you're seeing black boxes because of the specific way your shapes are encoded.
Try changing the styles of drawn paths for "fill" and "stroke" to none and black.
Hope this helps,
ioniATX
My issue was due to having an id attr mentioned instead of a class. Could be a similar issue:
async ngOnInit() {
this.topo = await this._geoService.getUSTopoJson();
const projection = d3.geoAlbersUsa().scale(1070).translate([ this._width / 2, this._height / 2 ]);
const path = d3.geoPath().projection(projection);
this._svg = d3.select('svg')
.attr('width', this._width)
.attr('height', this._height);
this._svg.append('rect')
.attr('class', 'background')
.attr('width', this._width)
.attr('height', this._height);
const g = this._svg.append('g');
g.append('g')
.attr('class', 'states') // <-- Needed to be 'class' instead of 'id'
.selectAll('path')
.data(topojson.feature(this.topo, this.topo.objects.states).features)
.enter().append('path')
.attr('d', path);
g.append('path')
.datum(topojson.mesh(this.topo, this.topo.objects.states, (a, b) => a !== b ))
.attr('id', 'state-borders')
.attr('d', path);
}

D3 trouble while appending text on mouseover

I would like to show a text on mouseover.
var circle = elemEnter.append("circle")
.attr("r", function(d){return d.r*2} )
.attr("dx", function(d){return d.x} )
.attr("stroke","gray")
.attr("fill", "#91c6ed")
.on("mouseover", function()
{d3.select(this).style("stroke", "#ff7f0e");
d3.select(this).style("stroke-width", "2px");
elemEnter.append("text")
.text(function(d){return d.name})})
.on("mouseout", function()
{d3.select(this).style("stroke", "gray");
d3.select(this).style("stroke-width", "1px");});
This piece of code works but show all the names on all the circles and when I try to replace
elemEnter.append("text").text(function(d){return d.name})
by
d3.select(this).append("text").text(d.name)
nothing happens.
I think it is possible to do it but I don't know what I'm doing wrong.
You can't append text to a circle. You need to start with a g and append the circle to the g and append the text to the g. Keep in mind that the g will not have cx/cy attributes and so you'll need to put that data into the following syntax:
.attr("transform", function (d) {return "translate("+d.X+","+d.Y")"})
If you're binding data, bind it to the g and then just naked append the circle and text to the g.

joining multiple parts of a g element to the same data in d3

I am trying to figure out how to transition paths and text bound to the same data within a g element.
This gist shows the behavior I want. When you click the change button the path and text positions transition smoothly. The problem is I accomplish this by separately joining the path and text elements to the data. I am not the only one using join this way, but I think it violates the goal of maintaining one to one mapping between elements and data with d3.
Is there a way to emulate the
var path = svg.selectAll("path")
.data(dataset);
path
.enter().append("path")
.attr("d", function(d) { return line(d.values) + "Z"; })
.style("fill", function(d, i) { return color(i); });
path
.transition().duration(500)
.attr("d", function(d) { return line(d.values) + "Z"; });
path
.exit().remove();
pattern for elements linked to the same data within a g element?
Here is one of my failed attempts. Rather than updating the paths and text, the change button causes new g elements to be added.
Added on edit: failed attempt now works after correcting the typo found by explunit
You have a typo in your class name on the join. This code:
var g = svg.selectAll(".shapes")
.data(dataset);
Should be this:
var g = svg.selectAll(".shape")
.data(dataset);
When I change it as above it works fine for me.

Resources