Legend that updates at the click of the mouse - d3.js

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" }
]

Related

How to get day of week on left side of D3 calendar heat map

I'm trying to create a calendar heatmap with D3, very similar to the Github contribution calendar.
I can't get the day of week to align correctly. It seems to repeat for every month and doesn't have correct margins or alignment. I only want the days to display once, on the left side of the calendar.
Just like this:
Here is what mine looks like:
Here is my code:
<style>
#calendar {
margin: 20px;
}
.month {
margin-right: 8px;
}
.month-name {
font-size: 85%;
fill: #777;
font-family: Muli, san-serif;
}
.day.hover {
stroke: #6d6E70;
stroke-width: 2;
}
.day.focus {
stroke: #ffff33;
stroke-width: 2;
}
</style>
<div style="text-align:center;" id="calendar"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script>
function drawCalendar(dateData){
var weeksInMonth = function(month){
var m = d3.timeMonth.floor(month)
return d3.timeWeeks(d3.timeWeek.floor(m), d3.timeMonth.offset(m,1)).length;
}
//var minDate = new Date(2018, 12, 31);
var minDate = d3.min(dateData, function(d) { return new Date(2018, 12, 1 ) });
//var minDate = d3.min(dateData, function(d) { return new Date(d.day) });
console.log(minDate);
//var maxDate = new Date(2019, 11, 30);
var maxDate = d3.max(dateData, function(d) { return new Date(2019, 11, 30 ) });
console.log(maxDate);
var cellMargin = 2,
calY=10,//offset of calendar in each group
xOffset=-5,
dayName = ['Su','Mo','Tu','We','Th','Fr','Sa'],
cellSize = 20;
var day = d3.timeFormat("%w"),
week = d3.timeFormat("%U"),
format = d3.timeFormat("%Y-%m-%d"),
titleFormat = d3.utcFormat("%a, %d-%b"),
monthName = d3.timeFormat("%B"),
months= d3.timeMonth.range(d3.timeMonth.floor(minDate), maxDate);
var svg = d3.select("#calendar").selectAll("svg")
.data(months)
.enter().append("svg")
.attr("class", "month")
.attr("height", ((cellSize * 7) + (cellMargin * 8) + 20) ) // the 20 is for the month labels
.attr("width", function(d) {
var columns = weeksInMonth(d);
return ((cellSize * columns) + (cellMargin * (columns + 1)));
})
.append("g")
svg.append("text")
.attr("class", "month-name")
.attr("y", (cellSize * 7) + (cellMargin * 8) + 15 )
.attr("x", function(d) {
var columns = weeksInMonth(d);
return (((cellSize * columns) + (cellMargin * (columns + 1))) / 2);
})
.attr("text-anchor", "middle")
.text(function(d) { return monthName(d); })
//create day labels
var days = ['Su','Mo','Tu','We','Th','Fr','Sa'];
var dayLabels=svg.append("g").attr("id","dayLabels")
days.forEach(function(d,i) {
dayLabels.append("text")
.attr("class","dayLabel")
.attr("x",xOffset)
.attr("y",function(d) { return calY+(i * cellSize); })
.text(d);
})
var rect = svg.selectAll("rect.day")
.data(function(d, i) { return d3.timeDays(d, new Date(d.getFullYear(), d.getMonth()+1, 1)); })
.enter().append("rect")
.attr("class", "day")
.attr("width", cellSize)
.attr("height", cellSize)
.attr("rx", 3).attr("ry", 3) // rounded corners
.attr("fill", '#eaeaea') // default light grey fill
.attr("y", function(d) { return (day(d) * cellSize) + (day(d) * cellMargin) + cellMargin; })
.attr("x", function(d) { return ((week(d) - week(new Date(d.getFullYear(),d.getMonth(),1))) * cellSize) + ((week(d) - week(new Date(d.getFullYear(),d.getMonth(),1))) * cellMargin) + cellMargin ; })
.on("mouseover", function(d) {
d3.select(this).classed('hover', true);
})
.on("mouseout", function(d) {
d3.select(this).classed('hover', false);
})
.datum(format);
rect.append("title")
.text(function(d) { return titleFormat(new Date(d)); });
var lookup = d3.nest()
.key(function(d) { return d.day; })
.rollup(function(leaves) {
return d3.sum(leaves, function(d){ return parseInt(d.count); });
})
.object(dateData);
var scale = d3.scaleLinear()
.domain(d3.extent(dateData, function(d) { return parseInt(d.count); }))
.range([0.2,1]); // the interpolate used for color expects a number in the range [0,1] but i don't want the lightest part of the color scheme
rect.filter(function(d) { return d in lookup; })
.style("fill", function(d) { return d3.interpolateYlGn(scale(lookup[d])); })
.select("title")
.text(function(d) { return titleFormat(new Date(d)) + ": " + lookup[d]; });
}
d3.csv("dates.csv", function(response){
drawCalendar(response);
})
</script>
There is also an input csv file that contains the following values:
day,count
2019-05-12,171
2019-06-17,139
2019-05-02,556
2019-04-10,1
2019-05-04,485
2019-03-27,1
2019-05-26,42
2019-05-25,337
2019-05-23,267
2019-05-05,569
2019-03-31,32
2019-03-25,128
2019-05-13,221
2019-03-30,26
2019-03-15,3
2019-04-24,10
2019-04-27,312
2019-03-20,99
2019-05-10,358
2019-04-01,15
2019-05-11,199
2019-07-06,744
2019-05-08,23
2019-03-28,98
2019-03-29,64
2019-04-30,152
2019-03-21,148
2019-03-19,20
2019-05-07,69
2019-04-29,431
2019-04-25,330
2019-04-28,353
2019-04-18,9
2019-01-10,1
2019-01-09,2
2019-03-26,21
2019-05-27,18
2019-04-19,10
2019-04-06,1
2019-04-12,214
2019-05-03,536
2019-07-03,3
2019-06-16,1
2019-03-24,138
2019-04-26,351
2019-04-23,14
2019-05-01,19
2019-07-05,523
2019-05-22,3
2019-05-09,430
2019-05-24,472
2019-04-11,172
2019-03-17,7
2019-05-14,10
2019-05-06,449
2019-07-04,295
2019-05-15,12
2019-03-23,216
2019-03-18,47
2019-03-22,179
Typically you allow for a margin in your SVG, something like this:
const margin = { top: 10, right: 20, bottom: 10, left: 5 }
const svg = d3
.select('#chart')
.append('svg')
.attr('width', 900 + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
Basically you create an SVG element that is bigger than your drawing area, and then you move (translate) the chart in by the margins. Then your axis can appear in the margin

D3 - flare not showing all data

Here is the code I'm working with. I'm generating data in php and sending that to d3 via json:
php file:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
stroke: #fff;
}
</style>
<body>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"> </script>
<script type="text/javascript" src="flare.js"></script>
<?php
// Move php data to JSON to be used in d3 apps
$flare_child_1 = array("name"=> "subchild1", "size"=> 90);
$flare_child_2 = array("name"=> "subchild2", "size"=> 10);
$flare_child_3 = array("name"=> "subchild3", "size"=> 55);
$flare_child_4 = array("name"=> "subchild4", "size"=> 72);
$flare_child_5 = array("name"=> "subchild5", "size"=> 60);
$flare_children_1[] = $flare_child_1;
$flare_children_1[] = $flare_child_2;
$flare_children_1[] = $flare_child_3;
$flare_children_1[] = $flare_child_4;
$flare_children_1[] = $flare_child_5;
$flare_children[] = array('name'=> "first", 'children'=>$flare_children_1);
$flare = array('name'=> "flare", 'children'=>$flare_children);
echo "<script> var root = "; echo json_encode($flare); echo ";";
echo "input_data(root);</script>";
?>
</body>
js file
var width = 960,
height = 700,
radius = (Math.min(width, height) / 2) - 10;
var formatNumber = d3.format(",d");
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.sqrt()
.range([0, radius]);
var color = d3.scale.category20c();
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI,x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI,x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
function getRootmostAncestorByRecursion(node) {
return node.depth > 1 ? getRootmostAncestorByRecursion(node.parent) : node;
}
function input_data(root) {
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
svg.selectAll("path")
.data(partition.nodes(root))
.enter().append("path")
.attr("d", arc)
.style("fill", function(d) {
return color(getRootmostAncestorByRecursion(d).name);
})
.on("click", click)
.append("title")
.text(function(d) {
return d.name + "\n" + formatNumber(d.value);
});
}
function click(d) {
svg.transition()
.duration(750)
.tween("scale", function() {
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); };
})
.selectAll("path")
.attrTween("d", function(d) { return function() { return arc(d);}; });
}
d3.select(self.frameElement).style("height", height + "px");
I'm expecting something similar to this https://bl.ocks.org/mbostock/raw/4348373/
but all I'm seeing a blue spot. The children don't feature. I'm not sure what I'm doing wrong.
Thanks for looking at this.
Couple things wrong here.
First, while you are correct path creation is wrapped in a function and will execute after the php code but your svg creation will execute before the body tag, so you'll never get an svg tag.
Second, your JSON is malformed. I executed the php and it produces:
<script>
var root = {
"name": "flare",
"children": {
"name": "first",
"children": [{
"name": "subchild1",
"size": 90
}, {
"name": "subchild2",
"size": 10
}, {
"name": "subchild3",
"size": 55
}, {
"name": "subchild4",
"size": 72
}, {
"name": "subchild5",
"size": 60
}]
}
};
input_data(root);
Notice, that the first children is an object and not an array of objects.
Putting these two things together here.

