I have data nested into 3 levels, which I need to dynamically update. The kicker is that the elements for the mid-level need to actually display on TOP of the elements for the low-level due to some hover behavior I need, so I'm having trouble with what the enter/update/exit/merge pattern should look like. (There don't need to be any elements displayed for the high-level).
The code I have right now updates the data successfully but is not rendering the rectangles at all, instead giving me an error, Uncaught TypeError: this.setAttribute is not a function.
How do I fix this problem, please?
Here's what it should look like before updating:
And here's what it should look like after updating:
Here's a CodePen with the code Below
```
let width = 0.9 * window.innerWidth,
height = 0.9 * window.innerHeight,
colors = ['darkviolet', 'steelblue', 'coral', 'Turquoise', 'firebrick', 'mediumslateblue', 'palevioletred', 'green', 'aqua'];
let data1 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"1","item":"2"},
{"group":"A","segment":"1","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"3","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"4","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"6","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"7","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"9","item":"8"},
{"group":"C","segment":"9","item":"9"}],
data2 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"8","item":"2"},
{"group":"A","segment":"9","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"5","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"7","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"5","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"3","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"6","item":"8"},
{"group":"C","segment":"1","item":"9"}];
let button = d3.select('body')
.append('button')
.attr('type', 'button')
.style('display', 'block')
.text('Update')
.on('click', function() { update(data2) });
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
let color = d3.scaleOrdinal().range(colors);
update(data1);
function getxy(data) {
let grouped = Array.from(d3.group(data, d=> d.group, d=> d.segment), ([key, value]) => ({key, value}));
grouped.forEach(function(s) {
s.value = Array.from(s.value, ([key, value]) => ({key, value}));
s.value.forEach(function(d) {
d.start = d3.min(d.value, function(t) { t.segment = +t.segment; t.item = +t.item; return +t.item });
d.end = d3.max(d.value, function(t) { return t.item });
d.key = +d.key;
d.group = s.key;
})
})
let x1 = d3.scaleBand()
.domain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.range([width*0.05, width])
.padding(0.0);
let y1 = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([10, height])
.padding(0.1);
return [x1, y1, grouped];
}
function update(data) {
let xy = getxy(data);
let x = xy[0], y = xy[1], groupedData = xy[2];
let barsAll = svg
.selectAll('.bars')
.data(groupedData);
barsAll.exit().remove();
let barsEnter = barsAll
.enter()
.append('g')
.attr('class', 'bars');
barsEnter = barsEnter.merge(barsAll);
let segmentsAll = barsEnter
.selectAll('.segments')
.data(function(d) { return d.value });
segmentsAll.exit().remove();
let segmentsEnter = segmentsAll.enter();
let bitsAll = segmentsEnter
.selectAll('.bits')
.data(function(d) { return d.value });
bitsAll.exit().remove();
let bitsEnter = bitsAll
.enter()
.append('circle')
.attr('class', 'bits')
.attr('r', width*0.05)
.attr('stroke', 'none');
bitsEnter = bitsEnter.merge(bitsAll);
bitsEnter
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
segmentsEnter.append('rect')
.attr('stroke', 'black')
.attr('class', 'segments')
.style('fill-opacity', 0.2);
segmentsEnter = segmentsEnter.merge(segmentsAll);
segmentsEnter
.attr('fill', function(d) { return color(d.key) })
.attr('height', y.bandwidth()*0.75)
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
}
```
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
Well, by going around the “merge” step on the mid-level segments and low-level bits (but not on the top-level bars), I was able to hack a fix, finally. Working pen
Still open to help from others because I feel like I should really get the hang of the whole flow - update, enter, exit, merge - at some point.
let width = 0.9 * window.innerWidth,
height = 0.9 * window.innerHeight,
colors = ['darkviolet', 'steelblue', 'coral', 'Turquoise', 'firebrick', 'mediumslateblue', 'palevioletred', 'green', 'aqua'];
let data1 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"1","item":"2"},
{"group":"A","segment":"1","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"3","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"4","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"6","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"7","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"9","item":"8"},
{"group":"C","segment":"9","item":"9"}],
data2 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"8","item":"2"},
{"group":"A","segment":"9","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"5","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"7","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"5","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"3","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"6","item":"8"},
{"group":"C","segment":"1","item":"9"}];
let button = d3.select('body')
.append('button')
.attr('type', 'button')
.style('display', 'block')
.text('Update')
.on('click', function() { update(data2) });
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
let color = d3.scaleOrdinal().range(colors);
function getxy(data) {
let grouped = Array.from(d3.group(data, d=> d.group, d=> d.segment), ([key, value]) => ({key, value}));
grouped.forEach(function(s) {
s.value = Array.from(s.value, ([key, value]) => ({key, value}));
s.value.forEach(function(d) {
d.start = d3.min(d.value, function(t) { t.segment = +t.segment; t.item = +t.item; return +t.item });
d.end = d3.max(d.value, function(t) { return t.item });
d.key = +d.key;
d.group = s.key;
})
})
let x1 = d3.scaleBand()
.domain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.range([width*0.05, width])
.padding(0.0);
let y1 = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([10, height])
.padding(0.1);
return [x1, y1, grouped];
}
function update(data) {
let xy = getxy(data);
let x = xy[0], y = xy[1], groupedData = xy[2];
// update
let barsAll = svg
.selectAll('.bars')
.data(groupedData);
// exit
barsAll.exit().remove();
// enter
let barsEnter = barsAll
.enter();
barsEnter = barsEnter.merge(barsAll).append('g');
barsEnter.selectAll('.segments').remove();
d3.selectAll('.segments').remove();
let segmentsAll = barsEnter
.selectAll('.segments')
.data(function(d) { return d.value });
segmentsAll.exit().remove();
let segmentsEnter = segmentsAll
.enter();
let bitsAll = segmentsEnter
.selectAll('.bits')
.data(function(d) { return d.value });
bitsAll.exit().remove();
bitsAll
.enter()
.append('circle')
.attr('class', 'bits')
.attr('r', width*0.05)
.attr('stroke', 'none')
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
// bitsEnter = bitsEnter.merge(bitsAll);
bitsAll
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
segmentsEnter
.append('rect')
.attr('class', 'segments')
.attr('stroke', 'black')
.style('fill-opacity', 0.2)
.attr('fill', function(d) { return color(d.key) })
.attr('height', function() { return y.bandwidth()*0.75 })
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
segmentsAll
.attr('fill', function(d) { return color(d.key) })
.attr('height', function() { return y.bandwidth()*0.75 })
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
//segmentsAll = segmentsEnter.merge(segmentsAll);
// segmentsEnter
// .attr('fill', function(d) { return color(d.key) })
// .attr('height', function() { return y.bandwidth()*0.75 })
// .attr('x', function(d) { return x(d.start) - width*0.05 })
// .attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
// .attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
}
update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
I created this sample heatmap: Plunker
Initially I use a certain linear scale colorScale1 to color the heatmap.
When the user clicks on the legend, the color scale is updated and the threshold scale (colorScale2) is used.
This switch works well.
Now I don't know how to change the legend for the colorScale2.
The ticks and the gradient for colorScale2 are wrong. I looked for a linearGradient equivalent for scaleThreshold but I didn't find anything.
This is the code:
var itemSize = 20;
var cellBorderSize = 1;
var cellSize = itemSize - 1 + cellBorderSize;
var margin = {top: 10, right: 10, bottom: 10, left: 10};
var width = 80 - margin.right - margin.left;
var height = 80 - margin.top - margin.bottom;
var svg = d3.select('#heatmap')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var domain1 = [0, 80, 90, 95, 100];
var range1 = ['#EC93AB', '#CEB1DE', '#95D3F0', '#77EDD9', '#A9FCAA'];
var colorScale1 = d3.scaleLinear()
.domain(domain1)
.range(range1);
var domain2 = [0, 95, 100];
var range2 = ['white', 'lightgrey', 'grey'];
var colorScale2 = d3.scaleThreshold()
.domain(domain2)
.range(range2);
svg.append('defs')
.append('pattern')
.attr('id', 'pattern-stripes')
.attr('patternUnits', 'userSpaceOnUse')
.attr('patternTransform', 'rotate(45)')
.attr('width', 3)
.attr('height', 3)
.append('rect')
.attr('width', 1)
.attr('height', 3)
.attr('transform', 'translate(0, 0)')
.attr('fill', 'black');
///////////////////////////////////////////////////////////
// Load data files.
///////////////////////////////////////////////////////////
var files = ['./data.csv'];
var promises = [];
promises.push(d3.csv(files[0]));
Promise.all(promises)
.then(makeHeatmap)
.catch(function(err) {
console.log('Error loading files');
throw err;
});
///////////////////////////////////////////////////////////
// Data heatmap
///////////////////////////////////////////////////////////
function makeHeatmap(myData) {
var data = myData[0];
// get each element of data file and creates an object
var data = data.map(function(item) {
var newItem = {};
newItem.name = item.NAME;
newItem.year = item.YEAR;
newItem.val = item.VAL;
return newItem;
});
var names = data.map(function(d) {
return d.name;
});
regionsName = d3.set(names).values();
numRegions = regionsName.length;
var years = data.map(function(d) {
return d.year;
});
yearsName = d3.set(years).values();
numYears = yearsName.length;
///////////////////////////////////////////////////////////
// Draw heatmap
///////////////////////////////////////////////////////////
var cells = svg.selectAll('.cell')
.data(data)
.enter()
.append('g')
.append('rect')
.attr('data-value', function(d) {
return d.val;
})
.attr('data-r', function(d) {
var idr = regionsName.indexOf(d.name);
return idr;
})
.attr('data-c', function(d, i) {
if(regionsName.includes(d.name) & d.year == '1990') var idc = 0;
else if(regionsName.includes(d.name) && d.year == '1991') var idc = 1;
else if(regionsName.includes(d.name) && d.year == '1992') var idc = 2;
return idc;
})
.attr('class', function() {
var idr = d3.select(this).attr('data-r'); // row
var idc = d3.select(this).attr('data-c'); // column
return 'cell cr' + idr + ' cc' + idc;
})
.attr('width', cellSize)
.attr('height', cellSize)
.attr('x', function(d) {
var c = d3.select(this).attr('data-c');
return c * cellSize;
})
.attr('y', function() {
var r = d3.select(this).attr('data-r');
return r * cellSize;
})
.attr('fill', function(d) {
var col;
if(d.name == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale1(d.val);
}
return col;
});
} // end makeHeatmap
///////////////////////////////////////////////////////////
// Legend
///////////////////////////////////////////////////////////
// create tick marks
var xLegend = d3.scaleLinear()
.domain([0, 100])
.range([10, 409]); // larghezza dei tick
var axisLegend = d3.axisBottom(xLegend)
.tickSize(19) // height of ticks
.tickFormat(function(v, i) { // i is index of domain colorScale, v is the corrisponding value (v = domain[i])
if(v == 0) {
return v + '%';
}
else {
return v;
}
})
.tickValues(colorScale1.domain());
var svgLegend = d3.select('#legend').append('svg').attr('width', 600);
// append title
svgLegend.append('text')
.attr('class', 'legendTitle')
.attr('x', 10)
.attr('y', 20)
.style('text-anchor', 'start')
.text('Legend title');
// draw the rectangle and fill with gradient
svgLegend.append('rect')
.attr('class', 'legendRect')
.attr('x', 10) // position
.attr('y', 30)
.attr('width', 400) // larghezza fascia colorata
.attr('height', 15) // altezza fascia colorata
.style('fill', 'url(#linear-gradient1)')
.on('click', function() {
if(currentFill === '1') {
updateColor2();
currentFill = '2';
}
else {
updateColor1();
currentFill = '1';
}
});
svgLegend
.attr('class', 'legendLinAxis')
.append('g')
.attr('class', 'legendLinG')
.attr('transform', 'translate(0, 30)') // 47 è la posizione verticale dei tick (se l'aumenti, scendono) (47 per farli partire sotto, 30 per farli partire da sopra)
.call(axisLegend);
var defs = svgLegend.append('defs');
// horizontal gradient and append multiple color stops by using D3's data/enter step
var linearGradient1 = defs.append('linearGradient')
.attr('id', 'linear-gradient1')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data(colorScale1.domain())
.enter().append('stop')
.attr('offset', function(d) {
return d + '%';
})
.attr('stop-color', function(d) {
return colorScale1(d);
});
// horizontal gradient and append multiple color stops by using D3's data/enter step
var linearGradient2 = defs.append('linearGradient')
.attr('id', 'linear-gradient2')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data(colorScale2.domain())
.enter().append('stop')
.attr('offset', function(d) {
return d + '%';
})
.attr('stop-color', function(d) {
return colorScale2(d);
});
// update the colors to a different color scale (colorScale1)
function updateColor1() {
// fill the legend rectangle
svgLegend.select('.legendRect')
.style('fill', 'url(#linear-gradient1)');
// transition the cell colors
svg.selectAll('.cell')
.transition().duration(1000)
.style('fill', function(d, i) {
var col;
if(d.valuePol == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale1(d.val);
}
return col;
});
}
// update the colors to a different color scale (colorScale2)
function updateColor2() {
// fill the legend rectangle
svgLegend.select('.legendRect')
.style('fill', 'url(#linear-gradient2)');
// transition the cell colors
svg.selectAll('.cell')
.transition().duration(1000)
.style('fill', function(d, i) {
var col;
if(d.valuePol == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale2(d.val);
}
return col;
});
}
// start set-up
updateColor1();
var currentFill = '1';
Here is a slightly modified version of your code for which the legend's ticks and color scale are updated when clicking on the legend:
var itemSize = 20;
var cellBorderSize = 1;
var cellSize = itemSize - 1 + cellBorderSize;
var margin = {top: 10, right: 10, bottom: 10, left: 10};
var width = 80 - margin.right - margin.left;
var height = 80 - margin.top - margin.bottom;
var svg = d3.select('#heatmap')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var domain1 = [0, 80, 90, 95, 100];
var range1 = ['#EC93AB', '#CEB1DE', '#95D3F0', '#77EDD9', '#A9FCAA'];
var colorScale1 = d3.scaleLinear()
.domain(domain1)
.range(range1);
var domain2 = [0, 95, 100];
var range2 = ['white', 'lightgrey', 'grey'];
var colorScale2 = d3.scaleThreshold()
.domain(domain2)
.range(range2);
svg.append('defs')
.append('pattern')
.attr('id', 'pattern-stripes')
.attr('patternUnits', 'userSpaceOnUse')
.attr('patternTransform', 'rotate(45)')
.attr('width', 3)
.attr('height', 3)
.append('rect')
.attr('width', 1)
.attr('height', 3)
.attr('transform', 'translate(0, 0)')
.attr('fill', 'black');
var data = [
{ "NAME": "ronnie", "YEAR": 1990, "VAL": 90 },
{ "NAME": "ronnie", "YEAR": 1991, "VAL": 95 },
{ "NAME": "ronnie", "YEAR": 1992, "VAL": 98 },
{ "NAME": "bob", "YEAR": 1990, "VAL": 92 },
{ "NAME": "bob", "YEAR": 1991, "VAL": 90 },
{ "NAME": "bob", "YEAR": 1992, "VAL": 99 },
{ "NAME": "carl", "YEAR": 1990, "VAL": 98 },
{ "NAME": "carl", "YEAR": 1991, "VAL": 99 },
{ "NAME": "carl", "YEAR": 1992, "VAL": 995 }
];
makeHeatmap(data);
///////////////////////////////////////////////////////////
// Data heatmap
///////////////////////////////////////////////////////////
function makeHeatmap(data) {
//var data = myData[0];
// get each element of data file and creates an object
var data = data.map(function(item) {
var newItem = {};
newItem.name = item.NAME;
newItem.year = item.YEAR;
newItem.val = item.VAL;
return newItem;
});
var names = data.map(function(d) {
return d.name;
});
regionsName = d3.set(names).values();
numRegions = regionsName.length;
var years = data.map(function(d) {
return d.year;
});
yearsName = d3.set(years).values();
numYears = yearsName.length;
///////////////////////////////////////////////////////////
// Draw heatmap
///////////////////////////////////////////////////////////
var cells = svg.selectAll('.cell')
.data(data)
.enter()
.append('g')
.append('rect')
.attr('data-value', function(d) {
return d.val;
})
.attr('data-r', function(d) {
var idr = regionsName.indexOf(d.name);
return idr;
})
.attr('data-c', function(d, i) {
if(regionsName.includes(d.name) & d.year == '1990') var idc = 0;
else if(regionsName.includes(d.name) && d.year == '1991') var idc = 1;
else if(regionsName.includes(d.name) && d.year == '1992') var idc = 2;
return idc;
})
.attr('class', function() {
var idr = d3.select(this).attr('data-r'); // row
var idc = d3.select(this).attr('data-c'); // column
return 'cell cr' + idr + ' cc' + idc;
})
.attr('width', cellSize)
.attr('height', cellSize)
.attr('x', function(d) {
var c = d3.select(this).attr('data-c');
return c * cellSize;
})
.attr('y', function() {
var r = d3.select(this).attr('data-r');
return r * cellSize;
})
.attr('fill', function(d) {
var col;
if(d.name == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale1(d.val);
}
return col;
});
} // end makeHeatmap
///////////////////////////////////////////////////////////
// Legend
///////////////////////////////////////////////////////////
// create tick marks
var xLegend = d3.scaleLinear()
.domain([0, 100])
.range([10, 409]); // larghezza dei tick
var axisLegend = d3.axisBottom(xLegend)
.tickSize(19) // height of ticks
.tickFormat(function(v, i) { // i is index of domain colorScale, v is the corrisponding value (v = domain[i])
if(v == 0) {
return v + '%';
}
else {
return v;
}
});
var svgLegend = d3.select('#legend').append('svg').attr('width', 600);
// append title
svgLegend.append('text')
.attr('class', 'legendTitle')
.attr('x', 10)
.attr('y', 20)
.style('text-anchor', 'start')
.text('Legend title');
// draw the rectangle and fill with gradient
svgLegend.append('rect')
.attr('class', 'legendRect')
.attr('x', 10) // position
.attr('y', 30)
.attr('width', 400) // larghezza fascia colorata
.attr('height', 15) // altezza fascia colorata
.style('fill', 'url(#linear-gradient1)')
.on('click', function() {
if(currentFill === '1') {
updateColor2();
currentFill = '2';
}
else {
updateColor1();
currentFill = '1';
}
});
var legend = svgLegend
.attr('class', 'legendLinAxis')
.append('g')
.attr('class', 'legendLinG')
.attr('transform', 'translate(0, 30)'); // 47 è la posizione verticale dei tick (se l'aumenti, scendono) (47 per farli partire sotto, 30 per farli partire da sopra)
var defs = svgLegend.append('defs');
// horizontal gradient and append multiple color stops by using D3's data/enter step
var linearGradient1 = defs.append('linearGradient')
.attr('id', 'linear-gradient1')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data(colorScale1.domain())
.enter().append('stop')
.attr('offset', function(d) {
return d + '%';
})
.attr('stop-color', function(d) {
return colorScale1(d);
});
// horizontal gradient and append multiple color stops by using D3's data/enter step
function getGradient2data() {
// Duplicates elements of domain2:
var duplicatedDomain = domain2.reduce(function (res, current, index, array) { return res.concat([current, current]); }, []).slice(1, -1);
// Duplicates elements of range2:
var duplicatedRange = range2.slice(1).reduce(function (res, current, index, array) { return res.concat([current, current]); }, []);
// Zips both domain and range:
return duplicatedDomain.map( function(e, i) { return { "offset": e + "%", "color": duplicatedRange[i] }; [e, duplicatedRange[i]]; });
}
var linearGradient2 = defs.append('linearGradient')
.attr('id', 'linear-gradient2')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data(getGradient2data())
//.data([
// { offset: "0%", color: "lightgrey" },
// { offset: "95%", color: "lightgrey" },
// { offset: "95%", color: "grey" },
// { offset: "100%", color: "grey" }
//])
.enter().append('stop')
.attr('offset', function(d) {
return d.offset;
})
.attr('stop-color', function(d) {
return d.color;
});
// update the colors to a different color scale (colorScale1)
function updateColor1() {
// fill the legend rectangle
svgLegend.select('.legendRect')
.style('fill', 'url(#linear-gradient1)');
// transition the cell colors
svg.selectAll('.cell')
.transition().duration(1000)
.style('fill', function(d, i) {
var col;
if(d.valuePol == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale1(d.val);
}
return col;
});
axisLegend.tickValues(colorScale1.domain());
legend.call(axisLegend);
}
// update the colors to a different color scale (colorScale2)
function updateColor2() {
// fill the legend rectangle
svgLegend.select('.legendRect')
.style('fill', 'url(#linear-gradient2)');
// transition the cell colors
svg.selectAll('.cell')
.transition().duration(1000)
.style('fill', function(d, i) {
var col;
if(d.valuePol == '') {
col = 'url(#pattern-stripes)';
}
else {
col = colorScale2(d.val);
}
return col;
});
axisLegend.tickValues(colorScale2.domain());
legend.call(axisLegend);
}
// start set-up
updateColor1();
var currentFill = '1';
#heatmap {
float: left;
background-color: whitesmoke;
}
.cell {
stroke: #E6E6E6;
stroke-width: 1px;
}
/**
* Legend linear.
*/
.legendTitle {
font-size: 15px;
fill: black;
font-weight: 12;
font-family: Consolas, courier;
}
#legendLin {
background-color: yellow;
}
.legendLinAxis path, .legendLinAxis line {
fill: none;
stroke: none;
shape-rendering: crispEdges;
}
.legendLinAxis text {
font-family: Consolas, courier;
font-size: 8pt;
fill: black;
}
.legendLinG .tick line {
stroke: black;
stroke-width: 1px;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="./style.css" media="screen"/>
</head>
<body>
<div id='heatmap'></div>
<div id='legend'></div>
<script src="./script.js"></script>
</body>
</html>
Ticks update:
Within the update functions (updateColor1, updateColor2), in addition to the update of the color scale gradient, we can also include the update of legend ticks (similar to how it was first initialized):
axisLegend.tickValues(colorScale1.domain());
legend.call(axisLegend);
Gradient update:
The creation of "abrupt gradients" is slightly different from the one of "linear gradients". Here is a slightly modified version of your linear-gradient2 threshold gradient:
var linearGradient2 = defs.append('linearGradient')
.attr('id', 'linear-gradient2')
.attr('x1', '0%').attr('y1', '0%')
.attr('x2', '100%').attr('y2', '0%')
.selectAll('stop')
.data([
{ offset: "0%", color: "lightgrey" },
{ offset: "95%", color: "lightgrey" },
{ offset: "95%", color: "grey" },
{ offset: "100%", color: "grey" }
])
.enter().append('stop')
.attr('offset', function(d) { return d.offset; })
.attr('stop-color', function(d) { return d.color; });
Or if the threshold gradient is to change, instead of hardcoding it, we can also get it from the defined domain and range:
function getGradient2data() {
// Duplicates elements of domain2:
var duplicatedDomain = domain2.reduce(function (res, current, index, array) { return res.concat([current, current]); }, []).slice(1, -1);
// Duplicates elements of range2:
var duplicatedRange = range2.slice(1).reduce(function (res, current, index, array) { return res.concat([current, current]); }, []);
// Zips both domain and range:
return duplicatedDomain.map( function(e, i) { return { "offset": e + "%", "color": duplicatedRange[i] }; [e, duplicatedRange[i]]; });
}
which produces:
[
{ offset: "0%", color: "lightgrey" },
{ offset: "95%", color: "lightgrey" },
{ offset: "95%", color: "grey" },
{ offset: "100%", color: "grey" }
]
My barchart draws fine when the page first loads.
But choose hour 2 from the drop-down, and it doesn't want to update to hour 2 data, it just keeps displaying hour 1.
FIDDLE
This is my d3 and js:
$('#bar_chart').css('overflow-x','scroll');
var margin = {top: 20, right: 20, bottom: 40, left: 80},
width = 220 - margin.left - margin.right,
height = 233 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1, 1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
var formatComma = d3.format('0,000');
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.ticks(5)
.outerTickSize(0)
.tickFormat(formatComma);
var svg = d3.select('th.chart-here').append('svg')
.attr('viewBox', '0 0 220 233')
.attr('preserveAspectRatio','xMinYMin meet')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left/1.5 + ',' + margin.top/1.5 + ')');
var table_i = 0;
var arr1 =
[
{'hour':1,'car':[{'audi':1377},{'bmw':716},{'ford':3819},{'mazda':67},{'toyota':11580},{'tesla':0}]},
{'hour':2,'car':[{'audi':9000},{'bmw':2000},{'ford':7000},{'mazda':1000},{'toyota':5000},{'tesla':700}]},
];
var hour = arr1[table_i];
var car=hour.car;
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
var data = newobj;
}
}
x.domain(data.map(function(d) { return d.car; }));
y.domain([0, d3.max(data, function(d) { return d.miles; })]);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'start');
function changeHour(){
svg.selectAll('.bar')
.data(data)
.enter().append('rect')
.attr('class', 'bar')
.attr('transform','translate(-20)') //move rects closer to Y axis
.attr('x', function(d) { return x(d.car); })
.attr('width', x.rangeBand()*1)
.attr('y', function(d) { return y(d.miles); })
.attr('height', function(d) { return height - y(d.miles); });
xtext = svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(-20,' + height + ')') //move tick text so it aligns with rects
.call(xAxis);
xtext.selectAll('text')
.attr('transform', function(d) {
return 'translate(' + this.getBBox().height*50 + ',' + this.getBBox().height + ')rotate(0)';
});
//code to enable jqm checkbox
$('#checkbox-2a').on('change', function(e){
originalchange(e);
});
$( '#checkbox-2a' ).checkboxradio({
defaults: true
});
var sortTimeout = setTimeout(function() {
$('#checkbox-2a').prop('checked', false).checkboxradio( 'refresh' ).change();
}, 1000);
function originalchange() {
clearTimeout(sortTimeout);
var IsChecked = $('#checkbox-2a').is(':checked');
// Copy-on-write since tweens are evaluated after a delay.
var x0 = x.domain(data.sort(IsChecked
? function(a, b) { return b.miles - a.miles; }
: function(a, b) { return d3.ascending(a.car, b.car); })
.map(function(d) { return d.car; }))
.copy();
svg.selectAll('.bar')
.sort(function(a, b) { return x0(a.car) - x0(b.car); });
var transition = svg.transition().duration(950),
delay = function(d, i) { return i * 50; };
transition.selectAll('.bar')
.delay(delay)
.attr('x', function(d) { return x0(d.car); });
transition.select('.x.axis')
.call(xAxis)
.selectAll('g')
.delay(delay);
};
}
changeHour();
$('select').change(function() { //function to change hourly data
table_i = $(this).val();
var hour = arr1[table_i];
var car=hour.car;
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
var data = newobj;
}
}
x.domain(data.map(function(d) { return d.car; }));
y.domain([0, d3.max(data, function(d) { return d.miles; })]);
changeHour();
})
I thought that by updating in the function changeHour I could isolate just the rects and the text that goes with them, and redraw them based on the selected hour's data.
But it just keeps drawing the first hour.
What am I doing wrong?
2 things not working:
firstly "data" needs to be declared without 'var' in the change function at the end. Declaring it with 'var' makes it a local variable to that function, and once you leave that function it's gone. Saying "data = " without the var means you're using the data variable you've declared further up. It's all to do with scope which is something I still struggle with, but basically with 'var' it doesn't work.
var newobj = [];
for(var hourx1=0;hourx1<car.length;hourx1++){
var xx = car[hourx1];
for (var value in xx) {
var chartvar = newobj.push({car:value,miles:xx[value]});
}
}
data = newobj;
Secondly, your changeHour function only looks for new elements as it hangs all its attribute settings on an .enter() selection, changeHour should be like this:
var dataJoin = svg.selectAll('.bar')
.data(data, function(d) { return d.car; });
// possible new elements, fired first time, set non-data dependent attributes
dataJoin
.enter()
.append('rect')
.attr('class', 'bar')
.attr('transform','translate(-20)') //move rects closer to Y axis
// changes to existing elements (now including the newly appended elements from above) which depend on data values (d)
dataJoin
.attr('x', function(d) { return x(d.car); })
.attr('width', x.rangeBand()*1)
.attr('y', function(d) { return y(d.miles); })
.attr('height', function(d) { return height - y(d.miles); });
For completeness there should be a dataJoin.exit().remove() in there as well but its not something that happens in this dataset
I've created a multi-series line chart with tooltips. Please check it out at this JSFiddle.
Currently, the tooltip works as expected: when you hover over a circle, it shows a tooltip with the data's value at that point.
var tooltip = d3.tip()
.attr('class', 'tooltip')
.offset([-10, 0])
.html(function (d) {
return '<strong>Population ' + (d.date).getFullYear() + ':</strong> ' + format(d.population) + ' people';
});
svg.call(tooltip);
...
city.selectAll('.circle')
.data( function(d) { return(d. values); } )
.enter()
.append('svg:circle')
.attr('class', 'circle')
.attr('cx', function (d, i) {
return x(d.date);
})
.attr('cy', function (d, i) {
return y(d.population);
})
.attr('r', 5)
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide)
However, I would like to open tooltips at all data points with that x-value. So it would look like this:
Of course with the correct values for those points.
How could I modify my code to achieve this? Any help is greatly appreciated!
How about:
var tooltip = d3.tip()
.attr('class', 'tooltip')
.offset([-10, 0])
.html(function (d) {
for (var i = 0; i < data.length; i++){
if (data[i].date === d.date){
return '<strong>Population ' + (d.date).getFullYear() + '</strong><br/> City 1: ' + format(data[i].City1) + ' people <br/> City 2: ' + format(data[i].City2) + ' people <br/> City 4: ' + format(data[i].City3) + ' people <br/> City 4: ' + format(data[i].City4) + ' people';
}
}
});
Full code (fiddle here):
var myData = "date,City1,City2,City3,City4\n\
20100101,85328,19658,33384,37822\n\
20110101,73527,20295,31127,37130\n\
20120101,71092,20394,31038,34788\n\
20130101,75263,19520,30751,33868";
var margin = { top: 50, right: 50, bottom: 50, left: 50 },
width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var format = d3.format(',.');
var parseDate = d3.time.format('%Y%m%d').parse;
var x = d3.time.scale()
.range([0, width]);
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
var y = d3.scale.linear()
.range([height, 0]);
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.tickFormat(format);
var color = d3.scale.category10();
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.population); });
var svg = d3.select('body').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var tooltip = d3.tip()
.attr('class', 'tooltip')
.offset([-10, 0])
.html(function (d) {
for (var i = 0; i < data.length; i++){
if (data[i].date === d.date){
return '<strong>Population ' + (d.date).getFullYear() + '</strong><br/> City 1: ' + format(data[i].City1) + ' people <br/> City 2: ' + format(data[i].City2) + ' people <br/> City 4: ' + format(data[i].City3) + ' people <br/> City 4: ' + format(data[i].City4) + ' people';
}
}
});
svg.call(tooltip);
var data = d3.csv.parse(myData);
color.domain(d3.keys(data[0]).filter(function(key) { return (key !== 'date'); }));
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var cities = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {date: d.date, population: +d[name]};
})
};
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([
0, d3.max(cities, function(c) { return d3.max(c.values, function(v) { return v.population; }); })
]);
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
var city = svg.selectAll('.region')
.data(cities)
.enter().append('g')
.attr('class', 'region');
city.append('path')
.attr('class', 'line')
.attr('d', function(d) { return line(d.values); })
.style('stroke', function(d) { return color(d.name); });
city.selectAll('.circle')
.data( function(d) { return(d. values); } )
.enter()
.append('svg:circle')
.attr('class', 'circle')
.attr('cx', function (d, i) {
return x(d.date);
})
.attr('cy', function (d, i) {
return y(d.population);
})
.attr('r', 5)
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide)
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke: #444;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://rawgit.com/Caged/d3-tip/master/index.js"></script>