Published
Edited
Nov 14, 2019
Fork of Streamgraph
Insert cell
Insert cell
// Initialize variable for aggregate
// count, sum, mean, min, max, stdev, variance
aggregate = 'min';
Insert cell
// Initialize variable for period
// H, D, W, M, Q, Y
period = 'W';
Insert cell
no_of_x_ticks = width / 80;
Insert cell
Insert cell
chart = {
// Initialize svg
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
// .style("background-color", "#eee");

// Draw area graphs
svg.append("g")
.selectAll("path")
.data(series)
.join("path")
.attr("fill", ({key}) => areaColor(key))
.attr("d", area)
.append("title")
.text(({key}) => key + '%');

// add the Y gridlines
svg.append("g")
.attr("class", "grid")
.attr("transform", `translate(${margin.left},0)`)
.attr("stroke", "lightgrey")
.attr("stroke-opacity", "0.3")
.attr("stroke-width", ".3")
.attr("shape-rendering", "crispEdges")
.call(make_y_gridlines()
.tickSize(-width + margin.left + margin.right)
.tickFormat("")
)

// Loop through each symbol / key
dataNest.forEach(function(d,i) {
svg.append("path")
.attr("class", "line")
.style("stroke", function() { // Add the colours dynamically
return d.color = lineColor(d.key); })
.style("fill", "none")
.style("stroke-width", "3px")
.attr("id", 'tag'+d.key.replace(/\s+/g, '')) // assign ID
.attr("d", priceline(d.values))
.append("title")
.text(d.key + '%');
});

// Append the X axis
let x_axis = svg.append("g")
.attr("id", "x-axis")
.attr("class", "x axis")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(xAxis);
// Set the tick format on the X axis
// Options: H, D, W, M, Q, Y
setTickFormatX(period);
// Rotate tick labels for certain periods
if (['H','D','W','M'].includes(period)) {
x_axis.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)");
}
// console.log(x.ticks());
// Check if number of tick marks are more than fitting the width
// if (x.ticks().length > no_of_x_ticks) {
// xAxis.ticks(d3.timeWeek.every(50));
// }
// Append the Y axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);

// Return the svg
return svg.node();
}
Insert cell
// Use this input data for hourly/daily/weekly
input_data = (
await d3.json("https://gist.githubusercontent.com/sashtur/d17c51a6d8824b6b0bc32b57b31b71e5/raw/31d1b6509fc2ce9f346b670edd5403de9f0234a0/streamgraph", d3.autoType))
// Use this input data for monthly/quarterly/yearly
// input_data = (
// await d3.json("https://gist.githubusercontent.com/sashtur/a19aacff5cee92f89d2864eee3050b23/raw/0a479481ddd4ad47e9f78e8cb4596f795c9e1663/gistfile1.txt", d3.autoType))
Insert cell
getPrimary = (data) => {
// Get primary value by finding the array having a 'primary' key with good value
let primary_array = data.filter( d => d.hasOwnProperty("primary") && d['primary'] !== undefined );
return primary_array;
}
Insert cell
primary_array = getPrimary(input_data)
Insert cell
primary_value = primary_array[0].value;
Insert cell
getSecondary = (data) => {
// Find the arrays that do not have a 'primary' key
let secondary_arrays = data.filter( d => d.value != primary_value );
return secondary_arrays;
}
Insert cell
secondary_arrays = getSecondary(input_data);
Insert cell
secondary_values = secondary_arrays.map( d => d.value );

