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

feat: allow for multi-column-width charts and improve responsivity #114

Merged
merged 9 commits into from
Oct 25, 2023
10 changes: 4 additions & 6 deletions src/js/components/Overview/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 (
<BarChart
data={data}
height={CHART_HEIGHT + 50}
height={CHART_HEIGHT}
units={units}
preFilter={removeMissing}
dataMap={translateMap}
Expand All @@ -35,7 +34,7 @@ const Chart = memo(({ chartConfig, data, units, id }: ChartProps) => {
return (
<PieChart
data={data}
height={CHART_HEIGHT}
height={PIE_CHART_HEIGHT}
preFilter={removeMissing}
dataMap={translateMap}
onClick={(d) => {
Expand All @@ -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 (
<ChoroplethMap
data={data}
height={CHART_HEIGHT + 50}
height={CHART_HEIGHT}
preFilter={removeMissing}
dataMap={translateMap}
categoryProp={categoryProp}
Expand Down
23 changes: 17 additions & 6 deletions src/js/components/Overview/ChartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { useTranslation } from 'react-i18next';
import CustomEmpty from '../Util/CustomEmpty';
import { DEFAULT_TRANSLATION, NON_DEFAULT_TRANSLATION } from '@/constants/configConstants';
import { ChartDataField } from '@/types/data';
import { CHART_HEIGHT } from '@/constants/overviewConstants';

const CARD_STYLE = { width: '100%', height: '415px', margin: '5px 0', borderRadius: '11px' };
const CARD_STYLE = { width: '100%', height: '415px', borderRadius: '11px' };
const ROW_EMPTY_STYLE = { height: `${CHART_HEIGHT}px` };

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);

Expand Down Expand Up @@ -38,12 +40,13 @@ const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => {
// missing count label
const missingCount = data.find((e) => e.x === 'missing')?.y ?? 0;

if (missingCount)
if (missingCount) {
ed.push(
<Typography.Text key={0} type="secondary" italic>
<TeamOutlined /> {missingCount} {td('missing')}
</Typography.Text>
);
}

// controls (buttons)
extraOptionsData.forEach((opt) => {
Expand All @@ -54,13 +57,20 @@ const ChartCard = memo(({ section, chart, onRemoveChart }: ChartCardProps) => {
);
});

// We add a key to the chart which includes width to force a re-render if width changes.
return (
<div key={id} style={{ height: '100%', width: '430px' }}>
<div key={id} style={{ height: '100%', width }}>
<Card title={t(title)} style={CARD_STYLE} size="small" extra={<Space size="small">{ed}</Space>}>
{data.filter((e) => !(e.x === 'missing')).length !== 0 ? (
<Chart chartConfig={chartConfig} data={data} units={config?.units || ''} id={id} />
<Chart
chartConfig={chartConfig}
data={data}
units={config?.units || ''}
id={id}
key={`${id}-width-${width}`}
/>
) : (
<Row style={{ height: '350px ' }} justify="center" align="middle">
<Row style={ROW_EMPTY_STYLE} justify="center" align="middle">
<CustomEmpty text="No Data" />
</Row>
)}
Expand All @@ -75,6 +85,7 @@ export interface ChartCardProps {
section: string;
chart: ChartDataField;
onRemoveChart: (arg: { section: string; id: string }) => void;
width: number;
}

export default ChartCard;
36 changes: 30 additions & 6 deletions src/js/components/Overview/Drawer/ChartTree.tsx
Original file line number Diff line number Diff line change
@@ -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;
}

Expand All @@ -17,7 +17,31 @@ 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: (
<div style={{ display: 'flex' }}>
<span style={{ flex: 1 }}>{t(title)}</span>
<span>
Width:{' '}
<InputNumber
size="small"
min={1}
max={3}
value={width}
onChange={(v) => {
if (v) {
dispatch(setChartWidth({ section, chart: id, width: v }));
}
}}
controls={true}
style={{ width: 50 }}
/>
</span>
</div>
),
key: id,
})),
[charts]
);

Expand All @@ -38,7 +62,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]);
Expand Down
50 changes: 32 additions & 18 deletions src/js/components/Overview/OverviewDisplayData.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
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) {
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 < 990) {
return 360;
if (width < 820) {
return 440;
} else if (width < 1060) {
return 780;
} else if (width < 1420) {
return 910;
} else return 1325;
return 960;
} else {
return 1325;
}
};

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<CSSProperties>(
() => ({ width: frameWidth, display: 'flex', flexWrap: 'wrap', gap: CHART_GUTTER }),
[frameWidth]
);

