diff --git a/packages/ui-chart-components/src/components/Axes/VerticalTick.tsx b/packages/ui-chart-components/src/components/Axes/VerticalTick.tsx index d476ce856d..845976ba2f 100644 --- a/packages/ui-chart-components/src/components/Axes/VerticalTick.tsx +++ b/packages/ui-chart-components/src/components/Axes/VerticalTick.tsx @@ -23,8 +23,11 @@ interface VerticalTickProps { export const VerticalTick = ({ x, y, payload }: VerticalTickProps) => { const stringVal = payload.value !== undefined && payload.value !== null ? String(payload.value) : ''; + const marginX = 10; + const marginY = 5; + return ( - + { - if (label && isEnlarged && !isExporting) { + if (label && isEnlarged) { return { value: label, fill: fillColor, - offset: -5, + offset: isExporting ? -50 : -5, position: 'insideBottom', }; } @@ -76,6 +76,7 @@ interface XAxisProps { export const XAxis = ({ config, report, isExporting = false, isEnlarged = false }: XAxisProps) => { const fillColor = isExporting ? DARK_BLUE : getContrastTextColor(); + const tickMargin = isExporting ? 20 : 0; const { Bar, Composed } = ChartType; const { chartType, chartConfig } = config; const { data = [] } = report; @@ -154,6 +155,7 @@ export const XAxis = ({ config, report, isExporting = false, isEnlarged = false return { left: 0, right: 10 }; }; + const renderVerticalTick = (tickProps: TickProps & { x: number; y: number }) => { const { payload, x, y } = tickProps; @@ -177,7 +179,7 @@ export const XAxis = ({ config, report, isExporting = false, isEnlarged = false tick={getXAxisTickMethod()} tickFormatter={formatXAxisTick} padding={getXAxisPadding()} - tickSize={6} + tickMargin={tickMargin} {...(isTimeSeries ? AXIS_TIME_PROPS : { dataKey: 'name' })} /> ); diff --git a/packages/ui-chart-components/src/components/CartesianChart.tsx b/packages/ui-chart-components/src/components/CartesianChart.tsx index a809f9c391..4442073b53 100644 --- a/packages/ui-chart-components/src/components/CartesianChart.tsx +++ b/packages/ui-chart-components/src/components/CartesianChart.tsx @@ -23,7 +23,8 @@ import { LineChart as LineChartComponent, AreaChart as AreaChartComponent, } from './Charts'; -import { getCartesianLegend, ReferenceLines, ChartTooltip as CustomTooltip } from './Reference'; +import { ReferenceLines, ChartTooltip as CustomTooltip } from './Reference'; +import { getCartesianChartLegend } from './Legend'; import { XAxis as XAxisComponent, YAxes } from './Axes'; const { Area, Bar, Composed, Line } = ChartType; @@ -74,9 +75,9 @@ const getRealDataKeys = (chartConfig: CartesianChartConfig['chartConfig'] | {}) const getLegendAlignment = ( legendPosition: LegendPosition, isExporting: boolean, -): Pick => { +): Pick => { if (isExporting) { - return { verticalAlign: 'top', align: 'right', layout: 'vertical' }; + return { verticalAlign: 'top', align: 'right' }; } if (legendPosition === 'bottom') { return { verticalAlign: 'bottom', align: 'center' }; @@ -218,7 +219,7 @@ export const CartesianChart = ({ const aspect = !isEnlarged && !isMobileSize && !isExporting ? 1.6 : undefined; const height = getHeight(isExporting, isEnlarged, hasLegend, isMobileSize); - const { verticalAlign, align, layout } = getLegendAlignment(legendPosition, isExporting); + const { verticalAlign, align } = getLegendAlignment(legendPosition, isExporting); const presentationOptions = 'presentationOptions' in config ? config.presentationOptions : {}; @@ -252,8 +253,7 @@ export const CartesianChart = ({ { + if (isExporting) { + return '#2c3236'; + } + + if (theme.palette.type === 'light') { + return theme.palette.text.primary; + } + return 'white'; +}; + +export const LegendItem = styled(({ isExporting, ...props }) => )` + text-align: left; + font-size: 0.75rem; + padding-bottom: 0; + padding-top: 0; + margin-right: 1.2rem; + + .MuiButton-label { + display: flex; + white-space: nowrap; + align-items: center; + color: ${({ theme, isExporting }) => getLegendTextColor(theme, isExporting)}; + > div { + width: ${isExporting => (isExporting ? '100%' : '')}; + } + } + + &.Mui-disabled { + color: ${({ theme, isExporting }) => getLegendTextColor(theme, isExporting)}; + } + + // small styles + &.small { + font-size: 0.5rem; + padding-bottom: 0; + padding-top: 0; + margin-right: 0; + } +`; diff --git a/packages/ui-chart-components/src/components/Legend/getCartesianChartLegend.tsx b/packages/ui-chart-components/src/components/Legend/getCartesianChartLegend.tsx new file mode 100644 index 0000000000..56eefefb86 --- /dev/null +++ b/packages/ui-chart-components/src/components/Legend/getCartesianChartLegend.tsx @@ -0,0 +1,95 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import React from 'react'; +import { CartesianChartConfig } from '@tupaia/types'; +import styled from 'styled-components'; +import { TooltipPayload } from 'recharts'; +import Tooltip from '@material-ui/core/Tooltip'; +import { LegendPosition } from '../../types'; +import { isMobile } from '../../utils'; +import { LegendItem } from './LegendItem'; + +const LegendContainer = styled.div<{ + $position?: LegendPosition; +}>` + display: flex; + flex-wrap: wrap; + justify-content: center; + flex-direction: row; + // Add more padding at the bottom for exports + padding: ${props => (props.$position === 'bottom' ? '1em 0 0 3.5em' : '0 0 3em 0')}; +`; + +const TooltipContainer = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + padding-bottom: 0.3rem; + padding-top: 0.3rem; +`; + +const Box = styled.span` + display: block; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.625rem; + border-radius: 3px; + + // small styles + &.small { + width: 0.8rem; + min-width: 0.8rem; + height: 0.8rem; + margin-right: 0.4rem; + } +`; + +const Text = styled.span` + line-height: 1.4; +`; + +interface CartesianLegendProps { + chartConfig: CartesianChartConfig['chartConfig']; + onClick: Function; + getIsActiveKey: Function; + isExporting?: boolean; + legendPosition?: LegendPosition; +} + +export const getCartesianChartLegend = + ({ chartConfig, onClick, getIsActiveKey, isExporting, legendPosition }: CartesianLegendProps) => + ({ payload }: any) => { + const isMobileSize = isMobile(isExporting); + return ( + + {payload.map(({ color, value, dataKey }: TooltipPayload) => { + // check the type here because according to TooltipPayload, value can be a number or a readonly string | number array + const displayValue = (typeof value === 'string' && chartConfig?.[value]?.label) || value; + + return ( + onClick(dataKey)} + isExporting={isExporting} + className={isMobileSize ? 'small' : 'enlarged'} + style={{ textDecoration: getIsActiveKey(value) ? '' : 'line-through' }} + > + + + + {displayValue} + + + + ); + })} + + ); + }; diff --git a/packages/ui-chart-components/src/components/Legend/getPieChartLegend.tsx b/packages/ui-chart-components/src/components/Legend/getPieChartLegend.tsx new file mode 100644 index 0000000000..daded925af --- /dev/null +++ b/packages/ui-chart-components/src/components/Legend/getPieChartLegend.tsx @@ -0,0 +1,109 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import React from 'react'; +import { PieChartConfig } from '@tupaia/types'; +import { TooltipPayload } from 'recharts'; +import { formatDataValueByType } from '@tupaia/utils'; +import { LegendPosition } from '../../types'; +import { isMobile } from '../../utils'; +import { LegendItem } from './LegendItem'; +import styled from 'styled-components'; + +const PieLegendContainer = styled.div<{ + $position?: LegendPosition; + $isExporting?: boolean; +}>` + display: flex; + flex-wrap: ${props => (props.$isExporting ? 'nowrap' : 'wrap')}; + justify-content: ${props => (props.$position === 'bottom' ? 'center' : 'flex-start')}; + flex-direction: ${props => (props.$isExporting ? 'column' : 'row')}; + padding: 0; +`; + +const TooltipContainer = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + padding-bottom: 0.3rem; + padding-top: 0.3rem; +`; + +const Box = styled.span` + display: block; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.625rem; + border-radius: 3px; + + // small styles + &.small { + width: 0.8rem; + min-width: 0.8rem; + height: 0.8rem; + margin-right: 0.4rem; + } +`; + +const Text = styled.span` + line-height: 1.4; +`; + +interface PieLegendProps { + isEnlarged?: boolean; + isExporting?: boolean; + legendPosition?: LegendPosition; + config: PieChartConfig; +} + +const getPieLegendDisplayValue = ( + value: TooltipPayload['value'], + item: any, + config: PieLegendProps['config'], + isEnlarged?: PieLegendProps['isEnlarged'], + isMobileSize?: boolean, +) => { + const metadata = item[`${value}_metadata`]; + const labelSuffix = formatDataValueByType({ value: item.value, metadata }, config.valueType); + + // on mobile the legend will show the actual formatDataValueByType after the label value + return isMobileSize && isEnlarged ? `${value} ${labelSuffix}` : value; +}; + +export const getPieChartLegend = + ({ isEnlarged, isExporting, legendPosition, config }: PieLegendProps) => + ({ payload }: any) => { + const isMobileSize = isMobile(isExporting); + return ( + + {payload.map(({ color, value, payload: item }: TooltipPayload) => { + const displayValue = getPieLegendDisplayValue( + value, + item, + config, + isEnlarged, + isMobileSize, + ); + + return ( + + + + {displayValue} + + + ); + })} + + ); + }; diff --git a/packages/ui-chart-components/src/components/Legend/index.ts b/packages/ui-chart-components/src/components/Legend/index.ts new file mode 100644 index 0000000000..d6f9a75733 --- /dev/null +++ b/packages/ui-chart-components/src/components/Legend/index.ts @@ -0,0 +1,7 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +export { getCartesianChartLegend } from './getCartesianChartLegend'; +export { getPieChartLegend } from './getPieChartLegend'; diff --git a/packages/ui-chart-components/src/components/Reference/Legend.tsx b/packages/ui-chart-components/src/components/Reference/Legend.tsx deleted file mode 100644 index 0c65473f92..0000000000 --- a/packages/ui-chart-components/src/components/Reference/Legend.tsx +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Tupaia - * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd - */ - -import React from 'react'; -import styled from 'styled-components'; -import MuiButton from '@material-ui/core/Button'; -import Tooltip from '@material-ui/core/Tooltip'; -import { TooltipPayload } from 'recharts'; -import { CartesianChartConfig, ChartReport, PieChartConfig } from '@tupaia/types'; -import { formatDataValueByType } from '@tupaia/utils'; -import { LegendPosition } from '../../types'; -import { isMobile } from '../../utils'; - -const LegendContainer = styled.div<{ - $position?: LegendPosition; - $isExporting?: boolean; -}>` - display: flex; - flex-wrap: ${props => (props.$isExporting ? 'nowrap' : 'wrap')}; - justify-content: ${props => (props.$position === 'bottom' ? 'center' : 'flex-start')}; - flex-direction: ${props => (props.$isExporting ? 'column' : 'row')}; - // Add more padding at the bottom for exports - padding: ${props => { - if (props.$isExporting) { - return '1em'; - } - return props.$position === 'bottom' ? '1rem 0 0 3.5rem' : '0 0 2rem 0'; - }}; -`; - -const PieLegendContainer = styled(LegendContainer)` - padding: 0; -`; - -const getLegendTextColor = (theme: any, isExporting: boolean) => { - if (isExporting) { - return '#2c3236'; - } - - if (theme.palette.type === 'light') { - return theme.palette.text.primary; - } - return 'white'; -}; - -const LegendItem = styled(({ isExporting, ...props }) => )` - text-align: left; - font-size: 0.75rem; - padding-bottom: 0; - padding-top: 0; - margin-right: 1.2rem; - - .MuiButton-label { - display: flex; - white-space: nowrap; - align-items: ${isExporting => (isExporting ? 'left' : 'center')}; - color: ${({ theme, isExporting }) => getLegendTextColor(theme, isExporting)}; - > div { - width: ${isExporting => (isExporting ? '100%' : '')}; - } - } - - &.Mui-disabled { - color: ${({ theme, isExporting }) => getLegendTextColor(theme, isExporting)}; - } - - // small styles - &.small { - font-size: 0.5rem; - padding-bottom: 0; - padding-top: 0; - margin-right: 0; - } -`; - -const TooltipContainer = styled.div` - display: flex; - align-items: center; - justify-content: flex-start; - padding-bottom: 0.3rem; - padding-top: 0.3rem; -`; - -const Box = styled.span` - display: block; - width: 1.25rem; - height: 1.25rem; - margin-right: 0.625rem; - border-radius: 3px; - - // small styles - &.small { - width: 0.8rem; - min-width: 0.8rem; - height: 0.8rem; - margin-right: 0.4rem; - } -`; - -const Text = styled.span` - line-height: 1.4; -`; - -interface PieLegendProps { - isEnlarged?: boolean; - isExporting?: boolean; - legendPosition?: LegendPosition; - config: PieChartConfig; -} - -const getPieLegendDisplayValue = ( - value: TooltipPayload['value'], - item: any, - config: PieLegendProps['config'], - isEnlarged?: PieLegendProps['isEnlarged'], - isMobileSize?: boolean, -) => { - const metadata = item[`${value}_metadata`]; - const labelSuffix = formatDataValueByType({ value: item.value, metadata }, config.valueType); - - // on mobile the legend will show the actual formatDataValueByType after the label value - return isMobileSize && isEnlarged ? `${value} ${labelSuffix}` : value; -}; - -export const getPieLegend = - ({ isEnlarged, isExporting, legendPosition, config }: PieLegendProps) => - ({ payload }: any) => { - const isMobileSize = isMobile(isExporting); - return ( - - {payload.map(({ color, value, payload: item }: TooltipPayload) => { - const displayValue = getPieLegendDisplayValue( - value, - item, - config, - isEnlarged, - isMobileSize, - ); - - return ( - - - - {displayValue} - - - ); - })} - - ); - }; - -interface CartesianLegendProps { - chartConfig: CartesianChartConfig['chartConfig']; - onClick: Function; - getIsActiveKey: Function; - isExporting?: boolean; - legendPosition?: LegendPosition; -} - -export const getCartesianLegend = - ({ chartConfig, onClick, getIsActiveKey, isExporting, legendPosition }: CartesianLegendProps) => - ({ payload }: any) => { - const isMobileSize = isMobile(isExporting); - return ( - - {payload.map(({ color, value, dataKey }: TooltipPayload) => { - // check the type here because according to TooltipPayload, value can be a number or a readonly string | number array - const displayValue = (typeof value === 'string' && chartConfig?.[value]?.label) || value; - - return ( - onClick(dataKey)} - isExporting={isExporting} - className={isMobileSize ? 'small' : 'enlarged'} - style={{ textDecoration: getIsActiveKey(value) ? '' : 'line-through' }} - > - - - - {displayValue} - - - - ); - })} - - ); - }; diff --git a/packages/ui-chart-components/src/components/Reference/index.ts b/packages/ui-chart-components/src/components/Reference/index.ts index 84a88caf9a..6f891702c8 100644 --- a/packages/ui-chart-components/src/components/Reference/index.ts +++ b/packages/ui-chart-components/src/components/Reference/index.ts @@ -4,7 +4,6 @@ */ export { ChartTooltip } from './ChartTooltip'; -export * from './Legend'; export { ReferenceLabel } from './ReferenceLabel'; export { ReferenceLines } from './ReferenceLines'; export { TooltipContainer } from './TooltipContainer';