Public
Edited
Jun 6, 2023
9 stars
Insert cell
Insert cell
Insert cell
Insert cell
chartUpdatePositions = {
// Bring in the buttons
const enterBtn = document.getElementById('enter-btn')
const updateBtn = document.getElementById('update-btn')
const returnBtn = document.getElementById('return-btn')

// Graph container
const width = 600
const height = 600
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)

/*
Put all the graph-drawing logic in a function
so that we can re-draw from button events based on chaged data
*/
const drawGraph = (data) => {
const rects = svg.selectAll('circle')
.data(data, d => d.id)
.join(
enter => {
return enter.append('circle')
// Position and properties that we will animate *from* upon entry
.attr('cx', 0) // enter from 0 on the left (horizontally)
.attr('cy', (d, i) => 100 + i*80) // enter from final vertical position
.style('stroke-width', 0)
.style('fill', d => colourScale[d.category])
},
update => {
return update
// Position and properties that we will animate *from* upon update
.attr('cx', 300) // don't move horizontally on update
.style('stroke-width', 3)
},
exit => {
exit
// Position and properties that we will animate *to* for the exiting elements
.transition().duration(1000)
.attr('r', 0) // radius shrinks on exiting elements
.style('fill', '#000') // fill becomes black as the element shrinks
.attr('cx', 500) // element moves to the right as it disappears
//.remove() // uncomment this to see what happens if exit selection is removed
}
)
/*
All the properties before the transition are properties for the enter
and update selection to transition *from*; we could have added them in each of the enter and update definitions, too
*/
.style('stroke', '#000')
// Then these are the properties that both the enter and update selection will transition *to*
.transition().duration(1000)
.attr('cx', 300) // on enter: 0 -> 300; on update 300 -> 300 i.e. no transition
/* 👇🏻
on enter: no transition;
on update: shuffle elements from old to new vertical positon
(based on index so elements will move up if some elements are exiting)
*/
.attr('cy', (d, i) => 100 + i*80)
.attr('r', d => d.value) // old value to new value for that particular element
.style('fill', d => colourScale[d.category]) // only needed as exit selection is black; no effect on enter selection
.style('stroke-width', 0)
// Add text so we can see the elements
const text = svg
.selectAll("text")
.data(data, d => d.id)
.join('text')
.attr("x", 180)
.attr("dy", "0.35em")
.text(d => d.category)
.style('fill', d => colourScale[d.category])
.style('font-size', '20px')
.transition().duration(1000)
.attr("y", (d, i) => 100 + i*80)
}
drawGraph(initialData)

// Simulate an enter pattern
enterBtn.addEventListener('click', () => {
// Remove the elements before we draw again so we can see the enter pattern
d3.selectAll('circle').remove()
d3.selectAll('text').remove()
drawGraph(initialData)
})

updateBtn.addEventListener('click', () => {
drawGraph(changedData)
})
returnBtn.addEventListener('click', () => {
drawGraph(initialData)
})

return svg.node();
}
Insert cell
Insert cell
Insert cell
// viewof actionIndexBasedNoShift = Inputs.button([
// ["Enter", value => 'enter'],
// ["Update", value => 'update'],
// ["Return", value => 'return']
// ], {value: 0, label: "Action"})
Insert cell
chartKeepPositions = {

// Graph container
const width = 600
const height = 600
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)

const drawGraph = (data) => {
const rects = svg.selectAll('circle')
.data(data, d => d.id)
.join(
enter => {
return enter.append('circle')
// Position and properties that we will animate *from* upon entry
.attr('cx', 0) // enter from 0 on the left (horizontally)
.attr('cy', (d, i) => 100 + i*80) // enter from final vertical position
.style('stroke-width', 0)
.style('fill', d => colourScale[d.category])
},
update => {
return update
// Position and properties that we will animate *from* upon update
.attr('cx', 300) // don't move horizontally on update
.style('stroke-width', 3)
},
exit => {
exit
.transition().duration(1000)
// Position and properties that we will animate *to* for the exiting elements
.attr('r', 0) // radius shrinks on exiting elements
.style('fill', '#000') // fill becomes black as the element shrinks
.attr('cx', 500) // element moves to the right as it disappears
}
)
.style('stroke', '#000')
.transition().duration(1000)
.attr('cx', 300)
.attr('r', d => d.value)
.style('fill', d => colourScale[d.category])
.style('stroke-width', 0)
// Add text so we can see the elements
const text = svg
.selectAll("text")
.data(data, d => d.id)
.join(
enter => {
return enter.append('text')
.attr("y", (d, i) => 100 + i*80)
},
update => update,
exit => {
exit.transition().duration(1000)
.attr("x", (d, i) => 500)
}
)
.attr("x", 180)
.attr("dy", "0.35em")
.text(d => d.category)
.style('fill', d => colourScale[d.category])
.style('font-size', '20px')
}
drawGraph(initialData)

