Public
Edited
Jan 3, 2023
Importers
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data = (await (await fetch('https://data.cityofnewyork.us/resource/f7dc-2q9f.json?$limit=9999')).json())
.map(d => ({
...d,
sample_date: d3.utcParse('%Y-%m-%dT%H:%M:%S.%L')(d.sample_date),
test_date: d3.utcParse('%Y-%m-%dT%H:%M:%S.%L')(d.test_date),
copies_l: +d.copies_l,
copies_l_x_average_flowrate: +d.copies_l_x_average_flowrate,
population_served: +d.population_served,
// Standardize data to use abbreviations in map data
wrrf_abbreviation: (d => {
if (d.wrrf_abbreviation === 'JA') return 'JAM'
if (d.wrrf_abbreviation === 'RK') return 'ROC'
return d.wrrf_abbreviation
})(d)
}))
.sort((a, b) => a.sample_date - b.sample_date)
Insert cell
movingAverage = (i, arr, key, n = 7) => d3.mean(arr.slice(i - n + 1, i + 1), d => d[key])
Insert cell
chartWidth = Math.max(width, 600)
Insert cell
wrrfAbbrs = new Set(data.map(d => d.wrrf_abbreviation))
Insert cell
// Programatically generate a color scheme by choosing hues around a rainbow and making colors progressively darker, then light again every 3rd color
colors = new Array(wrrfAbbrs.size).fill().map((_, i, arr) => (
d3.color(
d3.interpolateRainbow(i / arr.length)
).darker(i % 3).formatRgb()
))
Insert cell
Insert cell
Insert cell
Insert cell
sewershedsTopology = FileAttachment("sewersheds.topo").json()
Insert cell
features = ({
shoreline: topojson.feature(sewershedsTopology, sewershedsTopology.objects.shoreline.geometries[0]),
sewersheds: topojson.feature(sewershedsTopology, sewershedsTopology.objects.sewersheds),
plants: topojson.feature(sewershedsTopology, sewershedsTopology.objects.plants),
})
Insert cell
colorScale = d3.scaleOrdinal()
.range(colors.concat(['#CCC', '#CCC']))
.domain(Array.from(wrrfAbbrs).concat(['UNKNOWN', 'NA']))
Insert cell
map = (initialValue = []) => {
const mapWidth = 400
const mapHeight = mapWidth
const mapNode = htl.html`<svg viewBox="0 0 ${mapWidth} ${mapHeight}" />`
const map = d3.select(mapNode)
map.attr('style', `max-width: ${mapWidth}px; width: 100%`)
const deselectedOpacity = 0.25

// Projections

// NAD83 / New York Long Island (EPSG:32118)
// https://github.com/veltman/d3-stateplane#nad83--new-york-long-island-epsg32118
// Added our own fitExtent to center on our map data
const projection = d3.geoIdentity()
.fitExtent([[20, 20], [mapWidth - 20, mapWidth - 20]], features.sewersheds)
const path = d3.geoPath(projection)
// Water
map.append('rect')
.attr('x', 0.5)
.attr('y', 0.5)
.attr('width', mapWidth - 1)
.attr('height', mapWidth - 1)
.attr('fill', '#F5F5F5')
.attr('stroke', '#CCC')
.attr('pointer-events', 'none')
// New York
const newYork = map.append('path')
.attr('d', path(features.shoreline))
.attr('fill', '#CCC')
.attr('stroke', '#FFF')
.attr('pointer-events', 'none')

// State container
const selectedCities = initialValue

const sewershedShapes = map.append('g')
.selectAll()
.data(features.sewersheds.features)
.join('path')
.attr('class', 'plant-sewershed')
.attr('data-plant', d => d.properties.Sewershed)
.attr('d', path)
.attr('fill', d => colorScale(d.properties.Sewershed))
.attr('opacity', d => initialValue.includes(d.properties.Sewershed) ? 1 : deselectedOpacity)
.attr('stroke', '#FFF')
.attr('data-selected', d => initialValue.includes(d.properties.Sewershed))
.on('mouseover', function (e, d) {
map.select(`.plant-name[data-plant="${d.properties.Sewershed}"]`).attr('opacity', 1)
})
.on('mouseout', function (e, d) {
map.select(`.plant-name[data-plant="${d.properties.Sewershed}"]`).attr('opacity', 0)
})
.on('click', function (e, d) {{
const sel = d3.select(this)
const sewershed = d.properties.Sewershed
if (sewershed === 'UNKNOWN') return
if (view.value.includes(sewershed)) { // if already selected, remove from value array
const valueIdx = view.value.findIndex(v => sewershed === v)
view.value.splice(valueIdx, 1)
updateSelection(view.value)
} else { // if not already selected, add to value array
updateSelection(view.value.concat([sewershed]))
}
}})

const plantPoints = map.append('g')
.selectAll()
.data(features.plants.features.filter(d => d.properties.wpcp))
.join(g => {
g.append('circle')
.attr('class', 'plant-dot')
.attr('data-plant', d => d.properties.wpcp)
.attr('cx', d => projection(d.geometry.coordinates)[0])
.attr('cy', d => projection(d.geometry.coordinates)[1])
.attr('r', 5)
.attr('stroke', '#FFF')
.attr('fill', d => colorScale(d.properties.wpcp))
.attr('pointer-events', 'none')

g.append('text')
.attr('class', 'plant-name')
.attr('data-plant', d => d.properties.wpcp)
.attr('x', d => projection(d.geometry.coordinates)[0])
.attr('y', d => projection(d.geometry.coordinates)[1])
.attr('dx', 8)
.attr('dy', 4)
.attr('font-family', 'var(--sans-serif), sans-serif')
.attr('font-size', 11)
.attr('fill', '#000')
.attr('stroke', '#FFF')
.attr('stroke-width', 3)
.attr('paint-order', 'stroke')
.attr('text-anchor', 'start')
.attr('opacity', 0)
.attr('pointer-events', 'none')
.text(d => d.properties.item_id)
})

const updateSelection = (arr) => {
view.value = arr
view.dispatchEvent(new Event('input', { bubbles: true }))
sewershedShapes
.attr('opacity', d => arr.includes(d.properties.Sewershed) ? 1 : deselectedOpacity)
.attr('data-selected', d => arr.includes(d.properties.Sewershed))
}
const selectAll = htl.html`<button>All</button>`
const selectNone = htl.html`<button>None</button>`

selectAll.addEventListener('click', () => updateSelection(Array.from(wrrfAbbrs)))
selectNone.addEventListener('click', () => updateSelection([]))
const view = htl.html`
<div>
<div style=${{ position: 'relative' }}>
${map.node()}
<div style=${{
position: 'absolute',
top: '10px',
left: '10px',
fontSize: '12px',
fontFamily: 'var(--sans-serif), sans-serif',
verticalAlign: 'middle',
}}>
Select: ${selectAll} ${selectNone}
</div>
</div>
</div>
`
view.value = selectedCities

return view
}
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