Published
Edited
Aug 26, 2020
Importers
5 stars
Insert cell
Insert cell
Insert cell
air = d3
.json(
"https://www.purpleair.com/data.json?opt=1/mAQI/a10/cC0&fetch=true&nwlat=43.167493434353474&selat=32.25997597364069&nwlng=-127.87343156694496&selng=-114.15416006614362&fields=pm_1"
)
.then(d =>
Object.assign(
d.data.map(b => Object.fromEntries(d.fields.map((f, i) => [f, b[i]]))),
{ columns: d.fields }
)
)
Insert cell
station = d3.json(`https://www.purpleair.com/json?show=${airfile[0].ID}`)
Insert cell
thing = station.results[0]
Insert cell
pm1 = d3.json(`https://api.thingspeak.com/channels/${thing.THINGSPEAK_PRIMARY_ID}/fields/8.json?start=2020-08-10%2023:12:35&offset=0&round=2&average=60&api_key=${thing.THINGSPEAK_PRIMARY_ID_READ_KEY}`)
Insert cell
p03 = d3.json(`https://api.thingspeak.com/channels/${thing.THINGSPEAK_PRIMARY_ID}/fields/1.json?start=2020-08-10%2023:12:35&offset=0&round=2&average=60&api_key=${thing.THINGSPEAK_PRIMARY_ID_READ_KEY}`)
Insert cell
p03csv = d3.csvFormat(p03.feeds.map(d => {
return {
id: thing.ID,
thingId: thing.THINGSPEAK_PRIMARY_ID,
created_at: +new Date(d.created_at)/1000,
value: d.field1
}
}))
Insert cell
airfile = FileAttachment("air.json").json()
Insert cell
sensors = airfile.filter(d => d.Type == 0)
Insert cell
day = "2020-08-23"
Insert cell
projection = d3.geoAlbersUsa()
.fitSize([width, height], countyExtent)
Insert cell
projection.scale()
Insert cell
projection.translate()
Insert cell
map1 = {
const canvas = d3.create("canvas").node()
canvas.width = width
canvas.height = height
const ctx = canvas.getContext("2d")

let path = d3.geoPath(projection)
.context(ctx)
ctx.strokeStyle = "#111"
ctx.lineWidth = 1
ctx.fillStyle = "darkslategray"
ctx.beginPath()
path(california)
ctx.stroke()
ctx.fill()
ctx.globalAlpha = 0.9
ctx.fillStyle = "#999"
sensors.forEach(d => {
let p = projection([d.Lon, d.Lat])
if(p) {
ctx.fillStyle = color2(d.pm_1)
ctx.fillRect(p[0], p[1], 3,3)
}
})
// let copy = V1_08_2020_CA
// .filter(d => new Date(d.acq_date) < new Date(day3_2020) && d.bright_ti4 > 320)
// .forEach(d => {
// let p = projection([d.longitude, d.latitude])
// ctx.fillRect(p[0], p[1], 1,1)
// })
// daily_V1_08_2020_CA.get(day3_2020).forEach(d => {
// let p = projection([d.longitude, d.latitude])
// ctx.fillStyle = ti4Color(d.bright_ti4)
// ctx.fillRect(p[0], p[1], 1,1)
// })
ctx.fillStyle = "#111"
ctx.font = "bold 32px Courier New"
ctx.fillText(day, 20, 50)
return canvas
}
Insert cell
color = d3.scaleSequential(d3.interpolateSpectral)
// .domain(d3.extent(airfile, d => d.pm_1))
.domain([250,0])
Insert cell
legend({ color: color})
Insert cell
color2 = d3.scaleThreshold()
.domain([50, 100, 150, 200, 250])
.range(["green", "yellow", "orange", "red", "maroon"])
Insert cell
legend({ color: color2})
Insert cell
colorPM1 = d3.scaleLinear()
.domain([0,50, 100, 150, 200, 250])
.range(["green", "yellow", "orange", "red", "maroon", "maroon"])
Insert cell
opacityPM1 = d3.scaleLinear()
.domain([0,50, 100, 150, 200, 250])
.range([0.1, 0.5, 0.75, 0.9, 0.9, 0.9])
Insert cell
legend({ color: colorPM1})
Insert cell
d3.max(fullhexbin, d => d3.max(d.days, day => day.p03))
Insert cell
d3.min(fullhexbin, d => d3.min(d.days, day => day.p03))
Insert cell
colorP03 = d3.scaleLinear()
.domain([1,1000, 5000, 10000, 20000, 25000])
.range(["green", "yellow", "orange", "red", "maroon", "maroon"])
Insert cell
legend({ color: colorP03})
Insert cell
opacityP03 = d3.scaleLinear()
.domain([1,1000, 5000, 10000, 20000, 25000])
.range([0.1, 0.5, 0.75, 0.9, 0.9, 0.9])
Insert cell
import {legend} from "@d3/color-legend"
Insert cell
hexdata = {
const data = sensors.map(d => {
const p = projection([d.Lon, d.Lat]);
if(p) {
p.pm_1 = d.pm_1
p.ID = d.ID
p.Label = d.Label
if(p[0] < 0 || p[0] > width) return
if(p[1] < 0 || p[1] > height) return
return p
}
return p;
}).filter(d => !!d);
return Object.assign(
hexbin(data)
.map(d => (d.pm_1 = d3.median(d, d => d.pm_1), d))
.sort((a, b) => b.length - a.length),
{title: "foo"}
);
}
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
svg.append("path")
.datum(california)
.attr("fill", "none")
.attr("stroke", "#777")
.attr("stroke-width", 0.5)
.attr("stroke-linejoin", "round")
.attr("d", d3.geoPath(projection));

