Public
Edited
Oct 26, 2022
Importers
15 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
parseDate = d3.timeParse("%Y-%m-%d")
Insert cell
Insert cell
processIncident = ({ categories, state, date, ...rest }) => ({
categories: categories.split(', '),
state,
date: parseDate(date),
...rest
})
Insert cell
incidents = d3.csvParse(rawData, processIncident)
Insert cell
Insert cell
incidentsByDate = d3.group(incidents, d => d3.timeFormat('%Y-%-m-%-d')(d.date))
Insert cell
Insert cell
Insert cell
Insert cell
narrow = width < breakpoint
Insert cell
columnMax = narrow ? 20 : 50
Insert cell
margin = narrow ? {left: 40, top: 20, bottom: 20, right: 0 } : { left: 20, right: 20, top: 10, bottom: 20 }
Insert cell
chartWidth = narrow ? 280 : 800
Insert cell
chartHeight = narrow ? incidentsByDate.length * 75 : 400
Insert cell
Insert cell
timeScale = d3.scaleBand()
.domain(
d3.timeDay.range(
d3.min(Array.from(incidentsByDate), ([key, entries]) => parseDate(key)),
END_DATE ? END_DATE : new Date(+d3.max(incidentsByDate, d => parseDate(d.key)) + 24 * 60 * 60 * 1000)
)
)
.range([
narrow ? margin.top : margin.left,
narrow ? chartHeight - margin.bottom : chartWidth - margin.right
])
Insert cell
timeScaleCentroid = i => timeScale(i) + timeScale.bandwidth() / 2
Insert cell
countScale = d3.scaleBand()
.domain(d3.range(0, columnMax + 1, 1))
.range([
narrow ? margin.left : chartHeight - margin.bottom,
narrow ? chartWidth - margin.right : margin.top
])
.padding(0.15)
Insert cell
countScaleCentroid = i => countScale(i) - countScale.bandwidth() / 2
Insert cell
circleRadius = 3
Insert cell
Insert cell
shouldHighlight = (d, h) => {
if (h === 'all') {
return true
} else if (Object.keys(ASSAILANTS).includes(h)) {
// It's an assailant filter
if (d.assailant === h) return true
// Special handling for the law enforcement filter to consider "arrests" law enforcement assailants
if (d.assailant === 'law_enforcement' && d.categories.includes('Arrest / Criminal Charge')) return true
} else {
// It's a city filter
if (`${d.city}, ${d.state}` === h) return true
}
return false
}
Insert cell
Insert cell
Insert cell
highlighter = () => {
const cities = Array.from(new Set(incidents.map(d => `${d.city}, ${d.state}`)))
const options = [
{ label: 'All', value: 'all' },
{
label: 'Assailant',
options: Object.entries(ASSAILANTS).map(([value, label]) => {
const incidentCount = incidents.filter(inc => inc.assailant === value).length
if (incidentCount === 0) return // Don't include assailants which we have no incidents for yet
return { value, label: `${label} (${incidentCount})` }
}).filter(d => d)
},
{
label: 'City',
options: cities.sort().map(c => {
const incidentCount = incidents.filter(inc => `${inc.city}, ${inc.state}` === c).length
return { label: `${c} (${incidentCount})`, value: c }
})
}
]
const defaultValue = 'all'
const option = ({ label, value }) => html`<option value="${value}">${label}</option>`
const optGroup = ({ label, options }) => html`<optgroup label="${label}">${options.map(option)}</optgroup>`
const select = html`<select value="${defaultValue}">
${options.map(opt => {
if ('value' in opt) return option(opt);
return optGroup(opt);
})}
</select>`

const output = html`
<div>
<div style="font: 700 0.9rem sans-serif; margin-bottom: 3px;">Highlight</div>
${select}
</div>
`
output.value = select.value
select.addEventListener('change', (e) => {
output.value = select.value
output.dispatchEvent(new CustomEvent("input"))
})
return output
}
Insert cell
Insert cell
legend = () => {
const legendRadius =5
// Create a set of all categories that are in use
let usedCategories = new Set()
incidents.forEach(inc => inc.categories.forEach(cat => usedCategories.add(cat)))
return html`<div class="legend">
${Array.from(usedCategories).sort().map(cat => html.fragment`
<div class="legend__item">
<svg
viewbox="0 0 ${legendRadius * 2} ${legendRadius * 2}"
width=${legendRadius * 2}
height=${legendRadius * 2}
class="legend__dot"
>
<g transform="translate(${legendRadius}, ${legendRadius})">
${categoryArcs([cat], legendRadius)}
</g>
</svg>
<span class="legend__label">
${cat}
<span class="legend__count">
${incidents.filter(inc => inc.categories.includes(cat)).length}
</span>
</span>
</div>
`)}
<div class="legend__item">
<svg
viewbox="0 0 ${legendRadius * 2} ${legendRadius * 2}"
width=${legendRadius * 2}
height=${legendRadius * 2}
class="legend__dot"
>
<g transform="translate(${legendRadius}, ${legendRadius})">
${categoryArcs(Array.from(usedCategories).slice(0, 3), legendRadius)}
</g>
</svg>
<span class="legend__label">
Multiple Categories
</span>
</div>
<div>`
}
Insert cell
Insert cell
popOverInner = (d) => `
<header class="popover__header">
${d.teaser_image !== 'None' ? `<img class="popover__image" data-src="${d.teaser_image}" />` : ''}
<div class="popover__superhead">
${d.categories.map(
c => `<span class="category-color--${slugify(c)}">${c}</span>`).join(' ')
}
</div>
<h1 class="popover__title">${d.title}</h1>
</header>
`
Insert cell
Insert cell
dateHighlighter = (date) => narrow ? svg.fragment`<rect
x="0"
y=${timeScale(date)}
height=${timeScale.bandwidth()}
width=${chartWidth}
fill="rgba(0, 0, 0, 0.05)"
/>` : svg.fragment`<rect
x=${timeScale(date)}
y="0"
height=${chartHeight}
width=${timeScale.bandwidth()}
fill="rgba(0, 0, 0, 0.05)"
/>`

