Published
Edited
Jan 21, 2021
2 forks
3 stars
Insert cell
md`# Timeline Chart`
Insert cell
Insert cell
chart = {
const margin = { top: 60, right: 20, left: 40 },
//width = 1200,
height = 400,
//width2 = 1200,
width2 = width,
height2 = 140

var b = '1px solid black'

var render = renderQueue(plot).rate(30000).clear(clear_canvas)
var render2 = renderQueue(plot2).rate(30000).clear(clear_canvas2)
const svgWrapper = d3.create("svg")
.attr("viewBox", [0, 0, width, height+height2+margin.top])
const svg = svgWrapper
.append('g')
.attr('width', width - margin.left + 'px')
.attr('height', height + height2 - margin.top + 'px')
.attr('transform', 'translate(10,30)')
/*var svg = d3.select('svg')
.attr('width', width)
.attr('height', height + height2 + 300)
.append('g')
.attr('width', width - margin.left + 'px')
.attr('height', height + height2 - margin.top + 'px')
.attr('x', margin.left)
.attr('y', margin.top)
*/
const brush = d3.brush()
.extent([[0, 0], [width, height]])
.on("start end", updateChart)

const brush_leg = d3.brush()
.extent([[0, 0], [width2, height2]])

var tooltip = d3.select('body')
.append('div')
.style('padding', '5px')
.style('position', 'absolute')
.style('visibility', 'hidden')
.style('background-color', 'white')

var hiddenCanvas = d3.select('body')
.append('canvas')
.classed('hiddenCanvas', true)
.attr('width', width)
.attr('height', height)
.style('display', 'none')

var hidden_context = hiddenCanvas.node().getContext('2d')

var container = svg.append('g')
.attr('width', width)
.attr('height', height)
.attr('x', 0)
.attr('y', 0)
.attr('transform', 'translate(30,0)')

var con_fo = container.append('foreignObject')
.attr('width', width)
.attr('height', height)
.attr('x', 0)
.attr('y', 0)
.append('xhtml:body')
.style('margin', 0)
.style('padding', 0)
.style('background-color', 'none')
.style('width', width + 'px')
.style('height', height + 'px')

var con_canvas = con_fo.append('canvas')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height)

var con_context = con_canvas.node().getContext('2d')

var mover = svg.append('g')
.attr('width', width2)
.attr('height', height2 - margin.top)
.attr('transform', 'translate(30,' + (height + margin.top) + ')')

var brushLegend = svg.append('g')
.attr('width', width2)
.attr('height', height2 - margin.top)
.attr('transform', 'translate(30,' + (height + margin.top) + ')')
.call(brush_leg)

brushLegend.attr('pointer-events', 'none')
brushLegend.select('.overlay').attr('pointer-events', 'none')

var fo = mover.append('foreignObject')
.attr('width', width2)
.attr('height', height2)
.attr('x', 0)
.attr('y', 0)
.append('xhtml:body')
.style('margin', 0)
.style('padding', 0)
.style('background-color', 'none')
.style('width', width2 + 'px')
.style('height', height2 + 'px')

var canvas = fo.append('canvas')
.attr('x', 0)
.attr('y', 0)
.attr('width', width2)
.attr('height', height2)

//mover.append('g')
// .attr('width', width2)
// .attr('height', height2)
// .call(brush)

var context = canvas.node().getContext('2d')


var color = d3.scaleSequential().domain([1,120]).interpolator(d3.interpolatePuRd)
var color_elap = d3.scaleSequential().domain([1,120]).interpolator(d3.interpolatePuRd)
//var color = d3.scaleSequential().domain([0, 255]).interpolator(d3.interpolateTurbo)
var sc_color_map = new Set()
var normal_color_map_func = function (d){
return color(d.id%120)
}
var elap_color_map_func = function (d){
return color(d.elapsed)
}
var sc_color_map_func = function (d) {
return sc_color_map[d.sc]
}
var lw_color_map_func = function (d) {
return (d.lw) ? '#ff4d4f' : (d.sc != 0 && d.sc != 1) ? '#ffc53d' : '#7cb305'
}
// @observable set above
//var custom_color_map = normal_color_map_func
let custom_color_map = () => {}
if (palletechose == 'sc')
custom_color_map = sc_color_map_func
else if (palletechose == 'lw')
custom_color_map = lw_color_map_func
else if (palletechose == 'elap'){
color = color_elap
custom_color_map = elap_color_map_func
}else{
color = d3.scaleSequential().domain([1,120]).interpolator(d3[palletechose])
custom_color_map = normal_color_map_func
}

