d3.js - tween numbers on multiple elements - d3.js

I've built a chart with a series of "small multiples", each representing a percentage of completion, arranged in rows and columns. Each row has a different number that represents "complete".
You can view the chart in this fiddle: http://jsfiddle.net/rolfsf/Lhnm9a9m/
I'm using transitions to grow the bars from 0 to x% width.
I also want to use a transition to show the actual count of each measure (e.g. 10 out of 17) incrementing from 0.
I've got all the numbers incrementing from 0 using a text tween function, but all of the measures stop at the same count, rather than their correct count.
I must be either using the wrong data in the tween, or placing the tween in the wrong part of the script... but I can't figure out where the problem is.
How do I get the numbers to increment properly??
My data looks like this:
var sets = [
{"title": "Set-1", "count": 17, "measures": [10, 13, 16, 14]},
{"title": "Set-2", "count": 23, "measures": [12, 18, 19, 23]},
{"title": "Set-3", "count": 25, "measures": [19, 22, 23, 20]},
{"title": "Set-4", "count": 4, "measures": [4, 4, 4, 4]},
{"title": "Set-5", "count": 8, "measures": [5, 7, 8, 6]}
];
The chart is called like this:
d3 .select('#overview-graph')
.datum(sets)
.call(relativeCompletionChart()
//options
);
and here's the reusable chart script:
function relativeCompletionChart() {
var width = 1200,
margin = {top: 16, right: 16, bottom: 16, left: 16},
onSetMouseOver = null,
onSetClick = null;
function chart(selection) {
selection.each(function(data) {
var titleColWidth = 0.3*width,
setHeight = 24,
barHeight = 22,
setCount = data.length,
colCount = data[0].measures.length,
colWidth = (width - titleColWidth)/colCount,
rangeWidth = colWidth - 4,
height = ((setHeight * setCount) + margin.top + margin.bottom);
var svg = d3.select(this)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "xMidYMin meet");
svg .append('rect')
.attr("x", 0)
.attr('y', 0)
.attr('height', height)
.attr('width', width)
.attr('class', 'chart-bg');
/**
* Tween functions
*/
function tweenText( newValue ) {
return function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var i = d3.interpolateRound( currentValue, newValue );
return function(t) {
this.textContent = i(t);
};
}
}
function update(data) {
var set = svg.selectAll("g.set")
.data(data);
set.exit().remove();
var setEnter = set
.enter().append("g")
.attr("class", "set")
.attr('transform', function (d, i) {
return 'translate(0, ' + (margin.top + i*setHeight) + ')';
});
set.append("text")
.attr("class", "title")
.attr("x", (titleColWidth - 80))
.attr("y", 16)
.attr("text-anchor", "end")
.text(function (d){return d.title;});
set.append("text")
.attr("class", "count")
.attr("x", (titleColWidth - 32))
.attr("y", 16)
.attr("text-anchor", "end")
.text(function (d){return d.count;});
var ranges = set.selectAll("rect.range")
.data(function(d, i){return d.measures})
.enter().append('rect')
.attr("class", "range")
.attr("x",2)
.attr("y",0)
.attr('rx', 2)
.attr('ry', 2)
.attr("width", rangeWidth)
.attr("height",barHeight)
.attr("fill", "#CCCCCC")
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var measures = set.selectAll("rect.measure")
.data(function(d, i){return d.measures})
.enter().append('rect')
.attr("class", "measure")
.attr('rx', 2)
.attr('ry', 2)
.attr("x",2)
.attr("y",0)
.attr("width", 1)
.attr("height",barHeight)
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var markers = set.selectAll("line.marker")
.data(function(d, i){return d.measures})
.enter().append('line')
.attr("class", "marker")
.attr('x1', 2)
.attr('y1', 0)
.attr("x2",2)
.attr("y2",barHeight)
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var values = set.selectAll("text.value")
.data(function(d, i){return d.measures})
.enter().append('text')
.text('0')
.attr("class", "value")
.attr("x", 8)
.attr("y", 16)
.attr("text-anchor", "start")
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
//update widths
set.selectAll("rect.measure")
.transition()
.duration(1000)
.delay(function(d, i) { return i * 20; })
.attr("width", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth);});
set.selectAll("line.marker")
.transition()
.duration(1000)
.delay(function(d, i) { return i * 20; })
.attr("x1", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth + 1);})
.attr("x2", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth + 1);});
set.selectAll('text.value')
.transition()
.duration(1000)
.tween( 'text', function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var interpolator = d3.interpolateRound( currentValue, 10 );
// this returned function will be called a couple
// of times to animate anything you want inside
// of your custom tween
return function( t ) {
// set new value to current text element
this.textContent = interpolator(t) + '/' + d3.select(this.parentNode).datum().count;
};
});
}
update(data);
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onSetClick = function(_) {
if (!arguments.length) return onSetClick;
onSetClick = _;
return chart;
};
chart.onSetMouseOver = function(_) {
if (!arguments.length) return onSetMouseOver;
onSetMouseOver = _;
return chart;
};
return chart;
}
The relevant code for the tweening is pulled out here:
set.selectAll('text.value')
.transition()
.duration(1000)
.tween( 'text', function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var interpolator = d3.interpolateRound( currentValue, 10 );
// this returned function will be called a couple
// of times to animate anything you want inside
// of your custom tween
return function( t ) {
// set new value to current text element
this.textContent = interpolator(t) + '/' + d3.select(this.parentNode).datum().count;
};
});
Though I've also got an unused helper function in the script that I couldn't get to work:
/**
* Tween functions
*/
function tweenText( newValue ) {
return function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var i = d3.interpolateRound( currentValue, newValue );
return function(t) {
this.textContent = i(t);
};
}
}

