Skip to content
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

Refactor D3 Map to Support Both Rate Charts and Unknowns Charts #3949

Merged
merged 52 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
efa9cfa
initial commit
Jan 13, 2025
6913106
completed d3 map
Jan 22, 2025
7a859ea
Merge branch 'main' into migrate-unknown-map-to-d3
eriwarr Jan 22, 2025
c27398e
lint fixes
Jan 22, 2025
5ff5c8b
install npm package
Jan 22, 2025
b576a2e
updating typing
Jan 22, 2025
282add6
Merge branch 'main' into migrate-unknown-map-to-d3
eriwarr Jan 22, 2025
72d680f
Merge branch 'main' into migrate-unknown-map-to-d3
benhammondmusic Jan 22, 2025
685e5df
fixed map turning black
Jan 22, 2025
f373f6d
updating rendering for legend
Jan 22, 2025
088045f
making suggested changes
Jan 22, 2025
e9d2042
minor adjustments to map
Jan 23, 2025
b599d1f
Merge branch 'main' into migrate-unknown-map-to-d3
eriwarr Jan 23, 2025
de371bf
removing consoles
Jan 23, 2025
2fce47f
making ChoroplethMap responsive by adjusting margins and transforms f…
Jan 23, 2025
bbf1e4b
Make legend tick density responsive to screen size and add support fo…
Jan 23, 2025
d6b3548
adjusting legend scaling
Jan 23, 2025
c92944b
Merge branch 'main' into migrate-unknown-map-to-d3
eriwarr Jan 23, 2025
30ee896
using het color
Jan 23, 2025
9c6c51c
Merge branch 'main' into migrate-unknown-map-to-d3
benhammondmusic Jan 23, 2025
6976921
Frontend: Adds chr gun deaths behind flag (#3914)
benhammondmusic Jan 24, 2025
c6c6ff2
Frontend: cleanup (#3928)
benhammondmusic Jan 24, 2025
c4af332
Fix e2e: skip flaky link (#3932)
benhammondmusic Jan 27, 2025
8801ea5
updated tooltip and working map
Feb 4, 2025
fecb45d
adding logic for extremes mode
Feb 4, 2025
5a50a25
updating color scheme logic
Feb 6, 2025
3ee3d6b
updating color scheme logic
Feb 6, 2025
e6ca9ff
removing changes from other branch
Feb 6, 2025
f57b923
removing changes from other branch
Feb 6, 2025
b7a31c6
removing changes from other branch
Feb 6, 2025
cf7c454
updated typing
Feb 6, 2025
5ac4c6b
Merge branch 'main' into migrate-rate-map
eriwarr Feb 6, 2025
ad164aa
npx cleanup
Feb 6, 2025
3982336
made tooltip content into a react compoent
Feb 6, 2025
f9a44cf
deleting old tooltip file
Feb 6, 2025
5def2f4
npx cleanup
Feb 6, 2025
52189f3
Merge branch 'main' into migrate-rate-map
benhammondmusic Feb 7, 2025
ad70bfa
fixing failing tests
Feb 7, 2025
2239526
color scale working
Feb 10, 2025
a30dc49
working map
Feb 11, 2025
36f333f
Merge branch 'main' into migrate-rate-map
eriwarr Feb 11, 2025
e5401bf
fixing errors
Feb 11, 2025
7de65af
fixing console error
Feb 11, 2025
8cc3c46
fixed back button issue
Feb 11, 2025
61dceec
removing uneeded check
Feb 11, 2025
fe12f0f
small refactor
Feb 12, 2025
44bc8fb
Merge branch 'main' into migrate-rate-map
eriwarr Feb 12, 2025
697b40e
Merge branch 'main' into migrate-rate-map
eriwarr Feb 12, 2025
a68149d
fixed formatting issue
Feb 12, 2025
8740045
Merge branch 'main' into migrate-rate-map
eriwarr Feb 12, 2025
049d934
updated tests to work for new map
Feb 12, 2025
367d1b9
Merge branch 'main' into migrate-rate-map
eriwarr Feb 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions frontend/src/cards/MapCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { GridView } from '@mui/icons-material'
import { useMemo, useState } from 'react'
import { useLocation } from 'react-router-dom'
import ChoroplethMap from '../charts/ChoroplethMap'
import { Legend } from '../charts/Legend'
import ChoroplethMap from '../charts/choroplethMap/index'
import { type CountColsMap, RATE_MAP_SCALE } from '../charts/mapGlobals'
import { getHighestLowestGroupsByFips } from '../charts/mapHelperFunctions'
import { generateChartTitle, generateSubtitle } from '../charts/utils'
Expand Down Expand Up @@ -553,8 +553,8 @@ function MapCardWithKey(props: MapCardProps) {
}
signalListeners={signalListeners}
mapConfig={mapConfig}
scaleConfig={scale}
isPhrmaAdherence={isPhrmaAdherence}
updateFipsCallback={props.updateFipsCallback}
/>
</div>

Expand Down
7 changes: 5 additions & 2 deletions frontend/src/cards/UnknownsMapCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { useLocation } from 'react-router-dom'
import type { Topology } from 'topojson-specification'
import ChoroplethMap from '../charts/choroplethMap/index'
import type { DataPoint } from '../charts/choroplethMap/types'
import { MAP_SCHEMES, type MapConfig } from '../charts/choroplethMap/types'
import { MAP_SCHEMES } from '../charts/mapGlobals'
import { generateChartTitle, generateSubtitle } from '../charts/utils'
import type { DataTypeConfig } from '../data/config/MetricConfigTypes'
import type {
DataTypeConfig,
MapConfig,
} from '../data/config/MetricConfigTypes'
import {
Breakdowns,
DEMOGRAPHIC_DISPLAY_TYPES_LOWER_CASE,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/charts/ChoroplethMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export default function ChoroplethMap(props: ChoroplethMapProps) {
? UNKNOWNS_MAP_SCALE
: RATE_MAP_SCALE,
/* fieldRange? */ props.fieldRange,
/* scaleColorScheme? */ props.mapConfig.scheme,
/* scaleColorScheme? */ props.mapConfig.scheme as string,
/* isTerritoryCircle? */ props.fips.isTerritory(),
/* reverse? */ !props.mapConfig.higherIsBetter && !props.isUnknownsMap,
)
Expand Down
62 changes: 62 additions & 0 deletions frontend/src/charts/choroplethMap/TooltipContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type React from 'react'
import type { MetricConfig } from '../../data/config/MetricConfigTypes'
import { het } from '../../styles/DesignTokens'

import { formatMetricValue } from './mapHelpers'
import type { TooltipFeature } from './types'

const { borderColor } = het

interface TooltipContentProps {
feature: TooltipFeature
dataMap: Map<string, any>
metricConfig: MetricConfig
geographyType?: string
}

const TooltipContent: React.FC<TooltipContentProps> = ({
feature,
dataMap,
metricConfig,
geographyType = '',
}) => {
const name = feature.properties?.name || String(feature.id)
const data = dataMap.get(feature.id as string)

if (!data) {
return (
<div>
<strong>
{name} {geographyType}
</strong>
<br />
No data available
</div>
)
}

const entries = Object.entries(data).filter(([key]) => key !== 'value')
const [firstLabel, firstValue] = entries[0] ?? ['', 0]
const remainingEntries = entries.slice(1)

return (
<div>
<strong>
{name} {geographyType}
</strong>
<div style={{ textAlign: 'center' }}>
<div style={{ marginBottom: 4 }}>
<span style={{ color: borderColor }}>{firstLabel}:</span>{' '}
{formatMetricValue(firstValue as number, metricConfig)}
</div>
{remainingEntries.map(([label, value]) => (
<div key={label} style={{ marginBottom: 4 }}>
<span style={{ color: borderColor }}>{label}:</span> {String(value)}
</div>
))}
</div>
</div>
)
}

export default TooltipContent
47 changes: 47 additions & 0 deletions frontend/src/charts/choroplethMap/colorSchemes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as d3 from 'd3'
import type { ColorScheme } from 'vega'
import { het } from '../../styles/DesignTokens'

export const interpolateDarkGreen = d3.piecewise(d3.interpolateRgb.gamma(2.2), [
het.mapDarkest,
het.mapDarker,
het.mapDark,
het.mapLight,
het.mapLighter,
het.mapLightest,
het.mapLightZero,
])

export const interpolatePlasma = d3.piecewise(d3.interpolateRgb.gamma(2.2), [
het.mapWomenDarkZero,
het.mapWomenDarkest,
het.mapWomenDarker,
het.mapWomenDark,
het.mapWomenLight,
het.mapWomenLighter,
het.mapWomenLightest,
het.mapWomenLightZero,
])

export const interpolatedarkRed = d3.piecewise(d3.interpolateRgb.gamma(2.2), [
het.mapYouthDarkZero,
het.mapYouthDarkest,
het.mapYouthDarker,
het.mapYouthDark,
het.mapYouthMid,
het.mapYouthLight,
het.mapYouthLighter,
het.mapYouthLightest,
het.mapYouthLightZero,
])

export const D3_MAP_SCHEMES: Partial<
Record<ColorScheme, (t: number) => string>
> = {
darkgreen: interpolateDarkGreen,
plasma: interpolatePlasma,
inferno: d3.interpolateInferno,
viridis: d3.interpolateViridis,
greenblue: d3.interpolateGnBu,
darkred: interpolatedarkRed,
}
148 changes: 84 additions & 64 deletions frontend/src/charts/choroplethMap/index.tsx
Original file line number Diff line number Diff line change
@@ -1,120 +1,140 @@
import { useEffect, useRef } from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { scaleType } from 'vega-lite/build/src/compile/scale/type'
import { CAWP_METRICS } from '../../data/providers/CawpProvider'
import { PHRMA_METRICS } from '../../data/providers/PhrmaProvider'
import { useIsBreakpointAndUp } from '../../utils/hooks/useIsBreakpointAndUp'
import { useResponsiveWidth } from '../../utils/hooks/useResponsiveWidth'
import { INVISIBLE_PRELOAD_WIDTH } from '../mapGlobals'
import { embedHighestLowestGroups } from '../mapHelperFunctions'
import { HEIGHT_WIDTH_RATIO } from '../utils'
import {
createColorScale,
createFeatures,
createProjection,
processPhrmaData,
} from './mapHelpers'
import { renderMap } from './renderMap'
import {
createTooltipContainer,
createTooltipLabel,
getTooltipPairs,
} from './tooltipUtils'
import type { ChoroplethMapProps } from './types'
import { createTooltipContainer } from './tooltipUtils'
import type { ChoroplethMapProps, DataPoint } from './types'

const ChoroplethMap = (props: ChoroplethMapProps) => {
const {
data,
metric,
isUnknownsMap,
activeDemographicGroup,
demographicType,
fips,
geoData,
showCounties,
overrideShapeWithCircle,
updateFipsCallback,
mapConfig,
} = props
const { data, metric, highestLowestGroupsByFips } = props
const isMobile = !useIsBreakpointAndUp('md')
const [ref, width] = useResponsiveWidth()
const svgRef = useRef<SVGSVGElement | null>(null)
const isMobile = !useIsBreakpointAndUp('md')

const heightWidthRatio = overrideShapeWithCircle
const tooltipContainerRef = useRef<ReturnType<
typeof createTooltipContainer
> | null>(null)

const isCawp = CAWP_METRICS.includes(metric.metricId)
const isPhrma = PHRMA_METRICS.includes(metric.metricId)

const suppressedData = useMemo(
() => (isPhrma ? processPhrmaData(data, props.countColsMap) : data),
[data, isPhrma, props.countColsMap],
)

const dataWithHighestLowest: DataPoint[] = useMemo(
() =>
!props.isUnknownsMap && !props.isMulti
? embedHighestLowestGroups(suppressedData, highestLowestGroupsByFips)
: suppressedData,
[
suppressedData,
highestLowestGroupsByFips,
props.isUnknownsMap,
props.isMulti,
],
)

const heightWidthRatio = props.overrideShapeWithCircle
? HEIGHT_WIDTH_RATIO * 2
: HEIGHT_WIDTH_RATIO

const height = width * heightWidthRatio

const tooltipContainer = createTooltipContainer()

useEffect(() => {
if (!data.length || !svgRef.current || !width) return

if (!tooltipContainerRef.current) {
tooltipContainerRef.current = createTooltipContainer()
}

const initializeMap = async () => {
const colorScale = createColorScale({
data,
dataWithHighestLowest,
metricId: metric.metricId,
scaleType: 'sequentialSymlog',
colorScheme: mapConfig.scheme,
})

const features = await createFeatures({
showCounties,
parentFips: fips.code,
geoData,
colorScheme: props.mapConfig.scheme,
isUnknown: props.isUnknownsMap,
fips: props.fips,
reverse: !props.mapConfig.higherIsBetter && !props.isUnknownsMap,
})

const projection = createProjection({ fips, width, height, features })

const tooltipLabel = createTooltipLabel(
metric,
activeDemographicGroup,
demographicType,
isUnknownsMap,
const features = await createFeatures(
props.showCounties,
props.fips.code,
props.geoData,
)

const tooltipPairs = getTooltipPairs(tooltipLabel)
const projection = createProjection(props.fips, width, height, features)

renderMap({
svgRef,
geoData: { features, projection },
data,
dataWithHighestLowest,
metric,
width,
height,
tooltipContainer,
tooltipPairs,
tooltipLabel,
showCounties,
updateFipsCallback,
mapConfig,
tooltipContainer: tooltipContainerRef.current!,
showCounties: props.showCounties,
updateFipsCallback: props.updateFipsCallback,
colorScale,
fips,
fips: props.fips,
isMobile,
activeDemographicGroup: props.activeDemographicGroup,
demographicType: props.demographicType,
isCawp,
countColsMap: props.countColsMap,
isUnknownsMap: props.isUnknownsMap,
extremesMode: props.extremesMode,
})
}

initializeMap()

return () => {
tooltipContainer.remove()
if (tooltipContainerRef.current) {
tooltipContainerRef.current.remove()
tooltipContainerRef.current = null
}
}
}, [
data,
geoData,
props.geoData,
width,
height,
showCounties,
overrideShapeWithCircle,
props.showCounties,
props.overrideShapeWithCircle,
metric,
dataWithHighestLowest,
props.updateFipsCallback,
props.mapConfig,
props.fips,
isMobile,
props.activeDemographicGroup,
props.demographicType,
isCawp,
props.countColsMap,
props.isUnknownsMap,
scaleType,
])

return (
<div
className={`justify-center ${
width === INVISIBLE_PRELOAD_WIDTH ? 'hidden' : 'block'
}`}
ref={overrideShapeWithCircle ? undefined : ref}
className={`justify-center ${width === INVISIBLE_PRELOAD_WIDTH ? 'hidden' : 'block'}`}
ref={props.overrideShapeWithCircle ? undefined : ref}
>
<svg
ref={svgRef}
style={{
width: '100%',
}}
/>
<svg ref={svgRef} style={{ width: '100%' }} />
</div>
)
}
Expand Down
Loading
Loading