I am trying to create a data visualisation for some student related data (sample record below) but when d3 renders it, it goes through the data twice and overwrites it, leaving only the results for the second time through only on the screen. I am using a row counter here to so I have a way to set the y coord of each rectangle based how many rectangles there are. And I think this has somehow messed things up a little. Any help on how to make it so the data does not get iterated through twice would be greatly appreciated.
Also, just in case it matters, this code is living within an angular.js directive.
Apologies if I am just doing something really silly here
// student records sample...
var studentData = [
{
"studentID" : 1001,
"firstName" : "jill",
"lastName" : "smith",
"workLoadDifficulty" : 16,
"smileStartAngle" : -90,
"smileEndAngle" : 90,
},
{
"studentID" : 1008,
"firstName" : "bob",
"lastName" : "smith",
"workLoadDifficulty" : 99,
"smileStartAngle" : 90,
"smileEndAngle" : -90,
}
];
(function () {
'use strict';
angular.module('learnerApp.directives')
.directive('d3Bars', ['d3', function(d3) {
return {
restrict: 'EA',
scope: {
data: "=",
label: "#",
onClick: "&"
},
link: function(scope, iElement, iAttrs) {
var paddingForShape = 10;
var rowCounter = -1;
var height = 400;
var width = 300;
var svgContainer = d3.select(iElement[0])
.append("svg")
.attr("width", width)
.attr('height', height);
// on window resize, re-render d3 canvas
window.onresize = function() {
return scope.$apply();
};
scope.$watch(function(){
return angular.element(window)[0].innerWidth;
}, function(){
return scope.render(scope.data);
}
);
// watch for data changes and re-render
scope.$watch('studentData', function(newVals, oldVals) {
return scope.render(newVals);
}, true);
// define render function
scope.render = function(data){
// remove all previous items before render
svgContainer.selectAll("*").remove();
var workLoadColor = d3.scale.category10()
.domain([0,100])
.range(['#02FA28', '#73FA87', '#C0FAC9','#FAE4C0', '#FAC775', '#FAA823','#FA9A00','#FA8288', '#FC4750', '#FA0511' ])
var studentRects = svgContainer.selectAll('rect')
.data(studentData, function(d) {
console.log(d.studentID);
console.log('hello');
return "keyVal" + d.studentID;
})
.enter()
.append("rect");
var studentRectAttributes = studentRects
.attr("x", function(d,i) {
return ((i * 50) % width) + paddingForShape;
})
.attr("y", function(d,i) {
var value = ((i * 50) % width)
if (value === 0) {
rowCounter = rowCounter + 1;
}
var value = (rowCounter * 50);
console.log('Y Val: ', i);
console.log(value);
return value;
})
.attr("height", 30)
.attr("width", 40)
.style("fill", function(d) {
return workLoadColor(d.workLoadDifficulty)
});
};
}
};
}]);
}());
If you're performing a data join twice, you need to specify a key so you don't overwrite the elements that are in your current selection. You may want to change your studentRects definition to:
var studentRects = svgContainer.selectAll('rect')
.data(studentData, function(d) { return d.firstName + ' ' + d.lastName; });
studentRects.enter().append("rect");
See selection.data([values[, key]])
If a key function is not specified, then the first datum in the specified array is assigned to the first element in the current selection, the second datum to the second selected element, and so on.
Try changing your selector to var studentRects = svgContainer.selectAll('rect') which will match the <rect> elements you are adding on enter()
** UPDATED **
Along with the key advice #Wex gave, I plopped the code in plunker and got it working. You have an extra watch on your scope, removing it addresses the problem (you may want to revisit some of the d3 docs regarding enter/exit though):
scope.$watch(function(){
return angular.element(window)[0].innerWidth;
}, function(){
return scope.render(scope.data);
});
Plunker here: http://plnkr.co/edit/z0MXkUVFNmMEGJAaz7dw?p=preview
Related
I have two elements I need to render and a context of the big picture I am trying to achieve (a complete dashboard).
One is a chart that renders fine.
$scope.riskChart = new dc.pieChart('#risk-chart');
$scope.riskChart
.width(width)
.height(height)
.radius(Math.round(height/2.0))
.innerRadius(Math.round(height/4.0))
.dimension($scope.quarter)
.group($scope.quarterGroup)
.transitionDuration(250);
The other is a triangle, to be used for a more complex shape
$scope.openChart = d3.select("#risk-chart svg g")
.enter()
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
On invocation of render functions, the dc.js render function is recognized and the chart is seen, but the d3.js render() function is not recognized.
How do I add this shape to my dc.js canvas (an svg element).
$scope.riskChart.render(); <--------------Works!
$scope.openChart.render(); <--------------Doesn't work (d3.js)!
How do I make this work?
EDIT:
I modified dc.js to include my custom chart, it is a work in progress.
dc.starChart = function(parent, fill) {
var _chart = {};
var _count = null, _category = null;
var _width, _height;
var _root = null, _svg = null, _g = null;
var _region;
var _minHeight = 20;
var _dispatch = d3.dispatch('jump');
_chart.count = function(count) {
if(!arguments.length)
return _count;
_count = count;
return _chart;
};
_chart.category = function(category) {
if(!arguments.length)
return _category
_category = category;
return _chart;
};
function count() {
return _count;
}
function category() {
return _category;
}
function y(height) {
return isNaN(height) ? 3 : _y(0) - _y(height);
}
_chart.redraw = function(fill) {
var color = fill;
var triangle = d3.symbol('triangle-up');
this._g.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", triangle)
.attr("transform", function(d) { return "translate(" + 25 + "," + 25 + ")"; })
.style("fill", fill);
return _chart;
};
_chart.render = function() {
_g = _svg
.append('g');
_svg.on('click', function() {
if(_x)
_dispatch.jump(_x.invert(d3.mouse(this)[0]));
});
if (_root.select('svg'))
_chart.redraw();
else{
resetSvg();
generateSvg();
}
return _chart;
};
_chart.on = function(event, callback) {
_dispatch.on(event, callback);
return _chart;
};
_chart.width = function(w) {
if(!arguments.length)
return this._width;
this._width = w;
return _chart;
};
_chart.height = function(h) {
if(!arguments.length)
return this._height;
this._height = h;
return _chart;
};
_chart.select = function(s) {
return this._root.select(s);
};
_chart.selectAll = function(s) {
return this._root.selectAll(s);
};
function resetSvg() {
if (_root.select('svg'))
_chart.select('svg').remove();
generateSvg();
}
function generateSvg() {
this._svg = _root.append('svg')
.attr({width: _chart.width(),
height: _chart.height()});
}
_root = d3.select(parent);
return _chart;
}
I think I confused matters by talking about how to create a new chart, when really you just want to add a symbol to an existing chart.
In order to add things to an existing chart, the easiest thing to do is put an event handler on its pretransition or renderlet event. The pretransition event fires immediately once a chart is rendered or redrawn; the renderlet event fires after its animated transitions are complete.
Adapting your code to D3v4/5 and sticking it in a pretransition handler might look like this:
yearRingChart.on('pretransition', chart => {
let tri = chart.select('svg g') // 1
.selectAll('path.triangle') // 2
.data([0]); // 1
tri = tri.enter()
.append('path')
.attr('class', 'triangle')
.merge(tri);
tri
.attr("d", d3.symbol().type(d3.symbolTriangle).size(200))
.style("fill", 'darkgreen'); // 5
})
Some notes:
Use chart.select to select items within the chart. It's no different from using D3 directly, but it's a little safer. We select the containing <g> here, which is where we want to add the triangle.
Whether or not the triangle is already there, select it.
.data([0]) is a trick to add an element once, only if it doesn't exist - any array of size 1 will do
If there is no triangle, append one and merge it into the selection. Now tri will contain exactly one old or new triangle.
Define any attributes on the triangle, here using d3.symbol to define a triangle of area 200.
Example fiddle.
Because the triangle is not bound to any data array, .enter() should not be called.
Try this way:
$scope.openChart = d3.select("#risk-chart svg g")
.attr("width", 55)
.attr("height", 55)
.append('path')
.attr("d", d3.symbol('triangle-up'))
.attr("transform", function(d) { return "translate(" + 100 + "," + 100 + ")"; })
.style("fill", fill);
This is probably a pretty specific question:
My problem is that in d3.js i need to create a radial chart.
I created the axis and labels.
Now i want to draw the radialLine.
It creates the path objects in my HTML document,
but without any coordinates.
I think it has something to do with the way the radius/data is provided to the radialLine, but can't figure out what to change...
Hopefully someone sees my mistake.
I also created a JSfiddle:
complete JSfiddle
//Data:
var notebookData = [{
model: "Levecchio 620RE",
data: [579, 8, 2.4, 256, 13.3]
}];
var categories = [
"Price",
"RAM",
"CPU",
"Storage",
"Display"
];
var priceScale = d3.scaleLinear().domain([2500,300]).range([0,100]);
var ramScale = d3.scaleLinear().domain([0,32]).range([0,100]);
var cpuScale = d3.scaleLinear().domain([1.0,3.2]).range([0,100]);
var storageScale = d3.scaleLinear().domain([64,2048]).range([0,100]);
var displaySizeScale = d3.scaleLinear().domain([10.0,20.0]).range([0,100]);
function selectScale(category_name) {
switch(category_name) {
case "Price":
return priceScale;
case "RAM":
return ramScale;
case "CPU":
return cpuScale;
case "Storage":
return storageScale;
case "Display":
return displaySizeScale;
}
}
var scaledData = notebookData.map(function (el) {
return el.data.map(function (el2, i) { //el = 1 notebook
return selectScale(categories[i])(el2);
});
});
//My RadialLine
//generatorfunction
var radarLine = d3.radialLine()
.radius(function(d) { return scaledData(d.value); })
.angle(function(d,i) { return i*angleSlice; })
.curve(d3.curveLinearClosed)
;
//Create the wrapper
var radarWrapper = g.selectAll(".radarWrapper")
.data(notebookData)
.enter().append("g")
.attr("class", "radarWrapper")
;
//Create pathlines
radarWrapper.append("path")
.attr("class", "radarStroke")
.attr("d", function(d,i) { return radarLine(d); })
.style("stroke-width", cfg.strokeWidth + "px")
.style("stroke", function(d,i) { return cfg.color(i); })
.style("fill", "none")
;
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I've edited your fiddle a bit to make it work:
https://jsfiddle.net/2qgygksL/75/
Basicly what i've done:
fix the color scheme
var colors = d3.scale.category10();
instead of
var colors = d3.scale.ordinal(d3.schemeCategory10);
added data to path
radarWrapper.append("path")
.data(scaledData)
change radius to
.radius(function(d, i) {
return d;
})
since You used something like return scaledData(d.value); where your scaledData is an array.
Trying to implement object constancy. The concept is to update the DOM with text using progressive phrases within the text as data.
////////////////////////////////////////////////////////////////////////
// We need a way to change the data to illustrate the update pattern.
// We use lyrics to do this. Below is code for updating the data.
////////////////////////////////////////////////////////////////////////
var data = [];
function lyricIterator(phrases) {
var nextIndex = 0;
return {
next: function(){
if (nextIndex >= (phrases.length - 1)) {
nextIndex = 0;
debugger;
} else {
nextIndex = nextIndex + 1;
}
// console.log(phrases.slice(nextIndex - 1, nextIndex + 2));
return phrases.slice(nextIndex - 1, nextIndex + 2);
}
}
}
var lyrics = [
{i : 0, phrase : "Row, row, row your boat,"},
{i : 1, phrase : "Gently down the stream."},
{i : 2, phrase : "Merrily, merrily, merrily, merrily,"},
{i : 3, phrase : "Life is but a dream."}
]
// Instantiate an iterator for our lyrics
var rrryb = lyricIterator(lyrics)
/////////////
// Set up
//////////////
var width = 960,
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
//////////////////////////
// Redraw the elements
/////////////////////////
function update() {
data = rrryb.next()
var phrases = svg.selectAll("phrases")
.data(data, function(d) { return d.i; });
console.log("Update")
console.log(phrases)
console.log("Enter")
console.log(phrases.enter())
console.log("Exit")
console.log(phrases.exit())
phrases.exit().remove();
// UPDATE
// Update old elements as needed.
phrases.attr("class", "update");
phrases.enter().append("text")
.attr("class", "enter")
.attr("dy", function(d, i) { return i * 32; })
.attr("dx", ".35em");
phrases.text(function(d) { return d.phrase; })
}
////////////////////////////////////////////////////
// Register the function to run at some interval
///////////////////////////////////////////////////
setInterval(function() {
update()
}, 1500);
I get this as output to the console (enter and update with all the elements everytime):
And the output is just the text stacked on one another
Instead of this:
var phrases = svg.selectAll("phrases")//there is no DOM as phrases it will return an empty selection always.
.data(data, function(d) { return d.i; });
it should be this:
var phrases = svg.selectAll("text")//select all the text
.data(data, function(d) { return d.i; });
Reason: This will return an empty selection always svg.selectAll("phrases") that is why its appending all the time.
In second case it will return a selection of text DOMs.
working code here
I'm trying to rework a pen (http://codepen.io/anon/pen/JgyCz) by Travis Palmer so that I can use it on multiple elements. We are trying to place several <div class="donut" data-donut="x">'s on a page.
So it would look similar to the html below:
////// HTML
<div class="donut" data-donut="22"></div>
<div class="donut" data-donut="48"></div>
<div class="donut" data-donut="75></div>
The D3.js / jQuery example I'm trying to convert to a reusable compunent is below. (To see full working example go to this link - http://codepen.io/anon/pen/JgyCz)
////// D3.js
var duration = 500,
transition = 200;
drawDonutChart(
'.donut',
$('.donut').data('donut'),
290,
290,
".35em"
);
function drawDonutChart(element, percent, width, height, text_y) {
width = typeof width !== 'undefined' ? width : 290;
height = typeof height !== 'undefined' ? height : 290;
text_y = typeof text_y !== 'undefined' ? text_y : "-.10em";
var dataset = {
lower: calcPercent(0),
upper: calcPercent(percent)
},
radius = Math.min(width, height) / 2,
pie = d3.layout.pie().sort(null),
format = d3.format(".0%");
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius);
var svg = d3.select(element).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.lower))
.enter().append("path")
.attr("class", function(d, i) { return "color" + i })
.attr("d", arc)
.each(function(d) { this._current = d; }); // store the initial values
var text = svg.append("text")
.attr("text-anchor", "middle")
.attr("dy", text_y);
if (typeof(percent) === "string") {
text.text(percent);
}
else {
var progress = 0;
var timeout = setTimeout(function () {
clearTimeout(timeout);
path = path.data(pie(dataset.upper)); // update the data
path.transition().duration(duration).attrTween("d", function (a) {
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
}); // redraw the arcs
}, 200);
}
};
function calcPercent(percent) {
return [percent, 100-percent];
};
The best way to do this is to use angular directives. An angular directive basically wraps html inside a custom tag and let's you stamp the directive over and over across multiple pages or multiple times a page. See this video: http://www.youtube.com/watch?v=aqHBLS_6gF8
There is also a library that is out called nvd3.js that contains prebuilt angular directives that can be re-used: http://nvd3.org/
Hope this helps.
ok, I figured it out. I feel a bit dumb in hindsight, but what can I say, I'm a js n00b. All you have to do is make a few more call to the drawDonutChart() method. In short:
drawDonutChart(
'#donut1',
$('#donut1').data('donut'),
220,
220,
".35em"
);
drawDonutChart(
'#donut2',
$('#donut2').data('donut'),
120,
120,
".35em"
);
drawDonutChart(
'#donut3',
$('#donut3').data('donut'),
150,
150,
".2em"
);
I am loading data from a google spreadsheet that contains the GDP of selected countries from the 1955 to 2012. From this I want to draw a treemap. So far so good.
I've loaded the data through out internal link and formatted into an object that d3 can handle, then got the layout to draw on the screen-all well and good. I've based it on the Mike Bostock tutorial at http://bl.ocks.org/mbostock/4063582.
The problem comes when I try to transition from a set of data from say 1955 to 2010. I'm confident that the function I'm using to generate the treemap layout is working because the initial display is correct. I pass it a date and it creates the treemap structure.
However when I trigger a change the transition seems to occur and the individual squares change size. But when I examine them I realise that they are all wrong and that I seem to have mapped the new set of value onto the wrong countries.
The newstructure looks visually correct but all the names are wrong. So I get things like cyprus having the largest GDP in 2012. Its as if I've got a list in alphabetical order thats having another set of values in order of magnitude applied to the rather that the new value for say the US being mapped the old value.
Going around in circles here as I'm still faily new to d3 so all help gratefully received.
Code looks like this:
/*global app:true JST:true d3:true*/
(function (window, $) {
'use strict';
var menuItems = [];
var menuType='measure';
var checboxItems= ['advanced','emerging'];
var ddID = '0';
var model=[];
var yearValue="2012"
var group="gdp";
var treeStruc={
name:[],
children:[]
}
var margin = {top: 25, right: 5, bottom: 5, left: 5},
width = 965 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
var color = d3.scale.category10();
app.spreadsheet.get(function (data) {
// TODO: process the data
menuItems = data.measures
//console.log(data);
//console.log('menuItems', menuItems);
//crete dropdown and use toggle to swich display on and off
$('#dropDown').click(function () {
$('ul.ddMenuList').toggle();
});
//populate the dropdown menu
for (var k = 0; k <menuItems.length; k++) {
$('#ddList').append('<li id="dd_' + k + '"><a href="#">'+menuItems[k].menulist +'</li>');
};
//add functionality to dropDown menu
$('#ddList li').bind('click', function () {
ddID = this.id.split('_')[1];
var text = $(this).text();
//console.log ("ID=",ddID);
//console.log (text, "Measure=",menuItems[ddID].type);
$('#ddTitle').empty();
$('#ddTitle').append(text);
createCheckboxes()
});
function createCheckboxes() {
//decide which check boxes to populate
if (menuItems[ddID].type==="measure") {
group=menuItems[ddID].type
checboxItems=[];
$.each(menuItems, function (i) {
if (menuItems[i].type==="group"){
checboxItems.push (menuItems[i].checkbox);
}
//console.log (checboxItems);
});
}
else {
group=menuItems[ddID].type
checboxItems=[];
$.each(menuItems, function (i) {
if (menuItems[i].type==="measure"){
checboxItems.push (menuItems[i].checkbox);
}
//console.log (checboxItems);
});
}
//Populate the check boxes
console.log ("Populating check boxes");
$('#cbHolder').empty();
$('#cbHolder').append('<form>');
$.each(checboxItems, function (i) {
$('#cbHolder').append('<input type="checkbox" id="cb_'+i+'">'+checboxItems[i]);
$('#cbHolder').append('</form>');
//console.log ("checkboxItems",checboxItems[i]);
});
changed3 ()
}
//creates an object containing just the advanced countries
treeStruc={name:[],children:[]};
console.log ("group=",group);
$.each(checboxItems, function (k) {
console.log("Parent",checboxItems[k])
model=jQuery.grep(data.stats,function(e,i){return e[checboxItems[k]];});
console.log('model', model);
treeStruc.children.push({"name":checboxItems[k],"children":[]});
//Construct the children of 1 big group to be completed to be updated for each sheet
$.each(model, function (i) {
treeStruc.children[k].children.push({'name':model[i].countryname,'size':model[i] [group]});
});
});
console.log('treeStruc', treeStruc)
Handlebars.createOptionsHelper(data.options);
drawd3 ();
});
function generateTreemapLayout(filter){
return d3.layout.treemap()
.size([width, height])
.sticky(true)
.value(function(d) {
if(d.size[filter] < 0){
return 0;
}
return d.size[filter];
});
}
function drawd3() {
console.log ("function drawd3");
var treemap = generateTreemapLayout('y'+yearValue)
var div = d3.select("#d3Object").append("div")
.style("position", "relative")
.style("width", (width + margin.left + margin.right) + "px")
.style("height", (height + margin.top + margin.bottom) + "px")
.style("left", margin.left + "px")
.style("top", margin.top + "px");
var node = div.datum(treeStruc).selectAll(".node")
.data(treemap.nodes)
.enter().append("div")
.attr("class", "node")
.call(position)
.attr("id",function(d){
return d.name;
})
.style("background", function(d) { return d.children ? color(d.name) : null; })
.text(function(d) { return d.children ? null : d.name; });
};
function position() {
this.style("left", function(d) { return d.x + "px"; })
.style("top", function(d) { return d.y + "px"; })
.style("width", function(d) { return Math.max(0, d.dx - 1) + "px"; })
.style("height", function(d) { return Math.max(0, d.dy - 1) + "px"; });
}
function changed3() {
console.log ("function changed3");
//make a new treemap layout
var treemap = generateTreemapLayout('y'+1955);
console.log('treeStruc',treeStruc);
//redraw the treemap using transition instead of enter
var node = d3.select("#d3Object")
.datum(treeStruc).selectAll(".node")
.data(treemap.nodes)
.transition()
.duration(1500)
.call(position)
}
}(this, jQuery));
Many thanks to Tom Pearson my work colleague for this. The problem lies in where the data is bound to the item on the page. When you come to re draw the treemap because the data isn't bound to the div with a nique identifier like the object name it re maps the data to the first item o the list as it where. This means that something like China's gets given Belgium's information. simple solution is as follows Instead of
.data(treemap.nodes)
use
.data(treemap.nodes,function(d){
return d.name;
})
The are two instances of this in the original drawd3 function them in the changed3 function. Hope that helps anyone stuck with something similar