Published
Edited
Feb 18, 2020
1 fork
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
render = (selection, props) => {
const {
data,
height,
width,
margin,
circleRadius,
focusCircleRadius,
circleOpacity,
focusCircleOpacity,
unfocusCircleOpacity,
onMouseEnterCircle,
onMouseLeaveCircle,
onMouseEnterLegendTick,
onMouseLeaveLegendTick,
xAxisLabel,
yAxisLabel,
legendOffsetX,
legendOffsetY,
uniqueKeyValue,
colorValue,
labelValue,
xValue,
yValue,
} = props
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const g = selection.selectAll('.container').data([null])
const gEnter = g.enter()
.append('g')
.attr('class', 'container')
gEnter.merge(g)
.attr('transform', `translate(${margin.left},${margin.top})`)
const scaleX = d3.scaleLinear()
.domain(d3.extent(data, xValue))
.range([0, innerWidth])
.nice()
const scaleY = d3.scaleLinear()
.domain(d3.extent(data, yValue))
.range([innerHeight, 0])
.nice()
const scaleColor = d3.scaleOrdinal()
.domain(Object.keys(colors))
.range(Object.values(colors))
const xAxis = d3.axisBottom(scaleX)
.tickPadding(5)
const yAxis = d3.axisLeft(scaleY)
.tickPadding(5)
const xAxisG = g.select('.x-axis')
const xAxisGEnter = gEnter.append('g')
.attr('class', 'x-axis')
xAxisG.merge(xAxisGEnter)
.attr('transform', `translate(0,${innerHeight + 10})`)
.call(xAxis)
const yAxisG = g.select('.y-axis')
const yAxisGEnter = gEnter.append('g')
.attr('class', 'y-axis')
yAxisG.merge(yAxisGEnter)
.attr('transform', 'translate(-10,0)')
.call(yAxis)
const xAxisLabelEnter = xAxisGEnter.append('text')
.attr('class', 'axis-label')
.attr('fill', 'currentColor')
.attr('x', innerWidth / 2)
.attr('y', 45)
xAxisG.select('.axis-label').merge(xAxisLabelEnter)
.text(xAxisLabel)
const yAxisLabelEnter = yAxisGEnter.append('text')
.attr('class', 'axis-label')
.attr('fill', 'currentColor')
.attr('x', -innerHeight / 2)
.attr('y', -45)
.attr('transform', `rotate(-90)`)
yAxisG.select('.axis-label').merge(yAxisLabelEnter)
.text(yAxisLabel)
const translatePoint = d => `translate(${scaleX(xValue(d))},${scaleY(yValue(d))})`
const points = g.merge(gEnter).selectAll('.data-point')
.data(data, uniqueKeyValue)
const pointsEnter = points.enter()
.append('g')
.attr('class', 'data-point')
.attr('transform', translatePoint)
.on('mouseenter', onMouseEnterCircle)
.on('mouseleave', onMouseLeaveCircle)
pointsEnter.append('circle')
.attr('fill', d => scaleColor(colorValue(d)))
points.merge(pointsEnter).select('circle')
.transition().duration(125)
.attr('r', d => focus === d.name ? focusCircleRadius : circleRadius)
.attr('opacity', d => focusType
? d.type1 === focusType ? focusCircleOpacity : unfocusCircleOpacity
: circleOpacity
)
points
.transition().duration(1000)
.ease(d3.easeBackInOut)
.attr('transform', translatePoint)
// add tooltip
pointsEnter.append('title')
points.merge(pointsEnter).select('title')
.text(d => [
labelValue,
d => d.type1 + " type",
d => xAxisLabel + ": " + xValue(d),
d => yAxisLabel + ": " + yValue(d),
].map(fn => fn(d)).join('\n'))
// add legend
const legendGEnter = gEnter.append('g')
.attr('class', 'legend')
.attr('transform', `translate(${legendOffsetX},${innerHeight + margin.top + legendOffsetY})`)
g.select('.legend').merge(legendGEnter)
.call(renderLegend, {
innerWidth,
innerHeight,
margin,
scaleColor,
onMouseEnterLegendTick,
onMouseLeaveLegendTick,
tickOpacity: circleOpacity,
focusTickOpacity: focusCircleOpacity,
unfocusTickOpacity: unfocusCircleOpacity,
})
}
Insert cell
renderLegend = (selection, props) => {
const {
scaleColor,
innerWidth,
innerHeight,
onMouseEnterLegendTick,
onMouseLeaveLegendTick,
tickOpacity,
focusTickOpacity,
unfocusTickOpacity,
} = props
const items = scaleColor.domain()

const scaleX = d3.scalePoint()
.domain(items)
.range([0, innerWidth])
const ticksEnter = selection.selectAll('circle').data(items).enter()
.append('g')
.attr('transform', d => `translate(${scaleX(d)},0)`)
.on('mouseenter', onMouseEnterLegendTick)
.on('mouseleave', onMouseLeaveLegendTick)
ticksEnter.append('circle')
.attr('r', 10)
.attr('fill', scaleColor)
ticksEnter.append('text')
.attr('class', 'legend-label')
.attr('y', 20)
.text(d => d)
selection.selectAll('circle').merge(ticksEnter)
.transition().duration(125)
.attr('opacity', d => focusType !== null
? d === focusType ? focusTickOpacity : unfocusTickOpacity
: tickOpacity
)
}
Insert cell
renderIcon = (selection, props) => {
const { href, height, width, loading } = props
const icon = selection.selectAll('.data-icon').data([null])
const iconEnter = loading
? icon.enter().append('image').attr('class', 'data-icon')
: d3.select(null)
icon.merge(iconEnter)
.attr('href', href)
.transition()
.duration(loading ? 0 : 50)
.attr('height', height)
.attr('width', width)
.attr('x', -width / 2)
.attr('y', -height / 2)
}
Insert cell
onMouseEnterLegendTick = d => {
mutable focusType = d
}
Insert cell
onMouseLeaveLegendTick = () => {
mutable focusType = null
}
Insert cell
async function onMouseEnterCircle(d) {
mutable focus = d.name
const point = d3.select(this).raise()

const width = 100
const height = 100
const loadingWidth = 50
const loadingHeight = 50

point.call(renderIcon, {
href: loadingGifUrl,
height: loadingHeight,
width: loadingWidth,
loading: true,
})
const spriteHref = await getSprite(d.pokedex_number)
await loadImage(spriteHref)
point.call(renderIcon, {
href: spriteHref,
height,
width,
})

// Note: we'd have to call render in here if not for ObservableHQ
}
Insert cell
function onMouseLeaveCircle() {
mutable focus = null
const point = d3.select(this)
point.selectAll('.data-icon').remove()
// Note: we'd have to call render in here if not for ObservableHQ
}
Insert cell
d3.select(svg).call(render,{
data: pokemon,
height,
width,
margin: { left: 100, bottom: 130, right: 20, top: 30 },
legendOffsetX: 0,
legendOffsetY: 60,
circleRadius: 5,
focusCircleRadius: 30,
circleOpacity: .7,
focusCircleOpacity: .9,
unfocusCircleOpacity: .1,
onMouseEnterCircle,
onMouseLeaveCircle,
onMouseEnterLegendTick,
onMouseLeaveLegendTick,
xAxisLabel: xValueSelection.replace(/_/g,' '),
yAxisLabel: yValueSelection.replace(/_/g,' '),
uniqueKeyValue: d => d.pokedex_number,
colorValue: d => d.type1,
xValue: d => d[xValueSelection],
yValue: d => d[yValueSelection],
labelValue: d => d.name,
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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