D3 unusual stacked bar chart - javascript

I need to do an unusual stacked bar chart and acutally, i have no real idea how to do it.
In general it sounds really easy:
I have a CSV file with different values who can either be int or string. Each row has the same amount of values.
I now need to do a stacked bar chart who does the following:
-One bar for every column
-Every column needs to have the same height (cause same amount of entries for each column)
And EACH bar needs to have a different amount of stacks, one stack for every different value which exists. (The more often a value exists, the larger this stack should be)
An example CSV file would be:
Day, Value
Mo, 5
Mo, 3
Tu, 5
Tu, 6
So for the Day row i need 2 stacks the same height and for the Value row 3 stacks where one is 1/2 height and the two other 1/4 each.
And that's the problem which i have. Every example i can find on the internet works with the same amount of stacks for each bar. (For example: https://bl.ocks.org/mbostock/3886394 or https://bl.ocks.org/mbostock/1134768)
Any sugestions how i can solve this problem?

You need to handle each column as a separate stack and position them in your chart. This fiddle: https://jsfiddle.net/gwhn1sgv/2/ shows how.
The data for i. e. the first column need to be transformed to a histogram like this:
var column = 'Day';
var histo = { Mo: 2, Tu: 2 }
Then you can stack and transform them:
var keys = Object.keys(histo);
var stack = d3.stack()
.keys(keys)([histo])
//unpack the column
.map((d, i) => {
return {key: keys[i], data: d[0]}
});
so they look like this:
var stack = [
{ key: 'Mo', data: [0, 2] },
{ key: 'Tu', data: [2, 4] }
]
If you have a band scale
var x = d3.scaleBand()
.domain(data.columns);
you can build the bar chart column at x(column):
chart.append('g')
.selectAll('rect')
.data(stack)
.enter().append('rect')
// position in the x-axis
.attr('x', x(column))
.attr('y', d => y(d.data[0]))
.attr("height", d => y(d.data[1]) - y(d.data[0]))
.attr("width", x.bandwidth());

Related

Chart.js need to fix x axis number of vales

I am using chart.js JavaScript library for generating charts.
On my Yaxis I have float values and I am updating them after every one second.
On my Xaxis I have time line, actually I am displaying time line as data is updating after every second so no use of Xaxis time.
But my data keeps updating for n number of time and because of that as data increases my chart becomes unreadable and clumsy.
So I want to limit around 100 data points should be displayed at a given point. So every time graph displays <= 100 items at a time, even if I keep adding new data.
How this can be done using chart.js with simple line chart.
The time axis type in ChartJS supports min and max - so you can work out what the oldest value you want displayed it and set its x value as min.
let data = [/* some data */]
data.sort((a, b) => a.x < b.x)
const min = data.length > 100 ? data[99].x : data[data.length - 1].x
var chart = new Chart(ctx, {
type: 'line',
data,
options: {
scales: {
xAxes: [{
time: {
min
}
}]
}
}
})

Stack a matrix in d3 without remapping to json