const displayedCharts = useMemo(() => allCharts.filter((e) => e.isDisplayed), [allCharts]);
Expand All @@ -42,19 +52,23 @@ const OverviewDisplayData = ({ section, allCharts }: OverviewDisplayDataProps) =
);

const renderItem = useCallback(
(chart: ChartDataField) => (
<ChartCard key={chart.id} chart={chart} section={section} onRemoveChart={onRemoveChart} />
),
[section, onRemoveChart]
(chart: ChartDataField) => {
const columnWidth = Math.min(chart.width, columnCount);
const pixelWidth = (columnWidth / columnCount) * (frameWidth - CHART_GUTTER * (columnCount - columnWidth));
return (
<ChartCard key={chart.id} chart={chart} section={section} onRemoveChart={onRemoveChart} width={pixelWidth} />
);
},
[section, onRemoveChart, width]
);

return <List style={listStyle} grid={listGrid} dataSource={displayedCharts} renderItem={renderItem} />;
return <div style={containerStyle}>{displayedCharts.map(renderItem)}</div>;
};

const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: 0,
height: 0,
width: window.innerWidth,
height: window.innerHeight,
});

useEffect(() => {
Expand Down
14 changes: 12 additions & 2 deletions src/js/components/Overview/PublicOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,7 +30,7 @@ const PublicOverview = () => {

useEffect(() => {
// Save sections to localStorage when they change
saveValue(LOCALSTORAGE_CHARTS_KEY, convertSequenceAndDisplayData(sections));
saveToLocalStorage(sections);
}, [sections]);

useEffect(() => {
Expand All @@ -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 (
<>
Expand Down Expand Up @@ -86,4 +92,8 @@ const PublicOverview = () => {
);
};

const saveToLocalStorage = (sections: Sections) => {
saveValue(LOCALSTORAGE_CHARTS_KEY, convertSequenceAndDisplayData(sections));
};

export default PublicOverview;
6 changes: 3 additions & 3 deletions src/js/components/Search/SearchResultsPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { COUNTS_FILL, PIE_CHART_HEIGHT } from '@/constants/overviewConstants';
import { useTranslationDefault } from '@/hooks';
import { ChartData } from '@/types/data';

Expand Down Expand Up @@ -53,15 +53,15 @@ const SearchResultsPane = ({
<Col xs={24} lg={10}>
<Typography.Title level={5}>{t('Biosamples')}</Typography.Title>
{!hasInsufficientData && biosampleChartData.length ? (
<PieChart data={biosampleChartData} height={CHART_HEIGHT} sort={true} />
<PieChart data={biosampleChartData} height={PIE_CHART_HEIGHT} sort={true} />
) : (
<CustomEmpty text="No Results" />
)}
</Col>
<Col xs={24} lg={10}>
<Typography.Title level={5}>{t('Experiments')}</Typography.Title>
{!hasInsufficientData && experimentChartData.length ? (
<PieChart data={experimentChartData} height={CHART_HEIGHT} sort={true} />
<PieChart data={experimentChartData} height={PIE_CHART_HEIGHT} sort={true} />
) : (
<CustomEmpty text="No Results" />
)}
Expand Down
4 changes: 3 additions & 1 deletion src/js/constants/overviewConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
9 changes: 7 additions & 2 deletions src/js/features/data/data.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ 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)!
.charts.forEach((val, ind, arr) => {
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
Expand All @@ -59,6 +64,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;
9 changes: 6 additions & 3 deletions src/js/features/data/makeGetDataRequest.thunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
};
};

Expand All @@ -50,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,
}));
});

Expand Down
Loading