Insert cell
Insert cell
dateLabel = t => (
svg.fragment`<text
x=${narrow ? margin.left - circleRadius - 3 : timeScaleCentroid(t)}
y=${narrow ? timeScaleCentroid(t) + 5 : chartHeight - margin.bottom + 10}
text-anchor=${narrow ? 'end' : 'middle'}
font-family="sans-serif"
font-size="11"
stroke-width="4"
stroke="#FFF"
paint-order="stroke"
>
${!narrow ? svg.fragment`<tspan font-weight="bold">${d3.timeFormat('%a')(t)}</tspan>` : ''}
<tspan>${d3.timeFormat('%-m/%-e')(t)}</tspan>
</text>`
)
Insert cell
Insert cell
gridLine = i => narrow ? svg.fragment`
<line
x1=${countScale(i) - countScale.bandwidth() / 2 - countScale.paddingInner() * countScale.bandwidth() / 2}
x2=${countScale(i) - countScale.bandwidth() / 2 - countScale.paddingInner() * countScale.bandwidth() / 2}
y1="0"
y2=${chartHeight}
stroke="rgba(0, 0, 0, 0.1)"
stroke-dasharray="4 4"
/>
`: svg.fragment`
<line
x1="0"
x2=${chartWidth}
y1=${countScale(i) + countScale.bandwidth() / 2 + countScale.paddingInner() * countScale.bandwidth() / 2}
y2=${countScale(i) + countScale.bandwidth() / 2 + countScale.paddingInner() * countScale.bandwidth() / 2}
stroke="rgba(0, 0, 0, 0.1)"
stroke-dasharray="4 4"
/>
`
Insert cell
Insert cell
dateGridLine = t => narrow ? svg.fragment`
<line
x1="0"
y1=${timeScale(t)}
x2=${chartWidth}
y2=${timeScale(t)}
stroke="rgba(0, 0, 0, 0.1)"
/>
` : svg.fragment`
<line
x1=${timeScale(t)}
y1="0"
x2=${timeScale(t)}
y2=${chartHeight}
stroke="rgba(0, 0, 0, 0.1)"
/>
`
Insert cell
Insert cell
gridAndAxes = () => {
const allTicks = timeScale.domain()
// Only display every other label on wide screens
const labelTicks = allTicks.filter((_, i) => narrow ? true : !(i % 2))
return svg.fragment`
<g class="grid">
<!-- Highlight Weekends -->
${timeScale.domain().filter((date) => [0, 6].includes(date.getDay())).map(dateHighlighter)}
<!-- Time Grid -->
${allTicks.filter((_, i) => i !== 0).map(dateGridLine)}
<!-- Count Grid -->
${countScale.domain().filter(i => i % 5 === 0).map(gridLine)}
<!-- Time Labels -->
${labelTicks.map(dateLabel)}
</g>
`
}
Insert cell
Insert cell
dateTotalLabel = d => narrow ? svg.fragment`
<text
x=${countScaleCentroid(Math.min(d.values.length, columnMax) - 1) + circleRadius * 4}
y=${timeScaleCentroid(d.key) + 4.5}
text-anchor="middle"
font-size="11"
font-family="'Source Sans Pro', var(--sans-serif)"
fill="rgba(0, 0, 0, 0.4)"
>
${d.values.length}
</text>
` : svg.fragment`
<text
x=${timeScaleCentroid(d.key)}
y=${countScaleCentroid(Math.min(d.values.length, columnMax) - 1) - circleRadius}
text-anchor="middle"
font-size="11"
font-family="'Source Sans Pro', var(--sans-serif)"
fill="rgba(0, 0, 0, 0.4)"
>
${d.values.length}
</text>
`
Insert cell
Insert cell
incidentColumn = g => g.selectAll('g')
.data(([key, incidents]) => incidents.map(
(v, i, arr) => Object.assign({
subColNumber: Math.floor(i / columnMax),
totalSubCols: Math.ceil(arr.length / columnMax)
}, v)
))
.join('g')
.call(incidentCircle)
Insert cell
incidentCircle = (g, d) => {
g.attr(
'class', 'incident-circle incident-circle--highlight'
)
g.append((d, i) => {
const subColWidth = circleRadius + 1
const startOffset = - subColWidth * d.totalSubCols
const colOffset = startOffset + subColWidth + (2 * subColWidth * d.subColNumber)
const indexInCol = i % columnMax
const timePos = timeScaleCentroid(d.date) + colOffset
const countPos = countScaleCentroid(indexInCol) + circleRadius
const joinedCoords = narrow ? `${countPos} ${timePos}` : `${timePos} ${countPos}`
return svg.fragment`
<g
class="incident-circle__inner"
transform-origin=${joinedCoords}
>
<g transform=${`translate(${joinedCoords})`}>
${categoryArcs(d.categories)}
</g>
</g>
`
})
}
Insert cell
categoryArcs = (categories, radius=circleRadius) => svg.fragment`
${categories.map((cat, i, all) => svg.fragment`
<path
d=${d3.arc()({
innerRadius: 0,
outerRadius: radius,
startAngle: -i * Math.PI * 2 / categories.length,
endAngle: -(i + 1) * Math.PI * 2 / categories.length,
})}
fill=${CATEGORY_COLORS[cat]}
/>
`)}
`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
DATA_URL = 'https://pressfreedomtracker.us/api/edge/incidents/?tags=Black+Lives+Matter&date_lower=2020-01-01&date_upper=2020-12-31&fields=title,slug,teaser_image,categories,date,assailant,city,state&limit=9999&format=csv'
Insert cell
Insert cell
ASSAILANTS = ({
unknown: 'Unknown',
'law enforcement': 'Law Enforcement',
'private security': 'Private Security',
politician: 'Politician',
'public figure': 'Public Figure',
'private individual': 'Private Individual',
})
Insert cell
END_DATE = new Date(2020, 5, 26)
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