From b3719029ef56339684263b167c23dc1548f19f4d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 28 Sep 2023 15:29:36 -0400 Subject: [PATCH 1/7] feat: allow for multi-column-width charts and improve responsivity --- src/js/components/Overview/ChartCard.tsx | 7 ++-- .../Overview/OverviewDisplayData.tsx | 39 ++++++++++------- src/js/types/chartConfig.ts | 42 +++++++++++-------- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/src/js/components/Overview/ChartCard.tsx b/src/js/components/Overview/ChartCard.tsx index bb30490c..bbce4663 100644 --- a/src/js/components/Overview/ChartCard.tsx +++ b/src/js/components/Overview/ChartCard.tsx @@ -7,9 +7,9 @@ import CustomEmpty from '../Util/CustomEmpty'; import { DEFAULT_TRANSLATION, NON_DEFAULT_TRANSLATION } from '@/constants/configConstants'; import { ChartDataField } from '@/types/data'; -const CARD_STYLE = { width: '100%', height: '415px', margin: '5px 0', borderRadius: '11px' }; +const CARD_STYLE = { width: '100%', height: '415px', borderRadius: '11px' }; -const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => { +const ChartCard = memo(({ section, chart, onRemoveChart, width }: ChartCardProps) => { const { t } = useTranslation(NON_DEFAULT_TRANSLATION); const { t: td } = useTranslation(DEFAULT_TRANSLATION); @@ -55,7 +55,7 @@ const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => { }); return ( -
+
{ed}}> {data.filter((e) => !(e.x === 'missing')).length !== 0 ? ( @@ -75,6 +75,7 @@ export interface ChartCardProps { section: string; chart: ChartDataField; onRemoveChart: (arg: { section: string; id: string }) => void; + width: number; } export default ChartCard; diff --git a/src/js/components/Overview/OverviewDisplayData.tsx b/src/js/components/Overview/OverviewDisplayData.tsx index 73b637f9..28a2ecf6 100644 --- a/src/js/components/Overview/OverviewDisplayData.tsx +++ b/src/js/components/Overview/OverviewDisplayData.tsx @@ -1,10 +1,11 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { CSSProperties, useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { List } from 'antd'; -import ChartCard from './ChartCard'; import { disableChart } from '@/features/data/data.store'; import { ChartDataField } from '@/types/data'; +import ChartCard from './ChartCard'; + +const CHART_GUTTER = 16; const getColumnCount = (width: number): number => { if (width < 990) { @@ -15,10 +16,10 @@ const getColumnCount = (width: number): number => { }; const getFrameWidth = (width: number): number => { - if (width < 990) { - return 360; + if (width < 440) { + return 440; } else if (width < 1420) { - return 910; + return width - 60; } else return 1325; }; @@ -26,10 +27,12 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) = const dispatch = useDispatch(); const { width } = useWindowSize(); + const columnCount = getColumnCount(width); + const frameWidth = getFrameWidth(width); - const [listStyle, listGrid] = useMemo( - () => [{ width: `${getFrameWidth(width)}px` }, { gutter: 0, column: getColumnCount(width) }], - [width] + const containerStyle = useMemo( + () => ({ width: frameWidth, display: 'flex', flexWrap: 'wrap', gap: CHART_GUTTER }), + [frameWidth] ); const displayedCharts = useMemo(() => allCharts.filter((e) => e.isDisplayed), [allCharts]); @@ -42,19 +45,23 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) = ); const renderItem = useCallback( - (chart: ChartDataField) => ( - - ), - [section, onRemoveChart] + (chart: ChartDataField) => { + const columnWidth = Math.min(chart.chartConfig.width ?? 1, columnCount); + const pixelWidth = (columnWidth / columnCount) * (frameWidth - CHART_GUTTER * (columnCount - columnWidth)); + return ( + + ); + }, + [section, onRemoveChart, width] ); - return ; + return
{displayedCharts.map(renderItem)}
; }; const useWindowSize = () => { const [windowSize, setWindowSize] = useState({ - width: 0, - height: 0, + width: window.innerWidth, + height: window.innerHeight, }); useEffect(() => { diff --git a/src/js/types/chartConfig.ts b/src/js/types/chartConfig.ts index 5a2fc346..042367c7 100644 --- a/src/js/types/chartConfig.ts +++ b/src/js/types/chartConfig.ts @@ -10,21 +10,27 @@ ChartConfig: represents what is stored in the configuration file for describing the field metadata (description / mapping information / units / etc.) By using a sum type here, we can optionally mandate configuration information for certain types of charts. */ -export type ChartConfig = - | { - chart_type: typeof CHART_TYPE_PIE; - field: string; - } - | { - chart_type: typeof CHART_TYPE_BAR; - field: string; - } - | { - chart_type: typeof CHART_TYPE_CHOROPLETH; - field: string; - category_prop: ChoroplethMapProps['categoryProp']; - color_mode: ChoroplethMapProps['colorMode']; - features: ChoroplethMapProps['features']; - center: ChoroplethMapProps['center']; - zoom: ChoroplethMapProps['zoom']; - }; + +interface BaseChartConfig { + field: string; + width?: number; +} + +interface ChartConfigTypePie extends BaseChartConfig { + chart_type: typeof CHART_TYPE_PIE; +} + +interface ChartConfigTypeBar extends BaseChartConfig { + chart_type: typeof CHART_TYPE_BAR; +} + +interface ChartConfigTypeChoropleth extends BaseChartConfig { + chart_type: typeof CHART_TYPE_CHOROPLETH; + category_prop: ChoroplethMapProps['categoryProp']; + color_mode: ChoroplethMapProps['colorMode']; + features: ChoroplethMapProps['features']; + center: ChoroplethMapProps['center']; + zoom: ChoroplethMapProps['zoom']; +} + +export type ChartConfig = ChartConfigTypePie | ChartConfigTypeBar | ChartConfigTypeChoropleth; From 4587703ac08ee89284fd3dad624715505bf48b1c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Sat, 30 Sep 2023 15:47:09 -0400 Subject: [PATCH 2/7] feat(overview): user runtime-configurable chart width --- src/js/components/Overview/Chart.tsx | 10 +++--- src/js/components/Overview/ChartCard.tsx | 3 +- .../components/Overview/Drawer/ChartTree.tsx | 33 +++++++++++++++---- .../Overview/OverviewDisplayData.tsx | 2 +- .../components/Search/SearchResultsPane.tsx | 6 ++-- src/js/constants/overviewConstants.ts | 4 ++- src/js/features/data/data.store.ts | 11 +++++-- .../features/data/makeGetDataRequest.thunk.ts | 6 ++-- src/js/types/chartConfig.ts | 8 ++--- src/js/types/data.ts | 4 ++- 10 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/js/components/Overview/Chart.tsx b/src/js/components/Overview/Chart.tsx index 82eb7c7a..a1561ebb 100644 --- a/src/js/components/Overview/Chart.tsx +++ b/src/js/components/Overview/Chart.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { BarChart, PieChart } from 'bento-charts'; import { ChoroplethMap } from 'bento-charts/dist/maps'; -import { CHART_HEIGHT } from '@/constants/overviewConstants'; +import { CHART_HEIGHT, PIE_CHART_HEIGHT } from '@/constants/overviewConstants'; import { ChartData } from '@/types/data'; import { CHART_TYPE_BAR, CHART_TYPE_CHOROPLETH, CHART_TYPE_PIE, ChartConfig } from '@/types/chartConfig'; @@ -18,11 +18,10 @@ const Chart = memo(({ chartConfig, data, units, id }: ChartProps) => { switch (type) { case CHART_TYPE_BAR: - // bar charts can be rendered slightly larger as they do not clip return ( { return ( { @@ -44,12 +43,11 @@ const Chart = memo(({ chartConfig, data, units, id }: ChartProps) => { /> ); case CHART_TYPE_CHOROPLETH: { - // map charts can be rendered at full height as they do not clip const { category_prop: categoryProp, features, center, zoom, color_mode: colorMode } = chartConfig; return ( !(e.x === 'missing')).length !== 0 ? ( ) : ( - + )} diff --git a/src/js/components/Overview/Drawer/ChartTree.tsx b/src/js/components/Overview/Drawer/ChartTree.tsx index 79c50c5d..ed257313 100644 --- a/src/js/components/Overview/Drawer/ChartTree.tsx +++ b/src/js/components/Overview/Drawer/ChartTree.tsx @@ -1,14 +1,14 @@ -import React, { useMemo } from 'react'; +import React, { ReactNode, useMemo } from 'react'; import { useDispatch } from 'react-redux'; -import { Tree, TreeProps } from 'antd'; +import { InputNumber, Tree, TreeProps } from 'antd'; import { useTranslation } from 'react-i18next'; -import { rearrange, setDisplayedCharts } from '@/features/data/data.store'; +import { rearrange, setDisplayedCharts, setChartWidth } from '@/features/data/data.store'; import { NON_DEFAULT_TRANSLATION } from '@/constants/configConstants'; import { ChartDataField } from '@/types/data'; interface MappedChartItem { - title: string; + title: ReactNode; key: string; } @@ -17,7 +17,28 @@ const ChartTree = ({ charts, section }: ChartTreeProps) => { const { t } = useTranslation(NON_DEFAULT_TRANSLATION); const allCharts: MappedChartItem[] = useMemo( - () => charts.map(({ field: { title }, id }) => ({ title: t(title), key: id })), + () => charts.map(({ field: { title }, id, width }) => ({ + title:
+ {t(title)} + + Width:{" "} + { + if (v) { + dispatch(setChartWidth({ section, chart: id, width: v })); + } + }} + controls={true} + style={{ width: 50 }} + /> + +
, + key: id, + })), [charts] ); @@ -38,7 +59,7 @@ const ChartTree = ({ charts, section }: ChartTreeProps) => { const onCheck = useMemo(() => { const fn: TreeProps['onCheck'] = (checkedKeysValue) => { - dispatch(setDisplayedCharts({ section, charts: checkedKeysValue })); + dispatch(setDisplayedCharts({ section, charts: checkedKeysValue as string[] })); }; return fn; }, [dispatch, section]); diff --git a/src/js/components/Overview/OverviewDisplayData.tsx b/src/js/components/Overview/OverviewDisplayData.tsx index 28a2ecf6..9d0b6132 100644 --- a/src/js/components/Overview/OverviewDisplayData.tsx +++ b/src/js/components/Overview/OverviewDisplayData.tsx @@ -46,7 +46,7 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) = const renderItem = useCallback( (chart: ChartDataField) => { - const columnWidth = Math.min(chart.chartConfig.width ?? 1, columnCount); + const columnWidth = Math.min(chart.width, columnCount); const pixelWidth = (columnWidth / columnCount) * (frameWidth - CHART_GUTTER * (columnCount - columnWidth)); return ( diff --git a/src/js/components/Search/SearchResultsPane.tsx b/src/js/components/Search/SearchResultsPane.tsx index c642d1ee..42c8479a 100644 --- a/src/js/components/Search/SearchResultsPane.tsx +++ b/src/js/components/Search/SearchResultsPane.tsx @@ -5,7 +5,7 @@ import { BiDna } from 'react-icons/bi'; import { PieChart } from 'bento-charts'; import CustomEmpty from '../Util/CustomEmpty'; import ExpSvg from '../Util/ExpSvg'; -import { CHART_HEIGHT, COUNTS_FILL } from '@/constants/overviewConstants'; +import { CHART_HEIGHT, COUNTS_FILL, PIE_CHART_HEIGHT } from '@/constants/overviewConstants'; import { useTranslationDefault } from '@/hooks'; import { ChartData } from '@/types/data'; @@ -53,7 +53,7 @@ const SearchResultsPane = ({ {t('Biosamples')} {!hasInsufficientData && biosampleChartData.length ? ( - + ) : ( )} @@ -61,7 +61,7 @@ const SearchResultsPane = ({ {t('Experiments')} {!hasInsufficientData && experimentChartData.length ? ( - + ) : ( )} diff --git a/src/js/constants/overviewConstants.ts b/src/js/constants/overviewConstants.ts index 9c656d08..a9bf33cb 100644 --- a/src/js/constants/overviewConstants.ts +++ b/src/js/constants/overviewConstants.ts @@ -2,4 +2,6 @@ export const COUNTS_FILL = '#75787a'; export const LOCALSTORAGE_CHARTS_KEY = 'charts'; -export const CHART_HEIGHT = 300; +export const CHART_HEIGHT = 350; +export const PIE_CHART_HEIGHT = 300; // rendered slightly smaller since labels can clip +export const DEFAULT_CHART_WIDTH = 1; diff --git a/src/js/features/data/data.store.ts b/src/js/features/data/data.store.ts index 39f3a215..8eaa908a 100644 --- a/src/js/features/data/data.store.ts +++ b/src/js/features/data/data.store.ts @@ -34,7 +34,7 @@ const data = createSlice({ const { section, id } = payload; state.sections.find((e) => e.sectionTitle === section)!.charts.find((e) => e.id === id)!.isDisplayed = false; }, - setDisplayedCharts: (state, { payload }) => { + setDisplayedCharts: (state, { payload }: PayloadAction<{ section: string, charts: string[] }>) => { const { section, charts } = payload; state.sections .find((e) => e.sectionTitle === section)! @@ -42,6 +42,13 @@ const data = createSlice({ arr[ind].isDisplayed = charts.includes(val.id); }); }, + setChartWidth: (state, { payload }: PayloadAction<{ section: string, chart: string, width: number }>) => { + const { section, chart, width } = payload; + const chartObj = state.sections + .find((e) => e.sectionTitle === section)! + .charts.find((c) => c.id === chart)!; + chartObj.width = width; + }, }, extraReducers: (builder) => { builder @@ -59,6 +66,6 @@ const data = createSlice({ }, }); -export const { rearrange, disableChart, setDisplayedCharts } = data.actions; +export const { rearrange, disableChart, setDisplayedCharts, setChartWidth } = data.actions; export { makeGetDataRequestThunk }; export default data.reducer; diff --git a/src/js/features/data/makeGetDataRequest.thunk.ts b/src/js/features/data/makeGetDataRequest.thunk.ts index 5be3351d..8931ebca 100644 --- a/src/js/features/data/makeGetDataRequest.thunk.ts +++ b/src/js/features/data/makeGetDataRequest.thunk.ts @@ -4,7 +4,7 @@ import { MAX_CHARTS, publicOverviewUrl } from '@/constants/configConstants'; import { verifyData, saveValue, getValue, convertSequenceAndDisplayData } from '@/utils/localStorage'; -import { LOCALSTORAGE_CHARTS_KEY } from '@/constants/overviewConstants'; +import { DEFAULT_CHART_WIDTH, LOCALSTORAGE_CHARTS_KEY } from '@/constants/overviewConstants'; import { serializeChartData } from '@/utils/chart'; import { ChartConfig } from '@/types/chartConfig'; import { ChartDataField, LocalStorageData, Sections } from '@/types/data'; @@ -33,9 +33,11 @@ export const makeGetDataRequestThunk = createAsyncThunk< return { id: field.id, chartConfig: chart, - isDisplayed: i < MAX_CHARTS, field, data: serializeChartData(field.data), + // Initial display state + isDisplayed: i < MAX_CHARTS, + width: chart.width ?? DEFAULT_CHART_WIDTH, // initial configured width; users can change it from here }; }; diff --git a/src/js/types/chartConfig.ts b/src/js/types/chartConfig.ts index 042367c7..ff325372 100644 --- a/src/js/types/chartConfig.ts +++ b/src/js/types/chartConfig.ts @@ -16,15 +16,15 @@ interface BaseChartConfig { width?: number; } -interface ChartConfigTypePie extends BaseChartConfig { +interface ChartConfigPie extends BaseChartConfig { chart_type: typeof CHART_TYPE_PIE; } -interface ChartConfigTypeBar extends BaseChartConfig { +interface ChartConfigBar extends BaseChartConfig { chart_type: typeof CHART_TYPE_BAR; } -interface ChartConfigTypeChoropleth extends BaseChartConfig { +interface ChartConfigChoropleth extends BaseChartConfig { chart_type: typeof CHART_TYPE_CHOROPLETH; category_prop: ChoroplethMapProps['categoryProp']; color_mode: ChoroplethMapProps['colorMode']; @@ -33,4 +33,4 @@ interface ChartConfigTypeChoropleth extends BaseChartConfig { zoom: ChoroplethMapProps['zoom']; } -export type ChartConfig = ChartConfigTypePie | ChartConfigTypeBar | ChartConfigTypeChoropleth; +export type ChartConfig = ChartConfigPie | ChartConfigBar | ChartConfigChoropleth; diff --git a/src/js/types/data.ts b/src/js/types/data.ts index 6044b181..95c0c809 100644 --- a/src/js/types/data.ts +++ b/src/js/types/data.ts @@ -17,10 +17,12 @@ This represents a chart's "state", since it also has the isDisplayed property - export interface ChartDataField { id: string; // taken from field definition data: ChartData[]; - isDisplayed: boolean; // Field definition without data (we have mapped data in the data prop above instead): field: Omit; chartConfig: ChartConfig; + // display options: + isDisplayed: boolean; // whether the chart is currently displayed (state data) + width: number; // current width (state data); initial data taken from chart config } export interface ChartData { From 9c07f62368cb267a7c6557a81f67e9e9afe403c3 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 3 Oct 2023 15:38:47 -0400 Subject: [PATCH 3/7] lint --- .../components/Overview/Drawer/ChartTree.tsx | 47 ++++++++++--------- .../components/Search/SearchResultsPane.tsx | 2 +- src/js/constants/overviewConstants.ts | 2 +- src/js/features/data/data.store.ts | 8 ++-- .../features/data/makeGetDataRequest.thunk.ts | 2 +- src/js/types/data.ts | 4 +- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/js/components/Overview/Drawer/ChartTree.tsx b/src/js/components/Overview/Drawer/ChartTree.tsx index ed257313..c1b78c3b 100644 --- a/src/js/components/Overview/Drawer/ChartTree.tsx +++ b/src/js/components/Overview/Drawer/ChartTree.tsx @@ -17,28 +17,31 @@ const ChartTree = ({ charts, section }: ChartTreeProps) => { const { t } = useTranslation(NON_DEFAULT_TRANSLATION); const allCharts: MappedChartItem[] = useMemo( - () => charts.map(({ field: { title }, id, width }) => ({ - title:
- {t(title)} - - Width:{" "} - { - if (v) { - dispatch(setChartWidth({ section, chart: id, width: v })); - } - }} - controls={true} - style={{ width: 50 }} - /> - -
, - key: id, - })), + () => + charts.map(({ field: { title }, id, width }) => ({ + title: ( +
+ {t(title)} + + Width:{' '} + { + if (v) { + dispatch(setChartWidth({ section, chart: id, width: v })); + } + }} + controls={true} + style={{ width: 50 }} + /> + +
+ ), + key: id, + })), [charts] ); diff --git a/src/js/components/Search/SearchResultsPane.tsx b/src/js/components/Search/SearchResultsPane.tsx index 42c8479a..6536a9f5 100644 --- a/src/js/components/Search/SearchResultsPane.tsx +++ b/src/js/components/Search/SearchResultsPane.tsx @@ -5,7 +5,7 @@ import { BiDna } from 'react-icons/bi'; import { PieChart } from 'bento-charts'; import CustomEmpty from '../Util/CustomEmpty'; import ExpSvg from '../Util/ExpSvg'; -import { CHART_HEIGHT, COUNTS_FILL, PIE_CHART_HEIGHT } from '@/constants/overviewConstants'; +import { COUNTS_FILL, PIE_CHART_HEIGHT } from '@/constants/overviewConstants'; import { useTranslationDefault } from '@/hooks'; import { ChartData } from '@/types/data'; diff --git a/src/js/constants/overviewConstants.ts b/src/js/constants/overviewConstants.ts index a9bf33cb..467a4e94 100644 --- a/src/js/constants/overviewConstants.ts +++ b/src/js/constants/overviewConstants.ts @@ -3,5 +3,5 @@ export const COUNTS_FILL = '#75787a'; export const LOCALSTORAGE_CHARTS_KEY = 'charts'; export const CHART_HEIGHT = 350; -export const PIE_CHART_HEIGHT = 300; // rendered slightly smaller since labels can clip +export const PIE_CHART_HEIGHT = 300; // rendered slightly smaller since labels can clip export const DEFAULT_CHART_WIDTH = 1; diff --git a/src/js/features/data/data.store.ts b/src/js/features/data/data.store.ts index 8eaa908a..34c697c8 100644 --- a/src/js/features/data/data.store.ts +++ b/src/js/features/data/data.store.ts @@ -34,7 +34,7 @@ const data = createSlice({ const { section, id } = payload; state.sections.find((e) => e.sectionTitle === section)!.charts.find((e) => e.id === id)!.isDisplayed = false; }, - setDisplayedCharts: (state, { payload }: PayloadAction<{ section: string, charts: string[] }>) => { + setDisplayedCharts: (state, { payload }: PayloadAction<{ section: string; charts: string[] }>) => { const { section, charts } = payload; state.sections .find((e) => e.sectionTitle === section)! @@ -42,11 +42,9 @@ const data = createSlice({ arr[ind].isDisplayed = charts.includes(val.id); }); }, - setChartWidth: (state, { payload }: PayloadAction<{ section: string, chart: string, width: number }>) => { + setChartWidth: (state, { payload }: PayloadAction<{ section: string; chart: string; width: number }>) => { const { section, chart, width } = payload; - const chartObj = state.sections - .find((e) => e.sectionTitle === section)! - .charts.find((c) => c.id === chart)!; + const chartObj = state.sections.find((e) => e.sectionTitle === section)!.charts.find((c) => c.id === chart)!; chartObj.width = width; }, }, diff --git a/src/js/features/data/makeGetDataRequest.thunk.ts b/src/js/features/data/makeGetDataRequest.thunk.ts index 8931ebca..afcae1e5 100644 --- a/src/js/features/data/makeGetDataRequest.thunk.ts +++ b/src/js/features/data/makeGetDataRequest.thunk.ts @@ -37,7 +37,7 @@ export const makeGetDataRequestThunk = createAsyncThunk< data: serializeChartData(field.data), // Initial display state isDisplayed: i < MAX_CHARTS, - width: chart.width ?? DEFAULT_CHART_WIDTH, // initial configured width; users can change it from here + width: chart.width ?? DEFAULT_CHART_WIDTH, // initial configured width; users can change it from here }; }; diff --git a/src/js/types/data.ts b/src/js/types/data.ts index 95c0c809..8c6f46e4 100644 --- a/src/js/types/data.ts +++ b/src/js/types/data.ts @@ -21,8 +21,8 @@ export interface ChartDataField { field: Omit; chartConfig: ChartConfig; // display options: - isDisplayed: boolean; // whether the chart is currently displayed (state data) - width: number; // current width (state data); initial data taken from chart config + isDisplayed: boolean; // whether the chart is currently displayed (state data) + width: number; // current width (state data); initial data taken from chart config } export interface ChartData { From 7c114966f5c1300bcbd71618210bbc9a6b478028 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 11 Oct 2023 12:36:15 -0400 Subject: [PATCH 4/7] style: tweak breakpoints for overview display width --- .../components/Overview/OverviewDisplayData.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/js/components/Overview/OverviewDisplayData.tsx b/src/js/components/Overview/OverviewDisplayData.tsx index 9d0b6132..3b1f4daf 100644 --- a/src/js/components/Overview/OverviewDisplayData.tsx +++ b/src/js/components/Overview/OverviewDisplayData.tsx @@ -8,19 +8,26 @@ import ChartCard from './ChartCard'; const CHART_GUTTER = 16; const getColumnCount = (width: number): number => { - if (width < 990) { + if (width < 880) { return 1; } else if (width < 1420) { return 2; } else return 3; }; +// Keep these quantized rather than a function of width for two reasons: +// - makes design more predictable +// - we don't need to re-render children for all width changes, just for some const getFrameWidth = (width: number): number => { - if (width < 440) { + if (width < 820) { return 440; + } else if (width < 1060) { + return 780; } else if (width < 1420) { - return width - 60; - } else return 1325; + return 960; + } else { + return 1325; + } }; const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) => { From e6ef2a28785e25524f5e3b6ea3e761e567d2f2c7 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 24 Oct 2023 18:15:44 -0400 Subject: [PATCH 5/7] fix: redraw charts on width change --- src/js/components/Overview/ChartCard.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/js/components/Overview/ChartCard.tsx b/src/js/components/Overview/ChartCard.tsx index 8bd2de14..4ac93568 100644 --- a/src/js/components/Overview/ChartCard.tsx +++ b/src/js/components/Overview/ChartCard.tsx @@ -9,6 +9,7 @@ import { ChartDataField } from '@/types/data'; import { CHART_HEIGHT } from '@/constants/overviewConstants'; const CARD_STYLE = { width: '100%', height: '415px', borderRadius: '11px' }; +const ROW_EMPTY_STYLE = { height: `${CHART_HEIGHT}px` }; const ChartCard = memo(({ section, chart, onRemoveChart, width }: ChartCardProps) => { const { t } = useTranslation(NON_DEFAULT_TRANSLATION); @@ -39,12 +40,13 @@ const ChartCard = memo(({ section, chart, onRemoveChart, width }: ChartCardProps // missing count label const missingCount = data.find((e) => e.x === 'missing')?.y ?? 0; - if (missingCount) + if (missingCount) { ed.push( {missingCount} {td('missing')} ); + } // controls (buttons) extraOptionsData.forEach((opt) => { @@ -55,13 +57,20 @@ const ChartCard = memo(({ section, chart, onRemoveChart, width }: ChartCardProps ); }); + // We add a key to the chart which includes width to force a re-render if width changes. return (
{ed}}> {data.filter((e) => !(e.x === 'missing')).length !== 0 ? ( - + ) : ( - + )} From ce49226edc9b61e6d9ee4c0f4136650ff01cec03 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 24 Oct 2023 18:32:33 -0400 Subject: [PATCH 6/7] feat: save configured chart width to localstorage --- src/js/components/Overview/PublicOverview.tsx | 14 ++++++++++++-- src/js/features/data/makeGetDataRequest.thunk.ts | 3 ++- src/js/types/data.ts | 2 +- src/js/utils/localStorage.ts | 12 ++++++------ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/js/components/Overview/PublicOverview.tsx b/src/js/components/Overview/PublicOverview.tsx index 7c2bc4ca..f90cb19a 100644 --- a/src/js/components/Overview/PublicOverview.tsx +++ b/src/js/components/Overview/PublicOverview.tsx @@ -3,6 +3,8 @@ import { Row, Col, FloatButton, Card, Skeleton } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; import { convertSequenceAndDisplayData, saveValue } from '@/utils/localStorage'; +import type { Sections } from '@/types/data'; + import { LOCALSTORAGE_CHARTS_KEY } from '@/constants/overviewConstants'; import OverviewSection from './OverviewSection'; @@ -28,7 +30,7 @@ const PublicOverview = () => { useEffect(() => { // Save sections to localStorage when they change - saveValue(LOCALSTORAGE_CHARTS_KEY, convertSequenceAndDisplayData(sections)); + saveToLocalStorage(sections); }, [sections]); useEffect(() => { @@ -40,7 +42,11 @@ const PublicOverview = () => { const displayedSections = sections.filter(({ charts }) => charts.findIndex(({ isDisplayed }) => isDisplayed) !== -1); const onManageChartsOpen = useCallback(() => setDrawerVisible(true), []); - const onManageChartsClose = useCallback(() => setDrawerVisible(false), []); + const onManageChartsClose = useCallback(() => { + setDrawerVisible(false); + // When we close the drawer, save any changes to localStorage. This helps ensure width gets saved: + saveToLocalStorage(sections); + }, [sections]); return ( <> @@ -86,4 +92,8 @@ const PublicOverview = () => { ); }; +const saveToLocalStorage = (sections: Sections) => { + saveValue(LOCALSTORAGE_CHARTS_KEY, convertSequenceAndDisplayData(sections)); +}; + export default PublicOverview; diff --git a/src/js/features/data/makeGetDataRequest.thunk.ts b/src/js/features/data/makeGetDataRequest.thunk.ts index afcae1e5..8768fcc9 100644 --- a/src/js/features/data/makeGetDataRequest.thunk.ts +++ b/src/js/features/data/makeGetDataRequest.thunk.ts @@ -52,9 +52,10 @@ export const makeGetDataRequestThunk = createAsyncThunk< verifyData(val, convertedData) ); sectionData.forEach(({ sectionTitle, charts }, i, arr) => { - arr[i].charts = localValue[sectionTitle].map(({ id, isDisplayed }) => ({ + arr[i].charts = localValue[sectionTitle].map(({ id, isDisplayed, width }) => ({ ...charts.find((c) => c.id === id)!, isDisplayed, + width, })); }); diff --git a/src/js/types/data.ts b/src/js/types/data.ts index 8c6f46e4..043d3975 100644 --- a/src/js/types/data.ts +++ b/src/js/types/data.ts @@ -31,5 +31,5 @@ export interface ChartData { } export type LocalStorageData = { - [key in string]: { id: string; isDisplayed: boolean }[]; + [key in string]: { id: string; isDisplayed: boolean, width: number }[]; }; diff --git a/src/js/utils/localStorage.ts b/src/js/utils/localStorage.ts index 61f6072d..d1710590 100644 --- a/src/js/utils/localStorage.ts +++ b/src/js/utils/localStorage.ts @@ -7,8 +7,8 @@ export const verifyData = (nObj: any, oObj: LocalStorageData) => { if (nCharts.length !== oCharts.length) return false; const nChartsMap: { [key in string]: boolean } = {}; - for (const { id, isDisplayed } of nCharts) { - if (id && typeof isDisplayed === 'boolean') nChartsMap[id] = true; + for (const { id, isDisplayed, width } of nCharts) { + if (id && typeof isDisplayed === 'boolean' && typeof width === 'number') nChartsMap[id] = true; else return false; } return oCharts.every((e) => nChartsMap[e.id]); @@ -33,12 +33,12 @@ export const getValue = (key: string, defaultVal: T, verifyFunc: (arg: any) = if (serializedState === null) { return defaultVal; } - const unserializedState = JSON.parse(serializedState); - if (!verifyFunc(unserializedState)) { + const deserializedState = JSON.parse(serializedState); + if (!verifyFunc(deserializedState)) { return defaultVal; } - return unserializedState as T; + return deserializedState as T; } catch (err) { console.log(err); console.log('Error retrieving state from localStorage'); @@ -49,7 +49,7 @@ export const getValue = (key: string, defaultVal: T, verifyFunc: (arg: any) = export const convertSequenceAndDisplayData = (sections: Sections) => { const temp: LocalStorageData = {}; sections.forEach(({ sectionTitle, charts }) => { - temp[sectionTitle] = charts.map(({ id, isDisplayed }) => ({ id, isDisplayed })); + temp[sectionTitle] = charts.map(({ id, isDisplayed, width }) => ({ id, isDisplayed, width })); }); return temp; }; From 33d288dd8385918e4394c5eec2ad14521ca27039 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 24 Oct 2023 18:38:30 -0400 Subject: [PATCH 7/7] lint --- src/js/types/data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/types/data.ts b/src/js/types/data.ts index 043d3975..b7687e55 100644 --- a/src/js/types/data.ts +++ b/src/js/types/data.ts @@ -31,5 +31,5 @@ export interface ChartData { } export type LocalStorageData = { - [key in string]: { id: string; isDisplayed: boolean, width: number }[]; + [key in string]: { id: string; isDisplayed: boolean; width: number }[]; };