D3: Updating Fill Color of Inline SVGs - d3.js

I'm having some trouble updating (enter/update/exit) the fill color of several inline SVG Paths.
<svg>
<g id="section11">
<path id="section11p" d="m 431.78572,404.50506 0,36.875 20,-0.0893 22.14285,3.66072 8.92858,-38.48215 -7.5,-1.60714 z"/>
</g>
<g id="section10">
<path id="section10p" d="m 476.792,445.13425 8.83884,-38.38579 19.31917,6.43972 21.2132,11.23795 14.77348,9.21764 -21.97082,33.71384 -28.158,-16.66752 z"/>
</g>
</svg>
I used the code from this fiddle (http://jsfiddle.net/ybAj5/6/) to successfully (initially) color all of my inline SVG paths:
var colour = d3.scale.linear()
.domain([0, 20])
.range(["lightgreen", "darkgreen"]);
dataset1.forEach(function(d){ //d is of form [id,value]
d3.select("g#"+d[0]) //select the group matching the id
.datum(d) //attach this data for future reference
.selectAll("path, polygon")
.datum(d) //attach the data directly to *each* shape
.attr("fill", d?colour(d[1]):"lightgray");
});
And here is the dataset I've used:
var dataset1 = [["section10", 3],
["section11", 11],
["section13", 19]];
But I would like to incorporate these two datasets:
var dataset2 = [["section10", 0],
["section11", 11],
["section13", 15]];
var dataset3 = [["section10", 1],
["section11", 3],
["section13", 18]];
Using this dropdown menu:
<div class ="fixed"><select id = "opts">
<option value="dataset1" selected="selected">2012 Cohort</option>
<option value="dataset2">2013 Cohort</option>
<option value="dataset3">2014 Cohort</option>
</select></div>
I understand I need to encapsulate the process about within an enter/update/exit process with something like this:
function updateLegend(newData) {
// bind data
var appending = d3.selectAll('g')
.data(newData);
// add new elements
appending.enter().append('g');
// update existing elements
appending.transition()
.duration(0)
.style("fill", function(d,i){return colour(i);});
// remove old elements
appending.exit().remove();
}
// generate initial legend
updateLegend(ds2);
// handle on click event
d3.select('#opts')
.on('change', function() {
var newData = eval(d3.select(this).property('value'));
updateLegend(newData);
});
But its tough because I feel that all of the examples I've seen perform the update on SVGs which were created with d3, not custom SVGs created and pasted into the html page from Inkscape (an opensource SVG software).
I feel I'm very close to the solution and any help would be appreciated!

You don't need an "enter", "update" and "exit" selection for this. Just pass the value of dataset as an argument.
Also, you don't need to bind data. You just need this:
data.forEach(function(d){ //d is of form [id,value]
d3.select("#"+d[0]) //select the group matching the id
.attr("fill", colour(d[1]));
});
Check the demo:
var colour = d3.scale.linear()
.domain([0, 20])
.range(["lightgreen", "darkgreen"]);
var datasets = {dataset1: [["section10", 0],
["section11", 11]],
dataset2:[["section10", 1],
["section11", 2]],
dataset3:[["section10", 17],
["section11", 19]],
dataset4:[["section10", 2],
["section11", 19]]
};
d3.select('#opts').on('change', function() {
var value = d3.select(this).property('value');
var newData = datasets[value];
reColor(newData);
});
function reColor(data){
data.forEach(function(d){ //d is of form [id,value]
d3.select("#"+d[0]) //select the group matching the id
.attr("fill", colour(d[1]));
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div class ="fixed"><select id = "opts">
<option value="" selected="selected">Select</option>
<option value="dataset1">2013 Cohort</option>
<option value="dataset2">2014 Cohort</option>
<option value="dataset3">2015 Cohort</option>
<option value="dataset4">2016 Cohort</option>
</select></div>
<svg width="300" height="300">
<g id="section11">
<path id="section11p" d="m 31.78572,104.50506 0,36.875 20,-0.0893 22.14285,3.66072 8.92858,-38.48215 -7.5,-1.60714 z"/>
</g>
<g id="section10">
<path id="section10p" d="m 76.792,145.13425 8.83884,-38.38579 19.31917,6.43972 21.2132,11.23795 14.77348,9.21764 -21.97082,33.71384 -28.158,-16.66752 z"/>
</g>
</svg>

Related

Unable to supdate chart when switching dataset on button click

The first time I load data, the graph draws correctly, but when I load a different data set, the graph remains unchanged.
I switch between datasets using buttons. The first click always draws the graph correctly, no matter what button I click. But I can't update the graph after it is drawn by clicking on the other button. Any help is very much appreciated,thank you!
const dataA = [
{ population: 50, size: 100 },
{ population: 100, size: 100 },
];
const dataB = [
{ money: 4, currency: "usd" },
{ money: 10, currency: "eur" },
];
function drawChart(dataSet, prop) {
let width = 900;
let height = 200;
let x = d3.scale.ordinal().rangeRoundBands([0, width], 0.9);
let y = d3.scale
.linear()
.domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
.range([height, 0]);
let chart = d3.select("#chart").attr("width", width).attr("height", height);
let barWidth = width / dataSet.length;
let div = d3.select("body").append("div").attr("class", "tooltip");
let bar = chart
.selectAll("g")
.data(dataSet)
.enter()
.append("g")
.attr("transform", function (d, i) {
return "translate(" + i * barWidth + ",0)";
});
bar
.append("rect")
.attr("y", function (d) {
return y(d[prop]);
})
.attr("height", function (d) {
return height - y(d[prop]);
})
.attr("width", barWidth);
}
function drawDataA() {
drawChart(dataA, "population");
}
function drawDataB() {
drawChart(dataB, "money");
}
d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<head>
<script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<div id="app">
<svg class="chart" id="chart"></svg>
<button id="dataA">data1</button>
<button id="dataB">data2</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="./index.js"></script>
</body>
</html>
CodePen: https://codepen.io/rfripp2/pen/porpaLL
This is the expected behavior. Let's look at your code:
let bar = chart
.selectAll("g")
.data(dataSet)
.enter()
.append("g")
The select all statement selects all existing elements matching the selector that are children of elements in the selection chart.
The data method binds a new data array to this selection.
The enter method returns a new selection, containing a placeholder for every item in the data array which does not have a corresponding element in the selection.
The append method returns a newly appended child element for every element in the selection it is called on.
Running the code
The first time you call the draw function you have no g elements, so the selection is empty. You bind data to this empty selection. You then use the enter selection. Because there are two data items and no elements in the selection, enter contains two placeholders/elements. You then use append to add those elements.
The second time you call the draw function you have two g elements, so the selection has two elements in it. You bind data to this selection. You then use the enter selection. Because you already have two elements and you only have two data points, the enter selection is empty. As a consequence, append does not create any new elements.
You can see this by using selection.size():
const dataA = [
{ population: 50, size: 100 },
{ population: 100, size: 100 },
];
const dataB = [
{ money: 4, currency: "usd" },
{ money: 10, currency: "eur" },
];
function drawChart(dataSet, prop) {
let width = 900;
let height = 200;
let x = d3.scale.ordinal().rangeRoundBands([0, width], 0.9);
let y = d3.scale
.linear()
.domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
.range([height, 0]);
let chart = d3.select("#chart").attr("width", width).attr("height", height);
let barWidth = width / dataSet.length;
let div = d3.select("body").append("div").attr("class", "tooltip");
let bar = chart
.selectAll("g")
.data(dataSet)
.enter()
.append("g")
.attr("transform", function (d, i) {
return "translate(" + i * barWidth + ",0)";
});
console.log("The enter selection contains: " + bar.size() + "elements")
bar
.append("rect")
.attr("y", function (d) {
return y(d[prop]);
})
.attr("height", function (d) {
return height - y(d[prop]);
})
.attr("width", barWidth);
}
function drawDataA() {
drawChart(dataA, "population");
}
function drawDataB() {
drawChart(dataB, "money");
}
d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<head>
<script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<div id="app">
<svg class="chart" id="chart"></svg>
<button id="dataA">data1</button>
<button id="dataB">data2</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="./index.js"></script>
</body>
</html>
Solution
We want to use both the update and the enter selection (if the dataset ever changes in size, we'd likely want the exit selection too). We can use .join() to simplify this, join removes elements in the exit selection (surplus elements which don't have a corresponding data item), and returns the merged enter selection (new elements for surplus data items) and update selection (preexisting elements).
The nesting of your elements into a parent g and child rect is unnecessary here - and requires additional modifications. By positioning the bars directly we avoid the need for the parent g:
const dataA = [
{ population: 50, size: 100 },
{ population: 100, size: 100 },
];
const dataB = [
{ money: 4, currency: "usd" },
{ money: 10, currency: "eur" },
];
function drawChart(dataSet, prop) {
let width = 400;
let height = 200;
let x = d3.scaleBand().range([0, width], 0.9);
let y = d3.scaleLinear()
.domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
.range([height, 0]);
let chart = d3.select("#chart").attr("width", width).attr("height", height);
let barWidth = width / dataSet.length;
let div = d3.select("body").append("div").attr("class", "tooltip");
let bar = chart
.selectAll("rect")
.data(dataSet)
.join("rect")
.attr("transform", function (d, i) {
return "translate(" + i * barWidth + ",0)";
}).attr("y", function (d) {
return y(d[prop]);
})
.attr("height", function (d) {
return height - y(d[prop]);
})
.attr("width", barWidth);
}
function drawDataA() {
drawChart(dataA, "population");
}
function drawDataB() {
drawChart(dataB, "money");
}
d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<head>
</head>
<body>
<div id="app">
<svg class="chart" id="chart"></svg>
<br />
<button id="dataA">data1</button>
<button id="dataB">data2</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<script src="./index.js"></script>
</body>
</html>
This requires updating your version of D3 from v3 (you are actually using two versions of D3, v3 and v4, both fairly outdated, and both with different method names, actually with different ways of handling the enter selection).
If you wish to use d3v4, then the join method is not available, but we can merge enter and update:
When I say update selection, I'm refering to the initial selection:
// update selection:
let bar = chart
.selectAll("rect")
.data(dataSet);
// enter selection:
bar.enter().append("rect")
const dataA = [
{ population: 50, size: 100 },
{ population: 100, size: 100 },
];
const dataB = [
{ money: 4, currency: "usd" },
{ money: 10, currency: "eur" },
];
function drawChart(dataSet, prop) {
let width = 400;
let height = 200;
let x = d3.scaleBand().range([0, width], 0.9);
let y = d3.scaleLinear()
.domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
.range([height, 0]);
let chart = d3.select("#chart").attr("width", width).attr("height", height);
let barWidth = width / dataSet.length;
let div = d3.select("body").append("div").attr("class", "tooltip");
let bar = chart
.selectAll("rect")
.data(dataSet);
bar.enter().append("rect")
.merge(bar)
.attr("transform", function (d, i) {
return "translate(" + i * barWidth + ",0)";
}).attr("y", function (d) {
return y(d[prop]);
})
.attr("height", function (d) {
return height - y(d[prop]);
})
.attr("width", barWidth);
}
function drawDataA() {
drawChart(dataA, "population");
}
function drawDataB() {
drawChart(dataB, "money");
}
d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<head>
</head>
<body>
<div id="app">
<svg class="chart" id="chart"></svg>
<br />
<button id="dataA">data1</button>
<button id="dataB">data2</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.1.0/d3.min.js"></script>
<script src="./index.js"></script>
</body>
</html>
And lastly, if you wish to keep d3v3 (we are on v7 already), we can rely on an implicit merging of update and enter on enter (modifying the update selection). This "magic" was removed in v4, partly because it was not explicit. To do so we need to break your method chaining so that bar contains the
const dataA = [
{ population: 50, size: 100 },
{ population: 100, size: 100 },
];
const dataB = [
{ money: 4, currency: "usd" },
{ money: 10, currency: "eur" },
];
function drawChart(dataSet, prop) {
let width = 400;
let height = 200;
let x = d3.scale.ordinal().rangeRoundBands([0, width], 0.9);
let y = d3.scale.linear()
.domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
.range([height, 0]);
let chart = d3.select("#chart").attr("width", width).attr("height", height);
let barWidth = width / dataSet.length;
let div = d3.select("body").append("div").attr("class", "tooltip");
// update selection:
let bar = chart
.selectAll("rect")
.data(dataSet);
// enter selection:
bar.enter().append("rect")
bar.attr("transform", function (d, i) {
return "translate(" + i * barWidth + ",0)";
}).attr("y", function (d) {
return y(d[prop]);
})
.attr("height", function (d) {
return height - y(d[prop]);
})
.attr("width", barWidth);
}
function drawDataA() {
drawChart(dataA, "population");
}
function drawDataB() {
drawChart(dataB, "money");
}
d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<head>
</head>
<body>
<div id="app">
<svg class="chart" id="chart"></svg>
<br />
<button id="dataA">data1</button>
<button id="dataB">data2</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="./index.js"></script>
</body>
</html>
Note: d3v4 made changes to method names that break code from v3. This required changes to d3.scale.linear / d3.scale.ordinal in the snippets using v4 and 7 (using merge and join respectively).

Data visualization of different measures with different domains on a map

I have a map of Europe divided by countries and different measures to be represented using a choropletic map.
Based on the selected radio button, the map is colored according to the values ​​in the csv file.
Here is the code:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="//code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="./map.css" media="screen" />
</head>
<body>
<div id="map-container"></div>
<div id="radio-container">
<form id="radio-selector">
<input type="radio" name="radio-selector" id="rb1" value="m1" checked />
<label for="rb1">Measure 1</label>
<br>
<input type="radio" name="radio-selector" id="rb1" value="m2" />
<label for="rb2">Measure 2</label>
<br>
<input type="radio" name="radio-selector" id="rb1" value="m3" />
<label for="rb3">Measure 3</label>
</form>
</div>
<script src="./map.js"></script>
</body>
</html>
map.js
var csvValue = [];
var projection = d3.geoMercator()
.scale(500)
.translate([200, 700])
var path = d3.geoPath().projection(projection);
var width = 700;
var height = 400;
var svg = d3.select("#map-container").append("svg")
.attr("id", "container-map-svg")
.attr("width", width)
.attr("height", height);
// to color countries
var colors = d3.scaleLinear()
.domain([0, 100])
.range(["#131313", "#ba3c28"]);
var measSelected = document.querySelector('input[name=radio-selector]:checked').value;
var pathToNuts0 = 'https://gist.githubusercontent.com/rveciana/5919944/raw/2fef6be25d39ebeb3bead3933b2c9380497ddff4/nuts0.json';
d3.queue()
.defer(d3.json, pathToNuts0)
.defer(d3.csv, './data.csv')
.await(makeMap);
function makeMap(error, nuts0, data) {
if (error) {
console.log("*** ERROR LOADING FILES: " + error + " ***");
throw error;
}
csvValue = d3.nest()
.key(function(d) {
return d.MEASURE;
})
.object(data);
var country = topojson.feature(nuts0, nuts0.objects.nuts0);
// create map
svg.selectAll("path")
.data(country.features)
.enter().append("path")
.attr("class", "country")
.attr("id", function(d) {
return "country" + d.properties.nuts_id;
})
.attr("d", path);
colorCountries();
}
function colorCountries() {
svg.selectAll("path")
.attr("fill", function(d) {
var col = +getColor(d.properties.nuts_id);
return colors(col);
});
}
var getColor = function(nutsId) {
measSelected = document.querySelector('input[name=radio-selector]:checked').value;
var complete = csvValue[measSelected].slice();
var selectedValue = complete.find(function(tupla) {
return tupla.ID_NUT == nutsId;
});
if (selectedValue == null) {
return -1;
}
else {
var value = selectedValue.VALUE;
return value;
}
}
function measures() {
var measSelected = document.querySelector('input[name="radio-selector"]:checked').value;
}
var updateRadio = function() {
measSelected = $('input[name=radio-selector]:checked', '#desses').val();
colorCountries();
measures();
}
$("#radio-selector").on("change", updateRadio);
data.csv
ID_NUT,MEASURE,VALUE
AT,m1,97.1
AT,m2,74
AT,m3,28.53
BE,m1,98
BE,m2,97.1
BE,m3,8
BG,m1,94.5
BG,m2,56
BG,m3,38.42
CY,m1,99.32
CY,m2,91
CY,m3,23.42
CZ,m1,98.5
CZ,m2,4
CZ,m3,64.51
DE,m1,97
DE,m2,2
DE,m3,78.77
DK,m1,96.8
DK,m2,95
DK,m3,86.95
EE,m1,95.8
EE,m2,79
EE,m3,84.10
EL,m1,96.4
EL,m2,68
EL,m3,42.78
ES,m1,93.9
ES,m2,69
ES,m3,95.4
FI,m1,97.8
FI,m2,36
FI,m3,98.65
FR,m1,97.9
FR,m2,74
FR,m3,99.75
HR,m1,99.1
HR,m2,39
HR,m3,63.78
HU,m1,96.12
HU,m2,84
HU,m3,81
IE,m1,98.55
IE,m2,89
IE,m3,69.4
IT,m1,99.65
IT,m2,40
IT,m3,75.93
LT,m1,97.45
LT,m2,56
LT,m3,93.67
LU,m1,97.63
LU,m2,19
LU,m3,31.48
LV,m1,95.24
LV,m2,71
LV,m3,39
MT,m1,96.52
MT,m2,85
MT,m3,93
NL,m1,98
NL,m2,39
NL,m3,88.88
PL,m1,99.10
PL,m2,77
PL,m3,15
PT,m1,94.15
PT,m2,95
PT,m3,15
RO,m1,97
RO,m2,71
RO,m3,74
SE,m1,89.4
SE,m2,92
SE,m3,69.64
SI,m1,97.86
SI,m2,52
SI,m3,74.78
SK,m1,98
SK,m2,85
SK,m3,88
UK,m1,99.4
UK,m2,100
UK,m3,97
The code is correct, it works and doesn't generate errors.
The problem I would like to solve is the color question.
In csv file, all values ​​are in the range [0, 100] because they represent percentages.
As seen in the csv, the values ​​corresponding to m1 are very high values ​​(>=90) while those referring to m2 and m3 vary a lot.
If I use only one color scale (as written in the code) whose domain is [0, 100], the coropletic map ridden to m1 is not very significant.
How can I solve this problem?
What I would like to do is use a single color scale for all three measurements but make sure that the differences, even the smallest, between the values ​​are visible.
My question is more theory than practice. I don't care about the code, if there is better.
I would really like an idea-level solution, how can I deal with and solve this problem?
Thanks
Question off-topic: how do I use a snippet stack in this case? How can I add an external file (data.csv)?
----------
I modified my code like this:
var colors = d3.scaleQuantile()
.domain([0, 100])
.range(["#131313", "#241715", "#341b17", "#451f19", "#56231b", "#67281e", "#772c20", "#883022", "#993424", "#a93826", "#ba3c28"]);
Unfortunately I didn't get any visual result (or even errors).
With both scaleLinear() and scaleQuantile() the result is this:
Other question. Using this technique (which if I understand correctly fits according to the data domain) would I have different legends for each measure?
My data are in this form:
ID,COUNTRY,YEAR,A,B,C
AL,Albania,2000,98,50,10
AL,Albania,2001,41,2,14
AL,Albania,2002,75,51,10
DE,Germany,2000,74,21,25
DE,Germany,2001,46,2,48
DE,Germany,2002,74,81,90
...
So I don't have a simple array like var data = [..., ...].
There are several solutions for your problem. However, the "correct" solution in this case is using the adequate scale.
Before talking about that adequate scale, let's look at your problem closely and discuss some continuous scale approaches.
Your problem
As you explained in the question, the problem is that your data is heavily skewed towards one end of the domain.
To visualize it, I created this simple dataset...
var data = [2, 30, 60, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99];
... going from 0 to 100, but "skewed to the right".
Given your colours, this is the result of the use of a simple linear scale:
var data = [0, 30, 60, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 100];
var scale = d3.scaleLinear()
.domain([0, 100])
.range(["#131313", "#ba3c28"])
var div = d3.select("body").selectAll(null)
.data(data)
.enter()
.append("div")
.style("background-color", d => scale(d))
div {
width: 20px;
height: 20px;
display: inline-block;
margin: 4px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
As you know, we cannot easily see the differences for most of the values.
You could try to make your domain skewed as well. For instance, using a power scale with a high exponent:
var data = [0, 30, 60, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 100];
var scale = d3.scalePow()
.exponent(10)
.domain([0, 100])
.range(["#131313", "#ba3c28"])
var div = d3.select("body").selectAll(null)
.data(data)
.enter()
.append("div")
.style("background-color", d => scale(d))
div {
width: 20px;
height: 20px;
display: inline-block;
margin: 4px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
The problem of this approach is that it will be a game of trial and error, until you get the right exponent, which will be different for each data set...
So, let's drop the continuous scale and use the "correct" scale:
The quantile scale
Instead of a continuous scale, the best scale to see the differences in your population is a quantile scale. According to the API:
Quantile scales map a sampled input domain to a discrete range. The domain is considered continuous and thus the scale will accept any reasonable input value; however, the domain is specified as a discrete set of sample values. The number of values in (the cardinality of) the output range determines the number of quantiles that will be computed from the domain. To compute the quantiles, the domain is sorted, and treated as a population of discrete values. (emphasis mine)
So, the first step is creating the range array. Let's create an array of 10 colours. Based on your colours, it will be:
["#131313", "#241715", "#341b17", "#451f19", "#56231b", "#67281e", "#772c20", "#883022", "#993424", "#a93826", "#ba3c28"]
Then, using that range, we create our quantile scale:
var scale = d3.scaleQuantile()
.domain(data)
.range(["#131313", "#241715", "#341b17", "#451f19", "#56231b", "#67281e", "#772c20", "#883022", "#993424", "#a93826", "#ba3c28"])
This is the demo:
var data = [0, 30, 60, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 100];
var scale = d3.scaleQuantile()
.domain(data)
.range(["#131313", "#241715", "#341b17", "#451f19", "#56231b", "#67281e", "#772c20", "#883022", "#993424", "#a93826", "#ba3c28"])
var div = d3.select("body").selectAll(null)
.data(data)
.enter()
.append("div")
.style("background-color", d => scale(d));
div {
width: 20px;
height: 20px;
display: inline-block;
margin: 4px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>

svg manipulation app - record and play svg manipulations [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 6 years ago.
Improve this question
I'm looking to build a moble app where the user can take an SVG image, manipulate it under a "recording", and then send the recording to a friend for them to "replay".
I have some experience with D3.js and have also looked at Snap.svg library for SVG manipulation but I'm not fully wrapping my head around how to implement this.
In particular, what's a good way to be able to save the manipulations the user is making and then "replay" them? For example, I can use D3.js to manipulate the SVG, but since this is code-based, I can't exactly "serialize" the animation in order to send to someone else and for them to be able to "replay" it. I don't want to go down the route of code generation..
Any ideas how to do this?
I am not sure how complex your application targets are. However, it is easy to serialise updates in an SVG as JSON array and replay them using d3. Here is a small example for the same.
Steps:
Update SVG by using update buttons.
Revert the chart by clicking on clear button.
Now click play button to replay the last updates.
var defaults = [{
key: "r",
val: 15,
type: "attr"
}, {
key: "fill",
val: "blue",
type: "style"
}];
var updates = [];
var c20 = d3.scale.category20();
var circles = d3.selectAll("circle");
d3.select("#increase_rad")
.on("click", function() {
var val = parseInt(d3.select("circle").attr("r")) + 1;
circles.attr("r", val);
updates.push({
"key": "r",
"val": val,
"type": "attr"
});
enableButton();
});
d3.select("#decrease_rad")
.on("click", function() {
var val = parseInt(d3.select("circle").attr("r")) - 1;
circles.attr("r", val);
updates.push({
"key": "r",
"val": val,
"type": "attr"
});
enableButton();
});
d3.select("#change_color")
.on("click", function() {
var val = c20(Math.floor(Math.random() * (18) + 1));
circles.style("fill", val);
updates.push({
"key": "fill",
"val": val,
"type": "style"
});
enableButton();
});
d3.select("#clear")
.on("click", function() {
applyEffects(defaults);
});
d3.select("#play")
.on("click", function() {
applyEffects(updates);
});
function applyEffects(effects, delay) {
var trans = circles.transition()
effects.forEach(function(update) {
if (update.type == "attr") {
trans = trans.attr(update.key, update.val).transition();
} else {
trans = trans.style(update.key, update.val).transition();
}
});
}
function enableButton() {
d3.select("#clear").attr("disabled", null);
d3.select("#play").attr("disabled", null);
}
svg {
background: white;
}
.link {
stroke: black;
}
.node {
fill: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div>
<input type="button" value="Clear" id="clear" disabled />
<input type="button" value="Play" id="play" disabled />
<br/>
<br/>
<input type="button" value="Increase Radius" id="increase_rad" />
<input type="button" value="Decrease Radius" id="decrease_rad" />
<input type="button" value="Change Color" id="change_color" />
</div>
<svg width="250" height="250">
<g id="links">
<line class="link" x1="138.0538594815113" y1="55.927846328346924" x2="58.77306466322782" y2="110.43892621419347"></line>
<line class="link" x1="138.0538594815113" y1="55.927846328346924" x2="195.04044384802015" y2="133.44259356292176"></line>
</g>
<g id="nodes">
<g class="node" transform="translate(138.0538594815113,55.927846328346924)">
<circle r=15></circle>
</g>
<g class="node" transform="translate(58.77306466322782,110.43892621419347)">
<circle r=15></circle>
</g>
<g class="node" transform="translate(195.04044384802015,133.44259356292176)">
<circle r=15></circle>
</g>
</g>
</svg>

D3.js - Why are all my data being appended to the last appended group?

I'm pretty new to D3.js, and I've been able to create a simple animation that suits my needs from the Health and Wealth of Nations example. I had some trouble with adding labels to SVG circles, and when I investigated the source I found that while a group is being appended for each object in my dataset, all of the circles are being added to the last group only.
JSON data:
{"numSegments":4,
"movement":[
{"Id":"1",
"xValues":[[0,350],[1,400],[2,350],[3,300],[4,350]],
"yValues":[[0,690],[1,640],[2,590],[3,640],[4,690]]},
{"Id":"2",
"xValues":[[0,150],[1,150],[2,150],[3,150], [4, 150]],
"yValues":[[0,690],[1,490],[2,690],[3,490],[4, 690]]},
{"Id":"3",
"xValues":[[0,550],[1,650],[2,550],[3,650],[4,550]],
"yValues":[[0,690],[1,590],[2,490],[3,390],[4,490]]},
{"Id":"4",
"xValues":[[0,0],[1,200],[2,0],[3,200],[4,0]],
"yValues":[[0,0],[1,200],[2,0],[3,200],[4,0]]}
]
}
Relevant JavaScript:
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", height)
var g = chart.append("g")
.selectAll(".node")
.data(interpolateData(0));
var sections = g.enter().append("g")
.attr("class", "node");
console.log(sections.size()); // this returns "4" as expected
var dot = sections.append("circle")
.attr("class", "dot")
.attr("fill", function (d) { return colour(key(d)); })
.attr("r", "10")
.call(position)
.sort(order);
dot.append("title")
.text(function (d) { return key(d); });
var text = sections.append("text")
.text(function (d) { return key(d); })
.call(positionText)
.sort(order);
SVG output:
<svg class="chart" width="700" height="700">
<g>
<g class="node"></g>
<g class="node"></g>
<g class="node"></g>
<g class="node">
<circle class="dot" fill="#fff" r="10">
<title>1</title>
</circle>
<circle class="dot" fill="#fff" r="10">
<title>2</title>
</circle>
<circle class="dot" fill="#fff" r="10">
<title>3</title>
</circle>
<circle class="dot" fill="#fff" r="10">
<title>4</title>
</circle>
<text>1</text>
<text>2</text>
<text>3</text>
<text>4</text>
</g>
</g>
If the size of sections is 4 as the console reports, is it not an array of the four <g> elements that were just appended? And if so, why does sections.append() add four elements to the last group instead of adding one element to each of the four groups?

d3js : How to select nth element of a group?

I create a group with 9 elements (circles) such as:
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
//xml
<svg>
<g id="groupOfCircles">
<circle cx="20" cy="10" r="1" fill="green"></circle>
<circle cx="40" cy="20" r="2" fill="green"></circle>
<circle cx="60" cy="30" r="3" fill="green"></circle>
<circle cx="80" cy="40" r="4" fill="green"></circle>
<circle cx="100" cy="50" r="5" fill="green"></circle>
<circle cx="120" cy="60" r="6" fill="green"></circle>
<circle cx="140" cy="70" r="7" fill="green"></circle>
<circle cx="160" cy="80" r="8" fill="green"></circle>
<circle cx="180" cy="90" r="9" fill="green"></circle>
</g>
</svg>
But How to select the nth element (i.e : the 3rd circle) of the group groupOfCircles while not knowing the circles' id or attributes values ?
I will later on loop over all elements via a for loop, and color each for one second.
Note: I tried things such as :
circles[3].attr("fill","red") // not working
d3.select("#groupOfCircles:nth-child(3)").attr("fill","red") // not working
..
The selector needs to be circle:nth-child(3) -- the child means that the element is the nth child, not to select the nth child of the element (see here).
You could also use:
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
d3.select("circle:nth-child(3)").attr("fill","red"); // <== CSS selector (DOM)
d3.select(circles[0][4]).attr("fill","blue"); // <== D3 selector (node)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
d3 v4 now supports using selection.nodes() to return an array of all elements of this selection. Then, we can change the attributes of nth element by d3.select(selection.nodes()[n]).attr(something)
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
circleElements = circles.nodes(); // <== Get all circle elements
d3.select(circleElements[6]).attr("fill","red");
<script src="https://d3js.org/d3.v4.min.js"></script>
You can also use your circles array to set the element's attribute:
d3.select(circles[3]).attr("fill","red");
// JS
var data=[ 1,2,3,4,5,6,7,8,9 ];
var svg = d3.select("body").append("svg");
var circles = svg.append("g").attr("id", "groupOfCircles")
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d){ return d*20;})
.attr("cy", function(d){ return d*10;})
.attr("r" , function(d){ return d;})
.attr("fill","green");
var group = document.querySelector('#groupOfCircles');
var circleNodes = group.getElementsByTagName('circle');
d3.select(circleNodes[3]).attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
If you want to do it in d3 logic, the anonymous function always has an index parameter aside the data:
my_selection.attr("fill",function(d,i) {return i%3==0?"red":"green";});
http://jsfiddle.net/risto/os5fj9m6/
Here is another option by using a function as the selector.
circles.select(function (d,i) { return (i==3) ? this : null;})
.attr("fill", "red");
If the selector is a function it gets the datum (d) and the iterator (i) as parameter. It then returns either the object (this) if selected or null if not selected.

Resources