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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more