Public
Edited
Oct 29, 2023
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
canvasElement = html`<canvas id="plot" width="600" height="600"></canvas>`
Insert cell
Insert cell
Insert cell
canvas = canvasElement
Insert cell
ctx = canvas.getContext('2d')
Insert cell
Insert cell
numPoints = 10000
Insert cell
pointWidth = 3
Insert cell
Insert cell
distinctColors = d3.quantize(d3.interpolatePlasma, 5)
Insert cell
colorScale = d3.scaleThreshold()
.domain(indexBreakpoints)
.range(distinctColors)
Insert cell
Insert cell
layouts = [phyllotaxisLayout, barChartLayout]
Insert cell
mutable layoutIndex = 0
Insert cell
Insert cell
function draw() {
// erase any existing points on canvas before drawing
ctx.clearRect(0, 0, canvas.width, canvas.height)

// draw each point as a square
for (let i = 0; i < points.length; i++) {
const point = points[i]
ctx.fillStyle = point.color
ctx.fillRect(point.x, point.y, pointWidth, pointWidth)
}

ctx.restore()
}
Insert cell
phyllotaxisLayout(points, pointWidth)
Insert cell
draw()
Insert cell
Insert cell
animationDuration = 1500 // milliseconds
Insert cell
mutable timer = null
Insert cell
mutable isAnimating = false
Insert cell
function animate(nextLayout) {
if (isAnimating) return // do nothing if animation is in progress

mutable isAnimating = true // set flag to indicate animation is in progress

// Stop any current animation
if (timer) mutable timer.stop()

// Store the first position of the points
points.forEach(point => {
point.sx = point.x
point.sy = point.y
})

// Apply the next layout to adjust point positions
nextLayout(points, pointWidth)

// Store the destination position of the points
points.forEach(point => {
point.tx = point.x
point.ty = point.y
})

// Reset points to their starting positions for the animation.
points.forEach(point => {
point.x = point.sx
point.y = point.sy
})

// Animate the interpolation of points from one position to another
mutable timer = d3.timer((elapsed) => {
// Compute the current progress of the animation (0 to 1)
const progress = Math.min(1, d3.easeCubic(elapsed / animationDuration))

// Update point positions (interpolate between source and target)
points.forEach(point => {
point.x = point.sx + (point.tx - point.sx) * progress
point.y = point.sy + (point.ty - point.sy) * progress
})

// Draw the update
draw()

// Stop the animation when the progress is complete
if (progress === 1) {
// stop current layout's timer and start new one
mutable timer.stop()
mutable isAnimating = false
}
})
}
Insert cell
Insert cell
d3.select('.play-control')
.on('click', () => {
if(!isAnimating) {
const nextLayoutIndex = (layoutIndex + 1) % layouts.length
const nextLayout = layouts[nextLayoutIndex]
animate(nextLayout)
mutable layoutIndex = nextLayoutIndex
}
})
Insert cell
Insert cell
Insert cell
breakpointsPercent = [0.05, 0.2, 0.45, 0.85]
Insert cell
indexBreakpoints = breakpointsPercent.map(breakpoint => Math.floor(breakpoint * numPoints))
Insert cell
points = d3.range(numPoints).map(index => ({
id: index,
color: colorScale(index)
}))
Insert cell
Insert cell
function phyllotaxisLayout(points, pointWidth, xOffset = canvas.width / 2, yOffset = canvas.height / 2, iOffset = 0) {
// theta determines the spiral of the layout
const theta = Math.PI * (3 - Math.sqrt(5))
const pointRadius = pointWidth
points.forEach((point, i) => {
const index = (i + iOffset) % points.length
const phylloX = pointRadius * Math.sqrt(index) * Math.cos(index * theta)
const phylloY = pointRadius * Math.sqrt(index) * Math.sin(index * theta)
point.x = xOffset + phylloX - pointRadius
point.y = yOffset + phylloY - pointRadius
})
}
Insert cell
Insert cell
function barChartLayout(points, pointWidth, pointMargin = 1) {
const pointWithMargin = pointWidth + pointMargin
const pointsPerLayer = 28 // how many points wide each bar is
// Initialize each bar's starting position
let xOffset = 0
// Process each color bar
for (let color of distinctColors) {
// Filter points for the current color
const currentPoints = points.filter(p => p.color === color)
// Initialize the position for the current bar
let rowIndex = 0
let verticalIndex = 0
// Process each point in the current bar
for (let i = 0; i < currentPoints.length; i++) {
const point = currentPoints[i]
// Check if we need to start a new layer
if (i !== 0 && i % pointsPerLayer === 0) {
rowIndex = 0 // start a new layer
verticalIndex++ // move the layer up
}
// Position the point in the current layer
point.x = xOffset + (rowIndex * pointWithMargin)
point.y = verticalIndex * pointWithMargin
rowIndex++ // move to the next point in the layer
}
// Calculate the width of the current bar
const barWidth = pointWithMargin * pointsPerLayer
// Update the starting position for the next bar
xOffset += barWidth + 5 // we're adding some space between bars
}
}
Insert cell
// If you want to run from a .html file

// <!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="UTF-8">
// <meta name="viewport" content="width=device-width, initial-scale=1.0">

// <title>Canvas Animated Visualization</title>

// <style>
// body { margin: 0; }
// #controls {
// display: flex;
// justify-content: center; /* Center the button horizontally */
// margin: 20px 0; /* Add some space between the button and canvas */
// }

// .play-control {
// padding: 5px 10px;
// background-color: #4CAF50;
// color: white;
// border: none;
// text-align: center;
// display: inline-block;
// font-size: 14px;
// cursor: pointer;
// border-radius: 8px;
// }

