Robert Andersson
Robert Andersson

Reputation: 1521

How to use the filter method with the update pattern?

In my snippet below I'm using the following filter method to get the desired data:

var data = csv.filter(d => d.date.includes(year))

The problem here is that whenever I'm updating the chart every bar gets redrawn from the top rather than updating from the previous data.

Is this because I'm redeclaring the data variable in my update function and therefore it's not updating from previous data?

Is there a simple way to get around this or do I have to re-map and separate each year in a new data array and base my update pattern on this?

Here's the code:

var csvData = `date,value
2018-jan,100
2018-feb,75
2018-mar,45
2018-apr,65
2019-jan,21
2019-feb,43
2019-mar,55
2019-apr,33`;

var csv = d3.csvParse(csvData, d => d)

var margin = {top: 25, bottom: 25, left: 25, right: 25};

var svg = d3.select("#chart"),
	width = +svg.attr("width") - margin.left - margin.right, 
	height = +svg.attr("height") - margin.top - margin.bottom;

var x = d3.scaleBand()
	.range([margin.left, width - margin.right])
	.padding(0.1)
	.paddingOuter(0.2)

var xAxis = g => g
	.attr("transform", "translate(0," + (height - margin.bottom) + ")")
	.call(d3.axisBottom(x).tickSizeOuter(0))

svg.append("g")
	.attr("class", "x-axis")

var y = d3.scaleLinear()
	.range([height - margin.bottom, margin.top])

update("2018")

function update(year) {

	var data = csv.filter(d => d.date.includes(year))

	y.domain([0, d3.max(data, d => Math.max(d.value))])

	x.domain(data.map(d => d.date))

	svg.selectAll(".x-axis")
		.call(xAxis);

	var bar = svg.selectAll(".bar")
		.data(data, d => d.date)
	
	bar.exit().remove();
	
	bar = bar
		.enter()
	.append("rect")
		.attr("class", "bar")
		.attr("fill", "steelblue")
		.attr("width", x.bandwidth())
		.attr("x", d => x(d.date))
		.merge(bar)
	.transition().duration(750)
		.attr("y", d => y(d.value))
		.attr("height", d => y(0) - y(d.value))
}

d3.select("#year").on("change", function() {
	update(this.value)
})
body {
	padding-top: 25px;
	margin: auto;
	width: 450px;
	font: 11px arial;
}	
<script src="https://d3js.org/d3.v5.min.js"></script>
Choose year
<select id="year">
	<option value="2018">2018</option>
	<option value="2019">2019</option>
</select><br>

<svg id="chart" width="450" height="200"></svg>

Upvotes: 1

Views: 250

Answers (2)

Robert Andersson
Robert Andersson

Reputation: 1521

So I found out on my own that I can just do the filtering within the data method itself like this...

    var bar = svg.selectAll(".bar")
        .data(data.filter(f => f.date.includes(year)))

...and everything gets updated properly, not sure if I should close the question but I'll leave it upf or now:

Here's the updated code:

var csvData = `date,value
2018-jan,100
2018-feb,75
2018-mar,45
2018-apr,65
2019-jan,21
2019-feb,43
2019-mar,55
2019-apr,33`;

var data = d3.csvParse(csvData, d => d)

var margin = {top: 25, bottom: 25, left: 25, right: 25};

var svg = d3.select("#chart"),
	width = +svg.attr("width") - margin.left - margin.right, 
	height = +svg.attr("height") - margin.top - margin.bottom;

var x = d3.scaleBand()
	.range([margin.left, width - margin.right])
	.padding(0.1)
	.paddingOuter(0.2)

var xAxis = g => g
	.attr("transform", "translate(0," + (height - margin.bottom) + ")")
	.call(d3.axisBottom(x).tickSizeOuter(0))

svg.append("g")
	.attr("class", "x-axis")

var y = d3.scaleLinear()
	.range([height - margin.bottom, margin.top])

update("2018")

function update(year) {

	var maxVal = d3.max(data, function(d) {
		var f = d.date.includes(year)
		return (f == true ? d.value : null)
	})

	y.domain([0, maxVal]).nice()

	x.domain(data.map(d => d.date).filter(f => f.includes(year)))
	
	svg.selectAll(".x-axis")
		.call(xAxis);

	var bar = svg.selectAll(".bar")
		.data(data.filter(f => f.date.includes(year)));
    
	bar.exit().remove();
  
	bar.enter()
	.append("rect")
		.attr("class", "bar")
		.attr("fill", "steelblue")
		.attr("width", x.bandwidth())
		.attr("x", d => x(d.date))
		.merge(bar)
	.transition().duration(750)
		.attr("y", d => y(d.value))
		.attr("height", d => y(0) - y(d.value))
}

d3.select("#year").on("change", function() {
	update(this.value)
})
body {
	padding-top: 25px;
	margin: auto;
	width: 650px;
	font: 11px arial;
}
<script src="https://d3js.org/d3.v5.min.js"></script>	
Choose year
<select id="year">
	<option value="2018">2018</option>
	<option value="2019">2019</option>
</select><br>

<svg id="chart" width="650" height="200"></svg>

Upvotes: 1

Gerardo Furtado
Gerardo Furtado

Reputation: 102198

The problem here has nothing to do with filtering the data, or re-assigning variables, or merging selections, or any other issue mentioned in the question and comments.

The problem here is simply the key function. When you specify the key function, the data for each element is bound according to its string identifier. And, since the dates are different, the expected behaviour is that the bars should be removed and new bars should be drawn.

What you want, instead of that, is a join-by-index behaviour. So, change your data method from...

.data(data, d => d.date)

..to:

.data(data)

Here is your code with that change only:

var csvData = `date,value
2018-jan,100
2018-feb,75
2018-mar,45
2018-apr,65
2019-jan,21
2019-feb,43
2019-mar,55
2019-apr,33`;

var csv = d3.csvParse(csvData, d => d)

var margin = {top: 25, bottom: 25, left: 25, right: 25};

var svg = d3.select("#chart"),
	width = +svg.attr("width") - margin.left - margin.right, 
	height = +svg.attr("height") - margin.top - margin.bottom;

var x = d3.scaleBand()
	.range([margin.left, width - margin.right])
	.padding(0.1)
	.paddingOuter(0.2)

var xAxis = g => g
	.attr("transform", "translate(0," + (height - margin.bottom) + ")")
	.call(d3.axisBottom(x).tickSizeOuter(0))

svg.append("g")
	.attr("class", "x-axis")

var y = d3.scaleLinear()
	.range([height - margin.bottom, margin.top])

update("2018")

function update(year) {

	var data = csv.filter(d => d.date.includes(year))

	y.domain([0, d3.max(data, d => Math.max(d.value))])

	x.domain(data.map(d => d.date))

	svg.selectAll(".x-axis")
		.call(xAxis);

	var bar = svg.selectAll(".bar")
		.data(data)
	
	bar.exit().remove();
	
	bar = bar
		.enter()
	.append("rect")
		.attr("class", "bar")
		.attr("fill", "steelblue")
		.attr("width", x.bandwidth())
		.attr("x", d => x(d.date))
		.merge(bar)
	.transition().duration(750)
		.attr("y", d => y(d.value))
		.attr("height", d => y(0) - y(d.value))
}

d3.select("#year").on("change", function() {
	update(this.value)
})
body {
	padding-top: 25px;
	margin: auto;
	width: 450px;
	font: 11px arial;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
Choose year
<select id="year">
	<option value="2018">2018</option>
	<option value="2019">2019</option>
</select><br>

<svg id="chart" width="450" height="200"></svg>

Upvotes: 2

Related Questions