The docs for d3's stacking function d3.stack show an example with an array of objects (each json object representing the ensemble of points for whatever the x-axis is measuring). Eg:
var data = [
{month: new Date(2015, 0, 1), apples: 3840, bananas: 1920, cherries: 960},
{month: new Date(2015, 1, 1), apples: 1600, bananas: 1440, cherries: 720}
]
I'm trying to produce a stacked histogram with a matrix of data series ([ [], [], [], etc ]). It's easy enough to iterate through the rows and get a series of histogram bins (having pre-defined the x scale and domain elsewhere):
for(let i=0; i<data.length; i++){
bins[i] = d3.histogram()
.domain(x.domain())
.thresholds(x.ticks(10))
(data[i]);
}
And create groups for each data series inside another loop:
let bars = this.svg.selectAll(".series" + i)
.data(this.bins[i])
.enter().append("g")
.classed("series" + i, true)
But of course doing it like that I get stuck here. How am I supposed to bars.append("rect") at the correct x,y coords for that particular series? Stated differently, I have a really useful array of bins at the moment, looking something like:
[
[[1,2,3,3], [5,8,9], [10], ... etc], //series0 grouping by bins of 5
[[1,3], [7,7,9,9], [11], ... etc], //series1
[[2,3,3], [8,9], [10,12], ... etc], //series2
...etc
]
Is there a way to invoke stack without munging all the data into json key,value pairs?
I took a glance at the source and no comments + single char variables = me understanding that it's not going to happen without munging. I present therefore my shoddy attempt at saving someone else some time:
/*
* Static helper method to transform an array of histogram bins into an array of objects
* suitable for feeding into the d3.stack() function.
* Args:
* bins (array): an array of d3 histogram bins
*/
static processBins(bins){
let temp = {}; // the keys for temp will be the bin name (i.e. the bin delimiter value)
// now create an object with a key for each bin, and an empty object as a placeholder for the data
bins[0].map( (bin) => { temp[bin.x0] = {}});
for(let i=0; i<bins.length; i++){
//traverse each series
bins[i].map( bin => {
temp[bin.x0]["series"+i] = bin.length; //push the frequency counts for each series
});
}
/* now we have an object whose top-level keys are the bins:
{
binName0: { series0: freqCount0, series1: freqCount1, ...},
binName1: {...},
...
}
now, finally we're going to make an arrays of objects containing all the series' freqencies for that bin
*/
let result = [];
for(let binName in temp){ // iterate through the bin objects
let resultRow = {};
if(temp.hasOwnProperty(binName)){
resultRow["bin"] = binName; //put the bin name key/value pair into the result row
for(let seriesName in temp[binName]){ //iterate through the series keys
if(temp[binName].hasOwnProperty([seriesName])){
resultRow[seriesName] = temp[binName][seriesName];
}
}
}
result.push(resultRow);
}
return result;
}
Call like:
let stack = d3.stack().keys( bins.map( (d,i)=>{return "series"+i})); //stack based on series name keys
let layers = stack(MyCoolHistogram.processBins(bins));
//and now your layers are ready to enter() into a d3 selection.
Edit:
I note that the stack data third argument in anonymous functions seems to be the array of elements. I.e. it's no longer the stack layer index. Eg, when grouping bars side-by-side: http://bl.ocks.org/mbostock/3943967
This breaks grouping functions that rely on this index number to calculate the x position:
rect.attr("x", (d,i,j) => { return x(d.data.bin) + j*barWidth/numberOfSeries});
I guess it's telling that Mike's gist still uses v3, despite being updated long after v4 came out.
To get the layer index you have to use the layer.index attribute directly. So when grouping you would translate the entire layer (which screws up bar-by-bar animations, of course... sigh).
let layers = d3.stack(yourData);
let layer = this.svg.selectAll(".layer")
.data(layers)
layer.transition()
.attr("transform", d => { return "translate(" + d.index*barWidth/numberOfSeries + ",0)"; });

The scale on the axis doesn't automatically narrow

