Skip to content

Commit

Permalink
Frontend: Refactors Rate bar chart to D3 (#3713)
Browse files Browse the repository at this point in the history
# Description and Motivation
<!--- bulleted, high level items. use keywords (eg "closes #144" or
"fixes #4323") -->

- create drop in replacement component for the rate chart using d3
rather than Vega
- splits out sub components like Axes, Gridlines etc for smaller code
files and mimicking pattern from Mia (d3 trend charts) and @eriwarr
(stacked share vs population charts)
- rename rate chart card component that wraps the chart
- renames ambiguous prop `metric` to more specific `mertricConfig`
- hides the demographic type label (like "race and ethnicity")
vertically along the y axis on small screens to give more horizontal
space to the chart
- hide the bar label qualifier ` per 100k` from the smallest screens

## Has this been tested? How?

- e2e tests updated and passing
- passing unit tests added for rate bar chart helper functions

## Screenshots (if appropriate)

### Screen Reader Walk Through Demo (Captions)


https://github.com/user-attachments/assets/9fd9ff56-df1e-441d-b545-c34dc80376aa

### Mobile, Compare Mode, Hover Tooltip

<img width="927" alt="Screenshot 2024-10-11 at 8 37 30 AM"
src="https://github.com/user-attachments/assets/0c2f7eb9-77f7-4da3-8df0-4e7d82f3bcd2">
<img width="927" alt="Screenshot 2024-10-11 at 8 37 41 AM"
src="https://github.com/user-attachments/assets/8cdf4817-d975-4f12-8762-7015cae4c4f7">
<img width="927" alt="Screenshot 2024-10-11 at 8 37 51 AM"
src="https://github.com/user-attachments/assets/5faedd6c-f7f1-4249-83c4-42ddb4dd7b72">
<img width="927" alt="Screenshot 2024-10-11 at 8 38 31 AM"
src="https://github.com/user-attachments/assets/ff85245d-464b-4e8d-9865-7ae3844cc3d3">
<img width="927" alt="Screenshot 2024-10-11 at 8 38 34 AM"
src="https://github.com/user-attachments/assets/9b33de81-ff17-4485-8fbb-896c179515d8">
<img width="927" alt="Screenshot 2024-10-11 at 8 39 08 AM"
src="https://github.com/user-attachments/assets/ffaa00cf-67fe-4de3-a5db-064da92b3f1c">

### Desktop Various Units

![HET RATE-CHART Investigate rates of Depression in United States Oct
2024
(1)](https://github.com/user-attachments/assets/3e047cb0-a4be-4ae8-81a6-d70ecdfdf2d0)
![HET RATE-CHART Investigate rates of Depression in United States Oct
2024](https://github.com/user-attachments/assets/32433d2c-396f-422b-8c01-1fab49a20d2b)
![HET RATE-CHART Investigate rates of Excessive Drinking in United
States Oct
2024](https://github.com/user-attachments/assets/592d3b6c-2d64-445f-a1f2-5a931f195701)
![HET RATE-CHART Investigate rates of HIV Stigma in United States Oct
2024](https://github.com/user-attachments/assets/e4eec7dc-4675-4f29-b2a6-a4dede506d80)
![HET RATE-CHART Investigate rates of Linkage to HIV Care in United
States Oct
2024](https://github.com/user-attachments/assets/be17c5ff-23f8-4e26-b791-f5a3f2d6daa3)



## Types of changes

(leave all that apply)

- New content or feature
- Refactor / chore

## New frontend preview link is below in the Netlify comment 😎
  • Loading branch information
benhammondmusic authored Oct 11, 2024
1 parent 613b523 commit 6c02577
Show file tree
Hide file tree
Showing 25 changed files with 897 additions and 481 deletions.
59 changes: 44 additions & 15 deletions frontend/playwright-tests/depression.nightly.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,55 @@ import { test } from '@playwright/test'
test('Depression Flow', async ({ page }) => {
await page.goto('/exploredata?mls=1.depression-3.00&group1=All')
await page
.getByLabel(
'Map showing Depression in the United States : including data from 50 states/territories'
)
.getByRole('img')
await page.getByRole('button', { name: 'Expand state/territory rate' }).click();
.locator('#rate-map')
.getByRole('heading', { name: 'Depression in the United' })
.click()
await page
.locator('#rate-map')
.getByRole('heading', { name: 'Ages 18+' })
.click()
await page
.getByRole('button', { name: 'Rates over time', exact: true })
.click()
await page
.getByRole('heading', { name: 'Depression cases over time in' })
.click()
await page
.getByLabel(
'Bar Chart showing Depression in the United States, by Race and Ethnicity'
)
.getByRole('img')
.locator('#rates-over-time')
.getByRole('heading', { name: 'Ages 18+' })
.click()
await page.getByText('cases per 100k adults →').click()
await page.getByText('time →').click()
await page
.locator('#rates-over-time')
.getByText('Note. (NH) indicates ‘Non-')
.click()
await page.getByRole('button', { name: 'Rate chart' }).click()
await page
.locator('#rate-chart')
.getByRole('heading', { name: 'Depression in the United' })
.click()
await page
.locator('#rate-chart')
.getByRole('heading', { name: 'Ages 18+' })
.click()
await page.locator('#rate-chart').getByText('All').click()

Check failure on line 38 in frontend/playwright-tests/depression.nightly.spec.ts

View workflow job for this annotation

GitHub Actions / Runs frontend E2E_STAGING on main whenever code is pushed (or merged)

[E2E_NIGHTLY] › depression.nightly.spec.ts:3:1 › Depression Flow

1) [E2E_NIGHTLY] › depression.nightly.spec.ts:3:1 › Depression Flow ────────────────────────────── Error: locator.click: Error: strict mode violation: locator('#rate-chart').getByText('All') resolved to 2 elements: 1) <text fill="#000" opacity="1" font-size="10px" text-anchor="end" font-family="sans-serif" transform="translate(-7,32.5)">All</text> aka getByLabel('Bar Chart showing Depression').getByText('All', { exact: true }) 2) <text fill="#000" opacity="0" font-size="0px" text-anchor="start" font-family="sans-serif" transform="translate(0,44.400000000000006)">All: 21,700 cases per 100k adults</text> aka getByText('All: 21,700 cases per 100k') Call log: - waiting for locator('#rate-chart').getByText('All') 36 | .getByRole('heading', { name: 'Ages 18+' }) 37 | .click() > 38 | await page.locator('#rate-chart').getByText('All').click() | ^ 39 | await page.locator('#rate-chart').getByText('race and ethnicity').click() 40 | await page.locator('#rate-chart').getByText('cases per 100k adults').click() 41 | await page.getByRole('button', { name: 'Unknown demographic map' }).click() at /home/runner/work/health-equity-tracker/health-equity-tracker/frontend/playwright-tests/depression.nightly.spec.ts:38:54

Check failure on line 38 in frontend/playwright-tests/depression.nightly.spec.ts

View workflow job for this annotation

GitHub Actions / Runs frontend E2E_STAGING on main whenever code is pushed (or merged)

[E2E_NIGHTLY] › depression.nightly.spec.ts:3:1 › Depression Flow

1) [E2E_NIGHTLY] › depression.nightly.spec.ts:3:1 › Depression Flow ────────────────────────────── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.click: Error: strict mode violation: locator('#rate-chart').getByText('All') resolved to 2 elements: 1) <text fill="#000" opacity="1" font-size="10px" text-anchor="end" font-family="sans-serif" transform="translate(-7,32.5)">All</text> aka getByLabel('Bar Chart showing Depression').getByText('All', { exact: true }) 2) <text fill="#000" opacity="0" font-size="0px" text-anchor="start" font-family="sans-serif" transform="translate(0,44.400000000000006)">All: 21,700 cases per 100k adults</text> aka getByText('All: 21,700 cases per 100k') Call log: - waiting for locator('#rate-chart').getByText('All') 36 | .getByRole('heading', { name: 'Ages 18+' }) 37 | .click() > 38 | await page.locator('#rate-chart').getByText('All').click() | ^ 39 | await page.locator('#rate-chart').getByText('race and ethnicity').click() 40 | await page.locator('#rate-chart').getByText('cases per 100k adults').click() 41 | await page.getByRole('button', { name: 'Unknown demographic map' }).click() at /home/runner/work/health-equity-tracker/health-equity-tracker/frontend/playwright-tests/depression.nightly.spec.ts:38:54

Check failure on line 38 in frontend/playwright-tests/depression.nightly.spec.ts

View workflow job for this annotation

GitHub Actions / Runs frontend E2E_STAGING on main whenever code is pushed (or merged)

[E2E_NIGHTLY] › depression.nightly.spec.ts:3:1 › Depression Flow

1) [E2E_NIGHTLY] › depression.nightly.spec.ts:3:1 › Depression Flow ────────────────────────────── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: locator.click: Error: strict mode violation: locator('#rate-chart').getByText('All') resolved to 2 elements: 1) <text fill="#000" opacity="1" font-size="10px" text-anchor="end" font-family="sans-serif" transform="translate(-7,32.5)">All</text> aka getByLabel('Bar Chart showing Depression').getByText('All', { exact: true }) 2) <text fill="#000" opacity="0" font-size="0px" text-anchor="start" font-family="sans-serif" transform="translate(0,44.400000000000006)">All: 21,700 cases per 100k adults</text> aka getByText('All: 21,700 cases per 100k') Call log: - waiting for locator('#rate-chart').getByText('All') 36 | .getByRole('heading', { name: 'Ages 18+' }) 37 | .click() > 38 | await page.locator('#rate-chart').getByText('All').click() | ^ 39 | await page.locator('#rate-chart').getByText('race and ethnicity').click() 40 | await page.locator('#rate-chart').getByText('cases per 100k adults').click() 41 | await page.getByRole('button', { name: 'Unknown demographic map' }).click() at /home/runner/work/health-equity-tracker/health-equity-tracker/frontend/playwright-tests/depression.nightly.spec.ts:38:54
await page.locator('#rate-chart').getByText('race and ethnicity').click()
await page.locator('#rate-chart').getByText('cases per 100k adults').click()
await page.getByRole('button', { name: 'Unknown demographic map' }).click()
await page.getByText('No unknown values for race').click()
await page.getByRole('button', { name: 'Data table' }).click()
await page
.getByRole('heading', { name: 'Summary for depression in the' })
.click()
await page
.getByRole('heading', {
name: 'Share of total adult depression cases with unknown race and ethnicity in the United States',
})
.getByRole('figure', { name: 'Summary for depression in the' })
.locator('h4')
.click()
await page.getByRole('columnheader', { name: 'Race and Ethnicity' }).click()
await page
.getByText(
'No unknown values for race and ethnicity reported in this dataset at the state/t'
)
.getByRole('columnheader', { name: 'Cases of depression per 100k' })
.click()
await page.getByRole('columnheader', { name: 'Share of total adult' }).click()
await page.getByRole('columnheader', { name: 'Population share' }).click()
})
2 changes: 1 addition & 1 deletion frontend/playwright-tests/diabetes.ci.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test('Diabetes County', async ({ page }) => {
.locator('#rate-map')
.getByRole('heading', { name: 'Ages 18+' })
.click()
await page.getByLabel('Legend for rate map').getByRole('img').click()
await page.getByLabel('Legend for rate map').click()
await page.locator('li').filter({ hasText: 'Denver County' }).click()
await page
.locator('#rate-chart')
Expand Down
33 changes: 19 additions & 14 deletions frontend/playwright-tests/modals.ci.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { test, expect } from '@playwright/test'

Check failure on line 1 in frontend/playwright-tests/modals.ci.spec.ts

View workflow job for this annotation

GitHub Actions / Runs frontend E2E_STAGING on main whenever code is pushed (or merged)

[E2E_NIGHTLY] › modals.ci.spec.ts:5:1 › Topic Info Modal from Sidebar

2) [E2E_NIGHTLY] › modals.ci.spec.ts:5:1 › Topic Info Modal from Sidebar ───────────────────────── Test timeout of 120000ms exceeded.

test.setTimeout(120000);
test.setTimeout(120000)

test('Topic Info Modal from Sidebar', async ({ page }) => {
// Compare Topics Page Loads
await page.goto(
'/exploredata?mls=1.incarceration-3.poverty-5.13&mlp=comparevars&dt1=prison',
{ waitUntil: 'commit' }
{ waitUntil: 'commit' },
)

// Clicking topic info modal button launched modal
Expand All @@ -27,24 +27,29 @@ test('Topic Info Modal from Sidebar', async ({ page }) => {

test.describe('Topic Info Modal from Map Legend', () => {
test('Topic Info Modal from Map Legend', async ({ page }) => {
await page.goto('/', { waitUntil: 'commit' });
await page.locator('#landingPageCTA').click();
const reportSection = await page.getByRole('heading', { name: 'Uninsurance in FL' }).locator('..');
await reportSection.locator('text=Explore this report').click();
await page.locator('#rate-map').getByRole('button', { name: 'Click for more info on uninsured people' }).click();
});
});
await page.goto('/', { waitUntil: 'commit' })
await page.locator('#landingPageCTA').click()
const reportSection = await page
.getByRole('heading', { name: 'Uninsurance in FL' })
.locator('..')
await reportSection.locator('text=Explore this report').click()
await page
.locator('#rate-map')
.getByRole('button', { name: 'Click for more info on uninsured people' })
.click()
})
})

test('Multiple Maps 1 (Left Side)', async ({ page }) => {
// Compare Topics Page With Multimap Open Loads
await page.goto(
'/exploredata?mls=1.incarceration-3.poverty-5.13&mlp=comparevars&dt1=prison&multiple-maps=true',
{ waitUntil: 'commit' }
{ waitUntil: 'commit' },
)
await expect(
page.getByRole('heading', {
name: 'Prison incarceration in Georgia across all race and ethnicity groups',
})
}),
).toBeVisible()

// CLOSE IT
Expand All @@ -56,21 +61,21 @@ test('Multiple Maps 2 (Right Side)', async ({ page }) => {
// Compare Topics Page Loads
await page.goto(
'/exploredata?mls=1.incarceration-3.poverty-5.13&mlp=comparevars&dt1=prison',
{ waitUntil: 'commit' }
{ waitUntil: 'commit' },
)

// Clicking right side multiple maps button launches POVERTY multimap modal
await page
.locator('#rate-map2')
.getByLabel(
'Launch multiple maps view with side-by-side maps of each race and ethnicity group'
'Launch multiple maps view with side-by-side maps of each race and ethnicity group',
)
.click()
await expect(page).toHaveURL(/.*multiple-maps2=true/)
await expect(
page.getByRole('heading', {
name: 'People below the poverty line in Georgia across all race and ethnicity groups',
})
}),
).toBeVisible()

// CLOSE IT
Expand Down
2 changes: 1 addition & 1 deletion frontend/playwright-tests/voter_participation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ test('Voter Participation Flow', async ({ page }) => {
.locator('#rate-chart')
.getByRole('heading', { name: 'U.S. citizens, Ages 18+' })
.click()
await page.getByLabel('Bar Chart showing Voter').getByRole('img').click()
await page.getByLabel('Bar Chart showing Voter').click()
await page.getByRole('heading', { name: 'Share of all voter' }).click()
await page.getByText('No unknown values for race').click()
await page
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { addComparisonAllsRowToIntersectionalData } from '../charts/simpleBarHelperFunctions'
import { SimpleHorizontalBarChart } from '../charts/SimpleHorizontalBarChart'
import { addComparisonAllsRowToIntersectionalData } from '../charts/rateBarChart/helpers'
import { RateBarChart } from '../charts/rateBarChart/Index'
import { generateChartTitle, generateSubtitle } from '../charts/utils'
import type { DataTypeConfig, MetricId } from '../data/config/MetricConfigTypes'
import { isPctType } from '../data/config/MetricConfigUtils'
Expand Down Expand Up @@ -35,8 +35,7 @@ import MissingDataAlert from './ui/MissingDataAlert'
/* minimize layout shift */
const PRELOAD_HEIGHT = 668

interface SimpleBarChartCardProps {
key?: string
interface RateBarChartCardProps {
demographicType: DemographicType
dataTypeConfig: DataTypeConfig
fips: Fips
Expand All @@ -46,16 +45,7 @@ interface SimpleBarChartCardProps {

// This wrapper ensures the proper key is set to create a new instance when
// required rather than relying on the card caller.
export default function SimpleBarChartCard(props: SimpleBarChartCardProps) {
return (
<SimpleBarChartCardWithKey
key={props.dataTypeConfig.dataTypeId + props.demographicType}
{...props}
/>
)
}

function SimpleBarChartCardWithKey(props: SimpleBarChartCardProps) {
export default function RateBarChartCard(props: RateBarChartCardProps) {
const rateConfig =
props.dataTypeConfig.metrics?.per100k ??
props.dataTypeConfig.metrics?.pct_rate ??
Expand Down Expand Up @@ -185,11 +175,10 @@ function SimpleBarChartCardWithKey(props: SimpleBarChartCardProps) {
) : (
<>
<ChartTitle title={chartTitle} subtitle={subtitle} />

<SimpleHorizontalBarChart
<RateBarChart
data={data}
demographicType={props.demographicType}
metric={rateConfig}
metricConfig={rateConfig}
filename={filename}
usePercentSuffix={isPctType(rateConfig.type)}
fips={props.fips}
Expand Down
86 changes: 0 additions & 86 deletions frontend/src/charts/SimpleHorizontalBarChart.tsx

This file was deleted.

28 changes: 28 additions & 0 deletions frontend/src/charts/rateBarChart/BarChartTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export interface BarChartTooltipData {
x: number
y: number
content: string
}

interface BarChartTooltipProps {
data: BarChartTooltipData | null
}

export default function BarChartTooltip({ data }: BarChartTooltipProps) {
if (!data) return null
const clickIsLeftHalfOfScreen = data.x < window.innerWidth / 2
return (
<div
className='bg-white text-altBlack rounded-sm p-3 text-title absolute cursor-help z-top shadow-raised opacity-95 smMd:whitespace-nowrap'
style={{
left: `${data.x}px`,
top: `${data.y}px`,
transform: clickIsLeftHalfOfScreen
? 'translate(0, 5%)'
: 'translate(-100%, 5%)',
}}
>
{data.content}
</div>
)
}
33 changes: 33 additions & 0 deletions frontend/src/charts/rateBarChart/EndOfBarLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { ScaleBand } from 'd3'
import type { MetricConfig } from '../../data/config/MetricConfigTypes'
import { formatValue } from './helpers'

interface EndOfBarLabelProps {
metricConfig: MetricConfig
d: Record<string, any>
shouldLabelBeInside: boolean
barWidth: number
yScale: ScaleBand<string>
barLabelColor: string
isTinyAndUp: boolean
}

export default function EndOfBarLabel(props: EndOfBarLabelProps) {
return (
<text
x={props.shouldLabelBeInside ? props.barWidth - 5 : props.barWidth + 5}
y={props.yScale.bandwidth() / 2}
dy='1.3em'
textAnchor={props.shouldLabelBeInside ? 'end' : 'start'}
className={`text-smallest ${props.barLabelColor}`}
aria-hidden='true'
tabIndex={-1}
>
{formatValue(
props.d[props.metricConfig.metricId],
props.metricConfig,
props.isTinyAndUp,
)}
</text>
)
}
Loading

0 comments on commit 6c02577

Please sign in to comment.