// Job Status toogle
//var buttonState = 'Hide brush'
//var button = d3.select('body')
// .append('button')
// .text('Hide brush')
// .on("click", function (){
// if (buttonState == 'Hide brush'){
// buttonState = 'Show brush'
// d3.select(this).text(buttonState)
// brushArea.style('display', 'none')
// }else{
// buttonState = 'Hide brush'
// d3.select(this).text(buttonState)
// brushArea.style('display', 'block')
// }
// })
/*
d3.select("#colorchoose")
.on("change", function (){
var selval = d3.select(this).node().value
console.log('trigger', selval)
if (selval == 'sc')
custom_color_map = sc_color_map_func
else if (selval == 'lw')
custom_color_map = lw_color_map_func
else
custom_color_map = normal_color_map_func
redraw()
})*/
//var buttonState = 'Color by Crit1'
//var button = d3.select('#button_color')
// .text(buttonState)
// .on("click", function (){
// if (buttonState == 'Color by Crit1'){
// buttonState = 'Color by Crit2'
// d3.select(this).text(buttonState)
// custom_color_map = sc_color_map_func
// }else{
// buttonState = 'Color by Crit1'
// d3.select(this).text(buttonState)
// custom_color_map = lw_color_map_func
// }
// redraw()
// })

var container_x = d3.scaleTime().range([0, width])
var container_x2 = d3.scaleTime().range([0, width])
var container_y = d3.scaleLinear().range([0, height])
var container_y2 = d3.scaleLinear().range([0, height])

var mover_x = d3.scaleTime().range([0, width2])
var mover_y = d3.scaleLinear().range([0, height2])

var container_ax = d3.axisBottom(container_x),
container_ay = d3.axisLeft(container_y)

var gX, gY, maxy, brushArea, gdata
var color_map = new Set()
var nextCol = 10
function genColor() {
var ret = [];
// via http://stackoverflow.com/a/15804183
if (nextCol < 16777215) {
ret.push(nextCol & 0xff); // R
ret.push((nextCol & 0xff00) >> 8); // G
ret.push((nextCol & 0xff0000) >> 16); // B

nextCol += 5;
}
var col = "rgb(" + ret.join(',') + ")";
return col;
}
//d3.json("./rooms.json").then((rs) => {
//d3.json("./test22.json").then((rs) => {
let data = []
let sC = new Set()
rooms.forEach(function (l, li) {
l.forEach(function (d, di) {
sC.add(d.sc)
data.push({
name: d.name,
s: new Date(+d.s * 1000),
e: new Date(+d.e * 1000),
id: d.id,
sc: d.sc,
l: li,
lw: d.lw,
elapsed: +d.e-+d.s
})
})
})
Array.from(sC).map((d) => {
sc_color_map[d] = randomColor()
})
let tmp = Array.from(sC)
tmp.sort((a,b) => +a-+b)
console.log(tmp)

d3.select('#sc_legend')
.selectAll('span')
.data(tmp)
.enter()
.append('span')
.each(function (d,i){
this.innerHTML = `
<div style='height:10px;width:10px;display:inline-block;background-color:${sc_color_map[d+'']};'></div>
${d}
`
})
for (let dind in data) {
let hiddenColor = genColor()
data[dind]['hiddenColor'] = hiddenColor
color_map[hiddenColor] = dind
}
console.log(data.slice(0, 30))
gdata = data
console.log(sC)

maxy = rooms.length

container_x.domain([d3.min(data, (d) => d.s), d3.max(data, (d) => d.e)])
container_x2.domain([d3.min(data, (d) => d.s), d3.max(data, (d) => d.e)])
mover_x.domain([d3.min(data, (d) => d.s), d3.max(data, (d) => d.e)])

container_y.domain([0, maxy])
container_y2.domain([0, maxy])
mover_y.domain([0, rooms.length])

