// this is the file we downloaded above in R. // It'll get embedded in the document.us =awaitFileAttachment('counties-10m.json').json()
// this puts the data frame into a form we can more easily usecounty_data_transposed =transpose(county_data)
// rather than stick the data into the `us` topojson object// we will build a simple mapped object so we can use the // FIPS code as a key to retrieve a value. map_data =newMap(county_data_transposed.map(d => [ d.FIPS, d.value ]));
chart = {const width =960const height =600// this will help make our svg responsiveconst svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0,0, width, height]).attr("style","width: 100%; height: auto; height: intrinsic;")// we will display the value from the data frame we created in the tooltipconst tip =d3tip().attr('class','d3-tip').html((evt, d) => map_data.get(d.id))// enable the tooltips svg.call(tip)// enable the tooltips svg.append("g").attr("class","county").selectAll("path").data(topojson.feature(us, us.objects.counties).features).enter().append("path").style('fill', d =>scale_fill_viridis_cividis(d.val= map_data.get(d.id))).on('mouseover', tip.show).on('mouseout', tip.hide)// .on('click', (evt, d) => console.log(d)) //window.location.href = "https://kagi.com/search?q=" + d.id }) // whatev.attr('stroke','#c3c3c399').attr('stroke-width',0.5).attr('d', alb_path) // projected path// overlay the state layer svg.append("path").datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b)).attr('class','state').attr('fill','none').attr('stroke','#ffffff').attr('stroke-width',2).attr("d", alb_path) // projected path// overlay the hazards layer svg.append("g").selectAll("path").data(alt_us.features).join('path').attr('stroke','#e31a1c').attr('stroke-width',0.5).attr('fill','#252525aa').attr('d', alb_path) // projected pathreturn(svg.node())}
Source Code
---title: "OJS Choropleth With Data From R"params: update: FALSEformat: html: message: false warning: false echo: false code-tools: true---A small Quarto project/document [[GH Repo](https://github.com/hrbrmstr/quarto-r-ojs-choropleth)] that:- (optional) grabs the latest NOAA watches/warnings/hazaards shapefile- displays an AlbersUSA projected map - uses synthesized data from R as the basis for county-level data- fills by county based on ^^ data- overlays the areas with current hazards/warnings/watchesGo to this project directory and do:- `quarto render` or `quarto render -P update:TRUE` if you want a new NOAA shapefile```{r data-create}#| message: false#| warning: falselibrary(tidyverse)if (!file.exists("counties-10m.json")) {download.file(url ="https://cdn.jsdelivr.net/npm/us-atlas@3/counties-10m.json", destfile ="counties-10m.json",quiet =TRUE )}# we pass in a parameter at cmdline render if we want to re-download# and re-process the watches/warnings/hazards. It's usually a big shapefile# so we convert it to GeoJSON and then make it a reasonable size.if ((params$update) || (!(file.exists("current-all.geojson")))) {require(gdalUtilities)require(sf)download.file(url ="https://tgftp.nws.noaa.gov/SL.us008001/DF.sha/DC.cap/DS.WWA/current_all.tar.gz",destfile ="current_all.tar.gz",quiet =TRUE )unlink("current-all.geojson", force =TRUE) gdalUtilities::ogr2ogr(src_datasource_name ="/vsitar/current_all.tar.gz",dst_datasource_name ="current-all.geojson",f ="GeoJSON",t_srs ="crs:84" ) sf::st_read(dsn ="current-all.geojson",quiet =TRUE ) |> rmapshaper::ms_simplify() -> current_allunlink("current-all.geojson", force =TRUE) sf::st_write(obj = current_all, dsn ="current-all.geojson",quiet =TRUE )}# Simulate some county-level data in Rtibble(FIPS =read_lines("https://rud.is/dl/counties.txt"),value =as.integer(runif(length(FIPS), 0, 1000))) -> county_data``````{r data-include}# this will put ^^ into the output document as a variable# you can access in javascript (ojs) blocksojs_define(county_data = county_data)``````{ojs html}// this will let us add our own CSS to the resultant documenthtml`<style>.d3-tip { background-color: white; color: black; border: 1px solid black; padding: 6px; font-size: 10pt; line-height: 0.9; border-radius: 6px; pointer-events: none;}.county {}.state { pointer-events: none;}</style>```````{ojs setup}// you can stick more in one {ojs} block than you can in// a typical ObservableHQ cell. That's nice.d3 =require('d3@7','d3-geo@3')topojson =require('topojson-client@3')d3tip =require('https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.js')``````{ojs}// for our legendary legendimport { Legend } from'@d3/color-legend'``````{ojs}// setup albersUSA projection and the functions we will need to // reproject the unprojected geojson filesprojection = d3.geoAlbersUsa()alb_path = d3.geoPath().projection(projection)alt_us =awaitFileAttachment('current-all.geojson').json()``````{ojs albers-map}// this is the file we downloaded above in R. // It'll get embedded in the document.us =awaitFileAttachment('counties-10m.json').json()``````{ojs transform-data}// this puts the data frame into a form we can more easily usecounty_data_transposed =transpose(county_data)``````{ojs mapify-data}// rather than stick the data into the `us` topojson object// we will build a simple mapped object so we can use the // FIPS code as a key to retrieve a value. map_data =newMap(county_data_transposed.map(d => [ d.FIPS, d.value ]));``````{ojs setup-fill-scale}// we are going to fill the counties with the values we synthesizedvalue_range = d3.extent(county_data_transposed.map(d => d.value))scale_fill_viridis_cividis = d3.scaleSequential(d3.interpolateCividis).domain(value_range)``````{ojs legendary}Legend(d3.scaleSequential(value_range, d3.interpolateCividis), {title:'Some Descriptive Legend Title'})``````{ojs the-choropleth}chart = {const width =960const height =600// this will help make our svg responsiveconst svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0,0, width, height]).attr("style","width: 100%; height: auto; height: intrinsic;")// we will display the value from the data frame we created in the tooltipconst tip =d3tip().attr('class','d3-tip').html((evt, d) => map_data.get(d.id))// enable the tooltips svg.call(tip)// enable the tooltips svg.append("g").attr("class","county").selectAll("path").data(topojson.feature(us, us.objects.counties).features).enter().append("path").style('fill', d =>scale_fill_viridis_cividis(d.val= map_data.get(d.id))).on('mouseover', tip.show).on('mouseout', tip.hide)// .on('click', (evt, d) => console.log(d)) //window.location.href = "https://kagi.com/search?q=" + d.id }) // whatev.attr('stroke','#c3c3c399').attr('stroke-width',0.5).attr('d', alb_path) // projected path// overlay the state layer svg.append("path").datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b)).attr('class','state').attr('fill','none').attr('stroke','#ffffff').attr('stroke-width',2).attr("d", alb_path) // projected path// overlay the hazards layer svg.append("g").selectAll("path").data(alt_us.features).join('path').attr('stroke','#e31a1c').attr('stroke-width',0.5).attr('fill','#252525aa').attr('d', alb_path) // projected pathreturn(svg.node())}```