I'm using chart.js but on some and only some of the graphs it creates the y-axis scale goes from 0-100 when a more appropriate scale might be 80-100. This means all the lines are bunched up at the top.
You can see what I mean if you visit mbi.dajubox.com and select '14 days' under waiting times. When the results come up beneath click the first entry (Calderdale And Huddersfield NHS Foundation Trust) and the graph appears. But the lines are bunched at the top.
If I go down to number 15 though (Stockport NHS Foundation Trust) it scales the axis ok.
The code that generates them is the same
var ctx = document.getElementById("myChart_"+provID).getContext("2d");
var myLineChart = new Chart(ctx).Line(data, {bezierCurve: false, multiTooltipTemplate: "<%= datasetLabel %> - <%= value %>"});
Can any one help me out?
This is because in your data you receive null values to display in the chart. Chartjs's min function is just a wrapper for the Math.min which will treat null as 0.
A fix for this can be to override the helper function calculateScaleRange
Just declare this after you have Chart.js (or apply the small change straight to your Chart.js)
Chart.helpers.calculateScaleRange = function (valuesArray, drawingSize, textSize, startFromZero, integersOnly) {
//Set a minimum step of two - a point at the top of the graph, and a point at the base
var minSteps = 2,
maxSteps = Math.floor(drawingSize / (textSize * 1.5)),
skipFitting = (minSteps >= maxSteps);
var maxValue = Chart.helpers.max(valuesArray),
minValue = Chart.helpers.min(valuesArray.map(function(value){
//using map to create a new array where all nulls are mapped to Infinity so they do not pull the result down to 0
return value === null ? Infinity: value;
}));
................ //lots more code that is part of calculateScaleRange
here is a full example http://jsfiddle.net/leighking2/L9kLxpe1/

Rescale axis ticks and text in d3.js multi y-axis plot

I have a parallel coordinates plot that is based off this code: http://bl.ocks.org/syntagmatic/2409451
I am trying to get the tick marks and the numbers on the y axes to scale from the min to the max of the data rather than autoscaling to the conveniently linear numbers like it currently done.
I have not been able to find any example of using d3 or js where a plot of any sort does this unless the data happens to land on those values.
I have been able to just show the min and max value, but cannot get ticks between these by replacing the 3rd line of //Add an axis and title with:
.each(function(d) {d3.select(this).call(d3.svg.axis().scale(y[d]).tickValues(y[d].domain()).orient("left")); })
For reference, the data file is read in as a .csv and ends up looking like this with alphabet representing the headings in the .csv file:
var example_data = [
{"a":5,"b":480,"c":250,"d":100,"e":220},
{"a":1,"b":90,"c":50,"d":33,"e":88}
];
EDIT:
The main issue is iterating over the array that has the domains for each column to create a new array with the tick values. Tick values can be set using:
d3.svg.axis().scale(y[d]).tickValues(value 1[d],value 2[d], etc)
y[d] is set by:
// Extract the list of dimensions and create a scale for each.
x.domain(dimensions = d3.keys(cars[0]).filter(function(d) {
return d != "name" && (y[d] = d3.scale.linear()
.domain(d3.extent(cars, function(p) { return +p[d]; }))
.range([h, 0]));
}));
Since you have the min and the max you can map them in any way you want to any scale you want [y0,yn]. For example with y0 = 100, yn = 500 (because HTML counts from top and down).
Here I use a linear scale
d3.scale.linear()
.domain([yourMin,yourMax])
.range([y0,yn]);
Does this help?

D3 - using strings for axis ticks

I want to crate a bar chart using strings as the labels for the ticks on the x-axis (e.g., Year 1, Year 2, etc instead of 0,1,2, etc).
I started by using the numeric values for the x-axis (e.g., 0,1,2,3, etc) as follows:
1) I generate my ranges:
x = d3.scale.ordinal()
.domain(d3.range(svg.chartData[0].length)) //number of columns is a spreadsheet-like system
.rangeRoundBands([0,width], .1);
y = d3.scale.linear()
.domain(Math.min(d3.min(svg.chartData.extent),0), Math.max(d3.min(svg.chartData.extent),0)])
.range([height, 0])
.nice();
2) Make the axes:
d3.svg.axis()
.scale(x);
3) Redraw the axes:
svg.select(".axis.x_axis")
.call(make_x_axis().orient("bottom").tickSubdivide(1).tickSize(6, 3, 0));
This works well with default numeric axis labels.
If I try to use an array of strings for the x tickValues like this....
d3.svg.axis()
.scale(x).tickValues(svg.pointsNames); //svg.pointsNames = ["Year 1", "year 2", etc.]
... when I redraw the chart (with or without changes to the data/settings), the labels swap like this.
Notice how Col 1 takes the place of Col 0 and vice versa.
Do you know why this happens?
Update
Just sort the svg.pointsNames before you apply them as tickValues. Make sure you sort them in exactly the same way that you sort your data. This way, a one-one mapping is always maintained between your labels and tick values.
Also if I may, check out the tickFormat` function here. This seems a better option to me.
//Tick format example
chart,xAxis.tickFormat(function(d, i){
return "Year" + d //"Year1 Year2, etc depending on the tick value - 0,1,2,3,4"
})
Thanks for that...I used this function with an inline if-clause to handle a different x-axis series with names instead of numbers.
The factions Array consists of all relevant names sorted by the indexes of the series who then just get matched with its corresponding index in the data.
xAxis = d3.svg.axis().scale(xScale)
.tickFormat(function(d) {
if(seriesX == 'seriesValue'){
return factions[d]}
else{
return d}
})
.orient("bottom");

Categories

Resources