D3.js Radar chart line drawing - d3.js

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");

Related

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
});

Not able to add the fill between the paths. using d3js

i am trying to fill the background between 2 lines, but i am not getting any correct output.
and i would like to remove the tick line in the y axis as well. how to get this both?
here is my code : any one correct me please?
$(function(){
var m = [80, 80, 80, 80]; // margins
var w = 300; // width
var h = 450; // height
var plan = 55;
var actual = 38;
var variation = plan - actual;
var data = [0,plan];
var data1 = [0,actual];
var x = d3.scale.linear().domain([0, 2]).range([0, w]);
var y = d3.scale.linear().domain([0, 100]).range([h, 0]);
var line = d3.svg.line()
.x(function(d,i) {
return x(i);
})
.y(function(d) {
return y(d);
})
var graph = d3.select("#graph").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
var yAxisLeft = d3.svg.axis().scale(y).tickSize(-w).orient("left");
graph.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(-0,0)")
.call(yAxisLeft);
graph.append("svg:path").attr("d", line(data));
graph.append("svg:path").attr("d", line(data1));
//not able to fill the bg between 2 lines
var area = d3.svg.area()
.x(function(d, i) { return 0 })
.x1(function(d, i) { return plan })
.y0(function(d, i) { return y(actual); })
.y1(function(d, i) { return y(variation); })
.interpolate("basis");
graph.append("path")
.datum(data)
.attr("d", area)
.attr("fill", "#CCC");
});
Live Demo
Concerning your area :
var area = d3.svg.area()
.x(function(d, i) { return 0 })
.x1(function(d, i) { return plan })
.y0(function(d, i) { return y(actual); })
.y1(function(d, i) { return y(variation); })
.interpolate("basis");
x and x1 are returning static values, so it won't draw an area but just a line
your both lines have same x axis so you just have to specify .x()
y0 and y1 are also returnin static values
Here is a working version :
var area = d3.svg.area()
.x(function(d, i) { return x(i) })
.y0(function(d, i) { return y(data[i]); })
.y1(function(d, i) { return y(data1[i]); })
Also be careful, you have a fill: none; in your css file so you won't see anything.
Functional plunker : http://plnkr.co/edit/xFNF3BQzd0IO5bauAiFU?p=preview

Dots are not appending to right graph?

