Notebooks 2.0 is here.

Published
Edited
Oct 2, 2019
5 stars
Insert cell
Insert cell
chartDiv = html`
<div id='chart-container' style='position: relative;'>
<div id="he-div" style='position: absolute; float: left; opacity: 1; background-color: white'>${he}</div>
<div id='chart-div' style='overflow-x: scroll'>${chart}</div>
</div>`
Insert cell
he = {
// Initialize svg
const svg = d3.create('svg')
.attr('width', margin.left)
.style("font", "10px sans-serif")
.style("width", "40px")
.style("height", height + margin.top + margin.bottom)
// .style("background-color", "lightgrey");

// Plot the hours in left margin
const hours = svg.selectAll("text")
.data(d3.range(1,25))
.join("text")
.attr("text-anchor", "end")
.attr("x", margin.left)
.attr("y", (d, i) => margin.top + i * cellSize + cellSize/2)
.attr("dx", "-.5em")
.attr("dy", ".3em")
.text(d => displayHour(d));

return svg.node();
}
Insert cell
chart = {
// Initialize svg
const svg = d3.create('svg')
.attr('width', width, height + margin.top + margin.bottom)
.style("font", "10px sans-serif")
.style("width", width)
.style("height", height + margin.top + margin.bottom)
// .style("background-color", "lightgrey");

// Build yearly bucket
const year = svg.selectAll("year")
.data(years)
.join("g")
.attr("class", d => yearClass(d))
.attr("transform", (d, i) => `translate(${(yearWidth + yearPadding) * i + margin.left}, ${margin.top})`)
// console.log('year', year.nodes());

// Show year
year.append("text")
.attr("x", cellSize/2)
.attr("dx", "-.2em")
.attr("y", 0)
.attr("dy", "-2em")
.attr("font-weight", "bold")
// .attr("text-anchor", "end")
.text(d => d.key);
// Show months -- use just the 0 hour to pick the month once per 24 hours
year.append("g")
.attr("text-anchor", "middle")
.selectAll("text")
.data(d => d.values.filter(d => d.datetime.getHours() == 0))
.join("text")
.attr("x", d => d.datetime.getMonth() * cellSize + cellSize/2)
.attr("y", 0)
.attr("dy", "-.5em")
.text(monthLetter);

// Draw the cells
year.append("g")
.selectAll("rect")
.data(d => d.values)
.join("rect")
.attr("width", cellSize - 1)
.attr("height", cellSize - 1)
.attr("x", d => d.datetime.getMonth() * cellSize)
.attr("y", d => d.datetime.getHours() * cellSize)
.attr("fill", d => (d.value === undefined) ? color(m(0)) : color(m(d.value))) // cells without data shown in white
.attr("class", d => cellClass(d))
.on("mouseover", handleMouseover)
.on("mouseout", handleMouseout)
.append("title")
.text(d => getTitle(d)); // title not shown for cells without data

return svg.node();
}
Insert cell
m(0)
Insert cell
d3.extent(data.series, d => d.value)
Insert cell
m = d3.scaleLinear()
.domain(colorDomain(undefined, data.series)) // pass dataExtent instead of undefined to override
.range([-1, +1]);
Insert cell
colorDomain = (dataExtent, dataValues) => {
if (dataExtent != undefined)
return [dataExtent.min, dataExtent.max];
else
// Get the range of available data
var dataRange = d3.extent(dataValues, d => d.value)
// Process the ranges to come up with linear distribution
dataRange[0] = Math.min(dataRange[0], -Math.abs(dataRange[1]))
dataRange[1] = Math.max(dataRange[1], Math.abs(dataRange[0]))
// return d3.extent(dataValues, d => d.value);
return dataRange;
}
Insert cell
years = d3.nest()
.key(d => d.datetime.getFullYear())
.entries(data.series)
// .reverse();
// console.log(years);
Insert cell
yearPadding = cellSize * 1.5
Insert cell
margin = ({ top: 40, right: 10, bottom: 10, left: 40 })
Insert cell
cellSize = 14
Insert cell
width = (yearWidth + yearPadding) * years.length - yearPadding + margin.right + margin.left
Insert cell
yearWidth = (cellSize * 12)
Insert cell
height = (cellSize * 24)
Insert cell
displayHour = h => 'HE ' + ('0' + h).slice(-2)
Insert cell
formatValue = d3.format("+.2")
Insert cell
formatMonth = m => ('0' + (m+1)).slice(-2)
Insert cell
formatHour = h => ('0' + (h+1)).slice(-2)
Insert cell
formatDate = d3.timeFormat("%d-%m-%Y %H:%M")
Insert cell
parseDate = d3.timeParse("%d-%m-%Y %H:%M");
Insert cell
monthLetter = d => "JFMAMJJASOND"[d.datetime.getMonth()]
Insert cell
color = {
// const max = d3.max(data.series.map(d => d.value));
return d3.scaleLinear().domain([-1, 0, 1]).range(["#ff0000", "#f8f8f8", "#00ff00"]).interpolate(d3.interpolateRgb);
// return d3.scaleSequential(d3.interpolatePiYG).domain([-max, +max]);
}
Insert cell
getDates = (startDate, endDate, interval) => {
const duration = endDate - startDate;
const steps = duration / interval;
return Array.from({length: steps+1}, (v,i) => new Date(startDate.valueOf() + (interval * i)));
}
Insert cell
yearClass = d => {
return 'year year-' + d.key;
}
Insert cell
// Function to add classes to each cell based on its month and hour coordinates
cellClass = d => {
const cellYear = d.datetime.getFullYear();
const cellMonth = d.datetime.getMonth();
const cellHour = d.datetime.getHours();
return 'cell cell-month-' + cellYear + '-' + cellMonth + ' cell-hour-' + cellHour;
}
Insert cell
handleMouseover = d => {
// Get year
const containerYear = d.datetime.getFullYear();
const cellYear = d.datetime.getFullYear();
const cellMonth = d.datetime.getMonth();
const cellHour = d.datetime.getHours();
// Highlight the month column for this year
$("g.year.year-" + containerYear).find("rect.cell.cell-month-" + cellYear + "-" + cellMonth).addClass("selected-cells");
// Highlight the hour row
$("rect.cell.cell-hour-" + cellHour).addClass("selected-cells");
// Highlight the intersection - the actual cell selected
$("g.year.year-" + containerYear).find("rect.cell.cell-month-" + cellYear + "-" + cellMonth + ".cell-hour-" + cellHour).addClass("selected-cell");
}
Insert cell
handleMouseout = d => {
// console.log(d);
// Reset highlight
$("rect.cell").removeClass("selected-cell selected-cells");
}
Insert cell
getTitle = d => {
const value = (d.value === undefined) ? formatValue(0) : formatValue(d.value);
const month = d.datetime.getFullYear() + '/' + formatMonth(d.datetime.getMonth());
const hour = formatHour(d.datetime.getHours());
return month + ':' + hour + ' ' + value;
}
Insert cell
dataExtent = {
const dataRange = await d3.tsv("https://gist.githubusercontent.com/sashtur/01c06be33afb5b6f2cf62bf39832494b/raw/9aedc5e0acaf72334526cf716f9d1972b502a876/gistfile1.txt",
// Get the min and max values
d => { return {min: +d.minvalue, max: +d.maxvalue}; }
);
return dataRange[0];
};
Insert cell
data = {
const data = await d3.tsv("https://gist.githubusercontent.com/gouriashturkar/9a0b3ad5724a0eb514a4f4063afb1df0/raw/fa12bb6cd3b3c2554b39a4635642e477fcd14de5/gistfile1.txt",
// Format data and build json structure
(d) => {
return {
datetime: new Date(d.datetime),
value: +d.value,
};
});
// Find missing head/tail months
const monthPoints = missingMonthPoints(data);
// console.log(monthPoints);
// Get 24 hourly points for just 1 day in each month
monthPoints.map(
m => d3.timeHour.range(new Date(m.getFullYear(), m.getMonth(), 1), new Date(m.getFullYear(), m.getMonth(), 2))
.map(d => data.push( { datetime: new Date(d), value: undefined } ))
);
// console.log(data);
return {
series: data.sort((a,b) => a.datetime-b.datetime)
};
}
Insert cell
// Get missing monthly data points from January for 1st year until available month for that year
// And also from available last month to December of last year.
missingMonthPoints = data => {
// Initialize month points
var monthPoints = [];
// Get min and max dates in the provided data
var minDate = d3.min(data, d => d.datetime); // Get the min date
var maxDate = d3.max(data, d => d.datetime); // Get the max date

// Check if 1st date is beyond 1st Jan of that year
if (new Date(minDate) > new Date(minDate.getFullYear(), 0, 1)) {
// Build the start date; end date is the min date
let startDate = new Date(minDate.getFullYear(), 0, 1);
let endDate = minDate;
// console.log("head", startDate, endDate, d3.utcMonths(startDate, endDate));
// 1 data point per missing month
d3.utcMonths(startDate, endDate).map(d => monthPoints.push(d));
}
// Check if last date is prior to Dec of that year
if (new Date(maxDate) < new Date(maxDate.getFullYear(), 12, 1)) {
// Build the end date; start date is the max date
let startDate = maxDate;
let endDate = new Date(maxDate.getFullYear(), 12, 1);
// console.log("tail", startDate, endDate, d3.utcMonths(startDate, endDate));
// 1 data point per missing month
d3.utcMonths(startDate, endDate).map(d => monthPoints.push(d));
}
return monthPoints;
}
Insert cell
html`<style>
.selected-cell {
fill: #ff9900!important;
// opacity: .8!important;
}
.selected-cells {
fill: khaki;
// opacity: .5;
}
#chart-container {
position: relative;
}
</style>`
Insert cell
d3 = require("d3@5")
Insert cell
jQuery = require("jquery")
Insert cell
import { jQuery as $ } from "@ddspog/useful-libs"
Insert cell
import {legend} from "@d3/color-legend"
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