Published
Edited
Nov 8, 2019
1 fork
1 star
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
// .style("background-color", "#eee");

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

svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);

return svg.node();
}
Insert cell
lineColor(50)
Insert cell
input_data = (
await d3.json("https://gist.githubusercontent.com/sashtur/d17c51a6d8824b6b0bc32b57b31b71e5/raw/5c9d08ca23d887de449cd4c927aecfd0f152014a/streamgraph", 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
processAreaData = (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++) {
// Get the nested data array
let nested_array = data[i].data;
// Get the entries in the nested array
let entries = Object.entries(nested_array);
// Loop thru all entries and create 1 row per entry
for (const [k, v] of entries) {
let row = {};
// Row attributes
// row['name'] = data[i].name;
row['date'] = new Date(k);
// Loop thru all good percentile values
for (const value of values) {
row[value + '%'] = v;
}
// Add row to array
processed_array.push(row);
}
}
return processed_array;
}
Insert cell
getEquidistantValues = (primary_value, secondary_values) => {
// 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 map = distances.reduce( (r, k) => { r[k] = ++r[k] || 1; return r }, {} );
// Now filter out those which have a count == 2
let match = Object.keys(map).filter( k => map[k] == 2 ).map( k => +k );
// Now get the corresponding secondary values
let equal_secondary_values = match.map( m => primary_value - m ).concat( match.map( m => primary_value + m ) );
return equal_secondary_values.sort( (a, b) => a - b );
}
Insert cell
// Get array which will be used for the area charts
getAreaData = (input_data, primary_value, secondary_values) => {
// Get equidistant secondary values
let equal_secondary_values = getEquidistantValues(primary_value, secondary_values);
// Now get the corresponding secondary arrays -- these will be used as input data for the series
let data_array = input_data.filter(d => equal_secondary_values.includes(d.value) );
// console.log(equal_secondary_values);
// Process the data array so that for each percentile there are as many rows as # of keys in data
const processed_data = processAreaData(data_array, equal_secondary_values);
return processed_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(d3.keys(area_data[0]).slice(1))
.offset(d3.stackOffsetWiggle)
// .order(d3.stackOrderInsideOut)
(area_data)
Insert cell
lineCurve = d3.curveCatmullRom;
Insert cell
processLineData = (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++) {
// Get the nested data array
let nested_array = data[i].data;
// Get the entries in the nested array
let entries = Object.entries(nested_array);
// Loop thru all entries and create 1 row per entry
for (const [k, v] of entries) {
let row = {};
// Row attributes
// row['name'] = data[i].name;
row['percentile'] = data[i].value + '%';
row['date'] = new Date(k);
row['value'] = +v;
// Add row to array
processed_array.push(row);
}
}
return processed_array;
}
Insert cell
// Get array which will be used for the line charts
getLineData = (input_data, primary_value, secondary_values) => {
// Get equidistant secondary values
let equal_secondary_values = getEquidistantValues(primary_value, secondary_values);
// Get the arrays which are not in the above set
let data_array = input_data.filter(d => !equal_secondary_values.includes(d.value) );
let line_values = data_array.map( d => d.value );
// Process the data array so that for each percentile there are as many rows as # of keys in data
const processed_data = processLineData(data_array, line_values);
console.log(processed_data);
return processed_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
area = d3.area()
.x(d => x(d.data.date))
.y0(d => y(d[0]))
.y1(d => y(d[1]))
Insert cell
priceline = d3.line()
.defined(d => !isNaN(d.value))
.x(d => x(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
x = d3.scaleUtc()
.domain(d3.extent(area_data, d => 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
areaColor = d3.scaleOrdinal()
.domain(series.map(d => d.key))
.range(["powderblue", "skyblue", "skyblue", "powderblue"])
.unknown("#ccc");
Insert cell
areaColor.domain()
Insert cell
lineColor.domain()
Insert cell
lineColor = d3.scaleOrdinal()
.domain(dataNest.map(d => d.key))
.range(["tomato", "darkgoldenrod", "firebrick"])
.unknown("#ccc");
Insert cell
xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).ticks(width / 80).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: 10, right: 10, bottom: 20, left: 30})
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