Public
Edited
May 2
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart3 = {
// Define our canvas
const height = width * 0.4;
const context = DOM.context2d(width, height);

// Define our original scale between data and canvas coordinates.
const X = d3.scaleTime()
.domain([new Date('2024-01-01'), new Date('2025-01-01')])
.range([0, width])
const Y = d3.scaleLinear()
.domain([0, 1])
.range([0, height])

// Our pixel scales to translate between pixel and canvas coordinates.
// To start with this is a 1:1 mapping.
const pixelX = d3.scaleLinear().domain([0, width]).range([0, width])
// pixelY will have the same scale factor as pixelX, but a different offset
// Though in theory we could allow for scaling x and y separately, there isn't a good ux for this.
const pixelY = d3.scaleLinear().domain([0, height]).range([0, height])

// Define the original bounds of our space in data-coordinates.
const extent = [
[X(new Date('2023-01-01')), Y(-1)],
[X(new Date('2026-01-01')), Y(2)]
];

// Create our zoom function.
const zoom = d3.zoom()
.scaleExtent([-4, 4])
.translateExtent(extent)
.on("zoom", zoomed)

// Attach it to our canvas.
d3.select(context.canvas).call(zoom)

// Helper for getting sizes rather than positions of values in different scales.
// This wont' work the way it was previously defined
const scaleSize = (scale, v, v0=0) => scale(v) - scale(v0)

// Our zoom handler function
function zoomed({transform} ) {
// Calculate the coordinates of the corners of the canvas in **canvas** coordinates.
const bounds ={
x0: (X.range()[0] - transform.x) / transform.k,
x1: (X.range()[1] - transform.x) / transform.k,
y0: (Y.range()[0]) - transform.y / transform.k,
y1: (Y.range()[1]) - transform.y / transform.k
}
// Update pixel scale with new canvas coords.
pixelX.range([bounds.x0, bounds.x1])
pixelY.range([bounds.y0, bounds.y1])

// Compensate for zooming in our y scale to keep heights of boxes the same.
Y.range([0, height / transform.k])


// Save Context, Apply Transform, And Redraw
context.save();
context.clearRect(0,0,width,height);
context.translate(transform.x, transform.y)
context.scale(transform.k, transform.k)
draw(transform, bounds)
context.restore()
}
function draw(transform, bounds) {
// Draw Grid
drawGrid(transform, bounds)

// Draw the original canvas bounds with coordinates in **canvas** coordinates.
context.strokeStyle = 'red'
context.beginPath()
context.rect(0,0, width, Y(1))
context.stroke()
// Draw a box at the origin in **data** cordinates
// With size of 10 in canvas space.
context.beginPath()
context.roundRect(X(new Date('2024-04-01')), Y(0), 10, 10, 3)
context.fill()

// Box at 50, 0.5 in **data** coordinates
// With a size of 10, 0.1 in **data** coordinates
context.beginPath
context.roundRect(
X(new Date('2024-06-01')),
Y(0.1),
scaleSize(X, new Date('2024-06-01'), new Date('2024-04-01')),
scaleSize(Y, 0.1),
10
)
context.fill()
}

function drawGrid(transform, bounds) {
// Get our grid size in canvas space.
const gridSize = {y: scaleSize(Y, 0.1)}

// Set up context for drawing our grid.
context.save()
context.strokeStyle = '#999'
// context.lineWidth = Math.min(1 / transform.k, 1) // Maintain stroke width at a max of 1px.
context.lineWidth = Math.min(scaleSize(pixelX, 1) , 1) // Works the same as the line above.


// x-axis gridlines - DIFFERENT METHOD
const canvasRange = [X.invert(pixelX(0)), X.invert(pixelX(width))]
const formatTime = d3.timeFormat("%-d %b %Y")
const fontSize = scaleSize(pixelX, 12);
const ticks = d3.utcTicks(...canvasRange, 5);
ticks.forEach(t => {
context.beginPath();
context.moveTo(X(t), bounds.y0);
context.lineTo(X(t),bounds.y1);
context.stroke()
context.beginPath()
context.font = `${fontSize}pt sans-serif`;
context.fillStyle = '#999'
context.fillText(formatTime(t), X(t) + 5, pixelY(height) - 10)
})
// y-axis gridlines
for (let _y= bounds.y0 - bounds.y0 % gridSize.y;
_y < bounds.y1 + - bounds.y0 % gridSize.y + gridSize.y;
_y+=gridSize.y) {
context.beginPath();
context.moveTo(bounds.x0, _y);
context.lineTo(bounds.x1, _y);
context.stroke()
}

// Restore context
context.restore()
}

zoomed({transform: d3.zoomIdentity})
return context.canvas;
}
Insert cell
Insert cell
import {assignLanes, archigos} from "@bensimonds/gantt-chart";
Insert cell
chart4 = {
const gantt = GanttCanvas(archigos, {
width: width,
key: d => d.obsid,
start: d => d.startdate,
end: d => d.enddate,
color: d => cm(d.exit),
label: d => d.leader,
lane: d => d.countryname,
title: d => `${d.countryname} - ${d.leader} - ${d3.timeFormat('%Y')(d.startdate)} to ${d3.timeFormat('%Y')(d.enddate)}`,
})
const node = d3.create('div')
.style('overflow','auto')
.node();
node.appendChild(gantt);
return Object.assign(node, {gantt: gantt});
}
Insert cell
cm = d3.scaleOrdinal(d3.schemeTableau10);
Insert cell
function GanttCanvas(_data, {
width=600,
height=null,
barHeight=20,
// Data Accessors
key = (d, i) => i, // given d in data, return a unique key
start = (d, i) => d.start, // given d in data, return the start of the bar as a date or number
end = (d, i) => d.end, // given d in data, return the end of the bar as a date or number
lane = (d, i) => 0, // given d in data, return the lane the bar belongs to
color = (d, i) => 'black', // given d in data, return the color of the bar (might be a d3.scale of some sort, or just a function)
label = undefined, // given d in data, return the label for the bar if desired
title = undefined, // given d in data, return the title to displayed on hover.
layout = assignLanes, // Function to use for layout of bars into lanes and rows. Defaults to assignlanes
// Chart Config
}={}) {
// Define our canvas
height = height || width * 0.4;
const context = DOM.context2d(width, height);

// Define our original scale between data and canvas coordinates.
const xDomain = [d3.min(_data, d => start(d)),
d3.max(_data, d => end(d))];
const X = d3.scaleTime()
.domain(xDomain)
.range([0, width])
// Lay out our data.
const data = layout(_data, {start, end, lane, xScale: X, xPadding: 0})
// Now do our y scale
const yDomain = [... new Set(data.map(d => d.rowNo))];
const Y = d3.scaleBand()
.domain(yDomain)
.range([0, barHeight * yDomain.length])
.padding(0.2)

// Our pixel scales to translate between pixel and canvas coordinates.
// To start with this is a 1:1 mapping.
const pixelX = d3.scaleLinear().domain([0, width]).range([0, width])
// pixelY will have the same scale factor as pixelX, but a different offset
// Though in theory we could allow for scaling x and y separately, there isn't a good ux for this.
const pixelY = d3.scaleLinear().domain([0, height]).range([0, height])

// Define the original bounds of our space in data-coordinates.
const extent = [
[X(xDomain[0]), Y.range()[0]],
[X(xDomain[1]), Y.range()[1]]
];

// Create our zoom function.
const zoom = d3.zoom()
.scaleExtent([0.5, 4])
.translateExtent(extent)
.on("zoom", zoomed)

// Attach it to our canvas.
d3.select(context.canvas).call(zoom)

// Helper for getting sizes rather than positions of values in different scales.
// This wont' work the way it was previously defined
const scaleSize = (scale, v, v0=0) => scale(v) - scale(v0)

// Our zoom handler function
function zoomed({transform} ) {
// Calculate the coordinates of the corners of the canvas in **canvas** coordinates.
const bounds ={
x0: (0 - transform.x) / transform.k,
x1: (width - transform.x) / transform.k,
y0: (0 - transform.y) / transform.k,
y1: (height - transform.y) / transform.k
}
// Update pixel scale with new canvas coords.
pixelX.range([bounds.x0, bounds.x1])
pixelY.range([bounds.y0, bounds.y1])

// Compensate for zooming in our y scale to keep heights of boxes the same.
// Y.range([0, height / transform.k])


// Save Context, Apply Transform, And Redraw
context.save();
context.clearRect(0,0,width,height);
context.translate(transform.x, transform.y)
context.scale(transform.k, transform.k)
draw(transform, bounds)
context.restore()
}
function draw(transform, bounds) {
data.forEach((d, i) => drawBar(d, i));
// Draw Grid
drawGrid(transform, bounds)
}

function drawBar(d, i) {

const barWidth = X(end(d, i)) - X(start(d, i)) - scaleSize(pixelX, 2);
context.textBaseline = 'middle';
const fontSize = Math.min(scaleSize(pixelX, 12), Y.bandwidth() * 0.7);
context.font = `${fontSize}px sans-serif`;

// Draw Bar
context.beginPath()
context.fillStyle = color(d,i)
context.roundRect(
X(start(d, i)),
Y(d.rowNo),
// shrink width by a couple pixels. Looks nicer.
barWidth,
Y.bandwidth(),
scaleSize(pixelX, 4))
context.fill()

// Draw Label
context.beginPath()
context.fillStyle = 'white'
context.fillText(label(d, i), X(start(d, i)) + scaleSize(pixelX, 4), Y(d.rowNo) + Y.bandwidth() / 2)
}

function drawGrid(transform, bounds) {
// Get our grid size in canvas space.
const gridSize = {y: scaleSize(Y, 0.1)}

// Set up context for drawing our grid.
context.save()
context.strokeStyle = '#999'
// context.lineWidth = Math.min(1 / transform.k, 1) // Maintain stroke width at a max of 1px.
context.lineWidth = Math.min(scaleSize(pixelX, 1) , 1) // Works the same as the line above.


// x-axis gridlines - DIFFERENT METHOD
const canvasRange = [X.invert(pixelX(0)), X.invert(pixelX(width))]
const formatTime = d3.timeFormat("%-d %b %Y")

// Background for ticks
context.fillStyle = 'white'
context.globalAlpha = 0.8
context.fillRect(0, pixelY(height-20), width, 20)
context.globalAlpha = 1
const ticks = d3.utcTicks(...canvasRange, 5);
ticks.forEach(t => {
context.beginPath();
context.moveTo(X(t), bounds.y0);
context.lineTo(X(t),bounds.y1);
context.stroke()
// Label
context.beginPath()
context.fillStyle = '#777'
context.fillText(formatTime(t), X(t) + 5, pixelY(height - 10))
})

// y-axis gridlines
const lanes = d3.flatRollup(data, v => d3.max(v, d => d.rowNo), d => lane(d));
lanes.forEach(([label, y]) => {
context.beginPath();
context.moveTo(bounds.x0, Y(y) - scaleSize(pixelX, 2));
context.lineTo(bounds.x1, Y(y) - scaleSize(pixelX, 2));
context.stroke()
// Label
context.beginPath()
context.fillStyle = '#777'
context.fillText(label, bounds.x0 + scaleSize(pixelX, 5), Y(y) + Y.bandwidth() / 2)
})

// Restore context
context.restore()
}

zoomed({transform: d3.zoomIdentity})
return context.canvas;
}
Insert cell
Insert cell
chart5 = {
// Define our canvas
const height = width * 0.4;
const context = DOM.context2d(width, height);

// Define our original scale between data and canvas coordinates.
const X = d3.scaleTime()
.domain([new Date('2024-01-01'), new Date('2025-01-01')])
.range([0, width])
const Y = d3.scaleLinear()
.domain([0, 1])
.range([0, height])

// Our pixel scales to translate between pixel and canvas coordinates.
// To start with this is a 1:1 mapping.
const pixelX = d3.scaleLinear().domain([0, width]).range([0, width])
// pixelY will have the same scale factor as pixelX, but a different offset
// Though in theory we could allow for scaling x and y separately, there isn't a good ux for this.
const pixelY = d3.scaleLinear().domain([0, height]).range([0, height])

// Define the original bounds of our space in data-coordinates.
const extent = [
[X(new Date('2023-01-01')), Y(-1)],
[X(new Date('2026-01-01')), Y(2)]
];

// Create our zoom function.
const zoom = d3.zoom()
.scaleExtent([-4, 4])
.translateExtent(extent)
.on("zoom", zoomed)


// Helper for getting sizes rather than positions of values in different scales.
// This wont' work the way it was previously defined
const scaleSize = (scale, v, v0=0) => scale(v) - scale(v0)

// Our zoom handler function
function zoomed({transform} ) {
// Calculate the coordinates of the corners of the canvas in **canvas** coordinates.
const bounds ={
x0: (X.range()[0] - transform.x) / transform.k,
x1: (X.range()[1] - transform.x) / transform.k,
y0: (Y.range()[0]) - transform.y / transform.k,
y1: (Y.range()[1]) - transform.y / transform.k
}
// Update pixel scale with new canvas coords.
pixelX.range([bounds.x0, bounds.x1])
pixelY.range([bounds.y0, bounds.y1])

// Compensate for zooming in our y scale to keep heights of boxes the same.
Y.range([0, height / transform.k])


// Save Context, Apply Transform, And Redraw
context.save();
context.clearRect(0,0,width,height);
context.translate(transform.x, transform.y)
context.scale(transform.k, transform.k)
draw(transform, bounds)
context.restore()
}
function draw(transform, bounds) {
// Draw Grid
drawGrid(transform, bounds)

// Draw the original canvas bounds with coordinates in **canvas** coordinates.
context.save()
context.strokeStyle = 'red'
context.beginPath()
context.rect(0,0, width, Y(1))
context.stroke()
context.restore()

// Box at 50, Y in **data** coordinates
// With a size of 10, 0.1 in **data** coordinates
d3.ticks(0, 1, 10).map(y => {
context.beginPath
context.roundRect(
X(new Date('2024-06-01')),
Y(y),
scaleSize(X, new Date('2024-06-01'), new Date('2024-04-01')),
scaleSize(Y, 0.08),
10
)
context.fill()
})
}

function drawGrid(transform, bounds) {
// Get our grid size in canvas space.
const gridSize = {y: scaleSize(Y, 0.1)}

// Set up context for drawing our grid.
context.save()
context.strokeStyle = '#999'
// context.lineWidth = Math.min(1 / transform.k, 1) // Maintain stroke width at a max of 1px.
context.lineWidth = Math.min(scaleSize(pixelX, 1) , 1) // Works the same as the line above.


// x-axis gridlines - DIFFERENT METHOD
const canvasRange = [X.invert(pixelX(0)), X.invert(pixelX(width))]
const formatTime = d3.timeFormat("%-d %b %Y")
const fontSize = scaleSize(pixelX, 12);
const ticks = d3.utcTicks(...canvasRange, 5);
ticks.forEach(t => {
context.beginPath();
context.moveTo(X(t), bounds.y0);
context.lineTo(X(t),bounds.y1);
context.stroke()
context.beginPath()
context.font = `${fontSize}pt sans-serif`;
context.fillStyle = '#999'
context.fillText(formatTime(t), X(t) + 5, pixelY(height) - 10)
})
// y-axis gridlines
for (let _y= bounds.y0 - bounds.y0 % gridSize.y;
_y < bounds.y1 + - bounds.y0 % gridSize.y + gridSize.y;
_y+=gridSize.y) {
context.beginPath();
context.moveTo(bounds.x0, _y);
context.lineTo(bounds.x1, _y);
context.stroke()
}

// Restore context
context.restore()
}

// Attach it all to our canvas.
d3.select(context.canvas)
.call(zoom)
.call(zoom.transform, d3.zoomIdentity);
return context.canvas;
}
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