Related

Convert from hive style plot to draw axes paralelly

Long story short. I need to display chart of target-source items and I found that d3.js can do the job. I spend already many hours to get something from there and I finished with using Hive Plot like this one:
https://bost.ocks.org/mike/hive/
But the problem is that I will have only one pair of items and I want to display nodes and corresponding to them items not in let's say circular manner but more conventional parallel (like in image below, left is actual result, right is desired one)[
I tried many things but unfortunately i get lost.
I tried also get jsfiddle up and running but for unknown reason for me is not displaying anything. Here's the jsfiddle:
https://jsfiddle.net/a7yrjfgc/2/
Code:
var width = 200,
height = 200,
innerRadius = 10,
outerRadius = 100,
majorAngle = 1 * Math.PI / 1,
minorAngle = 1 * Math.PI / 2;
var angle = d3.scaleOrdinal()
.domain(["source", "source-target", "target-source", "target"])
.range([0, majorAngle - minorAngle, majorAngle + minorAngle, 2 * majorAngle]);
radius = d3.scaleLinear()
.range([innerRadius, outerRadius]),
color = d3.scaleOrdinal(d3.schemeCategory10),
formatNumber = d3.format(",d");
var nodes = [
{x: 0, y: .4, name: "node1", color: "#0000FF"},
{x: 0, y: .2, name: "node2", color: "#FFA500"},
{x: 1, y: .2, name: "node3", color: "#008000"},
{x: 1, y: .3, name: "node4", color: "#A52A2A"},
];
var links = [
{source: nodes[0], target: nodes[2]},
{source: nodes[1], target: nodes[3]},
];
var info = d3.select("#info")
.text(defaultInfo = "Showing " + formatNumber(links.length) + " splices " + formatNumber(nodes.length) + " strands.");
console.log(info);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
svg.selectAll(".axis")
.data(d3.range(2))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")" })
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
// draw links
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", link()
.angle(function(d) { return angle(d.x); })
.radius(function(d) { return radius(d.y); }))
.style("stroke", function(d) { return color(d.color); })
.on("mouseover", linkMouseover)
.on("mouseout", mouseout);
// draw nodes
svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d.x)) + ")"; })
.attr("cx", function(d) { return radius(d.y); })
.attr("r", 5)
.style("fill", function(d) { return d3.color(d.color); })
.on("mouseover", nodeMouseover)
.on("mouseout", mouseout);
Many, many thanks for helping in this topic.
As i get to know more about how d3 works, I realized that I need to simply draw everything from scratch.
function DisplayGraph(){
//D3 Configuration
var width = 400,
height = 50,
color = d3.scaleOrdinal(d3.schemeCategory10),
formatNumber = d3.format(",d"),
defaultInfo ="";
var nodes = [
{x: 0, y: 0, name: "node1", color: "#0000FF"},
{x: 0, y: 15, name: "node2", color: "#FFA500"},
{x: 100, y: 15, name: "node3", color: "#008000"},
{x: 100, y: 0, name: "node4", color: "#A52A2A"},
];
var links = [
{source: nodes[0], target: nodes[2]},
{source: nodes[1], target: nodes[3]},
];
var info = d3.select("#info")
.text(defaultInfo = "Showing " + formatNumber(links.length) + " splices " + formatNumber(nodes.length) + " strands.");
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
var diagonal = function link(d) {
let sourceX = (d.source.x + 50);
let sourceY = (d.source.y + 5);
let targetX = (d.target.x);
let targetY = (d.target.y + 5);
var output = "M" + sourceX + "," + sourceY
+ "C" + (sourceX + targetX) / 2 + "," + sourceY
+ " " + (sourceX + targetX) / 2 + "," + targetY
+ " " + targetX + "," + targetY;
return output;
};
//draw squares
svg.selectAll(".node")
.data(nodes)
.enter().append("rect")
.attr("class", "node")
.attr("width", 50)
.attr("height", 10)
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.style("fill", function(d) { return d3.color(d.color); })
.on("mouseover", nodeMouseover)
.on("mouseout", mouseout);
// draw links
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal)
.style("stroke", function(d) { return color(d.color); })
.on("mouseover", linkMouseover)
.on("mouseout", mouseout);
//Helpers
function linkMouseover(d) {
svg.selectAll(".link")
.classed("turnedOn", function(dl) {
return dl === d;
})
.classed("turnedOff", function(dl) {
return !(dl === d);
})
svg.selectAll(".node")
.classed("turnedOn", function(dl) {
return dl === d.source || dl === d.target;
})
info.text(d.source.name + " → " + d.target.name);
}
// Highlight the node and connected links on mouseover.
function nodeMouseover(d) {
svg.selectAll(".node")
.classed("turnedOn", function(dl) {
return dl.source === d || dl.target === d;
})
.classed("turnedOff", function(dl) {
return !(dl.source === d || dl.target === d)
});
d3.select(this)
.classed("turnedOn", true);
info.text(d.name);
}
// Clear any highlighted nodes or links.
function mouseout() {
svg.selectAll(".turnedOn").classed("turnedOn", false);
svg.selectAll(".turnedOff").classed("turnedOff", false);
info.text("Hover to node or link");
}
}