svg.append("g")
.selectAll("path")
.data(hexdata)
.join("path")
.attr("transform", d => `translate(${d.x},${d.y})`)
.attr("d", d => hexbin.hexagon()) // radius(d.length)
.attr("fill", d => colorPM1(d.pm_1))
.attr("opacity", 0.7)
// .attr("stroke", d => d3.lab(color(d.date)).darker())
.append("title")
// .text(d => `${d.length.toLocaleString()} stores
// ${d.date.getFullYear()} median opening`);

return svg.node();
}
Insert cell
hexdata2 = hexdata.map(d => {

let a = d.map(d => Math.floor(d.pm_1))
let med = Math.floor(d3.median(a))
let i = a.indexOf(med)
let o = d[i] || d[0]
let ret = [o[0], o[1]]
ret.sensors = d
ret.x = d.x
ret.y = d.y
ret.pm_1 = o.pm_1
ret.median = med
ret.sensor = o
ret.count = d.length
return ret

})
Insert cell
hexdata2.map(d => d.sensor.ID)
Insert cell
chart2 = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
svg.append("path")
.datum(california)
.attr("fill", "none")
.attr("stroke", "#777")
.attr("stroke-width", 0.5)
.attr("stroke-linejoin", "round")
.attr("d", d3.geoPath(projection));

svg.append("g")
.selectAll("path")
.data(hexdata2)
.join("path")
.attr("transform", d => `translate(${d.x},${d.y})`)
.attr("d", d => hexbin.hexagon()) // radius(d.length)
.attr("fill", d => colorPM1(d.pm_1))
// .attr("stroke", d => d3.lab(color(d.date)).darker())
.append("title")
// .text(d => `${d.length.toLocaleString()} stores
// ${d.date.getFullYear()} median opening`);

return svg.node();
}
Insert cell
// historical = d3.csvParse(await FileAttachment("ids-out@1.csv").text(), d3.autoType)
historical = d3.csvParse(await FileAttachment("ids3-out.csv").text(), d3.autoType)
Insert cell
dayExtent = d3.extent(historical, d => new Date(d.created_at*1000))
Insert cell
// TODO: Generate the days
// have a slider over the days
// lookup the
Insert cell
grouped = d3.group(historical, d => d.id, d => d.type, d => d.created_at)
Insert cell
byhour = d3.group(historical, d => d.created_at, d => d.id, d => d.type)
Insert cell
byidbyhour = d3.group(historical, d => d.id, d => d.created_at, d => d.type)
Insert cell
Array.from(byidbyhour).filter(d => d[1].size > 260)
Insert cell
// time = 1597381200
Insert cell
new Date(time * 1000)
Insert cell
days =
d3.timeHour
.range(new Date(dayExtent[0]), new Date(dayExtent[1]))
.map(d => +d/1000)