gX = svg.append('g').call(container_ax)
.attr('transform', 'translate(30,' + (height) + ')')
gY = svg.append('g').call(container_ay)
.attr('transform', 'translate(30, 0)')

brushArea = container.append('g')
.attr('class', 'brush')
.call(brush)
let arr = []
let arr2 = []
data.forEach(function (d, i) {
context.fillStyle = custom_color_map(d)
let pos = [
container_x(d.s),
container_y(d.l),
container_x(d.e) - container_x(d.s),
container_y(d.l) - container_y(d.l - 0.8)
]
arr.push([
custom_color_map(d),
...pos
])
arr2.push([
d.hiddenColor,
...pos
])
context.fillRect(
mover_x(d.s),
mover_y(d.l),
mover_x(d.e) - mover_x(d.s),
mover_y(d.l) - mover_y(d.l - 0.8)
)
})
console.log(arr2.slice(0, 10))
render(arr)
render2(arr2)

var zoom_view = false
var last_extent = null
var legend_scale_x = d3.scaleLinear().domain([0, width]).range([0, width])
var legend_scale_y = d3.scaleLinear().domain([0, height2]).range([0, height2])
function updateChart(e) {
console.log('updatechart event', e)
//if (d3.event.sourceEvent.type === 'end') return
//if (e.type === 'end') return
//console.log('trigger', d3.event.sourceEvent)
//const extent = d3.event.selection
if (e.type == 'start'){
tooltip.style('visibility', 'hidden')
return
}
const extent = e.selection
if (!extent && !e.mode) return
console.log('brush extent', extent, last_extent)
if (extent == last_extent) return

console.log('container', container_x)
//for (i in extent){
//extent[i][1] *= 3
//}
var legend_x1, legend_x2, legend_y1, legend_y2
if (!extent) {
container_x = container_x2.copy()
container_y = container_y2.copy()
zoom_view = false
brushLegend.call(brush_leg.move)
legend_scale_x = d3.scaleLinear().domain([0, width]).range([0, width])
legend_scale_y = d3.scaleLinear().domain([0, height2]).range([0, height2])
} else {
for (let e of extent)
console.log(e)
legend_x1 = legend_scale_x.invert(extent[0][0])
legend_x2 = legend_scale_x.invert(extent[1][0])
legend_y1 = legend_scale_y.invert(extent[0][1] / 3)
legend_y2 = legend_scale_y.invert(extent[1][1] / 3)
zoom_view = true
if (zoom_view) {
container_x.domain([container_x.invert(extent[0][0]), container_x.invert(extent[1][0])])
container_y.domain([container_y.invert(extent[0][1]), container_y.invert(extent[1][1])])
legend_scale_x.domain([legend_x1, legend_x2])
legend_scale_y.domain([legend_y1, legend_y2])
} else {
container_x.domain([container_x2.invert(extent[0][0]), container_x2.invert(extent[1][0])])
container_y.domain([container_y2.invert(extent[0][1]), container_y2.invert(extent[1][1])])
}
brushArea.call(brush.move)
console.log('brush_leg X to', legend_x1, legend_x2)
brushLegend.call(brush_leg.move, [[legend_x1, legend_y1],
[legend_x2, legend_y2]])
brushLegend.select('.selection')
.attr('fill-opacity', 0.8)
}

gX.transition().duration(1000).call(d3.axisBottom(container_x))
gY.transition().duration(1000).call(d3.axisLeft(container_y))

console.log('ok')
//var x_dom = container_x.domain()
//var x_bounds = [x_dom[0], x_dom[x_dom.length-1]]
let arr = []
let arr2 = []
//con_context.clearRect(0,0,width,height)
data.forEach(function (d, i) {
//con_context.fillStyle = color(d.id%120)
//con_context.fillRect(
// container_x(d.s),
// container_y(d.l),
// container_x(d.e)-container_x(d.s),
// container_y(d.l)-container_y(d.l-0.8)
//)
let pos = [
container_x(d.s),
container_y(d.l),
container_x(d.e) - container_x(d.s),
container_y(d.l) - container_y(d.l - 0.8)
]
arr.push([custom_color_map(d), ...pos])
arr2.push([d.hiddenColor, ...pos])
})
render(arr)
render2(arr2)
//container
// .selectAll('rect.job')
// .transition().duration(1000)
// .attr('x', (d) => container_x(d.s))
// .attr('y', (d) => container_y(d.l))
// .attr('width', (d) => container_x(d.e)-container_x(d.s))
// .attr('height', (d) => container_y(d.l)-container_y(d.l-0.8))
last_extent = extent
}