// canvas {
// display: block; /* Eliminates a bottom margin that browsers add to the canvas by default */
// margin: auto; /* Center the canvas */
// }
// </style>

// <script src="https://unpkg.com/d3"></script>
// </head>
// <body>
// <!-- Create a placeholder for any controls -->
// <div id="controls"></div>

// <!-- Create the HTML canvas element -->
// <canvas id="plot" width="600" height="600"></canvas>

// <!-- Script can be here or linked to another file-->
// <script>
// // Grab the canvas
// const canvas = document.getElementById('plot')
// // Apply the '2d' context (as opposed to 'webgl')
// const ctx = canvas.getContext('2d')

// // Define visualization attributes
// const numPoints = 10000
// let pointWidth = 3

// // Categorical color scale with different sized groups
// const breakpointsPercent = [0.05, 0.2, 0.45, 0.85]
// const indexBreakpoints = breakpointsPercent.map(breakpoint => Math.floor(breakpoint * numPoints))
// const distinctColors = d3.quantize(d3.interpolatePlasma, 5)
// const colorScale = d3.scaleThreshold()
// .domain(indexBreakpoints)
// .range(distinctColors)

// // Generate the array of points with a unique ID and color
// const points = d3.range(numPoints).map(index => ({
// id: index,
// color: colorScale(index)
// }))

// // Specify the phyllotaxis layout
// // From https://bocoup.com/blog/smoothly-animate-thousands-of-points-with-html5-canvas-and-d3
// function phyllotaxisLayout(points, pointWidth, xOffset = canvas.width / 2, yOffset = canvas.height / 2, iOffset = 0) {
// // theta determines the spiral of the layout
// const theta = Math.PI * (3 - Math.sqrt(5))

// const pointRadius = pointWidth

// points.forEach((point, i) => {
// const index = (i + iOffset) % points.length
// const phylloX = pointRadius * Math.sqrt(index) * Math.cos(index * theta)
// const phylloY = pointRadius * Math.sqrt(index) * Math.sin(index * theta)

// point.x = xOffset + phylloX - pointRadius
// point.y = yOffset + phylloY - pointRadius
// })
// }

// // Specify the bar chart layout
// function barChartLayout(points, pointWidth, pointMargin = 1) {
// const pointWithMargin = pointWidth + pointMargin
// const pointsPerLayer = 28 // how many points wide each bar is

// // Initialize each bar's starting position
// let xOffset = 0

// // Process each color bar
// for (let color of distinctColors) {
// // Filter points for the current color
// const currentPoints = points.filter(p => p.color === color)

// // Initialize the position for the current bar
// let rowIndex = 0
// let verticalIndex = 0

// // Process each point in the current bar
// for (let i = 0; i < currentPoints.length; i++) {
// const point = currentPoints[i]

// // Check if we need to start a new layer
// if (i !== 0 && i % pointsPerLayer === 0) {
// rowIndex = 0 // start a new layer
// verticalIndex++ // move the layer up
// }

// // Position the point in the current layer
// point.x = xOffset + (rowIndex * pointWithMargin)
// point.y = verticalIndex * pointWithMargin

// rowIndex++ // move to the next point in the layer
// }

// // Calculate the width of the current bar
// const barWidth = pointWithMargin * pointsPerLayer
// // Update the starting position for the next bar
// xOffset += barWidth + 5 // we're adding some space between bars
// }
// }

// // Drawing & animation settings
// const layouts = [phyllotaxisLayout, barChartLayout]
// let layoutIndex = 0

// function draw() {
// // erase any existing points on canvas before drawing
// ctx.clearRect(0, 0, canvas.width, canvas.height)

// // draw each point as a square
// for (let i = 0; i < points.length; i++) {
// const point = points[i]
// ctx.fillStyle = point.color
// ctx.fillRect(point.x, point.y, pointWidth, pointWidth)
// }

// ctx.restore()
// }

// // Draw the points in their first layout
// phyllotaxisLayout(points, pointWidth)
// draw()

// // Animation setup
// const animationDuration = 1500 // milliseconds
// let timer
// let isAnimating = false // track if animation is happening

// function animate(nextLayout) {
// if (isAnimating) return // do nothing if animation is in progress

// isAnimating = true // set flag to indicate animation is in progress

// // Stop any current animation
// if (timer) timer.stop()

// // Store the first position of the points
// points.forEach(point => {
// point.sx = point.x
// point.sy = point.y
// })

// // Apply the next layout to adjust point positions
// nextLayout(points, pointWidth)

// // Store the destination position of the points
// points.forEach(point => {
// point.tx = point.x
// point.ty = point.y
// })

// // Reset points to their starting positions for the animation.
// points.forEach(point => {
// point.x = point.sx
// point.y = point.sy
// })

// // Animate the interpolation of points from one position to another
// timer = d3.timer((elapsed) => {
// // Compute the current progress of the animation (0 to 1)
// const progress = Math.min(1, d3.easeCubic(elapsed / animationDuration))

// // Update point positions (interpolate between source and target)
// points.forEach(point => {
// point.x = point.sx + (point.tx - point.sx) * progress
// point.y = point.sy + (point.ty - point.sy) * progress
// })

// // Draw the update
// draw()

// // Stop the animation when the progress is complete
// if (progress === 1) {
// // stop current layout's timer and start new one
// timer.stop()
// isAnimating = false
// }
// })
// }

// // Create a "PLAY" button
// d3.select('#controls').append('button')
// .attr('class', 'play-control')
// .text('PLAY')
// .on('click', () => {
// if(!isAnimating) {
// const nextLayoutIndex = (layoutIndex + 1) % layouts.length
// const nextLayout = layouts[nextLayoutIndex]
// animate(nextLayout)
// layoutIndex = nextLayoutIndex
// }
// })
// </script>
// </body>
// </html>
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