D3 - Selection in differents DIV

I've made a scatterplot and a choropleth map in the same web page. Data is stored in a .CSV and .json, and elements are linked with a "name" field.
I've made a tooltip on mouseover on both. I want now some interactivity beetween them: when mouse is over an element on scatterplot, this element on choropleth react and when mouse is over choropleth map scatterplot react.
Scatterplot and choropleth are in differents div with specifics ID and I don't how can I refeer from one to an other. I tried d3.select("this#scatterplot"); like this example but it doesn't work for me.
How can I select elements in differents DIV and differents functions ?
I want something like this :
function handleMouseOverMap(d, i) {
d3.select('this#choropleth').style('stroke-width', 3);
d3.select('this#scatterplot').attr('r', 8);
}
function handleMouseOverGraph(d, i) {
d3.select('this#scatterplot').attr('r', 8);
d3.select('this#choropleth').style('stroke-width', 3);
}
Code
<div id="scatterplot"></div>
<div id="choropleth"></div>
<script>
d3.queue()
.defer(d3.csv, 'data.csv', function (d) {
return {
name: d.name,
sau: +d.sau,
uta: +d.uta
}
})
.defer(d3.json, 'dept.json')
.awaitAll(initialize)
var color = d3.scaleThreshold()
.domain([150000, 300000, 450000])
.range(['#5cc567', '#e7dc2b', '#e59231', '#cb0000'])
function initialize(error, results) {
if (error) { throw error }
var data = results[0]
var features = results[1].features
var components = [
choropleth(features),
scatterplot(onBrush)
]
function update() {
components.forEach(function (component) { component(data) })
}
function onBrush(x0, x1, y0, y1) {
var clear = x0 === x1 || y0 === y1
data.forEach(function (d) {
d.filtered = clear ? false
: d.uta < x0 || d.uta > x1 || d.sau < y0 || d.sau > y1
})
update()
}
update()
}
/* Graphique */
function scatterplot(onBrush) {
var margin = { top: 10, right: 15, bottom: 40, left: 75 }
var width = 680 - margin.left - margin.right
var height = 550 - margin.top - margin.bottom
var x = d3.scaleLinear()
.range([0, width])
var y = d3.scaleLinear()
.range([height, 0])
// Tooltip
var xValue = function(d) { return d.sau;};
var yValue = function(d) { return d.uta;};
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var xAxis = d3.axisBottom()
.scale(x)
.tickFormat(d3.format(''))
var yAxis = d3.axisLeft()
.scale(y)
.tickFormat(d3.format(''))
// Selection
var brush = d3.brush()
.extent([[0, 0], [width, height]])
.on('start brush', function () {
var selection = d3.event.selection
var x0 = x.invert(selection[0][0])
var x1 = x.invert(selection[1][0])
var y0 = y.invert(selection[1][1])
var y1 = y.invert(selection[0][1])
onBrush(x0, x1, y0, y1)
})
var svg = d3.select('#scatterplot')
.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 bg = svg.append('g')
var gx = svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
var gy = svg.append('g')
.attr('class', 'y axis')
gx.append('text')
.attr('x', width)
.attr('y', 35)
.style('text-anchor', 'end')
.style('fill', '#000')
.style('font-weight', 'bold')
.text('UTA')
gy.append('text')
.attr('transform', 'rotate(-90)')
.attr('x', 0)
.attr('y', -55)
.style('text-anchor', 'end')
.style('fill', '#000')
.style('font-weight', 'bold')
.text('SAU - ha')
svg.append('g')
.attr('class', 'brush')
.call(brush)
return function update(data) {
x.domain(d3.extent(data, function (d) { return d.uta })).nice()
y.domain(d3.extent(data, function (d) { return d.sau })).nice()
gx.call(xAxis)
gy.call(yAxis)
var bgRect = bg.selectAll('rect')
.data(d3.pairs(d3.merge([[y.domain()[0]], color.domain(), [y.domain()[1]]])))
bgRect.exit().remove()
bgRect.enter().append('rect')
.attr('x', 0)
.attr('width', width)
.merge(bgRect)
.attr('y', function (d) { return y(d[1]) })
.attr('height', function (d) { return y(d[0]) - y(d[1]) })
.style('fill', function (d) { return color(d[0]) })
var circle = svg.selectAll('circle')
.data(data, function (d) { return d.name })
circle.exit().remove()
circle.enter().append('circle')
.attr('r', 4)
.style('stroke', '#fff')
.merge(circle)
.attr('cx', function (d) { return x(d.uta) })
.attr('cy', function (d) { return y(d.sau) })
.style('fill', function (d) { return color(d.sau) })
.style('opacity', function (d) { return d.filtered ? 0.5 : 1 })
.style('stroke-width', function (d) { return d.filtered ? 1 : 2 })
// Event
.on("mouseover", function(d) {
tooltipOverGraph.call(this, d);
handleMouseOverGraph.call(this, d);
})
.on("mouseout", function(d) {
tooltipOutGraph.call(this, d);
handleMouseOutGraph.call(this, d);
})
}
// Tooltip
function tooltipOverGraph(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d["name"] + "<br>" + xValue(d)
+ " ha" +", " + yValue(d) + " UTA" )
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
function tooltipOutGraph(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
}
}
// Create Event Handlers for mouse
function handleMouseOverGraph(d, i) {
d3.select(this).attr('r', 8);
}
function handleMouseOutGraph(d, i) {
d3.select(this).attr('r', 4);
}
/* Carte */
function choropleth(features) {
var width = 680
var height = 550
// Tooltip
var xValue = function(d) { return d.sau;};
var yValue = function(d) { return d.uta;};
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Projection et centrage de la carte
var projection = d3.geoMercator()
.center([ 3, 46.5 ])
.scale([width * 3.1])
.translate([width / 2, height / 2])
var path = d3.geoPath().projection(projection)
var svg = d3.select('#choropleth')
.append('svg')
.attr('width', width)
.attr('height', height)
svg.selectAll('path')
.data(features)
.enter()
.append('path')
.attr('d', path)
.style('stroke', '#fff')
.style('stroke-width', 1)
// Event
.on("mouseover", function(d) {
tooltipOverMap.call(this, d);
handleMouseOverMap.call(this, d);
})
.on("mouseout", function(d) {
tooltipOutMap.call(this, d);
handleMouseOutMap.call(this, d);
})
// Tooltip
function tooltipOverMap(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d["name"] + "<br>" + xValue(d)
+ " ha" +", " + yValue(d) + " UTA" )
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
function tooltipOutMap(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
}
return function update(data) {
svg.selectAll('path')
.data(data, function (d) { return d.name || d.properties.name })
.style('fill', function (d) { return d.filtered ? '#ddd' : color(d.sau) })
}
}
// Create Event Handlers for mouse
function handleMouseOverMap(d, i) {
d3.select(this).style('stroke-width', 3);
}
function handleMouseOutMap(d, i) {
d3.select(this).style('stroke-width', 1);
}
</script>
Example
First, add distinct class attributes to the rect and circle items when you enter + append them:
bgRect.enter().append('rect')
.attr('class', function(d,i) { return 'classRect' + i; })
circle.enter().append('circle')
.attr('class', function(d,i) { return 'classCircle' + i; })
Then, update your mouse over functions:
function handleMouseOverMap(d) {
// update the choropleth //
d3.select(d).style('stroke-width', 3);
// update the scatterplot //
// capture the number contained in class (e.g. "1" for "classRect1")
var i = d3.select(d).class.substr(-1);
// select corresponding circle in scatter and update
d3.select('circle.classCircle'+i).attr('r', 8);
}
function handleMouseOverGraph(d) {
// update the scatter //
d3.select(d).attr('r', 8);
// update the choropleth //
// capture the number contained in class (e.g. "1" for "classCircle1")
var i = d3.select(d).class.substr(-1);
// select corresponding rect in the choropleth and update
d3.select('rect.classRect'+i).style('stroke-width', 3);
}