// if (actionIndexBasedNoShift === 'enter') {
// // Remove the elements before we draw again so we can see the enter pattern
// d3.selectAll('circle').remove()
// d3.selectAll('text').remove()
// drawGraph(initialData)
// } else if (actionIndexBasedNoShift === 'update') {
// drawGraph(changedData)
// } else if (actionIndexBasedNoShift === 'return') {
// drawGraph(initialData)
// } else {
// console.log('waiting for button to be clicked')
// }

// Use this if getting buttons from the DOM
// Bring in the buttons
const enterBtn = document.getElementById('enter-btn-keep')
const updateBtn = document.getElementById('update-btn-keep')
const returnBtn = document.getElementById('return-btn-keep')
// Simulate an enter pattern
enterBtn.addEventListener('click', () => {
// Remove the elements before we draw again so we can see the enter pattern
d3.selectAll('circle').remove()
d3.selectAll('text').remove()
drawGraph(initialData)
})

updateBtn.addEventListener('click', () => {
drawGraph(changedData)
})
returnBtn.addEventListener('click', () => {
drawGraph(initialData)
})

return svg.node();
}
Insert cell
Insert cell
Insert cell
chartKeepPositionsWithoutIndex = {
// Bring in the buttons
const enterBtn = document.getElementById('enter-btn-keep-scales')
const updateBtn = document.getElementById('update-btn-keep-scales')
const returnBtn = document.getElementById('return-btn-keep-scales')

// Graph container
const width = 600
const height = 600
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)

// Scales
const yScale = d3.scalePoint()
.domain(initialData.map(d => d.id))
.range([50, height - 50])

// Graph
const drawGraph = (data) => {
const rects = svg.selectAll('circle')
.data(data, d => d.id)
.join(
enter => {
return enter.append('circle')
// Position and properties that we will animate *from* upon entry
.attr('cx', 0) // enter from 0 on the left (horizontally)
.attr('cy', d => yScale(d.id)) // enter from final vertical position
.style('stroke-width', 0)
.style('fill', d => colourScale[d.category])
},
update => {
return update
// Position and properties that we will animate *from* upon update
.attr('cx', 300) // don't move horizontally on update
.style('stroke-width', 3)
},
exit => {
exit
.transition().duration(1000)
// Position and properties that we will animate *to* for the exiting elements
.attr('r', 0) // radius shrinks on exiting elements
.style('fill', '#000') // fill becomes black as the element shrinks
.attr('cx', 500) // element moves to the right as it disappears
}
)
.style('stroke', '#000')
.transition().duration(1000)
.attr('cx', 300)
// .attr('cy', d => yScale(d.id)) // You can uncomment this but nothing will happen
.attr('r', d => d.value)
.style('fill', d => colourScale[d.category])
.style('stroke-width', 0)
// Add text so we can see the elements
const text = svg
.selectAll("text")
.data(data, d => d.id)
.join(
enter => {
return enter.append('text')
.attr("y", (d, i) => 100 + i*80)
},
update => update,
exit => {
exit.transition().duration(1000)
.attr("x", (d, i) => 500)
}
)
.attr("x", 180)
.attr("dy", "0.35em")
.text(d => d.category)
.style('fill', d => colourScale[d.category])
.style('font-size', '20px')
}
drawGraph(initialData)


// Simulate an enter pattern
enterBtn.addEventListener('click', () => {
// Remove the elements before we draw again so we can see the enter pattern
d3.selectAll('circle').remove()
d3.selectAll('text').remove()
drawGraph(initialData)
})

updateBtn.addEventListener('click', () => {
drawGraph(changedData)
})
returnBtn.addEventListener('click', () => {
drawGraph(initialData)
})

