Related
I genuinely hate to ask this question, particularly as I know it has been asked dozens of times - and I've read through the posts. But my problem remains - I simply do not understand how this mechanism works. I am new to d3js, and am using v3.x in meteor; I've gone through tutorials and have gotten something working, but can't get it to update with new data. Again, my apologies for rehashing this, but none of the other posts I've read has resolved the issue.
Here is a code fragment, I've stripped out all the stuff that shouldn't make a difference to focus on the core functionality:
var w = 800;
var h = 800;
var intensity = 25;
var margin = {
top: 75,
right: 100,
bottom: 75,
left: 60
};
var svg = d3.select('#heatmap')
.append('svg')
.attr('width', w + margin.left + margin.right)
.attr('height', h + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// get csv data, x & y coords, etc...
createHeatmap = function(csv, x, y) {
var data = d3.csv.parseRows(csv).map(function(row) {
return row.map(function(d) {
return +d;
});
});
// set some values
var min = 0;
var max = d3.max(data, function(d, i) {
return i + 1;
});
var rectSize = 4;
// set the scales for both axes
...
// set up the axes
...
// define colorScale
...
// create heatmap
svg.selectAll('g')
.data(data)
.enter()
.append('g')
.selectAll('rect')
.data(function(d, i, j) {
return d;
})
.enter() // start drawing rects
.append('rect')
.attr('x', function(d, i, j) {
return (i * rectSize);
})
.attr('y', function(d, i, j) {
return (j * rectSize);
})
.attr('width', w / max)
.attr('height', h / max)
.style('fill', function(d, i, j) {
return colorScale(d * intensity);
});
// append axes, scales, labels, etc.
}
// create heatmap
createHeatmap(csv, x, y);
My problem is that I do not understand why the chart doesn't update the heatmap when I pass new data into createHeatmap().
I stepped through it in the debugger and everything works as I would expect during the initial creation of the heatmap, which renders correctly. When I send new data is when the mystery starts. The debugger shows, deep within d3js itself (not in my code) that the enter() has an array full od null values instead of the data I am passing in. The data exists up until that point. So, as d3js processes the null data it naturally returns an empty object so no update occurs.
Obviously I am not doing the update correctly but am clueless about what I need to do to correct it.
Any advise is greatly appreciated.
Thx!
Update:
Andrew, thanks for the response. I tried your first suggestion, modifying your example to fit my data, but it doesn't update with new data.
My changes:
var w = 800;
var h = 800;
var intensity = 25;
var margin = {
top: 75,
right: 100,
bottom: 75,
left: 60
};
var svg = d3.select('#heatmap')
.append('svg')
.attr('width', w + margin.left + margin.right)
.attr('height', h + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// get csv data, x & y coords, etc...
createHeatmap = function(csv, x, y) {
var data = d3.csv.parseRows(csv).map(function(row) {
return row.map(function(d) {
return +d;
});
});
// set some values
var min = 0;
var max = d3.max(data, function(d, i) {
return i + 1;
});
var rectSize = 4;
// set the scales for both axes
...
// set up the axes
...
// define colorScale
...
// append group of svg elements bound to data
var rows = svg.selectAll('g')
.data(data);
// enter new rows where needed
rows.enter().append('g');
// select all rects
var rects = rows.selectAll('rect')
.data(function(d, i, j) {
return d;
});
// enter new rects:
rects.enter().append('rect')
.attr('x', function(d, i, j) {
return (i * rectSize);
})
.attr('y', function(d, i, j) {
return (j * rectSize);
})
.attr('width', w / max)
.attr('height', h / max)
.style('fill', function(d, i, j) {
return colorScale(d * intensity);
});
Added snippet:
var csv = "'3, 6, 0, 8'\n'1, 9, 0, 4'\n'3, 0, 1, 8'\n'4, 0, 2, 7";
csv = csv.replace(/'/g,'');
var button = d3.select('button')
.on('click', function() {
createHeatmap(update());
});
var w = 120;
var h = 120;
var intensity = 10;
var margin = {
top: 25,
right: 25,
bottom: 25,
left: 25
};
var svg = d3.select('#heatmap')
.append('svg')
.attr('width', w + margin.left + margin.right)
.attr('height', h + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
createHeatmap(csv);
function createHeatmap(csv) {
console.log(csv);
var data = d3.csv.parseRows(csv).map(function(row) {
return row.map(function(d) {
return +d;
});
});
var min = 0;
var max = d3.max(data, function(d, i) {
return i + 1;
});
var rectSize = 30;
// define a colorScale with domain and color range
var colorScale = d3.scale.linear()
.domain([0,0.5,1])
.range(['red', 'green', 'blue']);
// append group of svg elements bound to data
var rows = svg.selectAll('g')
.data(data);
// enter new rows where needed
rows.enter().append('g');
// select all rects
var rects = rows.selectAll('rect')
.data(function(d, i, j) {
return d;
});
// enter new rects:
rects.enter().append('rect')
.attr('x', function(d, i, j) {
return (i * rectSize);
})
.attr('y', function(d, i, j) {
return (j * rectSize);
})
.attr('width', w / max)
.attr('height', h / max)
.style('fill', function(d, i, j) {
return colorScale(d * intensity);
});
}
function update() {
var data = "'0, 1, 9, 5'\n'4, 0, 7, 2'\n'6, 3, 0, 8'\n'5, 3, 7, 0";
data = data.replace(/'/g,'');
return data;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<button>Update</button>
<div id="heatmap"></div>
The issue is in your method chaining.
On first run, things should run as expected:
// create heatmap
svg.selectAll('g') // 1. select all g elements
.data(data) // 2. assign data
.enter() // 3. enter and append a g for each item in the data array
.append('g') // that doesn't have a corresponding element in the DOM (or more accurately, the selection)
.selectAll('rect') // 4. For each newly entered g, select child rectangles
.data(function(d, i, j) { // 5. assign data to child selection.
return d;
})
.enter() // 6. Enter and append a rect for each item in the child g's data array
.append("rect") // that doesn't have a corresponding element in the DOM.
.... // 7. Style
On that first run, we select all the gs, there are none, so the enter selection will have an element for each item in the data array: we are entering everything. Same as with the child rectangles: there are no child rectangles existing when you make the selection, so you enter everything in the child data array.
On the second run, with svg.selectAll("g"), you select all the gs you created the first time around - there is no need to enter anything if the data array has the same number of items. You don't want to append anything: enter().append() the second time (not that you are appending more elements with .append() in any event).
Essentially on the second pass you are modifying an empty selection.
Instead you want to update. While the enter selection is empty on the second pass, the update selection has all the existing gs.
There are a few methods to do this, one is to break your chaining:
This is a version 3 solution:
var rows = svg.selectAll("g")
.data(data);
// enter new rows where needed
rows.enter().append("g")
// Select all rects
var rects = rows.selectAll("rect")
.data(function(d) { return d; })
// Enter new rects:
rects.enter().append("rect")
// Update rects (all rects, not just the newly entered):
rects.attr()...
The below snippet uses this pattern, it enters new rects and gs as needed. And then updates all the rects and gs afterwards. This takes advantage of a magic in d3v3, where the update selection and the enter selection are merged internally, this is not the case in d3v4,v5, which I'll show below.
var button = d3.select("button")
.on("click", function() {
update(random());
})
var svg = d3.select("div")
.append("svg");
var color = d3.scale.linear()
.domain([0,0.5,1])
.range(["red","orange","yellow"])
update(random());
function update(data) {
var rows = svg.selectAll("g")
.data(data);
// enter new rows where needed
rows.enter()
.append("g")
.attr("transform", function(d,i) {
return "translate("+[0,i*22]+")";
})
// Select all rects:
var rects = rows.selectAll("rect")
.data(function(d) { return d; })
// Enter new rects:
rects.enter().append("rect")
// Update rects:
rects.attr("fill", function(d) {
return color(d);
})
.attr("x", function(d,i) { return i*22; })
.attr("width", 20)
.attr("height", 20);
console.log("entered rows:" + rows.enter().size());
console.log("entered rects:" + rects.enter().size());
}
function random() {
return d3.range(5).map(function() {
return d3.range(5).map(function() {
return Math.random();
})
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<button>Update</button>
<div></div>
v4/v5:
For v4/v5, which I suggest upgrading to, the pattern is a bit different as you have to explicitly merge the enter and update selections:
var button = d3.select("button")
.on("click", function() {
update(random());
})
var svg = d3.select("div")
.append("svg");
var color = d3.scaleLinear()
.domain([0,0.5,1])
.range(["red","orange","yellow"])
update(random());
function update(data) {
var rows = svg.selectAll("g")
.data(data);
// enter new rows where needed
rows = rows.enter()
.append("g")
.merge(rows) // merge with existing rows
.attr("transform", function(d,i) {
return "translate("+[0,i*22]+")";
})
// Select all rects:
var rects = rows.selectAll("rect")
.data(function(d) { return d; })
// Enter new rects:
rects = rects.enter().append("rect")
.merge(rects);
// Update rects:
rects.attr("fill", function(d) {
return color(d);
})
.attr("x", function(d,i) { return i*22; })
.attr("width", 20)
.attr("height", 20);
}
function random() {
return d3.range(5).map(function() {
return d3.range(5).map(function() {
return Math.random();
})
})
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Update</button>
<div></div>
Update
Your snippet almost incorporates the changes, but you still need to break up the second selection, that of the rectangles, so that you enter new rectangles and then update all of them:
var csv = "'3, 6, 0, 8'\n'1, 9, 0, 4'\n'3, 0, 1, 8'\n'4, 0, 2, 7";
csv = csv.replace(/'/g,'');
var button = d3.select('button')
.on('click', function() {
createHeatmap(update());
});
var w = 120;
var h = 120;
var intensity = 10;
var margin = {
top: 25,
right: 25,
bottom: 25,
left: 25
};
var svg = d3.select('#heatmap')
.append('svg')
.attr('width', w + margin.left + margin.right)
.attr('height', h + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
createHeatmap(csv);
function createHeatmap(csv) {
console.log(csv);
var data = d3.csv.parseRows(csv).map(function(row) {
return row.map(function(d) {
return +d;
});
});
var min = 0;
var max = d3.max(data, function(d, i) {
return i + 1;
});
var rectSize = 30;
// define a colorScale with domain and color range
var colorScale = d3.scale.linear()
.domain([0,0.5,1])
.range(['red', 'green', 'blue']);
// append group of svg elements bound to data
var rows = svg.selectAll('g')
.data(data);
// enter new rows where needed
rows.enter().append('g');
// select all rects
var rects = rows.selectAll('rect')
.data(function(d, i, j) {
return d;
});
// enter new rects:
rects.enter().append('rect');
// CHANGES HERE:
// Broke chain so that update actions aren't carried out on the enter selection:
rects.attr('x', function(d, i, j) {
return (i * rectSize);
})
.attr('y', function(d, i, j) {
return (j * rectSize);
})
.attr('width', w / max)
.attr('height', h / max)
.style('fill', function(d, i, j) {
return colorScale(d * intensity);
});
}
function update() {
var data = "'0, 1, 9, 5'\n'4, 0, 7, 2'\n'6, 3, 0, 8'\n'5, 3, 7, 0";
data = data.replace(/'/g,'');
return data;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<button>Update</button>
<div id="heatmap"></div>
var arcMin = 75; // inner radius of the first arc
var arcWidth = 25; // width
var arcPad = 10; // padding between arcs
var arc = d3.arc()
.innerRadius(function(d, i) {
return arcMin + i*(arcWidth) + arcPad;
})
.outerRadius(function(d, i) {
return arcMin + (i+1)*(arcWidth);
})
.startAngle(0 * (PI/180))
.endAngle(function(d, i) {
// console.log(d); <----getting undefine under attrTween Call
return 2*PI*d.value/100;
});
var path = g.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return d.data.color;
})
.transition()
.delay(function(d, i) {
return i * 800;
});
// .attrTween('d', function(d) {
// // This part make my chart disapear
// var i = d3.interpolate(d.startAngle, d.endAngle);
// return function(t) {
// d.endAngle = i(t);
// return arc(d);
// }
// // This part make my chart disapear
// });
arc(d) always return "M0,0Z"..
I found that the reason is when calling arc under arcTween, all d,i return undefine. How can i solve this.
Codes here: https://jsfiddle.net/m8oupfne/3/
Final product:
Couple things:
At first glance your attrTween function doesn't work because your arc function is dependent on both d,i and you only pass d to it.
But, fixing that doesn't make your chart transition nicely? Why? Because your arc function doesn't seem to make any sense. You use pie to calculate angles and then overwrite them in your arc function. And each call to the arc function calculates endAngle the same since it's based on d.value.
So, if you want a custom angle calculation, don't call pie at all, but pre-calculate your endAngle and don't do it in your arc function.
arc becomes:
var arc = d3.arc()
.innerRadius(function(d, i) {
return arcMin + i*(arcWidth) + arcPad;
})
.outerRadius(function(d, i) {
return arcMin + (i+1)*(arcWidth);
});
Pre-calculate the data:
dataset.forEach(function(d,i){
d.endAngle = 2*PI*d.value/100;
d.startAngle = 0;
});
arcTween becomes:
.attrTween('d', function(d,i) {
var inter = d3.interpolate(d.startAngle, d.endAngle);
return function(t) {
d.endAngle = inter(t);
return arc(d,i);
}
});
Running code:
(function(d3) {
'use strict';
var dataset = [
{ label: 'a', value: 88, color : '#898989'},
{ label: 'b', value: 56 , color : '#898989'},
{ label: 'c', value: 20 , color : '#FDD000'},
{ label: 'd', value: 46 , color : '#898989'},
];
var PI = Math.PI;
var arcMin = 75; // inner radius of the first arc
var arcWidth = 25; // width
var arcPad = 10; // padding between arcs
var arcBgColor = "#DCDDDD";
var width = 360;
var height = 360;
var radius = Math.min(width, height) / 2;
var donutWidth = 15; // NEW
var svg = d3.select('#canvas')
.append('svg')
.attr('width', width)
.attr('height', height);
var gBg = svg.append('g').attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var g = svg.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var arc = d3.arc()
.innerRadius(function(d, i) {
return arcMin + i*(arcWidth) + arcPad;
})
.outerRadius(function(d, i) {
return arcMin + (i+1)*(arcWidth);
});
var arcBg = d3.arc()
.innerRadius(function(d, i) {
return arcMin + i*(arcWidth) + arcPad;
})
.outerRadius(function(d, i) {
return arcMin + (i+1)*(arcWidth);
})
.startAngle(0 * (PI/180))
.endAngle(function(d, i) {
return 2*PI;
});
var pie = d3.pie()
.value(function(d) { return d.value; })
.sort(null);
var pathBg = gBg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arcBg)
.attr('fill', arcBgColor );
dataset.forEach(function(d,i){
d.endAngle = 2*PI*d.value/100;
d.startAngle = 0;
});
var path = g.selectAll('path')
.data(dataset)
.enter()
.append('path')
.attr('fill', function(d, i) {
return d.color;
})
.transition()
.duration(800)
.delay(function(d, i) {
return i * 800;
})
.attrTween('d', function(d,i) {
var inter = d3.interpolate(d.startAngle, d.endAngle);
return function(t) {
d.endAngle = inter(t);
return arc(d,i);
}
});
})(window.d3);
<script src="https://cdn.jsdelivr.net/jquery/2.1.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/d3js/4.6.0/d3.min.js"></script>
<div id="canvas"></div>
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'm trying to plot a pie chart with a legend inside of it. And I got into troubles to get it plotted, since I get the errors abound undefined variables. I managed to draw the chart itself and the half of the legend, but not in the right colors, what should match the pie chart.
function drawPieChart(d3div, chart_data) {
// chart_data.data is a list of data elements.
// each should contain fields: val, col, name
d3div.html(""); // clear the div
var title = getopt(chart_data, 'title', '');
// desired width and height of chart
var w = getopt(chart_data, 'width', 300);
var h = getopt(chart_data, 'height', 300);
var pad = getopt(chart_data, 'pad', 50);
var textmargin = getopt(chart_data, 'textmargin', 20);
var r = Math.min(w, h) / 2 - pad; // radius of pie chart
var div = d3div.append('div');
if(title !== '') {
div.append('p').attr('class', 'pietitle').text(title);
}
var arc = d3.svg.arc()
.outerRadius(r)
.cornerRadius(20)
.innerRadius(150);
var arcLarge = d3.svg.arc()
.innerRadius(150)
.cornerRadius(20)
.outerRadius(r + 50);
var toggleArc = function(p){
p.state = !p.state;
var dest = p.state ? arcLarge : arc;
d3.select(this).select("path").transition()
.duration(160)
.attr("d", dest);};
var pie = d3.layout.pie()
.padAngle(.03)
.sort(null)
.value(function(d) { return d.val; });
var svg = d3.select("#piechart").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
var g = svg.selectAll(".arc")
.data(pie(chart_data.data))
.enter().append("g")
.attr("class", "arc")
.attr("stroke", "#999")
.attr("id",function(d){return d.data;})
.on("mouseover",toggleArc)
.on("mouseout",toggleArc);
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return d.data.col; });
var color = d3.scale.category20b();
var legendRectSize = 18;
var legendSpacing = 4;
// FROM here the code is not produced the desired result
var legend = svg.selectAll('.legend')
.data(chart_data.data)
.enter()
.append('g')
.attr('class', 'legend')
.attr("id",function(d){return d.data;})
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * chart_data.data.length / 2;
var horz = -2 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.data(chart_data.data)
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style("fill", function(d) { return d.data.col; });
legend.append("text")
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d.data.name; });
}
The code actually works fine untill the line var legend = svg.selectAll('.legend')
Then i start to define the legend, but D3 complains about undefined d.data every time i try to access d.data below the line I written above(also in the last line of the code).
I don't understand where i got on the wrong way.
If instead of defining the whole non working part(var legend...) i write this code:
g.append("text")
.attr("stroke", "none")
.attr("fill", function(d) { return d.data.col; })
.text(function(d) { return d.data.name; });
I'm able to access the d.data.name.
Unfortunately wrong colors of the boxes and not description.
Thanks!
I`m trying to make the rest of the chart transparent or set it to a specific color after I click on a specific slice of the doughnut. So far so good in console the filter is working if I hard-code the type it works( I set it to null at the beginning). I don't know why i can not get the slice that I click and make the rest of the chart set to that specific color. My though is that I have to update the chart somehow but with drawdata() function doesn't work ...
Here is my code:
var filter = {
device: null,
os_version: null,
app_version: null
};
// Creating the object Doughnut
var Doughnut = function(type) {
// Properties
var width = 160;
var height = 160
var radius = Math.min(width, height) / 2;
var donutWidth = 35;
var legendRectSize = 18;
var legendSpacing = 4;
var type = type;
// Array of Colors for the graph
var color = d3.scale.category20c();
var colorFunc = function(key) {
var normalColor = color(key);
if (filter[type] == null || key == filter[type]) {
console.log("normal color")
return normalColor;
}
console.log("trans color")
return "#d5eff2";
};
// Graph Elements
var chart = null;
var svg = null;
var path = null;
var legend = null;
// Our current dataSet
var dataSet = null;
// d3 functions
var arc = d3.svg.arc()
.innerRadius(radius - donutWidth)
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(d) {
return d.value;
});
// This is the initialize method - we create the basic graph, no data
var initialize = function(chartElement){
chart = chartElement;
svg = d3.select(chart)
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
};
var update = function() {
d3.json("./api/distribution/", function(data){
dataSet = data;
data.value = +data.value;
drawData();
});
}
var drawData = function() {
path = svg.selectAll('path')
.data(pie(dataSet[type]))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d) {
return colorFunc(d.data.key);
})
.on('click', function(d) {
if (filter[type] == d.data.key) {
filter[type] = null;
} else {
filter[type] = d.data.key;
}
console.log(filter)
// $(chart).empty()
drawData();
});
createLegends();
};
var createLegends = function() {
legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length /2;
var horz = -2 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) {
return d;
});
};
return{
init: initialize,
update: update
}
};
// Here we create instance of doughnuts
var doughnutGraphs = (function() {
var init = function() {
// Create four doughnuts
var doughnut1 = new Doughnut("device");
var doughnut2 = new Doughnut("os_version");
var doughnut3 = new Doughnut("app_version");
// Initialize with an element
doughnut1.init("#chart_1");
doughnut2.init("#chart_2");
doughnut3.init("#chart_3");
// Update each of them with data
doughnut1.update();
doughnut2.update();
doughnut3.update();
};
return {
init: init
}
})();
I found the answer :
Create a method to clean then call it in the drawdata()
var clean = function() {
svg.selectAll('path').remove();
and call it .on('click')
.on('click', function(d) {
if (filter[type] == d.data.key) {
filter[type] = null;
} else {
filter[type] = d.data.key;
}
console.log(filter)
// $(chart).empty()
clean();
drawData();
});