How to change the size of dots in beeswarm plots in D3.js

I've been looking at this example of a beeswarm plot in d3.js and I'm trying to figure out how to change the size of the dots and without getting the circles to overlap. It seems if the radius of the dots change, it doesn't take this into account when running the calculations of where to place the dots.
This is a cool visualization.
I've made a plunk of it here: https://plnkr.co/edit/VwyXfbc94oXp6kXQ7JFx?p=preview and modified it to work a bit more like you're looking for (I think). The real key is changing the call to handle collision to vary based on the radius of the circles (in the original post it's hard coded to 4, which works well when r === 3 but fails as r grows). The changes:
Make the circle radius into a variable (line 7 of script.js, var r = 3;)
Change the d3.forceCollide call to use that radius and a multiplier - line 110 (.force("collide", d3.forceCollide(r * 1.333)))
Change the .enter() call to use that radius as well (line 130: .attr("r", r))
This works reasonably well for reasonable values of r - but you'll need to adjust the height, and it might even be nice to just change the whole thing so that r is based on height (e.g. var r = height * .01). You'll notice that as is now, the circles go off the bottom and top of the graph area.
This post might be of interest as well: Conflict between d3.forceCollide() and d3.forceX/Y() with high strength() value
Here's the whole of script.js for posterity:
var w = 1000, h = 280;
var padding = [0, 40, 34, 40];
var r = 5;
var xScale = d3.scaleLinear()
.range([ padding[3], w - padding[1] ]);
var xAxis = d3.axisBottom(xScale)
.ticks(10, ".0s")
.tickSizeOuter(0);
var colors = d3.scaleOrdinal()
.domain(["asia", "africa", "northAmerica", "europe", "southAmerica", "oceania"])
.range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33']);
d3.select("#africaColor").style("color", colors("africa"));
d3.select("#namericaColor").style("color", colors("northAmerica"));
d3.select("#samericaColor").style("color", colors("southAmerica"));
d3.select("#asiaColor").style("color", colors("asia"));
d3.select("#europeColor").style("color", colors("europe"));
d3.select("#oceaniaColor").style("color", colors("oceania"));
var formatNumber = d3.format(",");
var tt = d3.select("#svganchor").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var svg = d3.select("#svganchor")
.append("svg")
.attr("width", w)
.attr("height", h);
var xline = svg.append("line")
.attr("stroke", "gray")
.attr("stroke-dasharray", "1,2");
var chartState = {};
chartState.variable = "totalEmission";
chartState.scale = "scaleLinear";
chartState.legend = "Total emissions, in kilotonnes";
d3.csv("co2bee.csv", function(error, data) {
if (error) throw error;
var dataSet = data;
xScale.domain(d3.extent(data, function(d) { return +d.totalEmission; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h - padding[2]) + ")")
.call(xAxis);
var legend = svg.append("text")
.attr("text-anchor", "middle")
.attr("x", w / 2)
.attr("y", h - 4)
.attr("font-family", "PT Sans")
.attr("font-size", 12)
.attr("fill", "darkslategray")
.attr("fill-opacity", 1)
.attr("class", "legend");
redraw(chartState.variable);
d3.selectAll(".button1").on("click", function(){
var thisClicked = this.value;
chartState.variable = thisClicked;
if (thisClicked == "totalEmission"){
chartState.legend = "Total emissions, in kilotonnes";
}
if (thisClicked == "emissionPerCap"){
chartState.legend = "Per Capita emissions, in metric tons";
}
redraw(chartState.variable);
});
d3.selectAll(".button2").on("click", function(){
var thisClicked = this.value;
chartState.scale = thisClicked;
redraw(chartState.variable);
});
d3.selectAll("input").on("change", filter);
function redraw(variable){
if (chartState.scale == "scaleLinear"){ xScale = d3.scaleLinear().range([ padding[3], w - padding[1] ]);}
if (chartState.scale == "scaleLog"){ xScale = d3.scaleLog().range([ padding[3], w - padding[1] ]);}
xScale.domain(d3.extent(dataSet, function(d) { return +d[variable]; }));
var xAxis = d3.axisBottom(xScale)
.ticks(10, ".0s")
.tickSizeOuter(0);
d3.transition(svg).select(".x.axis").transition().duration(1000)
.call(xAxis);
var simulation = d3.forceSimulation(dataSet)
.force("x", d3.forceX(function(d) { return xScale(+d[variable]); }).strength(2))
.force("y", d3.forceY((h / 2)-padding[2]/2))
.force("collide", d3.forceCollide(r * 1.333))
.stop();
for (var i = 0; i < dataSet.length; ++i) simulation.tick();
var countriesCircles = svg.selectAll(".countries")
.data(dataSet, function(d) { return d.countryCode});
countriesCircles.exit()
.transition()
.duration(1000)
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.remove();
countriesCircles.enter()
.append("circle")
.attr("class", "countries")
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.attr("r", r)
.attr("fill", function(d){ return colors(d.continent)})
.merge(countriesCircles)
.transition()
.duration(2000)
.attr("cx", function(d) { console.log(d); return d.x; })
.attr("cy", function(d) { return d.y; });
legend.text(chartState.legend);
d3.selectAll(".countries").on("mousemove", function(d) {
tt.html("Country: <strong>" + d.countryName + "</strong><br>"
+ chartState.legend.slice(0, chartState.legend.indexOf(",")) + ": <strong>" + formatNumber(d[variable]) + "</strong>" + chartState.legend.slice(chartState.legend.lastIndexOf(" ")))
.style('top', d3.event.pageY - 12 + 'px')
.style('left', d3.event.pageX + 25 + 'px')
.style("opacity", 0.9);
xline.attr("x1", d3.select(this).attr("cx"))
.attr("y1", d3.select(this).attr("cy"))
.attr("y2", (h - padding[2]))
.attr("x2", d3.select(this).attr("cx"))
.attr("opacity", 1);
}).on("mouseout", function(d) {
tt.style("opacity", 0);
xline.attr("opacity", 0);
});
d3.selectAll(".x.axis, .legend").on("mousemove", function(){
tt.html("This axis uses SI prefixes:<br>m: 10<sup>-3</sup><br>k: 10<sup>3</sup><br>M: 10<sup>6</sup>")
.style('top', d3.event.pageY - 12 + 'px')
.style('left', d3.event.pageX + 25 + 'px')
.style("opacity", 0.9);
}).on("mouseout", function(d) {
tt.style("opacity", 0);
});
//end of redraw
}
function filter(){
function getCheckedBoxes(chkboxName) {
var checkboxes = document.getElementsByName(chkboxName);
var checkboxesChecked = [];
for (var i=0; i<checkboxes.length; i++) {
if (checkboxes[i].checked) {
checkboxesChecked.push(checkboxes[i].defaultValue);
}
}
return checkboxesChecked.length > 0 ? checkboxesChecked : null;
}
var checkedBoxes = getCheckedBoxes("continent");
var newData = [];
if (checkedBoxes == null){
dataSet = newData;
redraw();
return;
};
for (var i = 0; i < checkedBoxes.length; i++){
var newArray = data.filter(function(d){
return d.continent == checkedBoxes[i];
});
Array.prototype.push.apply(newData, newArray);
}
dataSet = newData;
redraw(chartState.variable);
//end of filter
}
//end of d3.csv
});

D3.js Radar chart line drawing

I am trying to create a radar chart similar to the link here (
http://www.larsko.org/v/euc/).
I was able to create axes (my work so far), but I am having a problem to draw lines in it.
For instance, if I have a list of values something like below, how can I draw a line in the radar chart?
var tempData = [56784, 5.898, 3417, 0, 0, 0]
Edit: I have included code. I am having a problem finding XY coordinates and I think XY value has to be derived from "scales".
var width = 1000,
height = 960,
r = (960 / 2) - 160;
var svg = d3.select("#radar")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + ", " + height / 2 + ")");
d3.csv("data/results.csv", function(data) {
var headerNames = d3.keys(data[0]);
headerNames.splice(0, 1); //Remove 'scenario'
var minList = $.map(headerNames, function(h) {
return d3.min($.map(data, function(d) {
return d[h];
}));
}),
maxList = $.map(headerNames, function(h) {
return d3.max($.map(data, function(d) {
return d[h];
}));
}),
scales = $.map(headerNames, function(h, i) {
return d3.scale.linear()
.domain([minList[i], maxList[i]])
.range([50, r]);
}),
axes = $.map(headerNames, function(h, i) {
return d3.svg.axis()
.scale(scales[i])
.tickSize(4);
});
function angle(i) {
return i * (2 * Math.PI / headerNames.length) + Math.PI / headerNames.length;
}
var line = d3.svg.line()
.interpolate("cardinal-closed")
/* computing X and Y: I am having a problem here
.x(function(d){ return scales(d); })
.y(function(d){ return scales(d); }); */
$.each(axes, function(i, a) {
svg.append("g")
.attr("transform", "rotate(" + Math.round(angle(i) * (180 / Math.PI)) + ")")
.call(a)
.selectAll("text")
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "rotate(" + -angle(i) * (180 / Math.PI) + ")";
})
//Drawing line
svg.selectAll(".layer")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d) {
return line(d);
})
}) // End CSV
Example results.csv
scenario,n_dead_oaks,percent_dead_oaks,infected_area_ha,money_spent,area_treated_ha,price_per_oak
baseline,56784,5.898,3417,0,0,0
scen2,52725,5.477,3294,382036,35,94.12071939
RS_1,58037,6.028,3407,796705,59,-635.8379888
RS_2,33571,3.487,2555,1841047,104,79.31103261
RS_3,46111,4.79,2762,1176461,61,110.227771
As Squeegy suggested, you should share some code showing your current progress and how you have achieved to create the axes.
Anyways, this is how I would go about this:
For a given list of values that you want to represent as a line, find the [x,y] coordinates of every point of the line, i.e. place your data-points on each axis. If you have a scale system in place already to draw your axes, this shouldn't be too hard.
Use d3.svg.line to draw a line that goes through all these points.
The code would end up looking like this:
var tempData = [56784, 5.898, 3417, 0, 0, 0];
/** compute tempPoints from tempData **/
var tempPoints = [[123, 30], [12, 123], [123, 123], [0,0], [0,0], [0,0]];
var line = d3.svg.line();
d3.select('svg').append('path').attr('d', line(tempPoints) + 'Z'); // the trailing Z closes the path
I think I have a solution for now and I appreciate all of your response! Here is my current solution for my posting.
function getRowValues(data) {
return $.map(data, function(d, i) {
if (i != "scenario") {
return d;
}
});
}
function getCoor(data) {
console.log(data);
var row = getRowValues(data),
x,
y,
coor = [];
for (var i = 0; i < row.length; i++) {
x = Math.round(Math.cos(angle(i)) * scales[i](row[i]));
y = Math.round(Math.sin(angle(i)) * scales[i](row[i]));
coor.push([x, y]);
}
return coor;
}
var line = d3.svg.line()
.interpolate("cardinal-closed")
.tension(0.85);
svg.selectAll(".layer")
.data(data)
.enter()
.append("path")
.attr("class", "layer")
.attr("d", function(d) { return line(getCoor(d)) + "Z"; })
.style("stroke", function(d, i){ return colors[i]; })
.style("fill", "none");

