diff --git a/.env.example b/.env.example
index d5b76c1..6b6f0a5 100644
--- a/.env.example
+++ b/.env.example
@@ -4,4 +4,4 @@ NEXT_PUBLIC_RISK_ZARR_URL=
NEXT_PUBLIC_REGIONS_URL=
HERE_API_KEY=
NEXT_PUBLIC_ADVANCED_MODE=
-NEXT_PUBLIC_OCR_ENV=
+NEXT_PUBLIC_GEOPARQUET_URL=
diff --git a/components/results/download.tsx b/components/results/download.tsx
index 6b29d19..6e36aa5 100644
--- a/components/results/download.tsx
+++ b/components/results/download.tsx
@@ -1,14 +1,15 @@
-import { useState } from 'react'
-import { Flex, Spinner } from 'theme-ui'
+import { useEffect, useRef, useState } from 'react'
+import { Box, Flex, Spinner } from 'theme-ui'
//@ts-expect-error - carbonplan components types not available
import { Button } from '@carbonplan/components'
//@ts-expect-error - carbonplan icons types not available
-import { Down } from '@carbonplan/icons'
-import { DATA_URLS, DATA_VERSION } from '@/lib/config'
+import { Down, X } from '@carbonplan/icons'
+import { DATA_VERSION, DATA_URLS, LICENSE_INFO } from '@/lib/config'
import { useStore } from '@/lib/store'
import { getGeographyName, getGeoid } from '@/lib/risk-utils'
import { GeographyKey } from '@/types/location'
import useTracking from '@/hooks/useTracking'
+import { getDuckDB } from '@/lib/duckdb'
export const DownloadButton = ({
label,
@@ -27,30 +28,47 @@ export const DownloadButton = ({
href?: string
showSuffix?: boolean
}) => {
+ const [hovered, setHovered] = useState(false)
+ const [keyboardFocused, setKeyboardFocused] = useState(false)
+ const showCancel = loading && (hovered || keyboardFocused)
+
let suffix
if (showSuffix) {
- suffix =
if (loading) {
- suffix =
+ suffix = showCancel ? :
+ } else {
+ suffix =
}
}
return (
-
+
+
)
}
@@ -60,9 +78,246 @@ const REGION_TYPES: Partial> = {
censusBlock: 'block',
}
+const S3_BUCKET = new URL(DATA_URLS.parquetBase).origin
+
+const DURATION_BUCKETS: [number, string][] = [
+ [1000, '<1s'],
+ [5000, '1-5s'],
+ [15000, '5-15s'],
+ [60000, '15-60s'],
+ [180000, '1-3m'],
+ [300000, '3-5m'],
+]
+const bucketDuration = (ms: number) =>
+ DURATION_BUCKETS.find(([max]) => ms < max)?.[1] ?? '5m+'
+
+// Single source of truth for CSV output columns: [header-name, sql-expression].
+// Score columns are cast to FLOAT because DuckDB-WASM otherwise promotes them
+// to DOUBLE in the COPY pipeline.
+const CSV_COLUMNS: [string, string][] = [
+ ['GEOID', 'GEOID'],
+ ['longitude', 'ROUND(ST_X(ST_Centroid(geometry)), 6)'],
+ ['latitude', 'ROUND(ST_Y(ST_Centroid(geometry)), 6)'],
+ ['rps_2011', 'rps_2011::FLOAT'],
+ ['rps_2047', 'rps_2047::FLOAT'],
+ ['bp_2011', 'bp_2011::FLOAT'],
+ ['bp_2047', 'bp_2047::FLOAT'],
+ ['crps_scott', 'crps_scott::FLOAT'],
+ ['bp_2011_riley', 'bp_2011_riley::FLOAT'],
+ ['bp_2047_riley', 'bp_2047_riley::FLOAT'],
+]
+const CSV_SELECT = CSV_COLUMNS.map(([name, expr]) => `${expr} AS ${name}`).join(
+ ', ',
+)
+const CSV_HEADER = CSV_COLUMNS.map(([name]) => name).join(',') + '\n'
+
+function trimGeoid(geoid: string, regionType: string): string {
+ if (regionType === 'county') return geoid.slice(0, 5)
+ if (regionType === 'tract') return geoid.slice(0, 11)
+ return geoid
+}
+
+async function getPartitionUrls(
+ geoid: string,
+ signal?: AbortSignal,
+): Promise {
+ const stateFips = geoid.slice(0, 2)
+ const countyFips = geoid.slice(2, 5)
+ const prefix = DATA_URLS.parquetBase.replace(S3_BUCKET + '/', '')
+ const partitionPrefix = `${prefix}/state_fips=${stateFips}/county_fips=${countyFips}/`
+
+ const res = await fetch(
+ `${S3_BUCKET}/?list-type=2&prefix=${partitionPrefix}`,
+ { signal },
+ )
+ const doc = new DOMParser().parseFromString(
+ await res.text(),
+ 'application/xml',
+ )
+
+ const urls: string[] = []
+ for (const el of doc.querySelectorAll('Contents > Key')) {
+ const key = el.textContent
+ if (key?.endsWith('.parquet')) urls.push(`${S3_BUCKET}/${key}`)
+ }
+
+ if (urls.length === 0) {
+ throw new Error(
+ `No data found for state=${stateFips}, county=${countyFips}`,
+ )
+ }
+ return urls
+}
+
+function triggerBlobDownload(blob: Blob, filename: string) {
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+}
+
+// One COPY per partition file, pulled out of the VFS between iterations —
+// keeps DuckDB's working set bounded by a single partition so very large
+// counties (e.g. LA) don't blow the WASM heap.
+async function copyParquetTo(
+ geoid: string,
+ regionType: string,
+ select: string,
+ formatClause: string,
+ signal?: AbortSignal,
+): Promise[]> {
+ const [db, partitionUrls] = await Promise.all([
+ getDuckDB(),
+ getPartitionUrls(geoid, signal),
+ ])
+ signal?.throwIfAborted()
+ const conn = await db.connect()
+ const trimmedGeoid = trimGeoid(geoid, regionType)
+ // Per-call UUID so concurrent downloads (e.g. CSV + GeoJSON at once) can't
+ // collide on VFS paths.
+ const runId = crypto.randomUUID()
+ // Best-effort interrupt the in-flight query when abort fires.
+ const onAbort = () => {
+ conn.cancelSent().catch(() => {})
+ }
+ signal?.addEventListener('abort', onAbort)
+
+ try {
+ const chunks: Uint8Array[] = []
+ for (let i = 0; i < partitionUrls.length; i++) {
+ signal?.throwIfAborted()
+ const outPath = `/tmp/dl-${runId}-${i}.out`
+ try {
+ await conn.query(`
+ COPY (
+ SELECT ${select}
+ FROM read_parquet('${partitionUrls[i]}')
+ WHERE GEOID LIKE '${trimmedGeoid}%'
+ ) TO '${outPath}' (${formatClause})
+ `)
+ // If abort fired during the query, surface as AbortError rather than
+ // whatever DuckDB threw from cancelSent.
+ signal?.throwIfAborted()
+ const chunk = await db.copyFileToBuffer(outPath)
+ if (chunk.length > 0) {
+ chunks.push(chunk as Uint8Array)
+ }
+ } finally {
+ try {
+ await db.dropFile(outPath)
+ } catch {
+ // ignore cleanup errors
+ }
+ }
+ }
+ if (chunks.length === 0) {
+ throw new Error(`No building data found for GEOID: ${trimmedGeoid}`)
+ }
+ return chunks
+ } finally {
+ signal?.removeEventListener('abort', onAbort)
+ await conn.close()
+ }
+}
+
+async function downloadCSV(
+ geoid: string,
+ regionType: string,
+ filename: string,
+ signal?: AbortSignal,
+) {
+ const chunks = await copyParquetTo(
+ geoid,
+ regionType,
+ CSV_SELECT,
+ 'FORMAT CSV, HEADER false',
+ signal,
+ )
+
+ const metadata = `# OCR Dataset Version: ${DATA_VERSION}
+# Provider: ${LICENSE_INFO.provider}
+# License: ${LICENSE_INFO.licenseName} (${LICENSE_INFO.licenseUrl})
+# Terms of Access: ${LICENSE_INFO.termsOfAccess}
+# Data Sources: ${LICENSE_INFO.dataSources}
+# Notice: ${LICENSE_INFO.notice}
+# ------------------------------------------
+`
+ triggerBlobDownload(
+ new Blob([metadata, CSV_HEADER, ...chunks], { type: 'text/csv' }),
+ `${filename}.csv`,
+ )
+}
+
+async function downloadGeoJSON(
+ geoid: string,
+ regionType: string,
+ filename: string,
+ signal?: AbortSignal,
+) {
+ // Build each GeoJSON Feature entirely in SQL via ST_AsGeoJSON + to_json,
+ // then COPY TO streams them to the VFS — no JS JSON parsing needed.
+ const chunks = await copyParquetTo(
+ geoid,
+ regionType,
+ // Build properties manually so FLOAT::VARCHAR gives the same precision
+ // as the CSV output. to_json would promote FLOATs to DOUBLE precision.
+ // COALESCE to 'null' so a NULL column doesn't nullify the whole feature.
+ `'{"type":"Feature","geometry":' || ST_AsGeoJSON(ST_ReducePrecision(ST_Centroid(geometry), 0.000001))
+ || ',"properties":{"GEOID":"' || GEOID
+ || '","rps_2011":' || COALESCE(rps_2011::FLOAT::VARCHAR, 'null')
+ || ',"rps_2047":' || COALESCE(rps_2047::FLOAT::VARCHAR, 'null')
+ || ',"bp_2011":' || COALESCE(bp_2011::FLOAT::VARCHAR, 'null')
+ || ',"bp_2047":' || COALESCE(bp_2047::FLOAT::VARCHAR, 'null')
+ || ',"crps_scott":' || COALESCE(crps_scott::FLOAT::VARCHAR, 'null')
+ || ',"bp_2011_riley":' || COALESCE(bp_2011_riley::FLOAT::VARCHAR, 'null')
+ || ',"bp_2047_riley":' || COALESCE(bp_2047_riley::FLOAT::VARCHAR, 'null')
+ || '}},' AS feature`,
+ `FORMAT CSV, HEADER false, QUOTE E'\\x01', DELIMITER E'\\x02'`,
+ signal,
+ )
+
+ // Each chunk is one Feature JSON per line, each with trailing comma.
+ // Strip the last comma from the final chunk and wrap with FeatureCollection.
+ const last = chunks[chunks.length - 1]
+ let end = last.length - 1
+ while (end > 0 && last[end] !== 44) end-- // find last comma (0x2C)
+ // subarray is a view (slice would copy). Cast narrows ArrayBufferLike →
+ // ArrayBuffer so BlobPart accepts it.
+ chunks[chunks.length - 1] = last.subarray(0, end) as Uint8Array
+
+ const metadata = JSON.stringify({
+ dataset_version: DATA_VERSION,
+ provider: LICENSE_INFO.provider,
+ license: `${LICENSE_INFO.licenseName} (${LICENSE_INFO.licenseUrl})`,
+ terms_of_access: LICENSE_INFO.termsOfAccess,
+ data_sources: LICENSE_INFO.dataSources,
+ notice: LICENSE_INFO.notice,
+ })
+
+ triggerBlobDownload(
+ new Blob(
+ [
+ `{"type":"FeatureCollection","metadata":${metadata},"features":[\n`,
+ ...chunks,
+ '\n]}',
+ ],
+ { type: 'application/geo+json' },
+ ),
+ `${filename}.geojson`,
+ )
+}
+
export const Download = () => {
const track = useTracking()
- const [loading, setLoading] = useState({ csv: false, gpkg: false })
+ const [loading, setLoading] = useState({ csv: false, geojson: false })
+ const abortRefs = useRef>({
+ csv: null,
+ geojson: null,
+ })
const selectedGeographyLevel = useStore(
(state) => state.selectedGeographyLevel,
)
@@ -76,6 +331,16 @@ export const Download = () => {
const isDownloadableLevel =
selectedGeographyLevel !== 'state' && selectedGeographyLevel !== 'nation'
const disabled = !activeGeographies[selectedGeographyLevel]
+
+ useEffect(() => {
+ return () => {
+ abortRefs.current.csv?.abort()
+ abortRefs.current.geojson?.abort()
+ abortRefs.current.csv = null
+ abortRefs.current.geojson = null
+ setLoading({ csv: false, geojson: false })
+ }
+ }, [geoid, selectedGeographyLevel])
let filename: string
if (selectedGeographyLevel === 'county') {
filename = `${countyName?.replaceAll(' ', '-')}-County-${geoid}`
@@ -85,46 +350,59 @@ export const Download = () => {
filename = `Census-Block-${geoid}`
}
- const handleClick = async (format: 'csv' | 'gpkg') => {
+ const handleClick = async (format: 'csv' | 'geojson') => {
+ if (loading[format]) {
+ abortRefs.current[format]?.abort()
+ setLoading((prev) => ({ ...prev, [format]: false }))
+ return
+ }
+ const controller = new AbortController()
+ abortRefs.current[format] = controller
+
setLoading((prev) => ({ ...prev, [format]: true }))
+ const startTime = performance.now()
try {
track('data_download', {
geography: selectedGeographyLevel,
geoid: geoid ?? '',
})
- const res = await fetch(DATA_URLS.downloads, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- environment: process.env.NEXT_PUBLIC_OCR_ENV ?? 'production',
- dataset_version: DATA_VERSION,
- data_format: format,
- geoid: geoid,
- region_type: REGION_TYPES[selectedGeographyLevel],
- file_name: `${filename}.${format}`,
- }),
- })
- const payload = await res.json()
- if (!payload?.url) {
- throw Error('Error generating download')
- }
- const a = document.createElement('a')
- a.href = payload.url
- a.download = `${filename}.${format}` // explicitly trigger download and suggest filename
- document.body.appendChild(a)
- a.click()
- document.body.removeChild(a)
+ if (!geoid) throw new Error('No geography selected')
- setLoading((prev) => ({ ...prev, [format]: false }))
+ const regionType = REGION_TYPES[selectedGeographyLevel]
+ if (!regionType) throw new Error('Invalid geography level')
+
+ if (format === 'csv') {
+ await downloadCSV(geoid, regionType, filename, controller.signal)
+ } else {
+ await downloadGeoJSON(geoid, regionType, filename, controller.signal)
+ }
} catch (error) {
- if (!(error instanceof DOMException && error.name === 'AbortError')) {
+ const isAbort =
+ controller.signal.aborted ||
+ (error instanceof DOMException && error.name === 'AbortError')
+ if (isAbort) {
+ track('data_download_cancel', {
+ geography: selectedGeographyLevel,
+ geoid: geoid ?? '',
+ format,
+ duration_bucket: bucketDuration(performance.now() - startTime),
+ })
+ } else {
track('data_download_error', {
geography: selectedGeographyLevel,
geoid: geoid ?? '',
})
+ console.error('Download failed:', error)
+ }
+ } finally {
+ // A newer run for the same format may have replaced this controller —
+ // only clear loading/ref when we're still the active run, otherwise the
+ // retry gets its state wiped out mid-flight.
+ if (abortRefs.current[format] === controller) {
+ abortRefs.current[format] = null
+ setLoading((prev) => ({ ...prev, [format]: false }))
}
- setLoading((prev) => ({ ...prev, [format]: false }))
}
}
@@ -150,14 +428,22 @@ export const Download = () => {
loading={loading.csv}
disabled={disabled}
onClick={() => handleClick('csv')}
- ariaLabel={`Download ${disabled ? 'regional' : selectedGeographyLevel} data as CSV`}
+ ariaLabel={
+ loading.csv
+ ? 'Cancel CSV download'
+ : `Download ${disabled ? 'regional' : selectedGeographyLevel} data as CSV`
+ }
/>
handleClick('gpkg')}
- ariaLabel={`Download ${disabled ? 'regional' : selectedGeographyLevel} data as GeoPackage`}
+ onClick={() => handleClick('geojson')}
+ ariaLabel={
+ loading.geojson
+ ? 'Cancel GeoJSON download'
+ : `Download ${disabled ? 'regional' : selectedGeographyLevel} data as GeoJSON`
+ }
/>
)
diff --git a/lib/config.ts b/lib/config.ts
index c10e5be..9dd03ab 100644
--- a/lib/config.ts
+++ b/lib/config.ts
@@ -17,7 +17,21 @@ export const DATA_URLS = {
raster:
process.env.NEXT_PUBLIC_RISK_ZARR_URL ??
`https://carbonplan-ocr.s3.amazonaws.com/output/fire-risk/pyramid/production/${DATA_VERSION}/pyramid.zarr`,
- downloads: `https://wywisai6r4dyxoib6aq2j2ewiy0sdsdg.lambda-url.us-west-2.on.aws/export`,
+ parquetBase:
+ process.env.NEXT_PUBLIC_GEOPARQUET_URL ??
+ `https://carbonplan-ocr.s3.us-west-2.amazonaws.com/output/fire-risk/vector/production/${DATA_VERSION}/geoparquet/buildings.parquet`,
+}
+
+export const LICENSE_INFO = {
+ provider: 'CarbonPlan',
+ termsOfAccess:
+ 'https://docs.carbonplan.org/ocr/en/latest/terms-of-data-access.html',
+ dataSources:
+ 'https://docs.carbonplan.org/ocr/en/latest/reference/data-sources.html',
+ licenseName: 'ODBL',
+ licenseUrl: 'https://opendatacommons.org/licenses/odbl/',
+ notice:
+ 'Contains information from the Overture Maps Foundation database, which is made available here under the Open Database License (ODbL), a copy of which is available at https://opendatacommons.org/licenses/odbl/1-0/.',
}
export const LAYERS = {
diff --git a/lib/duckdb.ts b/lib/duckdb.ts
new file mode 100644
index 0000000..a25a43d
--- /dev/null
+++ b/lib/duckdb.ts
@@ -0,0 +1,40 @@
+import type { AsyncDuckDB } from '@duckdb/duckdb-wasm'
+
+let db: AsyncDuckDB | null = null
+let initPromise: Promise | null = null
+
+export async function getDuckDB(): Promise {
+ if (db) return db
+ if (initPromise) return initPromise
+
+ initPromise = (async () => {
+ const duckdb = await import('@duckdb/duckdb-wasm')
+ const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles()
+ const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES)
+
+ const workerUrl = URL.createObjectURL(
+ new Blob([`importScripts("${bundle.mainWorker!}");`], {
+ type: 'text/javascript',
+ }),
+ )
+
+ const worker = new Worker(workerUrl)
+ const instance = new duckdb.AsyncDuckDB(new duckdb.VoidLogger(), worker)
+ await instance.instantiate(bundle.mainModule, bundle.pthreadWorker)
+ URL.revokeObjectURL(workerUrl)
+
+ const conn = await instance.connect()
+ await conn.query('SET builtin_httpfs = false;')
+ await conn.query('INSTALL httpfs; LOAD httpfs;')
+ await conn.query('INSTALL spatial; LOAD spatial;')
+ await conn.close()
+
+ db = instance
+ return instance
+ })().catch((err) => {
+ initPromise = null
+ throw err
+ })
+
+ return initPromise
+}
diff --git a/package-lock.json b/package-lock.json
index d9c15fe..ed48454 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@carbonplan/prettier": "^1.2.0",
"@carbonplan/theme": "^8.1.0",
"@carbonplan/zarr-layer": "^0.3.1",
+ "@duckdb/duckdb-wasm": "^1.32.0",
"@emotion/react": "^11.14.0",
"@protomaps/basemaps": "^5.0.1",
"@theme-ui/color": "^0.15.7",
@@ -323,6 +324,15 @@
"integrity": "sha512-ARk8IM6VPzL3ltAZNenZCAm9NkzqOnrxjEvf8m2t91bmkmloDPBkRCMxMzKrMJE79/XxcZyXDDvLOI1UKlYSBQ==",
"license": "MIT"
},
+ "node_modules/@duckdb/duckdb-wasm": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/@duckdb/duckdb-wasm/-/duckdb-wasm-1.32.0.tgz",
+ "integrity": "sha512-IewXTNYEjsZCPE9weUWgtjGxUlMRo7qhX0GF6tq/KjK8bnY+RAl4cyUdYUfcdzbyb4b9ZxPC+FOsCcxgaKFWMg==",
+ "license": "MIT",
+ "dependencies": {
+ "apache-arrow": "^17.0.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@@ -3697,6 +3707,18 @@
"integrity": "sha512-SFCr4edNkZ1bGaLzGz7rgR1bRzVX4MmMxwsIa3/Bh6ose8v+hRpneoizHv0KChdjxaXyjRtaMq7sCuZSzPomQA==",
"license": "MIT"
},
+ "node_modules/@types/command-line-args": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz",
+ "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/command-line-usage": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz",
+ "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==",
+ "license": "MIT"
+ },
"node_modules/@types/d3-format": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
@@ -3736,7 +3758,6 @@
"version": "20.17.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
"integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
@@ -4242,7 +4263,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -4254,6 +4274,35 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/apache-arrow": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-17.0.0.tgz",
+ "integrity": "sha512-X0p7auzdnGuhYMVKYINdQssS4EcKec9TCXyez/qtJt32DrIMGbzqiaMiQ0X6fQlQpw8Fl0Qygcv4dfRAr5Gu9Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.11",
+ "@types/command-line-args": "^5.2.3",
+ "@types/command-line-usage": "^5.0.4",
+ "@types/node": "^20.13.0",
+ "command-line-args": "^5.2.1",
+ "command-line-usage": "^7.0.1",
+ "flatbuffers": "^24.3.25",
+ "json-bignum": "^0.0.3",
+ "tslib": "^2.6.2"
+ },
+ "bin": {
+ "arrow2csv": "bin/arrow2csv.cjs"
+ }
+ },
+ "node_modules/apache-arrow/node_modules/@swc/helpers": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz",
+ "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -4283,6 +4332,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/array-back": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz",
+ "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/array-buffer-byte-length": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
@@ -4643,9 +4701,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -4657,6 +4713,21 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chalk-template": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz",
+ "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk-template?sponsor=1"
+ }
+ },
"node_modules/chroma-js": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.1.2.tgz",
@@ -4673,7 +4744,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -4686,9 +4756,56 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/command-line-args": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz",
+ "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==",
+ "license": "MIT",
+ "dependencies": {
+ "array-back": "^3.1.0",
+ "find-replace": "^3.0.0",
+ "lodash.camelcase": "^4.3.0",
+ "typical": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/command-line-usage": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.4.tgz",
+ "integrity": "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==",
+ "license": "MIT",
+ "dependencies": {
+ "array-back": "^6.2.2",
+ "chalk-template": "^0.4.0",
+ "table-layout": "^4.1.1",
+ "typical": "^7.3.0"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/command-line-usage/node_modules/array-back": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz",
+ "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.17"
+ }
+ },
+ "node_modules/command-line-usage/node_modules/typical": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz",
+ "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.17"
+ }
+ },
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -5893,6 +6010,18 @@
"node": ">=8"
}
},
+ "node_modules/find-replace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz",
+ "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "array-back": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
@@ -5933,6 +6062,12 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/flatbuffers": {
+ "version": "24.12.23",
+ "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz",
+ "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==",
+ "license": "Apache-2.0"
+ },
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
@@ -6289,9 +6424,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -6972,6 +7105,14 @@
"node": ">=6"
}
},
+ "node_modules/json-bignum": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz",
+ "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -7120,6 +7261,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -8959,9 +9106,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -8996,6 +9141,28 @@
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==",
"license": "ISC"
},
+ "node_modules/table-layout": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz",
+ "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==",
+ "license": "MIT",
+ "dependencies": {
+ "array-back": "^6.2.2",
+ "wordwrapjs": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=12.17"
+ }
+ },
+ "node_modules/table-layout/node_modules/array-back": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz",
+ "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.17"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -9263,6 +9430,15 @@
"node": ">=14.17"
}
},
+ "node_modules/typical": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz",
+ "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -9286,7 +9462,6 @@
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
- "dev": true,
"license": "MIT"
},
"node_modules/unzipit": {
@@ -9542,6 +9717,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wordwrapjs": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz",
+ "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.17"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
diff --git a/package.json b/package.json
index 92ff9a9..81fca26 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@carbonplan/prettier": "^1.2.0",
"@carbonplan/theme": "^8.1.0",
"@carbonplan/zarr-layer": "^0.3.1",
+ "@duckdb/duckdb-wasm": "^1.32.0",
"@emotion/react": "^11.14.0",
"@protomaps/basemaps": "^5.0.1",
"@theme-ui/color": "^0.15.7",