return svg.node();
}
Insert cell
Insert cell
Insert cell
chartSeparateTransitions = {
// Bring in the buttons
const enterBtn = document.getElementById('enter-btn-separate-transitions')
const updateBtn = document.getElementById('update-btn-separate-transitions')
const returnBtn = document.getElementById('return-btn-separate-transitions')

const customElastic = d3.easeElastic.period(0.6); // This is the custom ease we will use

// Graph container
const width = 600
const height = 600
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)

const drawGraph = (data) => {
const rects = svg.selectAll('circle')
.data(data, d => d.id)
.join(
enter => {
return enter.append('circle')
// Position and properties that we will animate *from* upon entry
.attr('cx', 0) // enter from 0 on the left (horizontally)
.attr('cy', (d, i) => 100 + i*80) // enter from final vertical position
.style('stroke-width', 0)
.style('fill', d => colourScale[d.category])
// Position and properties that we will animate *to* upon entry
.transition().duration(1000).ease(customElastic)
.attr('cx', 300) // 0 -> 300
.attr('r', d => d.value) // 0 -> value
},
update => {
return update
// Position and properties that we will animate *from* upon update
// 👇🏻 if you set cx here you won't get a transition from the exit cx value of 500
//.attr('cx', 300)
// 👇🏻 if you set the colour here you won't get a transition from the exit value of black
//.style('fill', d => colourScale[d.category])
.style('stroke-width', 3) // stroke will flash just for a second on update
.transition().duration(2000)
// 👇🏻 500 -> 300 here or 300 -> 300 i.e. no transition if cx is set above for elements
// coming back from having exited
.attr('cx', 300)
// 👇🏻 black -> colour here or no colour transition if fill is set above for elements
// coming back after exiting; no effect on elements which are only changing other properties
.style('fill', d => colourScale[d.category])
// 👇🏻 if this is set then the circles will shift up based on index whenever some
// of the circles exit; if this is not set then there will be gaps for missing circles
.attr('cy', (d, i) => 100 + i*80)
.attr('r', d => d.value)
.style('stroke-width', 0) // stroke will disappear just after flashing on update
},
exit => {
exit
// Position and properties that we will animate *to* for the exiting elements
.transition().duration(1000)
.attr('r', 0) // radius shrinks on exiting elements
.style('fill', '#000') // fill becomes black as the element shrinks
.attr('cx', 500) // element moves to the right as it disappears
}
) // Only add constant values that even change on enter update or exit below
.style('stroke', '#000')

// Add text so we can see the elements
const text = svg
.selectAll("text")
.data(data, d => d.id)
.join('text')
.attr("x", 180)
.attr("dy", "0.35em")
.text(d => d.category)
.style('fill', d => colourScale[d.category])
.style('font-size', '20px')
.transition().duration(1000)
.attr("y", (d, i) => 100 + i*80)
}
drawGraph(initialData)


// Simulate an enter pattern
enterBtn.addEventListener('click', () => {
// Remove the elements before we draw again so we can see the enter pattern
d3.selectAll('circle').remove()
d3.selectAll('text').remove()
drawGraph(initialData)
})

updateBtn.addEventListener('click', () => {
drawGraph(changedData)
})
returnBtn.addEventListener('click', () => {
drawGraph(initialData)
})

return svg.node();
}
Insert cell
Insert cell
colourScale = ({
'cat': 'plum',
'panda': 'rebeccapurple',
'dog': 'lightseagreen'
})
Insert cell
changedData = [
{ id: 1, value: 10, category: 'cat'}, // changed value
{ id: 2, value: 50, category: 'panda'}, // changed value
//{ id: 3, value: 40, category: 'cat'}, //exited
{ id: 4, value: 20, category: 'cat'}, // satyed the same
{ id: 5, value: 25, category: 'panda'}, // changed value
//{ id: 6, value: 25, category: 'dog'}, //exited
// { id: 7, value: 55, category: 'dog'} // entered -- uncomment to see what happens
]
Insert cell
initialData = [
{ id: 1, value: 20, category: 'cat'},
{ id: 2, value: 30, category: 'panda'},
{ id: 3, value: 40, category: 'cat'},
{ id: 4, value: 20, category: 'cat'},
{ id: 5, value: 45, category: 'panda'},
{ id: 6, value: 25, category: 'dog'}
]
Insert cell
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