-
Notifications
You must be signed in to change notification settings - Fork 0
feat(map): color map symbols with the viridis palette #350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jirhiker
wants to merge
3
commits into
staging
Choose a base branch
from
BDMS-349-viridis-map-colors
base: staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // --------------------------------------------------------------------------- | ||
| // Map symbol colors | ||
| // | ||
| // Every point, line, and polygon color on the map comes from the viridis ramp | ||
| // (see ./viridis). Two kinds of styling use it differently: | ||
| // | ||
| // * Classed/continuous layers (TDS, depth to water, water elevation) sample | ||
| // the ramp in order, so darker always means lower and yellow means higher. | ||
| // * Categorical layers (wells, springs, streams, ...) each pin one fixed | ||
| // position on the ramp. Positions are spaced evenly, and the order below | ||
| // interleaves related families — wells next to chemistry next to surface | ||
| // water — so two layers a user is likely to view together land far apart on | ||
| // the ramp and stay easy to tell apart. | ||
| // | ||
| // Note the inherent limit: a sequential ramp can only carry so many | ||
| // categories. With this many layers the gap between adjacent entries is a few | ||
| // percent of the ramp, so switching on every layer at once still produces near | ||
| // neighbors. In normal use only a handful are visible together. | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| import { VIRIDIS_HIGH, VIRIDIS_LOW, viridisColor } from './viridis' | ||
|
|
||
| /** | ||
| * Categorical map layers in ramp order: the first entry gets the dark purple | ||
| * end, the last gets yellow, and the rest are spread evenly in between. | ||
| */ | ||
| const LAYER_RAMP_ORDER = [ | ||
| 'locations', | ||
| 'waterElevationContours', | ||
| 'lakesPondsReservoirs', | ||
| 'waterWells', | ||
| 'waterElevationPoints', | ||
| 'latestDepthToWater', | ||
| 'waterWellSummary', | ||
| 'perennialStreams', | ||
| 'springs', | ||
| 'activelyMonitored', | ||
| 'meteorologicalStations', | ||
| 'latestTds', | ||
| 'majorChemistry', | ||
| 'averageTds', | ||
| 'outfallsReturnFlow', | ||
| 'ephemeralStreams', | ||
| 'rockSampleLocations', | ||
| 'minorChemistry', | ||
| 'depthToWaterTrend', | ||
| 'surfaceWaterDiversions', | ||
| 'projectAreas', | ||
| 'soilGasSampleLocations', | ||
| 'otherThingTypes', | ||
| ] as const | ||
|
|
||
| type MapLayerColorKey = (typeof LAYER_RAMP_ORDER)[number] | ||
|
|
||
| /** Viridis hex color for each categorical map layer. */ | ||
| export const MAP_LAYER_COLORS = Object.fromEntries( | ||
| LAYER_RAMP_ORDER.map((key, index) => [ | ||
| key, | ||
| viridisColor(index / (LAYER_RAMP_ORDER.length - 1)), | ||
| ]) | ||
| ) as Record<MapLayerColorKey, string> | ||
|
|
||
| /** | ||
| * Fallback for a layer with no color assigned. Sits mid-ramp so it reads as | ||
| * part of the same family rather than as an outlier. | ||
| */ | ||
| export const MAP_DEFAULT_LAYER_COLOR = viridisColor(0.5) | ||
|
|
||
| /** | ||
| * Features in a classed layer whose value is missing or unparseable. Kept | ||
| * deliberately outside the viridis ramp so "no data" never looks like a real | ||
| * class on the legend gradient. | ||
| */ | ||
| export const MAP_NO_DATA_COLOR = '#9e9e9e' | ||
|
|
||
| /** White outline that lifts every symbol off the satellite basemap. */ | ||
| export const MAP_SYMBOL_STROKE_COLOR = '#ffffff' | ||
|
|
||
| /** | ||
| * Translucent white disc drawn behind a highlighted symbol. Neutral on | ||
| * purpose — it has to read as a halo, not as another data class. | ||
| */ | ||
| export const MAP_HIGHLIGHT_HALO_COLOR = '#ffffff' | ||
|
|
||
| /** | ||
| * The selected/active symbol. Yellow fill against the dark end of the ramp | ||
| * gives the strongest contrast available inside the palette, so a highlighted | ||
| * point stands out from both the other points and the imagery underneath. | ||
| */ | ||
| export const MAP_HIGHLIGHT_COLOR = VIRIDIS_HIGH | ||
| export const MAP_HIGHLIGHT_STROKE_COLOR = VIRIDIS_LOW |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| // --------------------------------------------------------------------------- | ||
| // Viridis color palette | ||
| // | ||
| // Viridis is a perceptually uniform, colorblind-friendly colormap: equal steps | ||
| // in the data produce equal-looking steps in color, and the ramp reads the same | ||
| // under the common forms of color vision deficiency. It also stays legible on | ||
| // satellite imagery because it runs dark-purple -> teal -> green -> yellow, | ||
| // none of which collide with the browns and grays of aerial photography. | ||
| // | ||
| // Everything here is derived from ten evenly spaced anchors taken from the | ||
| // reference implementation (viridisLite::viridis(10)). Colors between anchors | ||
| // are linearly interpolated, which tracks the full 256-entry reference map | ||
| // closely enough to be visually indistinguishable at map-symbol sizes. | ||
| // | ||
| // Reference: https://sjmgarnier.github.io/viridisLite/reference/viridis.html | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** Evenly spaced samples of the reference viridis ramp, dark end first. */ | ||
| export const VIRIDIS_ANCHORS = [ | ||
| '#440154', | ||
| '#482878', | ||
| '#3e4a89', | ||
| '#31688e', | ||
| '#26828e', | ||
| '#1f9e89', | ||
| '#35b779', | ||
| '#6dcd59', | ||
| '#b4de2c', | ||
| '#fde725', | ||
| ] as const | ||
|
|
||
| const clamp01 = (value: number): number => { | ||
| // NaN has no position on the ramp, so it falls to the low end. The | ||
| // infinities are ordered, and clamp like any other out-of-range value. | ||
| if (Number.isNaN(value)) return 0 | ||
| if (value < 0) return 0 | ||
| if (value > 1) return 1 | ||
| return value | ||
| } | ||
|
|
||
| const hexToRgb = (hex: string): [number, number, number] => [ | ||
| Number.parseInt(hex.slice(1, 3), 16), | ||
| Number.parseInt(hex.slice(3, 5), 16), | ||
| Number.parseInt(hex.slice(5, 7), 16), | ||
| ] | ||
|
|
||
| const channelToHex = (value: number): string => | ||
| Math.round(value).toString(16).padStart(2, '0') | ||
|
|
||
| /** | ||
| * Color at `position` along the viridis ramp, where 0 is the dark purple end | ||
| * and 1 is the bright yellow end. Values outside 0..1 are clamped to the | ||
| * nearer end — including the infinities — and NaN maps to the dark end, so | ||
| * callers never have to sanitize a computed ratio. | ||
| */ | ||
| export const viridisColor = (position: number): string => { | ||
| const scaled = clamp01(position) * (VIRIDIS_ANCHORS.length - 1) | ||
| const lowerIndex = Math.floor(scaled) | ||
| const upperIndex = Math.min(VIRIDIS_ANCHORS.length - 1, lowerIndex + 1) | ||
| const fraction = scaled - lowerIndex | ||
|
|
||
| const [r1, g1, b1] = hexToRgb(VIRIDIS_ANCHORS[lowerIndex]) | ||
| const [r2, g2, b2] = hexToRgb(VIRIDIS_ANCHORS[upperIndex]) | ||
|
|
||
| const r = r1 + (r2 - r1) * fraction | ||
| const g = g1 + (g2 - g1) * fraction | ||
| const b = b1 + (b2 - b1) * fraction | ||
|
|
||
| return `#${channelToHex(r)}${channelToHex(g)}${channelToHex(b)}` | ||
| } | ||
|
|
||
| /** | ||
| * `count` colors spread evenly across the ramp, including both endpoints. | ||
| * Use this for binned/classed styling — e.g. six TDS classes get | ||
| * `viridisSamples(6)`, dark for the lowest class through yellow for the highest. | ||
| */ | ||
| export const viridisSamples = (count: number): string[] => { | ||
| if (count <= 0) return [] | ||
| if (count === 1) return [viridisColor(0.5)] | ||
| return Array.from({ length: count }, (_, index) => | ||
| viridisColor(index / (count - 1)) | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * A CSS `linear-gradient` across the ramp, for legend swatches. More stops | ||
| * means a smoother gradient; ten matches the anchor resolution. | ||
| */ | ||
| export const viridisGradient = (stopCount = 10, angle = '90deg'): string => { | ||
| const samples = viridisSamples(Math.max(2, stopCount)) | ||
| const stops = samples.map( | ||
| (color, index) => | ||
| `${color} ${Math.round((index / (samples.length - 1)) * 100)}%` | ||
| ) | ||
| return `linear-gradient(${angle}, ${stops.join(', ')})` | ||
| } | ||
|
|
||
| /** Dark purple end of the ramp — lowest values. */ | ||
| export const VIRIDIS_LOW = VIRIDIS_ANCHORS[0] | ||
|
|
||
| /** Teal middle of the ramp — mid values, and the neutral in diverging scales. */ | ||
| export const VIRIDIS_MID = viridisColor(0.5) | ||
|
|
||
| /** Bright yellow end of the ramp — highest values. */ | ||
| export const VIRIDIS_HIGH = VIRIDIS_ANCHORS[VIRIDIS_ANCHORS.length - 1] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.