function redraw(){
let arr = []
let arr2 = []

data.forEach(function (d, i) {
let pos = [
container_x(d.s),
container_y(d.l),
container_x(d.e) - container_x(d.s),
container_y(d.l) - container_y(d.l - 0.8)
]
arr.push([custom_color_map(d), ...pos])
arr2.push([d.hiddenColor, ...pos])
context.fillStyle = custom_color_map(d)
context.fillRect(
mover_x(d.s),
mover_y(d.l),
mover_x(d.e) - mover_x(d.s),
mover_y(d.l) - mover_y(d.l - 0.8)
)
})
render(arr)
render2(arr2)
}

container.on("mousemove", function (e) {
if (!gdata) return
//console.log('mousemove event', e)
const coordinates = d3.pointer(e)
let mouseX = coordinates[0]
let mouseY = coordinates[1]
//var mouseX = d3.event.layerX || d3.event.offsetX;
//var mouseY = d3.event.layerY || d3.event.offsetY;
//console.log('mouse_x', d3.event.layerX, d3.event.offsetX)
//console.log('mouse_y', d3.event.layerX, d3.event.offsetX)

var col = hidden_context.getImageData(mouseX - 41, mouseY - 48, 1, 1).data
var colKey = 'rgb(' + col[0] + ',' + col[1] + ',' + col[2] + ')';
//console.log(mouseX, mouseY, colKey)

var nodeData = null
if (color_map[colKey] < gdata.length)
nodeData = gdata[color_map[colKey]]

if (nodeData) {
tooltip
.style('visibility', 'visible')
.style('top', mouseY + 200 + 'px')
.style('left', mouseX + 50 + 'px')
.html(`id: ${nodeData.id}<br>name: ${nodeData.name}<br>s: ${nodeData.s}<br>e: ${nodeData.e}<br>sc: ${nodeData.sc}`)
} else {
tooltip.style('visibility', 'hidden')
}
})

function plot(pos) {
con_context.fillStyle = pos[0]
con_context.fillRect(
...pos.slice(1, 5)
//container_x(d.s),
//container_y(d.l),
//container_x(d.e)-container_x(d.s),
//container_y(d.l)-container_y(d.l-0.8)
)
}

function clear_canvas() {
con_context.clearRect(0, 0, width, height)
}

function plot2(pos) {
hidden_context.fillStyle = pos[0]
hidden_context.fillRect(
...pos.slice(1, 5)
//container_x(d.s),
//container_y(d.l),
//container_x(d.e)-container_x(d.s),
//container_y(d.l)-container_y(d.l-0.8)
)
}

function clear_canvas2() {
hidden_context.clearRect(0, 0, width, height)
}
return svgWrapper.node();
}
Insert cell
color_palette = ['lw', 'interpolatePuRd', 'sc', 'elap']
.concat(Object.keys(d3).filter(d => {
try{
if (d.includes('interpolate')){
if (typeof(d3[d](0)) == 'string')
return true
}
}catch{
}
return false
})
)
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
randomColor = (function () {
var golden_ratio_conjugate = 0.618033988749895;
var h = Math.random();

var hslToRgb = function (h, s, l) {
var r, g, b;

if (s == 0) {
r = g = b = l; // achromatic
} else {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}

var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}

return '#' + Math.round(r * 255).toString(16) + Math.round(g * 255).toString(16) + Math.round(b * 255).toString(16);
};

return function () {
h += golden_ratio_conjugate;
h %= 1;
return hslToRgb(h, 0.5, 0.60);
};
})();
Insert cell
height = 800
Insert cell
rooms[0][0]
Insert cell
Insert cell
rooms = FileAttachment("rooms.json").json()
Insert cell
import {select} from "@jashkenas/inputs"
Insert cell
d3 = require('d3@6', 'd3-scale-chromatic@1')
Insert cell
d3.interpolateBlues(0)
Insert cell
d3.interpolateArray(0)
Insert cell
d3.interpolate(0)
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