I am having a problem with my code where the circle nodes that I have created do not seem to be appending themselves to the graph. The debugger isn't pulling up any errors, so it seems like it's a logic issue.
Here's what is looks like:
(dot)
|
|
|
|
|
| <--(where dot should be)
|
__________________________________________
^
|
graph
And here's my code:
<script>
var slider = $("#myRange").val();;
var xtwo;
var xone;
var xzero;
var firstx = -1;
var firsty = 1;
var secondx = 2;
var secondy = 4;
var lineData = [];
var nodes = [];
//updates coefficients
function updateXs() {
xtwo = (75 - slider) / 50;
xone = (slider - 25) / 50;
xzero = (slider - 25) / 25;
}
//gets corresponding y from x and coefficients
function getY(xval) {
return (xval * xval * xtwo + xval * xone + xzero);
}
function displayVals() {
document.getElementById("demo").innerHTML = slider + " " + xtwo + " " + xone + " " + xzero + " " + lineData;
}
function updateLineData() {
//resets and fills points
lineData = [];
for (i = (firstx - 1); i < (secondx + 2); i++) {
lineData.push({
x: i,
y: getY(i)
});
}
}
//makes dots for static points
function makeDots(xvalue, xvalue2) {
nodes = [{
x: xvalue,
y: getY(xvalue)
}, {
x: xvalue2,
y: getY(xvalue2)
}]
}
function makeLine() {}
$(document).ready(function() {
updateXs();
updateLineData();
displayVals();
var vis = d3.select('#visual'),
WIDTH = 1000,
HEIGHT = 500,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
},
xRange = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([d3.min(lineData, function(d) {
return d.x;
}), d3.max(lineData, function(d) {
return d.x;
})]),
yRange = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([d3.min(lineData, function(d) {
return d.y;
}), d3.max(lineData, function(d) {
return d.y;
})]),
xAxis = d3.svg.axis()
.scale(xRange)
.tickSize(5)
.tickSubdivide(true),
yAxis = d3.svg.axis()
.scale(yRange)
.tickSize(5)
.orient("left")
.tickSubdivide(true);
vis.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (HEIGHT - MARGINS.bottom) + ")")
.call(xAxis);
vis.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(" + (MARGINS.left) + ",0)")
.call(yAxis);
var lineFunc = d3.svg.line()
.x(function(d) {
return xRange(d.x);
})
.y(function(d) {
return yRange(d.y);
})
.interpolate('basis');
vis.append("svg:path")
.attr("d", lineFunc(lineData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
makeDots(firstx, secondx);
//puts in dots
vis.selectAll("circle.nodes")
.data(nodes)
.enter()
.append("circle")
.attr("cx", function(d) {
console.log(d.x)
return 200;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", "10px")
.attr("fill", "black")
//updates when slider changes
$("#myRange").change(function() {
slider = $("#myRange").val();
updateXs();
updateLineData();
displayVals();
});
});
</script>
Please help and thanks in advance. Also, I was wondering if I could get some tips on how to make the line move using the .change function
You haven't applied your scale function to your dots:
vis.selectAll(".nodes")
.data(nodes)
.enter().append("circle")
.attr("class", "nodes")
.attr("cx", function (d) {
return xRange(d.x); //<-- convert from user space to pixel space
})
.attr("cy", function (d) {
return yRange(d.y); //<-- convert from user space to pixel space
});
To make your line update, you need to select it and change the d attribute:
d3.select(".myLine") //<-- select it by some unique class
.attr("d", lineFunc(lineData)); //<-- update the d attribute
To get a smooth curve without interpolation, just supply more points. See udpated fiddle.
Fiddle here.

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.

d3.js categorical time series (evolustrip)

Working in d3.js, I am looking for a good way to display categorical time series data. The data values cannot co-occur, and are not evenly spaced, so I've data exactly like:
location = [[time1: home], [time4: work], [time5: cafe], [time7: home]]
and so on. My ideal resulting graph is something like what might be called an evolustrip - one way of seeing this chart is as a time series chart with variable width bars, bar color corresponding to category (e.g. 'home').
Can anyone point me in the right direction? Thank you so much!
So I ended up crafting my own d3.js solution:
I used a d3.time.scale scale for the time dimension, and then a d3.scale.category20 scale to provide colors for the categories. I then plotted the categorical data as same-height rects on the time axis by start time, and used the d3.time.scale scale to compute the appropriate bin width for each rect.
A reusable component (following the pattern at http://bost.ocks.org/mike/chart/) example can be seen here:
function timeSeriesCategorical() {
var w = 860,
h = 70,
margin = {top: 20, right: 80, bottom: 30, left: 50},
width = w - margin.left - margin.right,
height = h - margin.top - margin.bottom;
var xValue = function(d) { return d[0]; },
yValue = function(d) { return d[1]; };
var yDomain = null;
var xScale = d3.time.scale()
.range([0, width]);
var yScale = d3.scale.category20();
var xAxis = d3.svg.axis()
.scale(xScale)
.tickSubdivide(1)
.tickSize(-height)
.orient('bottom');
var yAxis = d3.svg.axis()
.scale(yScale)
.ticks(5)
.orient('left');
var binwidth = 20;
function chart(selection) {
selection.each(function(data) {
// convert data to standard representation
data = data.map(function(d, i) {
return [xValue.call(data, d, i), yValue.call(data, d, i)];
//return d;
});
// scale the x and y domains based on the actual data
xScale.domain(d3.extent(data, function(d) { return d[0]; }));
if (!yDomain) {
yScale.domain(d3.extent(data, function(d) { return d[1]; }));
} else {
yScale.domain(yDomain);
}
// compute binwidths for TODO better comment
// d looks like {timestamp, category}
data.forEach(function(d, i) {
if (data[i+1]) {
w_current = xScale(data[i][0]);
w_next = xScale(data[i+1][0]);
binwidth = w_next - w_current;
}
d.binwidth = binwidth;
});
// create chart space as svg
// note: 'this' el should not contain svg already
var svg = d3.select(this).append('svg').data(data);
// external dimensions
svg.attr('width', w)
.attr('height', h);
// internal dimensions
svg = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// x axis
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
// TODO bars legend
// bars
svg.selectAll('rect')
.data(data)
.enter().append('rect')
.attr('x', function(d, i) { return xScale(d[0]); })
.attr('width', function(d, i) { return d.binwidth; })
.attr('height', height)
.attr('fill', function(d, i) { return yScale(d[1]); })
.attr('stroke', function(d, i) { return yScale(d[1]); });
});
}
chart.x = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return chart;
};
chart.y = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return chart;
};
chart.yDomain = function(_) {
if (!arguments.length) return yDomain;
yDomain = _;
return chart;
};
return chart;
}
and is callable with something like:
d3.csv('./data.csv', function(data) {
var chartActivity = timeSeriesCategorical()
.x(function(d) { return d.when; })
.y(function(d) { return d.activity; })
.yDomain([0,1]);
d3.select('#chart-activity')
.datum(data)
.call(chartActivity);
});
Hopefully this is helpful to someone! The project this was made for is at https://github.com/interaction-design-lab/stress-sense-portal

Resources