Insert cell
viewof time = Scrubber(days, { delay: 150 , format: d => new Date(d*1000)})
Insert cell
Insert cell
// The ids that do not have a value at the current time, so we can choose something else...
hexdata2.filter(d => {
let id = d.sensor.ID
let group = grouped.get(id)
if(!group) return true
let t = group.get("pm1").get(time)
// console.log(t)
if(!t) return true
return false
}).map(d => d.sensor.ID)
Insert cell
hexdata2
Insert cell
// The ids that do not have a value at the current time, so we can choose something else...
hexdata2.map(d => {
let id = d.sensor.ID
let group = grouped.get(id)
let ci = d.sensors.indexOf(d.sensor) // current index
// we want to choose something else
let newsensor = d.sensors[ci -1] || d.sensors[ci + 1] || d.sensors[ci]
if(!group) return newsensor.ID
let t = group.get("pm1").get(time)
// console.log(t)
if(!t) return newsensor.ID
return id
})
// .map(d => d.sensor.ID)
Insert cell
hexbin = d3.hexbin().extent([[0, 0], [width, height]]).radius(10 * height/500)
Insert cell
Insert cell
fullhexbin = FileAttachment("ids-full-hexbins.json").json()
Insert cell
fullhexbinmapped = fullhexbin.map(d => {
let ret = {...d}
// let p = projection([d.lng, d.lat])
// ret.x = p[0]
// ret.y = p[1]
ret.days = new Map(ret.days.map(day => [day.day, day]))
return ret
})
Insert cell
days.indexOf(1597647600)
Insert cell
viewof time2 = Scrubber(days.slice(152), { delay: 150 , format: d => new Date(d*1000)})
Insert cell
Insert cell
viewof chart4 = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
let node = svg.node()
node.value = ""
svg.append("path")
.datum(california)
.attr("fill", "none")
.attr("stroke", "#777")
.attr("stroke-width", 0.5)
.attr("stroke-linejoin", "round")
.attr("d", d3.geoPath(projection));

svg.append("g")
.selectAll("path")
.data(fullhexbinmapped)
.join("path")
.attr("transform", d => `translate(${d.x},${d.y})`)
.attr("d", d => hexbin.hexagon()) // radius(d.length)
.style("fill", d => {
let t = d.days.get(time2)
if(t)
return colorPM1(t.pm1)
return "#aaa"
})
.style("opacity", d => {
let t = d.days.get(time2)
if(t)
return opacityPM1(t.pm1)
return 0.05
})
.on("mouseover", (e,d) => {
let t = d.days.get(time2)
node.value = t
node.dispatchEvent(new CustomEvent("input", { bubbles: true }));
})
// .attr("stroke", d => d3.lab(color(d.date)).darker())
.append("title")
// .text(d => `${d.length.toLocaleString()} stores
// ${d.date.getFullYear()} median opening`);

return node
}
Insert cell
width
Insert cell
// Choose a couple counties that define the extent I care about
countyExtent = ({
type: "FeatureCollection",
// features: countyShapes.features.filter(d => (d.properties.name == "Tehama" || d.properties.name == "San Benito") && d.id.slice(0,2) == "06")
features: countyShapes.features.filter(d => (d.properties.name == "Marin" || d.properties.name == "Alameda") && d.id.slice(0,2) == "06")

})
Insert cell
us = d3.json("https://unpkg.com/us-atlas@3/counties-10m.json")
Insert cell
california = statesByName.get("California")
Insert cell
stateShapes = topojson.feature(us, us.objects.states)//.features
Insert cell
statesByName = new Map(stateShapes.features.map(d => [d.properties.name, d]))
Insert cell
countyShapes = topojson.feature(us, us.objects.counties)//.features
Insert cell
Insert cell
Insert cell
import {Scrubber} from "@mbostock/scrubber"
Insert cell
d3 = require("d3@6.0.0-rc.3", "d3-hexbin@0.2")
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