How can I refactor a d3 pie to accept more or less data points?

I have a project that almost works the way I want. When a smaller dataset is added, slices are removed. It fails when a larger dataset is added. The space for the arc is added but no label or color is added for it.
This is my enter() code:
newArcs.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc);
What am I doing wrong?
I've fixed the code such that it works now:
// Tween Function
var arcTween = function(a) {
var i = d3.interpolate(this.current || {}, a);
this.current = i(0);
return function(t) {
return arc(i(t));
};
};
// Setup all the constants
var duration = 500;
var width = 500
var height = 300
var radius = Math.floor(Math.min(width / 2, height / 2) * 0.9);
var colors = ["#d62728", "#ff9900", "#004963", "#3497D3"];
// Test Data
var d2 = [{
label: 'apples',
value: 20
}, {
label: 'oranges',
value: 50
}, {
label: 'pears',
value: 100
}];
var d1 = [{
label: 'apples',
value: 100
}, {
label: 'oranges',
value: 20
}, {
label: 'pears',
value: 20
}, {
label: 'grapes',
value: 20
}];
// Set the initial data
var data = d1
var updateChart = function(dataset) {
arcs = arcs.data(donut(dataset), function(d) { return d.data.label });
arcs.exit().remove();
arcs.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", arc);
arcs.transition()
.duration(duration)
.attrTween("d", arcTween);
sliceLabel = sliceLabel.data(donut(dataset), function(d) { return d.data.label });
sliceLabel.exit().remove();
sliceLabel.enter()
.append("text")
.attr("class", "arcLabel")
.attr("transform", function(d) {
return "translate(" + (arc.centroid(d)) + ")";
})
.attr("text-anchor", "middle")
.style("fill-opacity", function(d) {
if (d.value === 0) {
return 1e-6;
} else {
return 1;
}
})
.text(function(d) {
return d.data.label;
});
sliceLabel.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + (arc.centroid(d)) + ")";
})
.style("fill-opacity", function(d) {
if (d.value === 0) {
return 1e-6;
} else {
return 1;
}
});
};
var color = d3.scale.category20();
var donut = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.innerRadius(radius * .4)
.outerRadius(radius);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var arc_grp = svg.append("g")
.attr("class", "arcGrp")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
var label_group = svg.append("g")
.attr("class", "lblGroup")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
var arcs = arc_grp.selectAll("path");
var sliceLabel = label_group.selectAll("text");
updateChart(data);
// returns random integer between min and max number
function getRand() {
var min = 1,
max = 2;
var res = Math.floor(Math.random() * (max - min + 1) + min);
//console.log(res);
return res;
}
// Update the data
setInterval(function(model) {
var r = getRand();
return updateChart(r == 1 ? d1 : d2);
}, 2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

d3.js not updating graph with new data

I am trying to update d3.js based graph with some random data after each 3 seconds, but it only renders the data first time. Here is the code
function drawGraph(data) {
var areaWidth = 200;
var areaHeight = 200;
var gdpData = data;
var leftMargin = 10;
var rightMargin = 10;
var maxGDP = Math.max.apply(Math, gdpData);
var myScale = d3.scaleLinear()
.domain([0, maxGDP])
.range([0, areaWidth - leftMargin - rightMargin]);
var spacing = areaHeight/(gdpData.length+3);
var fda = d3.select('#fun-drawing-area');
var recs = fda.selectAll('rect')
.data(gdpData);
recs.exit().remove();
recs.enter()
.append('rect')
.attr('y', function(d, i) {
return spacing * (i+1);
})
.attr('x', '10')
.attr('width', function(d, i) {
return myScale(d);
})
.attr('height', '20')
.style({
fill: 'steelblue',
stroke: 'black',
'stroke-width': 1
});
}
setInterval(function() {
var arr = [];
for (var i=0, t=5; i<t; i++) {
arr.push(Math.round(Math.random() * t))
}
console.log(arr);
drawGraph(arr);
}, 3000);
here is the plunkr https://plnkr.co/edit/OfFABbrZ6Teet6atLQD0?p=preview
In the new D3 4.x, you have to merge the selections:
recs.enter()
.append('rect')
.merge(recs)
.attr('y', function(d, i) {
return spacing * (i+1);
})
.attr('x', '10')
.attr('width', function(d, i) {
return myScale(d);
})
.attr('height', '20')
.style({
fill: 'steelblue',
stroke: 'black',
'stroke-width': 1
});
Here's your plunker: https://plnkr.co/edit/kaFCLcUKNqSiJkpOCHvS?p=preview
PS: I changed setTimeout to setInterval.

Delete a line when ticks are clicked - d3js

I have two y-axis with time as data.
I am trying to add and delete a line when ticks are clicked on the respective axis.
Lines are getting generated but not sure how to remove the lines. i tried using
svg.data([thisData]).remove('line')
but that removes the chart completely.
MORE DETAILS
there is 1-1 relationship between ticks of respective axis.
var data = [{
"inTime": "2013-04-24T00:00:00-05:00",
"outTime": "2013-04-24T00:00:00-05:00"
}, {
"inTime": "2013-04-24T00:00:00-05:00",
"outTime": "2013-04-24T00:00:00-05:00"
}, {
"inTime": "2013-04-24T00:00:00-05:00",
"outTime": "2013-04-24T00:00:00-05:00"
}, {
"inTime": "2013-04-26T00:00:00-05:00",
"outTime": "2013-04-26T00:00:00-05:00"
},
];
var margin = {
top: 40,
right: 40,
bottom: 40,
left: 40
},
width = 600,
height = 700;
//Define Left Y axis
var y = d3.time.scale()
.domain([new Date(data[0].inTime), d3.time.day.offset(new Date(data[data.length - 1].inTime), 1)])
.rangeRound([0, width - margin.left - margin.right]);
//Define Right Y axis
var y1 = d3.time.scale()
.domain([new Date(data[1].inTime), d3.time.day.offset(new Date(data[data.length - 1].outTime), 1)])
.rangeRound([0, width - margin.left - margin.right]);
//Left Yaxis attributes
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.tickFormat(d3.time.format('%m/%d %H:%M'))
.tickSize(8)
.tickPadding(8);
//Right Yaxis attributes
var yAxisRight = d3.svg.axis()
.scale(y1)
.orient('right')
.tickFormat(d3.time.format('%m/%d %H:%M'))
.tickSize(8)
.tickPadding(8);
//Create chart
var svg = d3.select('body').append('svg')
.attr('class', 'chart')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
//Add left Yaxis to group
svg.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(100, 5)')
.call(yAxis);
//Add right Yaxis to group
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(400, 1)')
.call(yAxisRight);
var parse = d3.time.format('%m/%d %H:%M');
//Function to add a line between two ticks
function addLine(t1, t2) {
var ticks = {};
d3.selectAll('.tick text').each(function(d) {
ticks[d3.select(this).text()] = this;
});
var pos1 = ticks[t1].getBoundingClientRect();
var pos2 = ticks[t2].getBoundingClientRect();
svg.append('line')
.attr('x1', pos1.top - pos1.width)
.attr('y1', pos1.top + 5)
.attr('x2', pos2.left - 5)
.attr('y2', pos2.top + 5)
.style('stroke', 'black')
}
var ticks = svg.selectAll(".tick");
ticks.attr('class', function(d, i) {
return 'ticks' + i;
}).each(function(d, i) {
d3.select(this).append("circle")
.attr('id', function(d) {
return 'tickCircle' + i;
})
.attr('class', function(d) {
return 'tickCircles' + this.id
})
.attr("r", 5)
.on('click', function(d) {
console.log('clicked')
return addLineNew(this);
})
.on('mouseover', function(d){
d3.select(this).style('fill','red'); })
.on('mouseout', function(d){
d3.select(this).style('fill','black'); })
});
ticks.selectAll("line").remove();
var firstTick;
var secondTick;
var secondTickMap={};
var firstTickMap={};
var allLines=[];
//add Line
function addLineNew(element) {
if (firstTick && secondTick) {
firstTick = '';
secondTick = '';
}
if (!firstTick || firstTick === '') {
firstTick = element.id
}
else if ((secondTick != 'undefined' || secondTick === '') && !(secondTick in firstTickMap)) {
secondTick = element.id
}
if (firstTick && secondTick) {
if(firstTick == secondTick){
if(firstTick in firstTickMap){delete firstTickMap.firstTick;}
else if(firstTick in secondTickMap){delete secondTickMap.firstTick;}
if(secondTick in firstTickMap){delete firstTickMap.secondTick;}
else if(secondTick in secondTickMap ){delete secondTickMap.secondTick;}
}
if(!(firstTick in firstTickMap) && !(secondTick in secondTickMap) && !(firstTick in secondTickMap) && !(secondTick in firstTickMap))
{
var firstTickBBox = getBBox(firstTick)
var secondTickBBox = getBBox(secondTick);
var firstTickPos = getCenterPoint(firstTickBBox);
var secondTickPos = getCenterPoint(secondTickBBox);
firstTickMap[firstTick] = firstTick;
secondTickMap[secondTick] = secondTick;
createLine(firstTickPos, secondTickPos)
}
}
}
//get Center Point
function getCenterPoint(element) {
var thisX = element.left + element.width / 2;
var thisY = element.top + element.height / 2;
return [thisX, thisY]
}
function getBBox(element) {
var thisEl = document.getElementById(element).getBoundingClientRect();
return thisEl;
}
//create a line between pointA and pointB
function createLine(pointA, pointB) {
var thisData = {
x1: pointA[0],
y1: pointA[1],
x2: pointB[0],
y2: pointB[1]
};
allLines.push(svg.data([thisData]).append('line')
.attr('x1', function(d) {
console.log(d)
return d.x1;
})
.attr('y1', function(d) {
return d.y1;
})
.attr('x2', function(d) {
return d.x2;
})
.attr('y2', function(d) {
return d.y2;
}).style('stroke', 'black')
.style('stroke-width','1')
.attr('transform', 'translate(' + (-margin.left - 5) + ', ' + (- margin.top - 5) + ')'));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
First off lets clear a couple of things up. When you create the line you push your array of lines to an array like so :
allLines.push(svg.data([thisData]).append('line')...
This is not the correct way to do it. The best way to do it is, when you create a line, push that lines data, e.g x1,y1,x2,y2 etc to an array and use this array to create all the lines at once. This is how D3 works.
So I changed your functions around.
function createLine(pointA, pointB) {
var thisData = {
x1: pointA[0],
y1: pointA[1],
x2: pointB[0],
y2: pointB[1]
};
allLinesData.push(thisData) //push points into array
drawLines(allLinesData); //draw all lines at once from 'allLinesData'
}
Function to draw lines :
function drawLines(data) { //pass the data you want
var line = svg.selectAll('.line').data(data);
line.enter().append('line')
.attr('id', function(d, i) {return 'genLine' + i; })
.attr('x1', function(d) { return d.x1;})
.attr('y1', function(d) { return d.y1; })
.attr('x2', function(d) { return d.x2; })
.attr('y2', function(d) { return d.y2; })
.style('stroke', 'black')
.style('stroke-width', '3')
.on('mouseover', function(d) { d3.select(this).style('stroke', 'red') })
.on('mouseout', function(d) { d3.select(this).style('stroke', 'black') })
.attr('transform', 'translate(' + (-margin.left - 5) + ', ' + (-margin.top - 5) + ')')
line.on('dblclick', function(d) { //delete line
var thisLine = this;
line.each(function(e, i) {
var thisLine2 = this;
if (thisLine.id === thisLine2.id) {
console.log('splice')
allLinesData.splice(i--, 1); //remove from array you use to feed the line drawer
d3.select(this).remove(); //remove it from DOM
}
})
})
line.exit().remove(); //remove unwanted lines
}
Also added on 'mouseover' so you know what line youre on.
Here is a working fiddle : https://jsfiddle.net/reko91/vr09w905/1/
Also if you just want it here :
var data = [{
"inTime": "2013-04-24T00:00:00-05:00",
"outTime": "2013-04-24T00:00:00-05:00"
}, {
"inTime": "2013-04-24T00:00:00-05:00",
"outTime": "2013-04-24T00:00:00-05:00"
}, {
"inTime": "2013-04-24T00:00:00-05:00",
"outTime": "2013-04-24T00:00:00-05:00"
}, {
"inTime": "2013-04-26T00:00:00-05:00",
"outTime": "2013-04-26T00:00:00-05:00"
}, ];
var margin = {
top: 40,
right: 40,
bottom: 40,
left: 40
},
width = 600,
height = 700;
//Define Left Y axis
var y = d3.time.scale()
.domain([new Date(data[0].inTime), d3.time.day.offset(new Date(data[data.length - 1].inTime), 1)])
.rangeRound([0, width - margin.left - margin.right]);
//Define Right Y axis
var y1 = d3.time.scale()
.domain([new Date(data[1].inTime), d3.time.day.offset(new Date(data[data.length - 1].outTime), 1)])
.rangeRound([0, width - margin.left - margin.right]);
//Left Yaxis attributes
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.tickFormat(d3.time.format('%m/%d %H:%M'))
.tickSize(8)
.tickPadding(8);
//Right Yaxis attributes
var yAxisRight = d3.svg.axis()
.scale(y1)
.orient('right')
.tickFormat(d3.time.format('%m/%d %H:%M'))
.tickSize(8)
.tickPadding(8);
//Create chart
var svg = d3.select('body').append('svg')
.attr('class', 'chart')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
//Add left Yaxis to group
svg.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(100, 5)')
.call(yAxis);
//Add right Yaxis to group
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(400, 1)')
.call(yAxisRight);
var parse = d3.time.format('%m/%d %H:%M');
//Function to add a line between two ticks
function addLine(t1, t2) {
var ticks = {};
d3.selectAll('.tick text').each(function(d) {
ticks[d3.select(this).text()] = this;
});
var pos1 = ticks[t1].getBoundingClientRect();
var pos2 = ticks[t2].getBoundingClientRect();
svg.append('line')
.attr('x1', pos1.top - pos1.width)
.attr('y1', pos1.top + 5)
.attr('x2', pos2.left - 5)
.attr('y2', pos2.top + 5)
.style('stroke', 'black')
}
var ticks = svg.selectAll(".tick");
ticks.attr('class', function(d, i) {
return 'ticks' + i;
}).each(function(d, i) {
d3.select(this).append("circle")
.attr('id', function(d) {
return 'tickCircle' + i;
})
.attr('class', function(d) {
return 'tickCircles' + this.id
})
.attr("r", 5)
.on('click', function(d) {
console.log('clicked')
return addLineNew(this);
})
.on('mouseover', function(d) {
d3.select(this).style('fill', 'red');
})
.on('mouseout', function(d) {
d3.select(this).style('fill', 'black');
})
});
ticks.selectAll("line").remove();
var firstTick;
var secondTick;
var secondTickMap = {};
var firstTickMap = {};
//var allLines = [];
var allLinesData = [];
//add Line
function addLineNew(element) {
if (firstTick && secondTick) {
firstTick = '';
secondTick = '';
}
if (!firstTick || firstTick === '') {
firstTick = element.id
} else if ((secondTick != 'undefined' || secondTick === '') && !(secondTick in firstTickMap)) {
secondTick = element.id
}
if (firstTick && secondTick) {
if (firstTick == secondTick) {
if (firstTick in firstTickMap) {
delete firstTickMap.firstTick;
} else if (firstTick in secondTickMap) {
delete secondTickMap.firstTick;
}
if (secondTick in firstTickMap) {
delete firstTickMap.secondTick;
} else if (secondTick in secondTickMap) {
delete secondTickMap.secondTick;
}
}
if (!(firstTick in firstTickMap) && !(secondTick in secondTickMap) && !(firstTick in secondTickMap) && !(secondTick in firstTickMap)) {
var firstTickBBox = getBBox(firstTick)
var secondTickBBox = getBBox(secondTick);
var firstTickPos = getCenterPoint(firstTickBBox);
var secondTickPos = getCenterPoint(secondTickBBox);
firstTickMap[firstTick] = firstTick;
secondTickMap[secondTick] = secondTick;
createLine(firstTickPos, secondTickPos)
}
}
}
//get Center Point
function getCenterPoint(element) {
var thisX = element.left + element.width / 2;
var thisY = element.top + element.height / 2;
return [thisX, thisY]
}
function getBBox(element) {
var thisEl = document.getElementById(element).getBoundingClientRect();
return thisEl;
}
//create a line between pointA and pointB
function createLine(pointA, pointB) {
var thisData = {
x1: pointA[0],
y1: pointA[1],
x2: pointB[0],
y2: pointB[1]
};
allLinesData.push(thisData) //push points into array
drawLines(allLinesData); //draw all lines at once from 'allLinesData'
}
function drawLines(data) { //pass the data you want
var line = svg.selectAll('.line').data(data);
line.enter().append('line')
.attr('id', function(d, i) {
return 'genLine' + i;
})
.attr('x1', function(d) {
return d.x1;
})
.attr('y1', function(d) {
return d.y1;
})
.attr('x2', function(d) {
return d.x2;
})
.attr('y2', function(d) {
return d.y2;
})
.style('stroke', 'black')
.style('stroke-width', '3')
.on('mouseover', function(d) {
d3.select(this).style('stroke', 'red')
})
.on('mouseout', function(d) {
d3.select(this).style('stroke', 'black')
})
.attr('transform', 'translate(' + (-margin.left - 5) + ', ' + (-margin.top - 5) + ')')
line.on('dblclick', function(d) { //delete line
var thisLine = this;
line.each(function(e, i) {
var thisLine2 = this;
if (thisLine.id === thisLine2.id) {
console.log('splice')
allLinesData.splice(i--, 1); //remove from array you use to feed the line drawer
d3.select(this).remove(); //remove it from DOM
}
})
})
line.exit().remove(); //remove unwanted lines
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.10/d3.min.js"></script>

Resources