diff --git a/packages/desktop-client/src/components/reports/ChooseGraph.tsx b/packages/desktop-client/src/components/reports/ChooseGraph.tsx index c1da3e8b8a5..7a91d01dc74 100644 --- a/packages/desktop-client/src/components/reports/ChooseGraph.tsx +++ b/packages/desktop-client/src/components/reports/ChooseGraph.tsx @@ -25,6 +25,7 @@ type ChooseGraphProps = { scrollWidth: number; setScrollWidth: (value: number) => void; months: Month[]; + viewLabels: boolean; }; export function ChooseGraph({ @@ -37,6 +38,7 @@ export function ChooseGraph({ scrollWidth, setScrollWidth, months, + viewLabels, }: ChooseGraphProps) { const balanceTypeOp = ReportOptions.balanceTypeMap.get(balanceType); @@ -69,6 +71,7 @@ export function ChooseGraph({ style={{ flexGrow: 1 }} data={data} balanceTypeOp={balanceTypeOp} + viewLabels={viewLabels} /> ); } @@ -79,6 +82,7 @@ export function ChooseGraph({ data={data} groupBy={groupBy} balanceTypeOp={balanceTypeOp} + viewLabels={viewLabels} /> ); } @@ -92,6 +96,7 @@ export function ChooseGraph({ data={data} groupBy={groupBy} balanceTypeOp={balanceTypeOp} + viewLabels={viewLabels} /> ); } @@ -99,7 +104,13 @@ export function ChooseGraph({ return ; } if (graphType === 'StackedBarGraph') { - return ; + return ( + + ); } if (graphType === 'TableGraph') { return ( diff --git a/packages/desktop-client/src/components/reports/ReportTopbar.jsx b/packages/desktop-client/src/components/reports/ReportTopbar.jsx index 0f4bcda00e7..3a29388cfc2 100644 --- a/packages/desktop-client/src/components/reports/ReportTopbar.jsx +++ b/packages/desktop-client/src/components/reports/ReportTopbar.jsx @@ -137,8 +137,7 @@ export function ReportTopbar({ onChangeViews('viewLabels'); }} style={{ marginRight: 15 }} - title="Show labels" - disabled={true} + title="Show Labels" > diff --git a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx index 995d13d1105..d293e7e5355 100644 --- a/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/AreaGraph.tsx @@ -8,11 +8,15 @@ import { XAxis, YAxis, Tooltip, + LabelList, ResponsiveContainer, } from 'recharts'; import { usePrivacyMode } from 'loot-core/src/client/privacy'; -import { amountToCurrency } from 'loot-core/src/shared/util'; +import { + amountToCurrency, + amountToCurrencyNoDecimal, +} from 'loot-core/src/shared/util'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; @@ -22,6 +26,9 @@ import { Container } from '../Container'; import { type DataEntity } from '../entities'; import { numberFormatterTooltip } from '../numberFormatter'; +import { adjustTextSize } from './adjustTextSize'; +import { renderCustomLabel } from './renderCustomLabel'; + type PayloadItem = { payload: { date: string; @@ -91,11 +98,25 @@ const CustomTooltip = ({ } }; +const customLabel = (props, width, end) => { + //Add margin to first and last object + const calcX = + props.x + (props.index === end ? -10 : props.index === 0 ? 5 : 0); + const calcY = props.y - (props.value > 0 ? 10 : -10); + const textAnchor = props.index === 0 ? 'left' : 'middle'; + const display = + props.value !== 0 && `${amountToCurrencyNoDecimal(props.value)}`; + const textSize = adjustTextSize(width, 'area'); + + return renderCustomLabel(calcX, calcY, textAnchor, display, textSize); +}; + type AreaGraphProps = { style?: CSSProperties; data: DataEntity; balanceTypeOp: string; compact?: boolean; + viewLabels: boolean; }; export function AreaGraph({ @@ -103,8 +124,28 @@ export function AreaGraph({ data, balanceTypeOp, compact, + viewLabels, }: AreaGraphProps) { const privacyMode = usePrivacyMode(); + const dataMax = Math.max(...data.monthData.map(i => i[balanceTypeOp])); + const dataMin = Math.min(...data.monthData.map(i => i[balanceTypeOp])); + + const labelsMargin = viewLabels ? 30 : 0; + const dataDiff = dataMax - dataMin; + //Calculate how much to add to max and min values for graph range + const extendRangeAmount = Math.floor(dataDiff / 20); + const labelsMin = + //If min is zero or graph range passes zero then set it to zero + dataMin === 0 || Math.abs(dataMin) <= extendRangeAmount + ? 0 + : //Else add the range and round to nearest 100s + Math.floor((dataMin - extendRangeAmount) / 100) * 100; + //Same as above but for max + const labelsMax = + dataMax === 0 || Math.abs(dataMax) <= extendRangeAmount + ? 0 + : Math.ceil((dataMax + extendRangeAmount) / 100) * 100; + const lastLabel = data.monthData.length - 1; const tickFormatter = tick => { if (!privacyMode) return `${Math.round(tick).toLocaleString()}`; // Formats the tick values as strings with commas @@ -112,9 +153,6 @@ export function AreaGraph({ }; const gradientOffset = () => { - const dataMax = Math.max(...data.monthData.map(i => i[balanceTypeOp])); - const dataMin = Math.min(...data.monthData.map(i => i[balanceTypeOp])); - if (dataMax <= 0) { return 0; } @@ -143,7 +181,7 @@ export function AreaGraph({ width={width} height={height} data={data.monthData} - margin={{ top: 0, right: 0, left: 0, bottom: 0 }} + margin={{ top: 0, right: labelsMargin, left: 0, bottom: 0 }} > {compact ? null : ( @@ -158,7 +196,10 @@ export function AreaGraph({ {compact ? null : ( + > + {viewLabels && ( + customLabel(e, width, lastLabel)} + /> + )} + diff --git a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx index 40d8f2dfc7d..eadfe36a4df 100644 --- a/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/BarGraph.tsx @@ -10,11 +10,15 @@ import { XAxis, YAxis, Tooltip, + LabelList, ResponsiveContainer, } from 'recharts'; import { usePrivacyMode } from 'loot-core/src/client/privacy'; -import { amountToCurrency } from 'loot-core/src/shared/util'; +import { + amountToCurrency, + amountToCurrencyNoDecimal, +} from 'loot-core/src/shared/util'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; @@ -25,6 +29,9 @@ import { type DataEntity } from '../entities'; import { getCustomTick } from '../getCustomTick'; import { numberFormatterTooltip } from '../numberFormatter'; +import { adjustTextSize } from './adjustTextSize'; +import { renderCustomLabel } from './renderCustomLabel'; + type PayloadChild = { props: { name: string; @@ -106,12 +113,24 @@ const CustomTooltip = ({ } }; +const customLabel = props => { + const calcX = props.x + props.width / 2; + const calcY = props.y - (props.value > 0 ? 15 : -15); + const textAnchor = 'middle'; + const display = + props.value !== 0 && `${amountToCurrencyNoDecimal(props.value)}`; + const textSize = adjustTextSize(props.width, 'variable', props.value); + + return renderCustomLabel(calcX, calcY, textAnchor, display, textSize); +}; + type BarGraphProps = { style?: CSSProperties; data: DataEntity; groupBy: string; balanceTypeOp: string; compact?: boolean; + viewLabels: boolean; }; export function BarGraph({ @@ -120,11 +139,13 @@ export function BarGraph({ groupBy, balanceTypeOp, compact, + viewLabels, }: BarGraphProps) { const privacyMode = usePrivacyMode(); const yAxis = ['Month', 'Year'].includes(groupBy) ? 'date' : 'name'; const splitData = ['Month', 'Year'].includes(groupBy) ? 'monthData' : 'data'; + const labelsMargin = viewLabels ? 30 : 0; const getVal = obj => { if (balanceTypeOp === 'totalDebts') { @@ -155,9 +176,10 @@ export function BarGraph({ height={height} stackOffset="sign" data={data[splitData]} - margin={{ top: 0, right: 0, left: 0, bottom: 0 }} + margin={{ top: labelsMargin, right: 0, left: 0, bottom: 0 }} > )} getVal(val)} stackId="a"> + {viewLabels && ( + getVal(val)} + content={customLabel} + /> + )} {data.legend.map((entry, index) => ( {yAxis === 'date' && balanceTypeOp === 'totalTotals' && ( + {viewLabels && ( + + )} {data[splitData].map((entry, index) => ( { const { @@ -70,7 +75,7 @@ const ActiveShape = props => { dy={18} textAnchor={textAnchor} fill={fill} - >{`${value.toFixed(2)}`} + >{`${amountToCurrency(value)}`} { ); }; +const customLabel = props => { + const radius = + props.innerRadius + (props.outerRadius - props.innerRadius) * 0.5; + const size = props.cx > props.cy ? props.cy : props.cx; + + const calcX = props.cx + radius * Math.cos(-props.midAngle * RADIAN); + const calcY = props.cy + radius * Math.sin(-props.midAngle * RADIAN); + const textAnchor = calcX > props.cx ? 'start' : 'end'; + const display = props.value !== 0 && `${(props.percent * 100).toFixed(0)}%`; + const textSize = adjustTextSize(size, 'donut'); + const showLabel = props.percent; + const showLabelThreshold = 0.05; + const fill = theme.reportsInnerLabel; + + return renderCustomLabel( + calcX, + calcY, + textAnchor, + display, + textSize, + showLabel, + showLabelThreshold, + fill, + ); +}; + type DonutGraphProps = { style?: CSSProperties; data: DataEntity; groupBy: string; balanceTypeOp: string; compact?: boolean; + viewLabels: boolean; }; export function DonutGraph({ @@ -98,6 +130,7 @@ export function DonutGraph({ groupBy, balanceTypeOp, compact, + viewLabels, }: DonutGraphProps) { const yAxis = ['Month', 'Year'].includes(groupBy) ? 'date' : 'name'; const splitData = ['Month', 'Year'].includes(groupBy) ? 'monthData' : 'data'; @@ -139,6 +172,7 @@ export function DonutGraph({ innerRadius={Math.min(width, height) * 0.2} fill="#8884d8" labelLine={false} + label={e => (viewLabels ? customLabel(e) :
)} onMouseEnter={onPieEnter} > {data.legend.map((entry, index) => ( diff --git a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx index faf0a8e49bd..2f41809e820 100644 --- a/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/StackedBarGraph.tsx @@ -8,11 +8,15 @@ import { XAxis, YAxis, Tooltip, + LabelList, ResponsiveContainer, } from 'recharts'; import { usePrivacyMode } from 'loot-core/src/client/privacy'; -import { amountToCurrency } from 'loot-core/src/shared/util'; +import { + amountToCurrency, + amountToCurrencyNoDecimal, +} from 'loot-core/src/shared/util'; import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; @@ -23,6 +27,8 @@ import { type DataEntity } from '../entities'; import { getCustomTick } from '../getCustomTick'; import { numberFormatterTooltip } from '../numberFormatter'; +import { renderCustomLabel } from './renderCustomLabel'; + type PayloadItem = { name: string; value: number; @@ -91,16 +97,40 @@ const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { } }; +const customLabel = props => { + const calcX = props.x + props.width / 2; + const calcY = props.y + props.height / 2; + const textAnchor = 'middle'; + const display = props.value && `${amountToCurrencyNoDecimal(props.value)}`; + const textSize = '12px'; + const showLabel = props.height; + const showLabelThreshold = 20; + const fill = theme.reportsInnerLabel; + + return renderCustomLabel( + calcX, + calcY, + textAnchor, + display, + textSize, + showLabel, + showLabelThreshold, + fill, + ); +}; + type StackedBarGraphProps = { style?: CSSProperties; data: DataEntity; compact?: boolean; + viewLabels: boolean; }; export function StackedBarGraph({ style, data, compact, + viewLabels, }: StackedBarGraphProps) { const privacyMode = usePrivacyMode(); @@ -126,6 +156,7 @@ export function StackedBarGraph({ content={} formatter={numberFormatterTooltip} isAnimationActive={false} + cursor={{ fill: 'transparent' }} /> + > + {viewLabels && ( + + )} + ))}
diff --git a/packages/desktop-client/src/components/reports/graphs/adjustTextSize.ts b/packages/desktop-client/src/components/reports/graphs/adjustTextSize.ts new file mode 100644 index 00000000000..7c1e292bdc8 --- /dev/null +++ b/packages/desktop-client/src/components/reports/graphs/adjustTextSize.ts @@ -0,0 +1,89 @@ +export const adjustTextSize = ( + sized: number, + type: string, + values?: number, +): `${number}px` => { + let source; + switch (type) { + case 'variable': + source = variableLookup.find(({ value }) => values > value).arr; + break; + case 'donut': + source = donutLookup; + break; + default: + source = defaultLookup; + } + const lookup = source.find(({ size }) => sized >= size); + return `${lookup.font}px`; +}; + +const defaultLookup = [ + { size: 600, font: 16 }, + { size: 500, font: 15 }, + { size: 400, font: 14 }, + { size: 300, font: 13 }, + { size: 200, font: 12 }, + { size: 100, font: 11 }, + { size: 0, font: 10 }, +]; + +const donutLookup = [ + { size: 300, font: 20 }, + { size: 266, font: 18 }, + { size: 233, font: 16 }, + { size: 200, font: 14 }, + { size: 166, font: 12 }, + { size: 0, font: 10 }, +]; + +const variableLookup = [ + { + value: 10000, + arr: [ + { size: 66, font: 16 }, + { size: 60, font: 15 }, + { size: 54, font: 14 }, + { size: 48, font: 13 }, + { size: 42, font: 12 }, + { size: 36, font: 11 }, + { size: 0, font: 10 }, + ], + }, + { + value: 1000, + arr: [ + { size: 55, font: 16 }, + { size: 50, font: 15 }, + { size: 45, font: 14 }, + { size: 40, font: 13 }, + { size: 35, font: 12 }, + { size: 30, font: 11 }, + { size: 0, font: 10 }, + ], + }, + { + value: 100, + arr: [ + { size: 38, font: 16 }, + { size: 35, font: 15 }, + { size: 32, font: 14 }, + { size: 29, font: 13 }, + { size: 26, font: 12 }, + { size: 23, font: 11 }, + { size: 0, font: 10 }, + ], + }, + { + value: 0, + arr: [ + { size: 25, font: 16 }, + { size: 22, font: 15 }, + { size: 19, font: 14 }, + { size: 16, font: 13 }, + { size: 13, font: 12 }, + { size: 9, font: 11 }, + { size: 0, font: 10 }, + ], + }, +]; diff --git a/packages/desktop-client/src/components/reports/graphs/renderCustomLabel.tsx b/packages/desktop-client/src/components/reports/graphs/renderCustomLabel.tsx new file mode 100644 index 00000000000..acd7233fcef --- /dev/null +++ b/packages/desktop-client/src/components/reports/graphs/renderCustomLabel.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { theme } from '../../../style'; + +export const renderCustomLabel = ( + calcX: number, + calcY: number, + textAnchor: string, + display: string, + textSize?: string, + showLabel?: number, + showLabelThreshold?: number, + fill: string = theme.pageText, +) => { + return !showLabel || Math.abs(showLabel) > showLabelThreshold ? ( + + {display} + + ) : ( + + ); +}; diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx index b2080b0daeb..a5a149c93fa 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx @@ -27,8 +27,8 @@ import { ReportOptions } from '../ReportOptions'; import { ReportSidebar } from '../ReportSidebar'; import { ReportSummary } from '../ReportSummary'; import { ReportTopbar } from '../ReportTopbar'; -import { createSpreadsheet as defaultSpreadsheet } from '../spreadsheets/default-spreadsheet'; -import { createGroupedSpreadsheet as groupedSpreadsheet } from '../spreadsheets/grouped-spreadsheet'; +import { createCustomSpreadsheet } from '../spreadsheets/custom-spreadsheet'; +import { createGroupedSpreadsheet } from '../spreadsheets/grouped-spreadsheet'; import { useReport } from '../useReport'; import { fromDateRepr } from '../util'; @@ -114,7 +114,7 @@ export function CustomReport() { const accounts = useCachedAccounts(); const getGroupData = useMemo(() => { - return groupedSpreadsheet({ + return createGroupedSpreadsheet({ startDate, endDate, categories, @@ -140,7 +140,7 @@ export function CustomReport() { const getGraphData = useMemo(() => { setDataCheck(false); - return defaultSpreadsheet({ + return createCustomSpreadsheet({ startDate, endDate, categories, @@ -197,7 +197,7 @@ export function CustomReport() { savePrefs({ reportsViewSummary: !viewSummary }); } if (viewType === 'viewLabels') { - savePrefs({ reportsViewLabels: !viewLabels }); + savePrefs({ reportsViewLabel: !viewLabels }); } }; @@ -340,6 +340,7 @@ export function CustomReport() { scrollWidth={scrollWidth} setScrollWidth={setScrollWidth} months={months} + viewLabels={viewLabels} /> ) : ( diff --git a/packages/desktop-client/src/components/reports/reports/CustomReportCard.jsx b/packages/desktop-client/src/components/reports/reports/CustomReportCard.jsx index 7fc0b66bc1c..7269529b4e2 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReportCard.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReportCard.jsx @@ -10,7 +10,7 @@ import { DateRange } from '../DateRange'; import { BarGraph } from '../graphs/BarGraph'; import { LoadingIndicator } from '../LoadingIndicator'; import { ReportCard } from '../ReportCard'; -import { createSpreadsheet as defaultSpreadsheet } from '../spreadsheets/default-spreadsheet'; +import { createCustomSpreadsheet } from '../spreadsheets/custom-spreadsheet'; import { useReport } from '../useReport'; export function CustomReportCard() { @@ -21,7 +21,7 @@ export function CustomReportCard() { const groupBy = 'Category'; const getGraphData = useMemo(() => { - return defaultSpreadsheet({ + return createCustomSpreadsheet({ startDate, endDate, groupBy, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/default-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts similarity index 97% rename from packages/desktop-client/src/components/reports/spreadsheets/default-spreadsheet.ts rename to packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts index 0fde5493c09..c17f9bef860 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/default-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts @@ -19,7 +19,7 @@ import { filterHiddenItems } from './filterHiddenItems'; import { makeQuery } from './makeQuery'; import { recalculate } from './recalculate'; -export type createSpreadsheetProps = { +export type createCustomSpreadsheetProps = { startDate: string; endDate: string; categories: { list: CategoryEntity[]; grouped: CategoryGroupEntity[] }; @@ -37,7 +37,7 @@ export type createSpreadsheetProps = { graphType: string; }; -export function createSpreadsheet({ +export function createCustomSpreadsheet({ startDate, endDate, categories, @@ -53,7 +53,7 @@ export function createSpreadsheet({ accounts, setDataCheck, graphType, -}: createSpreadsheetProps) { +}: createCustomSpreadsheetProps) { const [categoryList, categoryGroup] = categoryLists( showOffBudgetHidden, showUncategorized, diff --git a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts index 968e6d5c338..1e8a5313239 100644 --- a/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts +++ b/packages/desktop-client/src/components/reports/spreadsheets/grouped-spreadsheet.ts @@ -5,7 +5,7 @@ import { integerToAmount } from 'loot-core/src/shared/util'; import { categoryLists } from '../ReportOptions'; -import { type createSpreadsheetProps } from './default-spreadsheet'; +import { type createCustomSpreadsheetProps } from './custom-spreadsheet'; import { filterHiddenItems } from './filterHiddenItems'; import { makeQuery } from './makeQuery'; import { recalculate } from './recalculate'; @@ -21,7 +21,7 @@ export function createGroupedSpreadsheet({ showOffBudgetHidden, showUncategorized, balanceTypeOp, -}: createSpreadsheetProps) { +}: createCustomSpreadsheetProps) { const [categoryList, categoryGroup] = categoryLists( showOffBudgetHidden, showUncategorized, diff --git a/packages/desktop-client/src/style/themes/dark.ts b/packages/desktop-client/src/style/themes/dark.ts index 8c1aa1737de..39f2aac8efd 100644 --- a/packages/desktop-client/src/style/themes/dark.ts +++ b/packages/desktop-client/src/style/themes/dark.ts @@ -188,3 +188,4 @@ export const pillBorderSelected = colorPalette.purple400; export const reportsRed = colorPalette.red300; export const reportsBlue = colorPalette.blue400; export const reportsLabel = pageText; +export const reportsInnerLabel = colorPalette.navy800; diff --git a/packages/desktop-client/src/style/themes/light.ts b/packages/desktop-client/src/style/themes/light.ts index 9c0d78d0d94..a891743253c 100644 --- a/packages/desktop-client/src/style/themes/light.ts +++ b/packages/desktop-client/src/style/themes/light.ts @@ -188,3 +188,4 @@ export const pillBorderSelected = colorPalette.purple500; export const reportsRed = colorPalette.red300; export const reportsBlue = colorPalette.blue400; export const reportsLabel = colorPalette.navy900; +export const reportsInnerLabel = colorPalette.navy800; diff --git a/packages/loot-core/src/shared/util.ts b/packages/loot-core/src/shared/util.ts index 3c32ab5cd70..c0bcdc11667 100644 --- a/packages/loot-core/src/shared/util.ts +++ b/packages/loot-core/src/shared/util.ts @@ -225,7 +225,7 @@ export function getNumberFormat({ format, hideFraction, }: { - format: NumberFormats; + format?: NumberFormats; hideFraction: boolean; } = numberFormatConfig) { let locale, regex, separator; @@ -310,6 +310,10 @@ export function amountToCurrency(n) { return getNumberFormat().formatter.format(n); } +export function amountToCurrencyNoDecimal(n) { + return getNumberFormat({ hideFraction: true }).formatter.format(n); +} + export function currencyToAmount(str: string) { const amount = parseFloat( str diff --git a/upcoming-release-notes/2124.md b/upcoming-release-notes/2124.md new file mode 100644 index 00000000000..77a1dd19f70 --- /dev/null +++ b/upcoming-release-notes/2124.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [carkom] +--- + +Enabling and formatting "viewLabels" button for custom reports page \ No newline at end of file