Public
Edited
Oct 26, 2022
Importers
14 stars
Also listed in…
Press Freedom Tracker
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

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