Insert cell
processData = (data, values) => {
// console.log(data);
// Initialize array to hold final array of processed rows
let processed_array = [];
// Loop thru all data items - each row of the complex structure
for (let i = 0; i < data.length; i++) {
// Loop thru all entries and create 1 row per entry
for (let [k, v] of Object.entries(data[i].data)) {
let row = {};
// Row attributes
// row['name'] = data[i].name;
row['date'] = new Date(k);
// Loop thru all good percentile values
for (let value of values) {
// row['percentile'] = data[i].value + '%';
row['percentile'] = data[i].value;
row['value'] = v;
}
// Add row to array
processed_array.push(row);
}
}
return processed_array;
}
Insert cell
rollupFunction = (v, aggregate) => {
switch (aggregate) {
case 'count':
return v.length;
break;
case 'sum':
return d3.sum( v, d => d.value );
break;
case 'min':
return d3.min( v, d => d.value );
break;
case 'max':
return d3.max( v, d => d.value );
break;
case 'mean':
return d3.mean( v, d => d.value );
break;
case 'stdev':
return d3.deviation( v, d => d.value );
break;
case 'variance':
return d3.variance( v, d => d.value );
break;
}
}
Insert cell
keyFunction = (d) => {
return d3.timeFormat(time_format)(d.date);
}
Insert cell
getFilteredData = (primary_value, secondary_values, matching) => {
// Get 'distances' of all secondary values from primary value
let distances = secondary_values.map( s => Math.abs(primary_value - s) );
// Get all secondary values that are equidistant from primary value
// First get the counts for each distance
let counts = distances.reduce( (r, k) => { r[k] = ++r[k] || 1; return r }, {} );
// Now filter out those which have a count == 2
let match = Object.keys(counts).filter( k => counts[k] == 2 ).map( k => +k );
// Now get the corresponding secondary values for matching distances from primary
let result_values = match.map( m => primary_value - m ).concat( match.map( m => primary_value + m ) );
// Get non-matching set of values if required
if (!matching)
result_values = secondary_values.filter(s => !result_values.includes(s)).concat([primary_value]);
// Extract corresponding secondary arrays from input data
let secondary_array = input_data.filter(d => result_values.includes(d.value) );
// Process the secondary array so that there is one row per date and percentile
// [{date: Fri Jan 02 2015 05:30:00 GMT+0530 (India Standard Time), percentile: "25", value: 215}, ...]
let processed_data = processData(secondary_array, result_values);
// console.log('processed_data', processed_data);
// Now rollup the data (sum) on month and percentile as keys
// This will be of the form
// {01-2015: {25: 69840, 75: 77541, 5: 82570, 95: 75823}, 02-2015: {…},...}
let rolledup_data = d3.nest()
.key( d => keyFunction(d) )
.key( d => d.percentile )
.rollup( v => rollupFunction(v, aggregate) )
.object( processed_data );
// console.log('rolledup_data', rolledup_data);
// Return sorted list
return rolledup_data;
}
Insert cell
// Get array which will be used for the area charts
getAreaData = (input_data, primary_value, secondary_values) => {
// Get rolledup data for matching secondary values
let rolledup_data = getFilteredData(primary_value, secondary_values, true);
// console.log('area filtered_data', filtered_data);
// Now flatten the structure to get the following structure
// [{25: 69840, 75: 77541, 5: 82570, 95: 75823, date: "01-2015"}, {}, ...]
let flattened_data = Object.keys(rolledup_data).map( d => {
let f = { date: d };
Object.assign(f, Object.fromEntries(
Object.entries(rolledup_data[d]).map( ([k, v]) =>
[k, v])
));
return f;
});
// console.log('flat areadata', flatdata);
// Return the flattened data sorted on date
return flattened_data.sort( (a, b) => a.date - b.date );
}
Insert cell
area_data = getAreaData(input_data, primary_value, secondary_values);
Insert cell
series = d3.stack()
.keys(Object.keys(area_data[0]).slice(0,-1)) // Get all elements except last (which is date)
.offset(d3.stackOffsetWiggle)
// .order(d3.stackOrderInsideOut)
(area_data)
Insert cell
area = d3.area()
.x(d => x(new Date(parseDate(d.data.date))))
.y0(d => y(d[0]))
.y1(d => y(d[1]))
Insert cell
// Get array which will be used for the line charts
getLineData = (input_data, primary_value, secondary_values) => {
// Get rolledup data for non-matching secondary values
let rolledup_data = getFilteredData(primary_value, secondary_values, false);
// console.log('line filtered_data', filtered_data);
// Now flatten the structure to get the following structure
// [{percentile: "50", date: 2019-10-01, value: 10}, {}, ...]
let flattened_data = [];
Object.keys(rolledup_data).forEach( d => {
Object.keys(rolledup_data[d]).forEach( p => {
let f = {
percentile: p,
date: d,
value: rolledup_data[d][p]
};
flattened_data.push(f);
});
});
// Return the flattened data sorted on date
return flattened_data.sort( (a, b) => a.date - b.date );
}
Insert cell
line_data = getLineData(input_data, primary_value, secondary_values)
Insert cell
dataNest = d3.nest()
.key(d => d.percentile)
.entries(line_data);
Insert cell
lineCurve = d3.curveCatmullRom;
Insert cell
priceline = d3.line()
.defined(d => !isNaN(d.value))
.x(d => x(new Date(parseDate(d.date))))
.y(d => y(d.value))
.curve(lineCurve);
Insert cell
// gridlines in x axis function
function make_x_gridlines() {
return d3.axisBottom(x);
}
Insert cell
// gridlines in y axis function
function make_y_gridlines() {
return d3.axisLeft(y);
}
Insert cell
d3.timeWeeks(x.domain()[0], x.domain()[1]).length
Insert cell
x = d3.scaleTime()
.domain(d3.extent(area_data, d => new Date(parseDate(d.date))))
.range([margin.left, width - margin.right])
Insert cell
y = d3.scaleLinear()
.domain([d3.min(series, d => d3.min(d, d => d[0])), d3.max(series, d => d3.max(d, d => d[1]))])
.range([height - margin.bottom, margin.top]).nice()
Insert cell
getAreaColors = (count) => {
let blues = d3.schemeBlues[count];
// Match 1st color with last, 2nd with 2nd last, etc.
// Loop thru half the blues
for (let i=0; i<blues.length/2; i++) {
blues[blues.length-i-1] = blues[i];
}
// Return result
return blues;
}
Insert cell
area_colors = getAreaColors(series.length)
Insert cell
areaColor = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(area_colors)
Insert cell
areaColor.domain()
Insert cell
lineColor.domain()
Insert cell
lineColor = d3.scaleOrdinal()
.domain(dataNest.map(d => d.key))
.range(d3.schemeDark2)
Insert cell
quarter = (d) => {
// Return quarter
return "Q" + (1 + ~~(d.getMonth() / 3)) + "-" + d.getFullYear();
}
Insert cell
quarter(new Date('2017-09-01'))
Insert cell
time_format = getTimeFormat(period)
Insert cell
getTimeFormat = p => {
switch (p) {
case 'H':
return '%d-%m-%Y:%H';
break;
case 'D':
return '%d-%m-%Y';
break;
case 'W':
return '%d-%m-%Y';
break;
case 'M':
return '%m-%Y';
break;
case 'Q':
return '%m-%Y';
break;
case 'Y':
return '%Y';
break;
}
}
Insert cell
parseDate = d3.timeParse(time_format);
Insert cell
setTickFormatX = (period) => {
return xAxis.tickFormat(d3.timeFormat(time_format));
}
Insert cell
xAxis = d3.axisBottom(x).ticks(no_of_x_ticks).tickSizeOuter(0)
// xAxis = d3.axisBottom(x).ticks(d3.timeWeek).tickSizeOuter(0);
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(null, "s"))
Insert cell
height = 500
Insert cell
margin = ({top: 5, right: 15, bottom: 50, left: 40})
Insert cell
d3 = require("d3@5")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more