Update the line charts without deleting the svg
Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
3
down vote
favorite
I have created line charts in d3.js which show the trend of two measures. Each chart consists of two linecharts and the graph is repeated as many times as there are countries.
So my dataset is this:
var data = [type:'APPLE', country:'Germany', year:'2000', firstVal:'99.5', secondVal:'4',
type:'APPLE', country:'Germany', year:'2001', firstVal:'98', secondVal:'2',
type:'APPLE', country:'Germany', year:'2002', firstVal:'84', secondVal:'2.3',
type:'APPLE', country:'Germany', year:'2003', firstVal:'86', secondVal:'2',
type:'PEAR', country:'Germany', year:'2000', firstVal:'99', secondVal:'3',
type:'PEAR', country:'Germany', year:'2001', firstVal:'98', secondVal:'10',
type:'PEAR', country:'Germany', year:'2002', firstVal:'97', secondVal:'8',
type:'PEAR', country:'Germany', year:'2003', firstVal:'98', secondVal:'10.9',
type:'APPLE', country:'France', year:'2000', firstVal:'NaN', secondVal:'4',
type:'APPLE', country:'France', year:'2001', firstVal:'85', secondVal:'50',
type:'APPLE', country:'France', year:'2002', firstVal:'86', secondVal:'27',
type:'APPLE', country:'France', year:'2003', firstVal:'86', secondVal:'5',
type:'PEAR', country:'France', year:'2000', firstVal:'87', secondVal:'NaN',
type:'PEAR', country:'France', year:'2001', firstVal:'99', secondVal:'NaN',
type:'PEAR', country:'France', year:'2002', firstVal:'70', secondVal:'5',
type:'PEAR', country:'France', year:'2003', firstVal:'90', secondVal:'9',
type:'APPLE', country:'Spain', year:'2000', firstVal:'88', secondVal:'14',
type:'APPLE', country:'Spain', year:'2001', firstVal:'85', secondVal:'2',
type:'APPLE', country:'Spain', year:'2002', firstVal:'86', secondVal:'1',
type:'APPLE', country:'Spain', year:'2003', firstVal:'84', secondVal:'5',
type:'PEAR', country:'Spain', year:'2000', firstVal:'65', secondVal:'3',
type:'PEAR', country:'Spain', year:'2001', firstVal:'NaN', secondVal:'4',
type:'PEAR', country:'Spain', year:'2002', firstVal:'95', secondVal:'2',
type:'PEAR', country:'Spain', year:'2003', firstVal:'78', secondVal:'1'
];
The result is:
The first column corresponds to Germany, the second to France and the third to Spain. The trend of the firstVal
data is represented by a blue line, the trend of the secondVal
data is represented by a tomato line.
At the top there are two radio buttons: when the user changes the selection, the graphs are updated. In reality they are eliminated and then recreated and this is certainly not a good solution.
I wanted to ask you how I could do to avoid having to delete the entire SVG
but only modify the element d
.
Here is the complete code.
index.html:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<link rel="stylesheet" href="style.css">
<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<script src='https://d3js.org/d3.v5.min.js'></script>
</head>
<body>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<div id='doublelineDiv'></div>
<script src="script.js"></script>
</body>
</html>
script.js:
var data = [type:'APPLE', country:'Germany', year:'2000', firstVal:'99.5', secondVal:'4',
type:'APPLE', country:'Germany', year:'2001', firstVal:'98', secondVal:'2',
type:'APPLE', country:'Germany', year:'2002', firstVal:'84', secondVal:'2.3',
type:'APPLE', country:'Germany', year:'2003', firstVal:'86', secondVal:'2',
type:'PEAR', country:'Germany', year:'2000', firstVal:'99', secondVal:'3',
type:'PEAR', country:'Germany', year:'2001', firstVal:'98', secondVal:'10',
type:'PEAR', country:'Germany', year:'2002', firstVal:'97', secondVal:'8',
type:'PEAR', country:'Germany', year:'2003', firstVal:'98', secondVal:'10.9',
type:'APPLE', country:'France', year:'2000', firstVal:'NaN', secondVal:'4',
type:'APPLE', country:'France', year:'2001', firstVal:'85', secondVal:'50',
type:'APPLE', country:'France', year:'2002', firstVal:'86', secondVal:'27',
type:'APPLE', country:'France', year:'2003', firstVal:'86', secondVal:'5',
type:'PEAR', country:'France', year:'2000', firstVal:'87', secondVal:'NaN',
type:'PEAR', country:'France', year:'2001', firstVal:'99', secondVal:'NaN',
type:'PEAR', country:'France', year:'2002', firstVal:'70', secondVal:'5',
type:'PEAR', country:'France', year:'2003', firstVal:'90', secondVal:'9',
type:'APPLE', country:'Spain', year:'2000', firstVal:'88', secondVal:'14',
type:'APPLE', country:'Spain', year:'2001', firstVal:'85', secondVal:'2',
type:'APPLE', country:'Spain', year:'2002', firstVal:'86', secondVal:'1',
type:'APPLE', country:'Spain', year:'2003', firstVal:'84', secondVal:'5',
type:'PEAR', country:'Spain', year:'2000', firstVal:'65', secondVal:'3',
type:'PEAR', country:'Spain', year:'2001', firstVal:'NaN', secondVal:'4',
type:'PEAR', country:'Spain', year:'2002', firstVal:'95', secondVal:'2',
type:'PEAR', country:'Spain', year:'2003', firstVal:'78', secondVal:'1'
];
console.log('data', data);
var countries = ['Germany', 'France', 'Spain'];
var numCountries = countries.length;
var dataSmall;
var minFirst;
var maxFirst;
var minSecond;
var maxSecond;
var years;
var typeSelected = 'APPLE';
var widthSingleChart = 130;
var heightSingleChart = 80;
createSmallMultiples();
function createSmallMultiples()
dataSmall = data.filter(function(d)
return d.type == typeSelected;
);
console.log('dataSmall', dataSmall);
var firsts = ;
var seconds = ;
dataSmall.filter(function(d)
if(!isNaN(d.firstVal))
firsts.push(Number(d.firstVal));
if(!isNaN(d.secondVal))
seconds.push(Number(d.secondVal));
);
minFirst = Math.min(...firsts);
maxFirst = Math.max(...firsts);
console.log('firsts [' + minFirst + ', ' + maxFirst + ']');
if(seconds.length > 1)
minSecond = Math.min(...seconds);
maxSecond = Math.max(...seconds);
else
minSecond = 0;
maxSecond = 0;
minSecond = Math.min(...seconds);
maxSecond = Math.max(...seconds);
console.log('seconds [' + minSecond + ', ' + maxSecond + ']');
years = [...new Set(dataSmall.map(function(d)
return +d.year;
))].sort();
console.log('countries', countries);
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
var updateRadio = function()
typeSelected = this.value;
d3.select('#doublelineDiv').html('');
createSmallMultiples();
d3.selectAll('input[name=type-selector]').on('change', updateRadio);
// Single line chart
function singleLineChart(thisCountry)
console.log('singleLineChart - ', thisCountry);
// filter data by country
var dataSmallFilter = dataSmall.filter(function(d)
return d.country == thisCountry;
);
console.log('dataSmallFilter', dataSmallFilter);
var margin = top: 0, right: 0, bottom: 3, left: 10;
// svg for both line chart (coverage and cases)
var doublelineDiv = d3.select('#doublelineDiv');
var doublelineSvg = doublelineDiv.append('svg')
.attr('class', thisCountry)
.attr('width', widthSingleChart)
.attr('height', heightSingleChart);
// FIRST LINE
var doublelineSvgFirst = doublelineSvg
.append('svg')
.attr('class', 'firstLine')
.attr('width', widthSingleChart)
.attr('height', heightSingleChart/2);
var width = doublelineSvgFirst.attr('width') - margin.left - margin.right;
var height = doublelineSvgFirst.attr('height') - margin.top - margin.bottom;
var doublelineSvgFirstG = doublelineSvgFirst.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var x = d3.scaleTime()
.rangeRound([0, width]);
var yFirst = d3.scaleLinear()
.rangeRound([height, 0]);
x.domain(d3.extent(dataSmallFilter, function(d)
return d.year;
));
yFirst.domain([minFirst, 100]);
var xAxis = d3.axisBottom(x).tickSize(0, 0);
var yAxisFirst = d3.axisLeft(yFirst);
doublelineSvgFirstG.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
doublelineSvgFirstG.append('g')
.attr('class', 'y axis')
.call(yAxisFirst)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
var lineFirst = d3.line()
.x(function(d)
return x(d.year);
)
.y(function(d)
return yFirst(d.firstVal);
)
.defined(function(d)
if(isNaN(d.firstVal))
d.firstVal = 0;
return d.firstVal !== 0;
);
var linesFirst = doublelineSvgFirstG.attr('transform', function(d)
return 'translate(' + margin.left + ', ' + margin.top + ')';
);
console.log('lineFirst', lineFirst);
linesFirst.append('path')
.data([dataSmallFilter])
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1)
.attr('d', lineFirst);
// SECOND LINE
var doublelineSvgSecond = doublelineSvg
.append('svg')
.attr('class', 'secondLine')
.attr('transform', 'translate(0, ' + heightSingleChart/2 + ')')
.attr('width', widthSingleChart)
.attr('height', heightSingleChart/2);
var width = doublelineSvgSecond.attr('width') - margin.left - margin.right;
var height = doublelineSvgSecond.attr('height') - margin.top - margin.bottom;
var doublelineSvgSecondG = doublelineSvgSecond.append('g');//.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var ySecond = d3.scaleSqrt()
.rangeRound([height, 0]);
x.domain(d3.extent(dataSmallFilter, function(d)
return d.year;
));
ySecond.domain([0, maxSecond]);
var xAxis = d3.axisBottom(x).tickSize(0, 0);
var yAxisSecond = d3.axisLeft(ySecond);
doublelineSvgSecondG.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
doublelineSvgSecondG.append('g')
.attr('class', 'y axis')
.call(yAxisSecond)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
var lineSecond = d3.line()
.x(function(d)
return x(d.year);
)
.y(function(d)
return ySecond(d.secondVal);
)
.defined(function(d)
if(isNaN(d.secondVal))
d.secondVal = 0;
return d.secondVal !== 0;
);
var linesSecond = doublelineSvgSecondG.attr('transform', function(d)
return 'translate(' + margin.left + ', ' + margin.top + ')';
);
console.log('lineSecond', lineSecond);
linesSecond.append('path')
.data([dataSmallFilter])
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', 'tomato')
.attr('stroke-width', 1)
.attr('d', lineSecond);
javascript performance d3.js
add a comment |Â
up vote
3
down vote
favorite
I have created line charts in d3.js which show the trend of two measures. Each chart consists of two linecharts and the graph is repeated as many times as there are countries.
So my dataset is this:
var data = [type:'APPLE', country:'Germany', year:'2000', firstVal:'99.5', secondVal:'4',
type:'APPLE', country:'Germany', year:'2001', firstVal:'98', secondVal:'2',
type:'APPLE', country:'Germany', year:'2002', firstVal:'84', secondVal:'2.3',
type:'APPLE', country:'Germany', year:'2003', firstVal:'86', secondVal:'2',
type:'PEAR', country:'Germany', year:'2000', firstVal:'99', secondVal:'3',
type:'PEAR', country:'Germany', year:'2001', firstVal:'98', secondVal:'10',
type:'PEAR', country:'Germany', year:'2002', firstVal:'97', secondVal:'8',
type:'PEAR', country:'Germany', year:'2003', firstVal:'98', secondVal:'10.9',
type:'APPLE', country:'France', year:'2000', firstVal:'NaN', secondVal:'4',
type:'APPLE', country:'France', year:'2001', firstVal:'85', secondVal:'50',
type:'APPLE', country:'France', year:'2002', firstVal:'86', secondVal:'27',
type:'APPLE', country:'France', year:'2003', firstVal:'86', secondVal:'5',
type:'PEAR', country:'France', year:'2000', firstVal:'87', secondVal:'NaN',
type:'PEAR', country:'France', year:'2001', firstVal:'99', secondVal:'NaN',
type:'PEAR', country:'France', year:'2002', firstVal:'70', secondVal:'5',
type:'PEAR', country:'France', year:'2003', firstVal:'90', secondVal:'9',
type:'APPLE', country:'Spain', year:'2000', firstVal:'88', secondVal:'14',
type:'APPLE', country:'Spain', year:'2001', firstVal:'85', secondVal:'2',
type:'APPLE', country:'Spain', year:'2002', firstVal:'86', secondVal:'1',
type:'APPLE', country:'Spain', year:'2003', firstVal:'84', secondVal:'5',
type:'PEAR', country:'Spain', year:'2000', firstVal:'65', secondVal:'3',
type:'PEAR', country:'Spain', year:'2001', firstVal:'NaN', secondVal:'4',
type:'PEAR', country:'Spain', year:'2002', firstVal:'95', secondVal:'2',
type:'PEAR', country:'Spain', year:'2003', firstVal:'78', secondVal:'1'
];
The result is:
The first column corresponds to Germany, the second to France and the third to Spain. The trend of the firstVal
data is represented by a blue line, the trend of the secondVal
data is represented by a tomato line.
At the top there are two radio buttons: when the user changes the selection, the graphs are updated. In reality they are eliminated and then recreated and this is certainly not a good solution.
I wanted to ask you how I could do to avoid having to delete the entire SVG
but only modify the element d
.
Here is the complete code.
index.html:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<link rel="stylesheet" href="style.css">
<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<script src='https://d3js.org/d3.v5.min.js'></script>
</head>
<body>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<div id='doublelineDiv'></div>
<script src="script.js"></script>
</body>
</html>
script.js:
var data = [type:'APPLE', country:'Germany', year:'2000', firstVal:'99.5', secondVal:'4',
type:'APPLE', country:'Germany', year:'2001', firstVal:'98', secondVal:'2',
type:'APPLE', country:'Germany', year:'2002', firstVal:'84', secondVal:'2.3',
type:'APPLE', country:'Germany', year:'2003', firstVal:'86', secondVal:'2',
type:'PEAR', country:'Germany', year:'2000', firstVal:'99', secondVal:'3',
type:'PEAR', country:'Germany', year:'2001', firstVal:'98', secondVal:'10',
type:'PEAR', country:'Germany', year:'2002', firstVal:'97', secondVal:'8',
type:'PEAR', country:'Germany', year:'2003', firstVal:'98', secondVal:'10.9',
type:'APPLE', country:'France', year:'2000', firstVal:'NaN', secondVal:'4',
type:'APPLE', country:'France', year:'2001', firstVal:'85', secondVal:'50',
type:'APPLE', country:'France', year:'2002', firstVal:'86', secondVal:'27',
type:'APPLE', country:'France', year:'2003', firstVal:'86', secondVal:'5',
type:'PEAR', country:'France', year:'2000', firstVal:'87', secondVal:'NaN',
type:'PEAR', country:'France', year:'2001', firstVal:'99', secondVal:'NaN',
type:'PEAR', country:'France', year:'2002', firstVal:'70', secondVal:'5',
type:'PEAR', country:'France', year:'2003', firstVal:'90', secondVal:'9',
type:'APPLE', country:'Spain', year:'2000', firstVal:'88', secondVal:'14',
type:'APPLE', country:'Spain', year:'2001', firstVal:'85', secondVal:'2',
type:'APPLE', country:'Spain', year:'2002', firstVal:'86', secondVal:'1',
type:'APPLE', country:'Spain', year:'2003', firstVal:'84', secondVal:'5',
type:'PEAR', country:'Spain', year:'2000', firstVal:'65', secondVal:'3',
type:'PEAR', country:'Spain', year:'2001', firstVal:'NaN', secondVal:'4',
type:'PEAR', country:'Spain', year:'2002', firstVal:'95', secondVal:'2',
type:'PEAR', country:'Spain', year:'2003', firstVal:'78', secondVal:'1'
];
console.log('data', data);
var countries = ['Germany', 'France', 'Spain'];
var numCountries = countries.length;
var dataSmall;
var minFirst;
var maxFirst;
var minSecond;
var maxSecond;
var years;
var typeSelected = 'APPLE';
var widthSingleChart = 130;
var heightSingleChart = 80;
createSmallMultiples();
function createSmallMultiples()
dataSmall = data.filter(function(d)
return d.type == typeSelected;
);
console.log('dataSmall', dataSmall);
var firsts = ;
var seconds = ;
dataSmall.filter(function(d)
if(!isNaN(d.firstVal))
firsts.push(Number(d.firstVal));
if(!isNaN(d.secondVal))
seconds.push(Number(d.secondVal));
);
minFirst = Math.min(...firsts);
maxFirst = Math.max(...firsts);
console.log('firsts [' + minFirst + ', ' + maxFirst + ']');
if(seconds.length > 1)
minSecond = Math.min(...seconds);
maxSecond = Math.max(...seconds);
else
minSecond = 0;
maxSecond = 0;
minSecond = Math.min(...seconds);
maxSecond = Math.max(...seconds);
console.log('seconds [' + minSecond + ', ' + maxSecond + ']');
years = [...new Set(dataSmall.map(function(d)
return +d.year;
))].sort();
console.log('countries', countries);
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
var updateRadio = function()
typeSelected = this.value;
d3.select('#doublelineDiv').html('');
createSmallMultiples();
d3.selectAll('input[name=type-selector]').on('change', updateRadio);
// Single line chart
function singleLineChart(thisCountry)
console.log('singleLineChart - ', thisCountry);
// filter data by country
var dataSmallFilter = dataSmall.filter(function(d)
return d.country == thisCountry;
);
console.log('dataSmallFilter', dataSmallFilter);
var margin = top: 0, right: 0, bottom: 3, left: 10;
// svg for both line chart (coverage and cases)
var doublelineDiv = d3.select('#doublelineDiv');
var doublelineSvg = doublelineDiv.append('svg')
.attr('class', thisCountry)
.attr('width', widthSingleChart)
.attr('height', heightSingleChart);
// FIRST LINE
var doublelineSvgFirst = doublelineSvg
.append('svg')
.attr('class', 'firstLine')
.attr('width', widthSingleChart)
.attr('height', heightSingleChart/2);
var width = doublelineSvgFirst.attr('width') - margin.left - margin.right;
var height = doublelineSvgFirst.attr('height') - margin.top - margin.bottom;
var doublelineSvgFirstG = doublelineSvgFirst.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var x = d3.scaleTime()
.rangeRound([0, width]);
var yFirst = d3.scaleLinear()
.rangeRound([height, 0]);
x.domain(d3.extent(dataSmallFilter, function(d)
return d.year;
));
yFirst.domain([minFirst, 100]);
var xAxis = d3.axisBottom(x).tickSize(0, 0);
var yAxisFirst = d3.axisLeft(yFirst);
doublelineSvgFirstG.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
doublelineSvgFirstG.append('g')
.attr('class', 'y axis')
.call(yAxisFirst)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
var lineFirst = d3.line()
.x(function(d)
return x(d.year);
)
.y(function(d)
return yFirst(d.firstVal);
)
.defined(function(d)
if(isNaN(d.firstVal))
d.firstVal = 0;
return d.firstVal !== 0;
);
var linesFirst = doublelineSvgFirstG.attr('transform', function(d)
return 'translate(' + margin.left + ', ' + margin.top + ')';
);
console.log('lineFirst', lineFirst);
linesFirst.append('path')
.data([dataSmallFilter])
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1)
.attr('d', lineFirst);
// SECOND LINE
var doublelineSvgSecond = doublelineSvg
.append('svg')
.attr('class', 'secondLine')
.attr('transform', 'translate(0, ' + heightSingleChart/2 + ')')
.attr('width', widthSingleChart)
.attr('height', heightSingleChart/2);
var width = doublelineSvgSecond.attr('width') - margin.left - margin.right;
var height = doublelineSvgSecond.attr('height') - margin.top - margin.bottom;
var doublelineSvgSecondG = doublelineSvgSecond.append('g');//.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var ySecond = d3.scaleSqrt()
.rangeRound([height, 0]);
x.domain(d3.extent(dataSmallFilter, function(d)
return d.year;
));
ySecond.domain([0, maxSecond]);
var xAxis = d3.axisBottom(x).tickSize(0, 0);
var yAxisSecond = d3.axisLeft(ySecond);
doublelineSvgSecondG.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
doublelineSvgSecondG.append('g')
.attr('class', 'y axis')
.call(yAxisSecond)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
var lineSecond = d3.line()
.x(function(d)
return x(d.year);
)
.y(function(d)
return ySecond(d.secondVal);
)
.defined(function(d)
if(isNaN(d.secondVal))
d.secondVal = 0;
return d.secondVal !== 0;
);
var linesSecond = doublelineSvgSecondG.attr('transform', function(d)
return 'translate(' + margin.left + ', ' + margin.top + ')';
);
console.log('lineSecond', lineSecond);
linesSecond.append('path')
.data([dataSmallFilter])
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', 'tomato')
.attr('stroke-width', 1)
.attr('d', lineSecond);
javascript performance d3.js
add a comment |Â
up vote
3
down vote
favorite
up vote
3
down vote
favorite
I have created line charts in d3.js which show the trend of two measures. Each chart consists of two linecharts and the graph is repeated as many times as there are countries.
So my dataset is this:
var data = [type:'APPLE', country:'Germany', year:'2000', firstVal:'99.5', secondVal:'4',
type:'APPLE', country:'Germany', year:'2001', firstVal:'98', secondVal:'2',
type:'APPLE', country:'Germany', year:'2002', firstVal:'84', secondVal:'2.3',
type:'APPLE', country:'Germany', year:'2003', firstVal:'86', secondVal:'2',
type:'PEAR', country:'Germany', year:'2000', firstVal:'99', secondVal:'3',
type:'PEAR', country:'Germany', year:'2001', firstVal:'98', secondVal:'10',
type:'PEAR', country:'Germany', year:'2002', firstVal:'97', secondVal:'8',
type:'PEAR', country:'Germany', year:'2003', firstVal:'98', secondVal:'10.9',
type:'APPLE', country:'France', year:'2000', firstVal:'NaN', secondVal:'4',
type:'APPLE', country:'France', year:'2001', firstVal:'85', secondVal:'50',
type:'APPLE', country:'France', year:'2002', firstVal:'86', secondVal:'27',
type:'APPLE', country:'France', year:'2003', firstVal:'86', secondVal:'5',
type:'PEAR', country:'France', year:'2000', firstVal:'87', secondVal:'NaN',
type:'PEAR', country:'France', year:'2001', firstVal:'99', secondVal:'NaN',
type:'PEAR', country:'France', year:'2002', firstVal:'70', secondVal:'5',
type:'PEAR', country:'France', year:'2003', firstVal:'90', secondVal:'9',
type:'APPLE', country:'Spain', year:'2000', firstVal:'88', secondVal:'14',
type:'APPLE', country:'Spain', year:'2001', firstVal:'85', secondVal:'2',
type:'APPLE', country:'Spain', year:'2002', firstVal:'86', secondVal:'1',
type:'APPLE', country:'Spain', year:'2003', firstVal:'84', secondVal:'5',
type:'PEAR', country:'Spain', year:'2000', firstVal:'65', secondVal:'3',
type:'PEAR', country:'Spain', year:'2001', firstVal:'NaN', secondVal:'4',
type:'PEAR', country:'Spain', year:'2002', firstVal:'95', secondVal:'2',
type:'PEAR', country:'Spain', year:'2003', firstVal:'78', secondVal:'1'
];
The result is:
The first column corresponds to Germany, the second to France and the third to Spain. The trend of the firstVal
data is represented by a blue line, the trend of the secondVal
data is represented by a tomato line.
At the top there are two radio buttons: when the user changes the selection, the graphs are updated. In reality they are eliminated and then recreated and this is certainly not a good solution.
I wanted to ask you how I could do to avoid having to delete the entire SVG
but only modify the element d
.
Here is the complete code.
index.html:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<link rel="stylesheet" href="style.css">
<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<script src='https://d3js.org/d3.v5.min.js'></script>
</head>
<body>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<div id='doublelineDiv'></div>
<script src="script.js"></script>
</body>
</html>
script.js:
var data = [type:'APPLE', country:'Germany', year:'2000', firstVal:'99.5', secondVal:'4',
type:'APPLE', country:'Germany', year:'2001', firstVal:'98', secondVal:'2',
type:'APPLE', country:'Germany', year:'2002', firstVal:'84', secondVal:'2.3',
type:'APPLE', country:'Germany', year:'2003', firstVal:'86', secondVal:'2',
type:'PEAR', country:'Germany', year:'2000', firstVal:'99', secondVal:'3',
type:'PEAR', country:'Germany', year:'2001', firstVal:'98', secondVal:'10',
type:'PEAR', country:'Germany', year:'2002', firstVal:'97', secondVal:'8',
type:'PEAR', country:'Germany', year:'2003', firstVal:'98', secondVal:'10.9',
type:'APPLE', country:'France', year:'2000', firstVal:'NaN', secondVal:'4',
type:'APPLE', country:'France', year:'2001', firstVal:'85', secondVal:'50',
type:'APPLE', country:'France', year:'2002', firstVal:'86', secondVal:'27',
type:'APPLE', country:'France', year:'2003', firstVal:'86', secondVal:'5',
type:'PEAR', country:'France', year:'2000', firstVal:'87', secondVal:'NaN',
type:'PEAR', country:'France', year:'2001', firstVal:'99', secondVal:'NaN',
type:'PEAR', country:'France', year:'2002', firstVal:'70', secondVal:'5',
type:'PEAR', country:'France', year:'2003', firstVal:'90', secondVal:'9',
type:'APPLE', country:'Spain', year:'2000', firstVal:'88', secondVal:'14',
type:'APPLE', country:'Spain', year:'2001', firstVal:'85', secondVal:'2',
type:'APPLE', country:'Spain', year:'2002', firstVal:'86', secondVal:'1',
type:'APPLE', country:'Spain', year:'2003', firstVal:'84', secondVal:'5',
type:'PEAR', country:'Spain', year:'2000', firstVal:'65', secondVal:'3',
type:'PEAR', country:'Spain', year:'2001', firstVal:'NaN', secondVal:'4',
type:'PEAR', country:'Spain', year:'2002', firstVal:'95', secondVal:'2',
type:'PEAR', country:'Spain', year:'2003', firstVal:'78', secondVal:'1'
];
console.log('data', data);
var countries = ['Germany', 'France', 'Spain'];
var numCountries = countries.length;
var dataSmall;
var minFirst;
var maxFirst;
var minSecond;
var maxSecond;
var years;
var typeSelected = 'APPLE';
var widthSingleChart = 130;
var heightSingleChart = 80;
createSmallMultiples();
function createSmallMultiples()
dataSmall = data.filter(function(d)
return d.type == typeSelected;
);
console.log('dataSmall', dataSmall);
var firsts = ;
var seconds = ;
dataSmall.filter(function(d)
if(!isNaN(d.firstVal))
firsts.push(Number(d.firstVal));
if(!isNaN(d.secondVal))
seconds.push(Number(d.secondVal));
);
minFirst = Math.min(...firsts);
maxFirst = Math.max(...firsts);
console.log('firsts [' + minFirst + ', ' + maxFirst + ']');
if(seconds.length > 1)
minSecond = Math.min(...seconds);
maxSecond = Math.max(...seconds);
else
minSecond = 0;
maxSecond = 0;
minSecond = Math.min(...seconds);
maxSecond = Math.max(...seconds);
console.log('seconds [' + minSecond + ', ' + maxSecond + ']');
years = [...new Set(dataSmall.map(function(d)
return +d.year;
))].sort();
console.log('countries', countries);
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
var updateRadio = function()
typeSelected = this.value;
d3.select('#doublelineDiv').html('');
createSmallMultiples();
d3.selectAll('input[name=type-selector]').on('change', updateRadio);
// Single line chart
function singleLineChart(thisCountry)
console.log('singleLineChart - ', thisCountry);
// filter data by country
var dataSmallFilter = dataSmall.filter(function(d)
return d.country == thisCountry;
);
console.log('dataSmallFilter', dataSmallFilter);
var margin = top: 0, right: 0, bottom: 3, left: 10;
// svg for both line chart (coverage and cases)
var doublelineDiv = d3.select('#doublelineDiv');
var doublelineSvg = doublelineDiv.append('svg')
.attr('class', thisCountry)
.attr('width', widthSingleChart)
.attr('height', heightSingleChart);
// FIRST LINE
var doublelineSvgFirst = doublelineSvg
.append('svg')
.attr('class', 'firstLine')
.attr('width', widthSingleChart)
.attr('height', heightSingleChart/2);
var width = doublelineSvgFirst.attr('width') - margin.left - margin.right;
var height = doublelineSvgFirst.attr('height') - margin.top - margin.bottom;
var doublelineSvgFirstG = doublelineSvgFirst.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var x = d3.scaleTime()
.rangeRound([0, width]);
var yFirst = d3.scaleLinear()
.rangeRound([height, 0]);
x.domain(d3.extent(dataSmallFilter, function(d)
return d.year;
));
yFirst.domain([minFirst, 100]);
var xAxis = d3.axisBottom(x).tickSize(0, 0);
var yAxisFirst = d3.axisLeft(yFirst);
doublelineSvgFirstG.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
doublelineSvgFirstG.append('g')
.attr('class', 'y axis')
.call(yAxisFirst)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
var lineFirst = d3.line()
.x(function(d)
return x(d.year);
)
.y(function(d)
return yFirst(d.firstVal);
)
.defined(function(d)
if(isNaN(d.firstVal))
d.firstVal = 0;
return d.firstVal !== 0;
);
var linesFirst = doublelineSvgFirstG.attr('transform', function(d)
return 'translate(' + margin.left + ', ' + margin.top + ')';
);
console.log('lineFirst', lineFirst);
linesFirst.append('path')
.data([dataSmallFilter])
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1)
.attr('d', lineFirst);
// SECOND LINE
var doublelineSvgSecond = doublelineSvg
.append('svg')
.attr('class', 'secondLine')
.attr('transform', 'translate(0, ' + heightSingleChart/2 + ')')
.attr('width', widthSingleChart)
.attr('height', heightSingleChart/2);
var width = doublelineSvgSecond.attr('width') - margin.left - margin.right;
var height = doublelineSvgSecond.attr('height') - margin.top - margin.bottom;
var doublelineSvgSecondG = doublelineSvgSecond.append('g');//.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var ySecond = d3.scaleSqrt()
.rangeRound([height, 0]);
x.domain(d3.extent(dataSmallFilter, function(d)
return d.year;
));
ySecond.domain([0, maxSecond]);
var xAxis = d3.axisBottom(x).tickSize(0, 0);
var yAxisSecond = d3.axisLeft(ySecond);
doublelineSvgSecondG.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
doublelineSvgSecondG.append('g')
.attr('class', 'y axis')
.call(yAxisSecond)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
var lineSecond = d3.line()
.x(function(d)
return x(d.year);
)
.y(function(d)
return ySecond(d.secondVal);
)
.defined(function(d)
if(isNaN(d.secondVal))
d.secondVal = 0;
return d.secondVal !== 0;
);
var linesSecond = doublelineSvgSecondG.attr('transform', function(d)
return 'translate(' + margin.left + ', ' + margin.top + ')';
);
console.log('lineSecond', lineSecond);
linesSecond.append('path')
.data([dataSmallFilter])
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', 'tomato')
.attr('stroke-width', 1)
.attr('d', lineSecond);
javascript performance d3.js
I have created line charts in d3.js which show the trend of two measures. Each chart consists of two linecharts and the graph is repeated as many times as there are countries.
So my dataset is this:
var data = [type:'APPLE', country:'Germany', year:'2000', firstVal:'99.5', secondVal:'4',
type:'APPLE', country:'Germany', year:'2001', firstVal:'98', secondVal:'2',
type:'APPLE', country:'Germany', year:'2002', firstVal:'84', secondVal:'2.3',
type:'APPLE', country:'Germany', year:'2003', firstVal:'86', secondVal:'2',
type:'PEAR', country:'Germany', year:'2000', firstVal:'99', secondVal:'3',
type:'PEAR', country:'Germany', year:'2001', firstVal:'98', secondVal:'10',
type:'PEAR', country:'Germany', year:'2002', firstVal:'97', secondVal:'8',
type:'PEAR', country:'Germany', year:'2003', firstVal:'98', secondVal:'10.9',
type:'APPLE', country:'France', year:'2000', firstVal:'NaN', secondVal:'4',
type:'APPLE', country:'France', year:'2001', firstVal:'85', secondVal:'50',
type:'APPLE', country:'France', year:'2002', firstVal:'86', secondVal:'27',
type:'APPLE', country:'France', year:'2003', firstVal:'86', secondVal:'5',
type:'PEAR', country:'France', year:'2000', firstVal:'87', secondVal:'NaN',
type:'PEAR', country:'France', year:'2001', firstVal:'99', secondVal:'NaN',
type:'PEAR', country:'France', year:'2002', firstVal:'70', secondVal:'5',
type:'PEAR', country:'France', year:'2003', firstVal:'90', secondVal:'9',
type:'APPLE', country:'Spain', year:'2000', firstVal:'88', secondVal:'14',
type:'APPLE', country:'Spain', year:'2001', firstVal:'85', secondVal:'2',
type:'APPLE', country:'Spain', year:'2002', firstVal:'86', secondVal:'1',
type:'APPLE', country:'Spain', year:'2003', firstVal:'84', secondVal:'5',
type:'PEAR', country:'Spain', year:'2000', firstVal:'65', secondVal:'3',
type:'PEAR', country:'Spain', year:'2001', firstVal:'NaN', secondVal:'4',
type:'PEAR', country:'Spain', year:'2002', firstVal:'95', secondVal:'2',
type:'PEAR', country:'Spain', year:'2003', firstVal:'78', secondVal:'1'
];
The result is:
The first column corresponds to Germany, the second to France and the third to Spain. The trend of the firstVal
data is represented by a blue line, the trend of the secondVal
data is represented by a tomato line.
At the top there are two radio buttons: when the user changes the selection, the graphs are updated. In reality they are eliminated and then recreated and this is certainly not a good solution.
I wanted to ask you how I could do to avoid having to delete the entire SVG
but only modify the element d
.
Here is the complete code.
index.html:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<link rel="stylesheet" href="style.css">
<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<script src='https://d3js.org/d3.v5.min.js'></script>
</head>
<body>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<div id='doublelineDiv'></div>
<script src="script.js"></script>
</body>
</html>
script.js:
var data = [type:'APPLE', country:'Germany', year:'2000', firstVal:'99.5', secondVal:'4',
type:'APPLE', country:'Germany', year:'2001', firstVal:'98', secondVal:'2',
type:'APPLE', country:'Germany', year:'2002', firstVal:'84', secondVal:'2.3',
type:'APPLE', country:'Germany', year:'2003', firstVal:'86', secondVal:'2',
type:'PEAR', country:'Germany', year:'2000', firstVal:'99', secondVal:'3',
type:'PEAR', country:'Germany', year:'2001', firstVal:'98', secondVal:'10',
type:'PEAR', country:'Germany', year:'2002', firstVal:'97', secondVal:'8',
type:'PEAR', country:'Germany', year:'2003', firstVal:'98', secondVal:'10.9',
type:'APPLE', country:'France', year:'2000', firstVal:'NaN', secondVal:'4',
type:'APPLE', country:'France', year:'2001', firstVal:'85', secondVal:'50',
type:'APPLE', country:'France', year:'2002', firstVal:'86', secondVal:'27',
type:'APPLE', country:'France', year:'2003', firstVal:'86', secondVal:'5',
type:'PEAR', country:'France', year:'2000', firstVal:'87', secondVal:'NaN',
type:'PEAR', country:'France', year:'2001', firstVal:'99', secondVal:'NaN',
type:'PEAR', country:'France', year:'2002', firstVal:'70', secondVal:'5',
type:'PEAR', country:'France', year:'2003', firstVal:'90', secondVal:'9',
type:'APPLE', country:'Spain', year:'2000', firstVal:'88', secondVal:'14',
type:'APPLE', country:'Spain', year:'2001', firstVal:'85', secondVal:'2',
type:'APPLE', country:'Spain', year:'2002', firstVal:'86', secondVal:'1',
type:'APPLE', country:'Spain', year:'2003', firstVal:'84', secondVal:'5',
type:'PEAR', country:'Spain', year:'2000', firstVal:'65', secondVal:'3',
type:'PEAR', country:'Spain', year:'2001', firstVal:'NaN', secondVal:'4',
type:'PEAR', country:'Spain', year:'2002', firstVal:'95', secondVal:'2',
type:'PEAR', country:'Spain', year:'2003', firstVal:'78', secondVal:'1'
];
console.log('data', data);
var countries = ['Germany', 'France', 'Spain'];
var numCountries = countries.length;
var dataSmall;
var minFirst;
var maxFirst;
var minSecond;
var maxSecond;
var years;
var typeSelected = 'APPLE';
var widthSingleChart = 130;
var heightSingleChart = 80;
createSmallMultiples();
function createSmallMultiples()
dataSmall = data.filter(function(d)
return d.type == typeSelected;
);
console.log('dataSmall', dataSmall);
var firsts = ;
var seconds = ;
dataSmall.filter(function(d)
if(!isNaN(d.firstVal))
firsts.push(Number(d.firstVal));
if(!isNaN(d.secondVal))
seconds.push(Number(d.secondVal));
);
minFirst = Math.min(...firsts);
maxFirst = Math.max(...firsts);
console.log('firsts [' + minFirst + ', ' + maxFirst + ']');
if(seconds.length > 1)
minSecond = Math.min(...seconds);
maxSecond = Math.max(...seconds);
else
minSecond = 0;
maxSecond = 0;
minSecond = Math.min(...seconds);
maxSecond = Math.max(...seconds);
console.log('seconds [' + minSecond + ', ' + maxSecond + ']');
years = [...new Set(dataSmall.map(function(d)
return +d.year;
))].sort();
console.log('countries', countries);
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
var updateRadio = function()
typeSelected = this.value;
d3.select('#doublelineDiv').html('');
createSmallMultiples();
d3.selectAll('input[name=type-selector]').on('change', updateRadio);
// Single line chart
function singleLineChart(thisCountry)
console.log('singleLineChart - ', thisCountry);
// filter data by country
var dataSmallFilter = dataSmall.filter(function(d)
return d.country == thisCountry;
);
console.log('dataSmallFilter', dataSmallFilter);
var margin = top: 0, right: 0, bottom: 3, left: 10;
// svg for both line chart (coverage and cases)
var doublelineDiv = d3.select('#doublelineDiv');
var doublelineSvg = doublelineDiv.append('svg')
.attr('class', thisCountry)
.attr('width', widthSingleChart)
.attr('height', heightSingleChart);
// FIRST LINE
var doublelineSvgFirst = doublelineSvg
.append('svg')
.attr('class', 'firstLine')
.attr('width', widthSingleChart)
.attr('height', heightSingleChart/2);
var width = doublelineSvgFirst.attr('width') - margin.left - margin.right;
var height = doublelineSvgFirst.attr('height') - margin.top - margin.bottom;
var doublelineSvgFirstG = doublelineSvgFirst.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var x = d3.scaleTime()
.rangeRound([0, width]);
var yFirst = d3.scaleLinear()
.rangeRound([height, 0]);
x.domain(d3.extent(dataSmallFilter, function(d)
return d.year;
));
yFirst.domain([minFirst, 100]);
var xAxis = d3.axisBottom(x).tickSize(0, 0);
var yAxisFirst = d3.axisLeft(yFirst);
doublelineSvgFirstG.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
doublelineSvgFirstG.append('g')
.attr('class', 'y axis')
.call(yAxisFirst)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
var lineFirst = d3.line()
.x(function(d)
return x(d.year);
)
.y(function(d)
return yFirst(d.firstVal);
)
.defined(function(d)
if(isNaN(d.firstVal))
d.firstVal = 0;
return d.firstVal !== 0;
);
var linesFirst = doublelineSvgFirstG.attr('transform', function(d)
return 'translate(' + margin.left + ', ' + margin.top + ')';
);
console.log('lineFirst', lineFirst);
linesFirst.append('path')
.data([dataSmallFilter])
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', 'steelblue')
.attr('stroke-width', 1)
.attr('d', lineFirst);
// SECOND LINE
var doublelineSvgSecond = doublelineSvg
.append('svg')
.attr('class', 'secondLine')
.attr('transform', 'translate(0, ' + heightSingleChart/2 + ')')
.attr('width', widthSingleChart)
.attr('height', heightSingleChart/2);
var width = doublelineSvgSecond.attr('width') - margin.left - margin.right;
var height = doublelineSvgSecond.attr('height') - margin.top - margin.bottom;
var doublelineSvgSecondG = doublelineSvgSecond.append('g');//.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var ySecond = d3.scaleSqrt()
.rangeRound([height, 0]);
x.domain(d3.extent(dataSmallFilter, function(d)
return d.year;
));
ySecond.domain([0, maxSecond]);
var xAxis = d3.axisBottom(x).tickSize(0, 0);
var yAxisSecond = d3.axisLeft(ySecond);
doublelineSvgSecondG.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
doublelineSvgSecondG.append('g')
.attr('class', 'y axis')
.call(yAxisSecond)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
var lineSecond = d3.line()
.x(function(d)
return x(d.year);
)
.y(function(d)
return ySecond(d.secondVal);
)
.defined(function(d)
if(isNaN(d.secondVal))
d.secondVal = 0;
return d.secondVal !== 0;
);
var linesSecond = doublelineSvgSecondG.attr('transform', function(d)
return 'translate(' + margin.left + ', ' + margin.top + ')';
);
console.log('lineSecond', lineSecond);
linesSecond.append('path')
.data([dataSmallFilter])
.attr('class', 'mylines')
.attr('fill', 'none')
.attr('stroke', 'tomato')
.attr('stroke-width', 1)
.attr('d', lineSecond);
javascript performance d3.js
edited Jun 6 at 13:35
Gerardo Furtado
1,1342420
1,1342420
asked Jun 6 at 7:13
Buster
434
434
add a comment |Â
add a comment |Â
1 Answer
1
active
oldest
votes
up vote
3
down vote
accepted
You are very right: deleting the elements and repainting the SVG again is not the correct way to update a D3 chart when the values of a given dataset change. You should frown upon (almost) any D3 code that does this.
The problem is not only that such approach (which I call a "lazy update") is not idiomatic, the problem is that it doesn't allow the use of the very famous enter-update-exit pattern, and most importantly, it doesn't allow the transition of the elements, which is very important for the user to follow the changes.
Don't get me wrong, sometimes we do that (hence the "almost" in the first paragraph), but in very specific situations, for very specific reasons.
Back to your code:
Unfortunately, the fact that you're deleting the SVGs to repaint them again...
d3.select('#doublelineDiv').html('');
... is not the only big problem here: another major problem is the use of a for
loop to append the elements:
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
As a rule of thumb, do not use loops (for
, while
, forEach
etc) to append elements in a D3 code. Use proper selections â which, by the way, will fix our first problem.
Those two advices combined make the required refactor so big that I decided to write an entire new code based on your outcome. This is necessary because refactoring your code will require so many changes that the answer would be a complete mess. However, I'll guide you through the new code step by step.
First of all, you don't need all that manipulation with the data. You can simply nest it:
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
That way we'll have an array of three objects, one for each small SVG. Check it:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
console.log(nestedData)
<script src='https://d3js.org/d3.v5.min.js'></script>
Then we create the scales. Since those years are just categorical variables treat them as such, using a point scale. Also, use the same scale for both lines, otherwise you'll be lying to the user:
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
As you can see, there are some magic numbers here, in the final code you should get rid of them.
Also, we can use the same line generator for both lines (more on that below):
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
And finally we come to the important part, the selections.
Everything that actually paints the SVGs is inside a function named draw
:
draw("APPLE");
function draw(fruit)
//code here
Inside draw, we'll have all our selections. First, the SVG selection:
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
As you can see, we bind the data to the SVGs themselves, using the countries as a key in the key function.
Then, we have the paths selection:
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
That data function is complicated, I know. It basically creates two arrays of data for each small SVG, one with firstVal
and another one with secondVal
. As those arrays have the same properties, we can use a single line generator.
The nice thing about it is that, now, we can update and transition the paths:
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
Here is the answer to your question: this line...
.attr("d", function(d) return lineGenerator(d); );
... is what makes possible updating the d
attribute of the paths without having to delete/repaint all the SVGs again.
And here is the complete demo:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
var widthSingleChart = 150;
var heightSingleChart = 80;
var vals = ["firstVal", "secondVal"];
var colors = ["steelblue", "tomato"];
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale)
.ticks(4)
var div = d3.select("#doublelineDiv");
draw("APPLE");
d3.selectAll('input[name=type-selector]').on('change', function()
draw(this.value)
)
function draw(fruit)
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
var gX = svgUpdate.selectAll(".xaxis")
.data([1]);
gX.enter()
.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + (heightSingleChart - 20) + ")")
.call(xAxis);
gX.call(xAxis);
var gY = svgUpdate.selectAll(".yaxis")
.data([1]);
gY.enter()
.append("g")
.attr("class", "yaxis")
.attr("transform", "translate(20,0)")
.call(yAxis);
gY.call(yAxis);
<script src='https://d3js.org/d3.v5.min.js'></script>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<br>
<div id='doublelineDiv'></div>
You may say that the transition is not the most beautiful one, and that's because the interpolator is interpolating strings. You can improve that, for instance, see what Mike Bostock (D3 creator) did here: https://bl.ocks.org/mbostock/3916621. I'll leave that for you, because it's way over the scope of this answer.
At the end, the code came from 254 lines to 129 lines. But that's not what's important here: what's important here is that now the code is a proper, idiomatic D3 code.
Edit: According to your comment, you need to use different scales.
There are several possible solutions here, the easiest one being using the second argument to get the index of the path:
.attr("d", function(d,i)
return i ? lineGenerator2(d) : lineGenerator1(d);
);
In the above snippet, lineGenerator1
will be called for the fist path, while lineGenerator2
will be called for the second one. That way, you can set different scales for each line generator.
However, here is an advice: make that very clear for the end user. Since that the chart has just one y axis (I'm referring to your Plunker, not to your image), almost all users will assume (correctly) that the lines use the same scale.
Thanks Gerardo, you are very kind to explain each step. I'm learning a lot. But I need to use two different scales (no, I'm not lying to users).
â Buster
Jun 6 at 14:22
If you need to use two different scales you can change the data function for the paths or simply using the second argument for choosing the scale. I said "lying" because, in a chart like this, where you have just one y axis, you have to explain clearly to the user that the scales are different, otherwise everybody will assume they are the same. Please, check the edit at the end of the answer.
â Gerardo Furtado
Jun 7 at 3:07
add a comment |Â
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
3
down vote
accepted
You are very right: deleting the elements and repainting the SVG again is not the correct way to update a D3 chart when the values of a given dataset change. You should frown upon (almost) any D3 code that does this.
The problem is not only that such approach (which I call a "lazy update") is not idiomatic, the problem is that it doesn't allow the use of the very famous enter-update-exit pattern, and most importantly, it doesn't allow the transition of the elements, which is very important for the user to follow the changes.
Don't get me wrong, sometimes we do that (hence the "almost" in the first paragraph), but in very specific situations, for very specific reasons.
Back to your code:
Unfortunately, the fact that you're deleting the SVGs to repaint them again...
d3.select('#doublelineDiv').html('');
... is not the only big problem here: another major problem is the use of a for
loop to append the elements:
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
As a rule of thumb, do not use loops (for
, while
, forEach
etc) to append elements in a D3 code. Use proper selections â which, by the way, will fix our first problem.
Those two advices combined make the required refactor so big that I decided to write an entire new code based on your outcome. This is necessary because refactoring your code will require so many changes that the answer would be a complete mess. However, I'll guide you through the new code step by step.
First of all, you don't need all that manipulation with the data. You can simply nest it:
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
That way we'll have an array of three objects, one for each small SVG. Check it:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
console.log(nestedData)
<script src='https://d3js.org/d3.v5.min.js'></script>
Then we create the scales. Since those years are just categorical variables treat them as such, using a point scale. Also, use the same scale for both lines, otherwise you'll be lying to the user:
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
As you can see, there are some magic numbers here, in the final code you should get rid of them.
Also, we can use the same line generator for both lines (more on that below):
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
And finally we come to the important part, the selections.
Everything that actually paints the SVGs is inside a function named draw
:
draw("APPLE");
function draw(fruit)
//code here
Inside draw, we'll have all our selections. First, the SVG selection:
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
As you can see, we bind the data to the SVGs themselves, using the countries as a key in the key function.
Then, we have the paths selection:
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
That data function is complicated, I know. It basically creates two arrays of data for each small SVG, one with firstVal
and another one with secondVal
. As those arrays have the same properties, we can use a single line generator.
The nice thing about it is that, now, we can update and transition the paths:
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
Here is the answer to your question: this line...
.attr("d", function(d) return lineGenerator(d); );
... is what makes possible updating the d
attribute of the paths without having to delete/repaint all the SVGs again.
And here is the complete demo:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
var widthSingleChart = 150;
var heightSingleChart = 80;
var vals = ["firstVal", "secondVal"];
var colors = ["steelblue", "tomato"];
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale)
.ticks(4)
var div = d3.select("#doublelineDiv");
draw("APPLE");
d3.selectAll('input[name=type-selector]').on('change', function()
draw(this.value)
)
function draw(fruit)
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
var gX = svgUpdate.selectAll(".xaxis")
.data([1]);
gX.enter()
.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + (heightSingleChart - 20) + ")")
.call(xAxis);
gX.call(xAxis);
var gY = svgUpdate.selectAll(".yaxis")
.data([1]);
gY.enter()
.append("g")
.attr("class", "yaxis")
.attr("transform", "translate(20,0)")
.call(yAxis);
gY.call(yAxis);
<script src='https://d3js.org/d3.v5.min.js'></script>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<br>
<div id='doublelineDiv'></div>
You may say that the transition is not the most beautiful one, and that's because the interpolator is interpolating strings. You can improve that, for instance, see what Mike Bostock (D3 creator) did here: https://bl.ocks.org/mbostock/3916621. I'll leave that for you, because it's way over the scope of this answer.
At the end, the code came from 254 lines to 129 lines. But that's not what's important here: what's important here is that now the code is a proper, idiomatic D3 code.
Edit: According to your comment, you need to use different scales.
There are several possible solutions here, the easiest one being using the second argument to get the index of the path:
.attr("d", function(d,i)
return i ? lineGenerator2(d) : lineGenerator1(d);
);
In the above snippet, lineGenerator1
will be called for the fist path, while lineGenerator2
will be called for the second one. That way, you can set different scales for each line generator.
However, here is an advice: make that very clear for the end user. Since that the chart has just one y axis (I'm referring to your Plunker, not to your image), almost all users will assume (correctly) that the lines use the same scale.
Thanks Gerardo, you are very kind to explain each step. I'm learning a lot. But I need to use two different scales (no, I'm not lying to users).
â Buster
Jun 6 at 14:22
If you need to use two different scales you can change the data function for the paths or simply using the second argument for choosing the scale. I said "lying" because, in a chart like this, where you have just one y axis, you have to explain clearly to the user that the scales are different, otherwise everybody will assume they are the same. Please, check the edit at the end of the answer.
â Gerardo Furtado
Jun 7 at 3:07
add a comment |Â
up vote
3
down vote
accepted
You are very right: deleting the elements and repainting the SVG again is not the correct way to update a D3 chart when the values of a given dataset change. You should frown upon (almost) any D3 code that does this.
The problem is not only that such approach (which I call a "lazy update") is not idiomatic, the problem is that it doesn't allow the use of the very famous enter-update-exit pattern, and most importantly, it doesn't allow the transition of the elements, which is very important for the user to follow the changes.
Don't get me wrong, sometimes we do that (hence the "almost" in the first paragraph), but in very specific situations, for very specific reasons.
Back to your code:
Unfortunately, the fact that you're deleting the SVGs to repaint them again...
d3.select('#doublelineDiv').html('');
... is not the only big problem here: another major problem is the use of a for
loop to append the elements:
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
As a rule of thumb, do not use loops (for
, while
, forEach
etc) to append elements in a D3 code. Use proper selections â which, by the way, will fix our first problem.
Those two advices combined make the required refactor so big that I decided to write an entire new code based on your outcome. This is necessary because refactoring your code will require so many changes that the answer would be a complete mess. However, I'll guide you through the new code step by step.
First of all, you don't need all that manipulation with the data. You can simply nest it:
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
That way we'll have an array of three objects, one for each small SVG. Check it:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
console.log(nestedData)
<script src='https://d3js.org/d3.v5.min.js'></script>
Then we create the scales. Since those years are just categorical variables treat them as such, using a point scale. Also, use the same scale for both lines, otherwise you'll be lying to the user:
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
As you can see, there are some magic numbers here, in the final code you should get rid of them.
Also, we can use the same line generator for both lines (more on that below):
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
And finally we come to the important part, the selections.
Everything that actually paints the SVGs is inside a function named draw
:
draw("APPLE");
function draw(fruit)
//code here
Inside draw, we'll have all our selections. First, the SVG selection:
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
As you can see, we bind the data to the SVGs themselves, using the countries as a key in the key function.
Then, we have the paths selection:
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
That data function is complicated, I know. It basically creates two arrays of data for each small SVG, one with firstVal
and another one with secondVal
. As those arrays have the same properties, we can use a single line generator.
The nice thing about it is that, now, we can update and transition the paths:
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
Here is the answer to your question: this line...
.attr("d", function(d) return lineGenerator(d); );
... is what makes possible updating the d
attribute of the paths without having to delete/repaint all the SVGs again.
And here is the complete demo:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
var widthSingleChart = 150;
var heightSingleChart = 80;
var vals = ["firstVal", "secondVal"];
var colors = ["steelblue", "tomato"];
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale)
.ticks(4)
var div = d3.select("#doublelineDiv");
draw("APPLE");
d3.selectAll('input[name=type-selector]').on('change', function()
draw(this.value)
)
function draw(fruit)
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
var gX = svgUpdate.selectAll(".xaxis")
.data([1]);
gX.enter()
.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + (heightSingleChart - 20) + ")")
.call(xAxis);
gX.call(xAxis);
var gY = svgUpdate.selectAll(".yaxis")
.data([1]);
gY.enter()
.append("g")
.attr("class", "yaxis")
.attr("transform", "translate(20,0)")
.call(yAxis);
gY.call(yAxis);
<script src='https://d3js.org/d3.v5.min.js'></script>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<br>
<div id='doublelineDiv'></div>
You may say that the transition is not the most beautiful one, and that's because the interpolator is interpolating strings. You can improve that, for instance, see what Mike Bostock (D3 creator) did here: https://bl.ocks.org/mbostock/3916621. I'll leave that for you, because it's way over the scope of this answer.
At the end, the code came from 254 lines to 129 lines. But that's not what's important here: what's important here is that now the code is a proper, idiomatic D3 code.
Edit: According to your comment, you need to use different scales.
There are several possible solutions here, the easiest one being using the second argument to get the index of the path:
.attr("d", function(d,i)
return i ? lineGenerator2(d) : lineGenerator1(d);
);
In the above snippet, lineGenerator1
will be called for the fist path, while lineGenerator2
will be called for the second one. That way, you can set different scales for each line generator.
However, here is an advice: make that very clear for the end user. Since that the chart has just one y axis (I'm referring to your Plunker, not to your image), almost all users will assume (correctly) that the lines use the same scale.
Thanks Gerardo, you are very kind to explain each step. I'm learning a lot. But I need to use two different scales (no, I'm not lying to users).
â Buster
Jun 6 at 14:22
If you need to use two different scales you can change the data function for the paths or simply using the second argument for choosing the scale. I said "lying" because, in a chart like this, where you have just one y axis, you have to explain clearly to the user that the scales are different, otherwise everybody will assume they are the same. Please, check the edit at the end of the answer.
â Gerardo Furtado
Jun 7 at 3:07
add a comment |Â
up vote
3
down vote
accepted
up vote
3
down vote
accepted
You are very right: deleting the elements and repainting the SVG again is not the correct way to update a D3 chart when the values of a given dataset change. You should frown upon (almost) any D3 code that does this.
The problem is not only that such approach (which I call a "lazy update") is not idiomatic, the problem is that it doesn't allow the use of the very famous enter-update-exit pattern, and most importantly, it doesn't allow the transition of the elements, which is very important for the user to follow the changes.
Don't get me wrong, sometimes we do that (hence the "almost" in the first paragraph), but in very specific situations, for very specific reasons.
Back to your code:
Unfortunately, the fact that you're deleting the SVGs to repaint them again...
d3.select('#doublelineDiv').html('');
... is not the only big problem here: another major problem is the use of a for
loop to append the elements:
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
As a rule of thumb, do not use loops (for
, while
, forEach
etc) to append elements in a D3 code. Use proper selections â which, by the way, will fix our first problem.
Those two advices combined make the required refactor so big that I decided to write an entire new code based on your outcome. This is necessary because refactoring your code will require so many changes that the answer would be a complete mess. However, I'll guide you through the new code step by step.
First of all, you don't need all that manipulation with the data. You can simply nest it:
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
That way we'll have an array of three objects, one for each small SVG. Check it:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
console.log(nestedData)
<script src='https://d3js.org/d3.v5.min.js'></script>
Then we create the scales. Since those years are just categorical variables treat them as such, using a point scale. Also, use the same scale for both lines, otherwise you'll be lying to the user:
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
As you can see, there are some magic numbers here, in the final code you should get rid of them.
Also, we can use the same line generator for both lines (more on that below):
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
And finally we come to the important part, the selections.
Everything that actually paints the SVGs is inside a function named draw
:
draw("APPLE");
function draw(fruit)
//code here
Inside draw, we'll have all our selections. First, the SVG selection:
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
As you can see, we bind the data to the SVGs themselves, using the countries as a key in the key function.
Then, we have the paths selection:
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
That data function is complicated, I know. It basically creates two arrays of data for each small SVG, one with firstVal
and another one with secondVal
. As those arrays have the same properties, we can use a single line generator.
The nice thing about it is that, now, we can update and transition the paths:
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
Here is the answer to your question: this line...
.attr("d", function(d) return lineGenerator(d); );
... is what makes possible updating the d
attribute of the paths without having to delete/repaint all the SVGs again.
And here is the complete demo:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
var widthSingleChart = 150;
var heightSingleChart = 80;
var vals = ["firstVal", "secondVal"];
var colors = ["steelblue", "tomato"];
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale)
.ticks(4)
var div = d3.select("#doublelineDiv");
draw("APPLE");
d3.selectAll('input[name=type-selector]').on('change', function()
draw(this.value)
)
function draw(fruit)
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
var gX = svgUpdate.selectAll(".xaxis")
.data([1]);
gX.enter()
.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + (heightSingleChart - 20) + ")")
.call(xAxis);
gX.call(xAxis);
var gY = svgUpdate.selectAll(".yaxis")
.data([1]);
gY.enter()
.append("g")
.attr("class", "yaxis")
.attr("transform", "translate(20,0)")
.call(yAxis);
gY.call(yAxis);
<script src='https://d3js.org/d3.v5.min.js'></script>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<br>
<div id='doublelineDiv'></div>
You may say that the transition is not the most beautiful one, and that's because the interpolator is interpolating strings. You can improve that, for instance, see what Mike Bostock (D3 creator) did here: https://bl.ocks.org/mbostock/3916621. I'll leave that for you, because it's way over the scope of this answer.
At the end, the code came from 254 lines to 129 lines. But that's not what's important here: what's important here is that now the code is a proper, idiomatic D3 code.
Edit: According to your comment, you need to use different scales.
There are several possible solutions here, the easiest one being using the second argument to get the index of the path:
.attr("d", function(d,i)
return i ? lineGenerator2(d) : lineGenerator1(d);
);
In the above snippet, lineGenerator1
will be called for the fist path, while lineGenerator2
will be called for the second one. That way, you can set different scales for each line generator.
However, here is an advice: make that very clear for the end user. Since that the chart has just one y axis (I'm referring to your Plunker, not to your image), almost all users will assume (correctly) that the lines use the same scale.
You are very right: deleting the elements and repainting the SVG again is not the correct way to update a D3 chart when the values of a given dataset change. You should frown upon (almost) any D3 code that does this.
The problem is not only that such approach (which I call a "lazy update") is not idiomatic, the problem is that it doesn't allow the use of the very famous enter-update-exit pattern, and most importantly, it doesn't allow the transition of the elements, which is very important for the user to follow the changes.
Don't get me wrong, sometimes we do that (hence the "almost" in the first paragraph), but in very specific situations, for very specific reasons.
Back to your code:
Unfortunately, the fact that you're deleting the SVGs to repaint them again...
d3.select('#doublelineDiv').html('');
... is not the only big problem here: another major problem is the use of a for
loop to append the elements:
for(var i = 0; i < numCountries; i++)
var country = countries[i];
singleLineChart(country);
As a rule of thumb, do not use loops (for
, while
, forEach
etc) to append elements in a D3 code. Use proper selections â which, by the way, will fix our first problem.
Those two advices combined make the required refactor so big that I decided to write an entire new code based on your outcome. This is necessary because refactoring your code will require so many changes that the answer would be a complete mess. However, I'll guide you through the new code step by step.
First of all, you don't need all that manipulation with the data. You can simply nest it:
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
That way we'll have an array of three objects, one for each small SVG. Check it:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
console.log(nestedData)
<script src='https://d3js.org/d3.v5.min.js'></script>
Then we create the scales. Since those years are just categorical variables treat them as such, using a point scale. Also, use the same scale for both lines, otherwise you'll be lying to the user:
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
As you can see, there are some magic numbers here, in the final code you should get rid of them.
Also, we can use the same line generator for both lines (more on that below):
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
And finally we come to the important part, the selections.
Everything that actually paints the SVGs is inside a function named draw
:
draw("APPLE");
function draw(fruit)
//code here
Inside draw, we'll have all our selections. First, the SVG selection:
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
As you can see, we bind the data to the SVGs themselves, using the countries as a key in the key function.
Then, we have the paths selection:
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
That data function is complicated, I know. It basically creates two arrays of data for each small SVG, one with firstVal
and another one with secondVal
. As those arrays have the same properties, we can use a single line generator.
The nice thing about it is that, now, we can update and transition the paths:
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
Here is the answer to your question: this line...
.attr("d", function(d) return lineGenerator(d); );
... is what makes possible updating the d
attribute of the paths without having to delete/repaint all the SVGs again.
And here is the complete demo:
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
var widthSingleChart = 150;
var heightSingleChart = 80;
var vals = ["firstVal", "secondVal"];
var colors = ["steelblue", "tomato"];
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale)
.ticks(4)
var div = d3.select("#doublelineDiv");
draw("APPLE");
d3.selectAll('input[name=type-selector]').on('change', function()
draw(this.value)
)
function draw(fruit)
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
var gX = svgUpdate.selectAll(".xaxis")
.data([1]);
gX.enter()
.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + (heightSingleChart - 20) + ")")
.call(xAxis);
gX.call(xAxis);
var gY = svgUpdate.selectAll(".yaxis")
.data([1]);
gY.enter()
.append("g")
.attr("class", "yaxis")
.attr("transform", "translate(20,0)")
.call(yAxis);
gY.call(yAxis);
<script src='https://d3js.org/d3.v5.min.js'></script>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<br>
<div id='doublelineDiv'></div>
You may say that the transition is not the most beautiful one, and that's because the interpolator is interpolating strings. You can improve that, for instance, see what Mike Bostock (D3 creator) did here: https://bl.ocks.org/mbostock/3916621. I'll leave that for you, because it's way over the scope of this answer.
At the end, the code came from 254 lines to 129 lines. But that's not what's important here: what's important here is that now the code is a proper, idiomatic D3 code.
Edit: According to your comment, you need to use different scales.
There are several possible solutions here, the easiest one being using the second argument to get the index of the path:
.attr("d", function(d,i)
return i ? lineGenerator2(d) : lineGenerator1(d);
);
In the above snippet, lineGenerator1
will be called for the fist path, while lineGenerator2
will be called for the second one. That way, you can set different scales for each line generator.
However, here is an advice: make that very clear for the end user. Since that the chart has just one y axis (I'm referring to your Plunker, not to your image), almost all users will assume (correctly) that the lines use the same scale.
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
console.log(nestedData)
<script src='https://d3js.org/d3.v5.min.js'></script>
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
console.log(nestedData)
<script src='https://d3js.org/d3.v5.min.js'></script>
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
var widthSingleChart = 150;
var heightSingleChart = 80;
var vals = ["firstVal", "secondVal"];
var colors = ["steelblue", "tomato"];
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale)
.ticks(4)
var div = d3.select("#doublelineDiv");
draw("APPLE");
d3.selectAll('input[name=type-selector]').on('change', function()
draw(this.value)
)
function draw(fruit)
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
var gX = svgUpdate.selectAll(".xaxis")
.data([1]);
gX.enter()
.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + (heightSingleChart - 20) + ")")
.call(xAxis);
gX.call(xAxis);
var gY = svgUpdate.selectAll(".yaxis")
.data([1]);
gY.enter()
.append("g")
.attr("class", "yaxis")
.attr("transform", "translate(20,0)")
.call(yAxis);
gY.call(yAxis);
<script src='https://d3js.org/d3.v5.min.js'></script>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<br>
<div id='doublelineDiv'></div>
var data = [
type: 'APPLE',
country: 'Germany',
year: '2000',
firstVal: '99.5',
secondVal: '4'
,
type: 'APPLE',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '2'
,
type: 'APPLE',
country: 'Germany',
year: '2002',
firstVal: '84',
secondVal: '2.3'
,
type: 'APPLE',
country: 'Germany',
year: '2003',
firstVal: '86',
secondVal: '2'
,
type: 'PEAR',
country: 'Germany',
year: '2000',
firstVal: '99',
secondVal: '3'
,
type: 'PEAR',
country: 'Germany',
year: '2001',
firstVal: '98',
secondVal: '10'
,
type: 'PEAR',
country: 'Germany',
year: '2002',
firstVal: '97',
secondVal: '8'
,
type: 'PEAR',
country: 'Germany',
year: '2003',
firstVal: '98',
secondVal: '10.9'
,
type: 'APPLE',
country: 'France',
year: '2000',
firstVal: 'NaN',
secondVal: '4'
,
type: 'APPLE',
country: 'France',
year: '2001',
firstVal: '85',
secondVal: '50'
,
type: 'APPLE',
country: 'France',
year: '2002',
firstVal: '86',
secondVal: '27'
,
type: 'APPLE',
country: 'France',
year: '2003',
firstVal: '86',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2000',
firstVal: '87',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2001',
firstVal: '99',
secondVal: 'NaN'
,
type: 'PEAR',
country: 'France',
year: '2002',
firstVal: '70',
secondVal: '5'
,
type: 'PEAR',
country: 'France',
year: '2003',
firstVal: '90',
secondVal: '9'
,
type: 'APPLE',
country: 'Spain',
year: '2000',
firstVal: '88',
secondVal: '14'
,
type: 'APPLE',
country: 'Spain',
year: '2001',
firstVal: '85',
secondVal: '2'
,
type: 'APPLE',
country: 'Spain',
year: '2002',
firstVal: '86',
secondVal: '1'
,
type: 'APPLE',
country: 'Spain',
year: '2003',
firstVal: '84',
secondVal: '5'
,
type: 'PEAR',
country: 'Spain',
year: '2000',
firstVal: '65',
secondVal: '3'
,
type: 'PEAR',
country: 'Spain',
year: '2001',
firstVal: 'NaN',
secondVal: '4'
,
type: 'PEAR',
country: 'Spain',
year: '2002',
firstVal: '95',
secondVal: '2'
,
type: 'PEAR',
country: 'Spain',
year: '2003',
firstVal: '78',
secondVal: '1'
];
var nestedData = d3.nest()
.key(function(d)
return d.country
)
.entries(data);
var widthSingleChart = 150;
var heightSingleChart = 80;
var vals = ["firstVal", "secondVal"];
var colors = ["steelblue", "tomato"];
var xScale = d3.scalePoint()
.range([20, widthSingleChart - 10])
.domain([...new Set(data.map(function(d)
return d.year;
))]);
var yScale = d3.scaleSqrt()
.range([heightSingleChart - 20, 0])
.domain([0, d3.max(data, function(d)
return +d.firstVal;
)]);
var lineGenerator = d3.line()
.x(function(d)
return xScale(d.year)
)
.y(function(d)
return yScale(d.value)
)
.defined(function(d)
return d.value === d.value
);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale)
.ticks(4)
var div = d3.select("#doublelineDiv");
draw("APPLE");
d3.selectAll('input[name=type-selector]').on('change', function()
draw(this.value)
)
function draw(fruit)
var svgUpdate = div.selectAll("svg")
.data(nestedData, function(d)
return d.key
);
svgUpdate = svgUpdate.enter()
.append("svg")
.attr("width", widthSingleChart)
.attr("height", heightSingleChart)
.merge(svgUpdate);
var paths = svgUpdate.selectAll("path")
.data(function(d)
return vals.map(function(e)
return d.values.filter(function(f)
return f.type === fruit
).map(function(f)
return
year: f.year,
value: +f[e]
)
)
);
var pathsEnter = paths.enter()
.append("path")
.attr("d", function(d)
return lineGenerator(d);
)
.style("stroke", function(_, i)
return colors[i]
)
.style("fill", "none");
paths.transition()
.duration(1000)
.attr("d", function(d)
return lineGenerator(d);
);
var gX = svgUpdate.selectAll(".xaxis")
.data([1]);
gX.enter()
.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + (heightSingleChart - 20) + ")")
.call(xAxis);
gX.call(xAxis);
var gY = svgUpdate.selectAll(".yaxis")
.data([1]);
gY.enter()
.append("g")
.attr("class", "yaxis")
.attr("transform", "translate(20,0)")
.call(yAxis);
gY.call(yAxis);
<script src='https://d3js.org/d3.v5.min.js'></script>
<div class='row'>
<div class='col' id='type-selector-container'>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-apple' name='type-selector' class='custom-control-input' value='APPLE' checked>
<label class='custom-control-label' for='rb-apple'>APPLE</label>
</div>
<div class='custom-control custom-radio custom-control-inline'>
<input type='radio' id='rb-pear' name='type-selector' class='custom-control-input' value='PEAR'>
<label class='custom-control-label' for='rb-pear'>PEAR</label>
</div>
</div>
</div>
<br>
<div id='doublelineDiv'></div>
edited Jun 7 at 4:51
answered Jun 6 at 13:06
Gerardo Furtado
1,1342420
1,1342420
Thanks Gerardo, you are very kind to explain each step. I'm learning a lot. But I need to use two different scales (no, I'm not lying to users).
â Buster
Jun 6 at 14:22
If you need to use two different scales you can change the data function for the paths or simply using the second argument for choosing the scale. I said "lying" because, in a chart like this, where you have just one y axis, you have to explain clearly to the user that the scales are different, otherwise everybody will assume they are the same. Please, check the edit at the end of the answer.
â Gerardo Furtado
Jun 7 at 3:07
add a comment |Â
Thanks Gerardo, you are very kind to explain each step. I'm learning a lot. But I need to use two different scales (no, I'm not lying to users).
â Buster
Jun 6 at 14:22
If you need to use two different scales you can change the data function for the paths or simply using the second argument for choosing the scale. I said "lying" because, in a chart like this, where you have just one y axis, you have to explain clearly to the user that the scales are different, otherwise everybody will assume they are the same. Please, check the edit at the end of the answer.
â Gerardo Furtado
Jun 7 at 3:07
Thanks Gerardo, you are very kind to explain each step. I'm learning a lot. But I need to use two different scales (no, I'm not lying to users).
â Buster
Jun 6 at 14:22
Thanks Gerardo, you are very kind to explain each step. I'm learning a lot. But I need to use two different scales (no, I'm not lying to users).
â Buster
Jun 6 at 14:22
If you need to use two different scales you can change the data function for the paths or simply using the second argument for choosing the scale. I said "lying" because, in a chart like this, where you have just one y axis, you have to explain clearly to the user that the scales are different, otherwise everybody will assume they are the same. Please, check the edit at the end of the answer.
â Gerardo Furtado
Jun 7 at 3:07
If you need to use two different scales you can change the data function for the paths or simply using the second argument for choosing the scale. I said "lying" because, in a chart like this, where you have just one y axis, you have to explain clearly to the user that the scales are different, otherwise everybody will assume they are the same. Please, check the edit at the end of the answer.
â Gerardo Furtado
Jun 7 at 3:07
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f195936%2fupdate-the-line-charts-without-deleting-the-svg%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password