d3.js: why doesn't the data update?

I've got a fairly simple reusable chart built in D3.js -- some circles and some text.
I'm struggling to figure out how to update the chart with new data, without redrawing the entire chart.
With the current script, I can see that the new data is bound to the svg element, but none of the data-driven text or attributes is updating. Why isn't the chart updating?
Here's a fiddle: http://jsfiddle.net/rolfsf/em5kL/1/
I'm calling the chart like this:
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 224,
Clusters: [
['Other', 45],
['FooBars', 30],
['Foos', 50],
['Bars', 124],
['BarFoos', 0]
]
})
.call( clusterChart() );
When the button is clicked, I'm simply calling the chart again, with different data:
$("#doSomething").on("click", function(){
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 122,
Clusters: [
['Other', 14],
['FooBars', 60],
['Foos', 22],
['Bars', 100],
['BarFoos', 5]
]
})
.call( clusterChart() );
});
The chart script:
function clusterChart() {
var width = 450,
margin = 0,
radiusAll = 72,
maxRadius = radiusAll - 5,
r = d3.scale.linear(),
padding = 1,
height = 3 * (radiusAll*2 + padding),
startAngle = Math.PI / 2,
onTotalMouseOver = null,
onTotalClick = null,
onClusterMouseOver = null,
onClusterClick = null;
val = function(d){return d};
function chart(selection) {
selection.each(function(data) {
var cx = width / 2,
cy = height / 2,
stepAngle = 2 * Math.PI / data.Clusters.length,
outerRadius = 2*radiusAll + padding;
r = d3.scale.linear()
.domain([0, d3.max(data.Clusters, function(d){return d[1];})])
.range([50, maxRadius]);
var svg = d3.select(this).selectAll("svg")
.data([data])
.enter().append("svg");
//enter
var totalCircle = svg.append("circle")
.attr("class", "total-cluster")
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radiusAll)
.on('mouseover', onTotalMouseOver)
.on('click', onTotalClick);
var totalName = svg.append("text")
.attr("class", "total-name")
.attr('x', cx)
.attr('y', cy + 16);
var totalValue = svg.append("text")
.attr("class", "total-value")
.attr('x', cx)
.attr('y', cy + 4);
var clusters = svg.selectAll('circle.cluster')
.data(data.Clusters)
.enter().append('circle')
.attr("class", "cluster");
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-value');
var clusterNames = svg.selectAll("text.cluster-name")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-name');
clusters .attr('cx', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('cy', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius; })
.attr("r", "10")
.on('mouseover', function(d, i, j) {
if (onClusterMouseOver != null) onClusterMouseOver(d, i, j);
})
.on('mouseout', function() { /*do something*/ })
.on('click', function(d, i){ onClusterClick(d); });
clusterNames
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius + 16; });
clusterValues
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius - 4; });
//update with data
svg .selectAll('text.total-value')
.text(val(data.Value));
svg .selectAll('text.total-name')
.text(val(data.Name));
clusters
.attr('class', function(d, i) {
if(d[1] === 0){ return 'cluster empty'}
else {return 'cluster'}
})
.attr("r", function (d, i) { return r(d[1]); });
clusterValues
.text(function(d) { return d[1] });
clusterNames
.text(function(d, i) { return d[0] });
$(window).resize(function() {
var w = $('.cluster-chart').width(); //make this more generic
svg.attr("width", w);
svg.attr("height", w * height / width);
});
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onClusterClick = function(_) {
if (!arguments.length) return onClusterClick;
onClusterClick = _;
return chart;
};
return chart;
}
I have applied the enter/update/exit pattern across all relevant svg elements (including the svg itself). Here is an example segment:
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters,function(d){ return d[1];});
clusterValues.exit().remove();
clusterValues
.enter().append('text')
.attr('class', 'cluster-value');
...
Here is a complete FIDDLE with all parts working.
NOTE: I tried to touch your code as little as possible since you have carefully gone about applying a re-usable approach. This the reason why the enter/update/exit pattern is a bit different between the total circle (and text), and the other circles (and text). I might have gone about this using a svg:g element to group each circle and associated text.

Resources