Skip to content

Commit

Permalink
Merge pull request #114 from bento-platform/feat/configurable-width
Browse files Browse the repository at this point in the history
feat: allow for multi-column-width charts and improve responsivity
  • Loading branch information
davidlougheed authored Oct 25, 2023
2 parents 9dc86e2 + 33d288d commit b67fdf5
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 73 deletions.
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

0 comments on commit b67fdf5

Please sign in to comment.