Mantine charts (#7419)

* Install @mantine/charts

* Import charts styles

* Refactor <PricingOverviewPanel />

* Refactor internal price panel

* Refactor variant price panel

* BOM bar chart

* BOM donut

* Refactor supplier pricing panel

* More refaactoring

* Cleanup unused imports

* Fix typo

* playwright test updates
This commit is contained in:
Oliver 2024-06-10 08:24:38 +10:00 committed by GitHub
parent 9f35971db1
commit 713d2ac925
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 136 additions and 310 deletions

View File

@ -27,6 +27,7 @@
"@lingui/core": "^4.10.0",
"@lingui/react": "^4.10.0",
"@mantine/carousel": "^7.8.0",
"@mantine/charts": "^7.10.1",
"@mantine/core": "^7.10.0",
"@mantine/dates": "^7.8.0",
"@mantine/dropzone": "^7.8.0",

View File

@ -1,12 +1,12 @@
export const CHART_COLORS: string[] = [
'#ffa8a8',
'#8ce99a',
'#74c0fc',
'#ffe066',
'#63e6be',
'#ffc078',
'#d8f5a2',
'#66d9e8',
'#e599f7',
'#dee2e6'
'blue',
'teal',
'lime',
'yellow',
'grape',
'red',
'orange',
'green',
'indigo',
'pink'
];

View File

@ -1,9 +1,12 @@
import { formatCurrency } from '../../defaults/formatters';
export function tooltipFormatter(label: any, currency: string) {
/*
* Render a chart label for a currency graph
*/
export function tooltipFormatter(value: any, currency?: string) {
return (
formatCurrency(label, {
formatCurrency(value, {
currency: currency
})?.toString() ?? ''
})?.toString() ?? value.toString()
);
}

View File

@ -1,4 +1,5 @@
import '@mantine/carousel/styles.css';
import '@mantine/charts/styles.css';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/spotlight/styles.css';

View File

@ -1,5 +1,7 @@
import { t } from '@lingui/macro';
import { BarChart, DonutChart } from '@mantine/charts';
import {
Center,
Group,
SegmentedControl,
SimpleGrid,
@ -7,18 +9,6 @@ import {
Text
} from '@mantine/core';
import { ReactNode, useMemo, useState } from 'react';
import {
Bar,
BarChart,
Cell,
Legend,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import { CHART_COLORS } from '../../../components/charts/colors';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
@ -44,42 +34,30 @@ function BomPieChart({
readonly data: any[];
readonly currency: string;
}) {
// Construct donut data
const maxPricing = useMemo(() => {
return data.map((entry, index) => {
return {
name: entry.name,
value: entry.total_price_max,
color: CHART_COLORS[index % CHART_COLORS.length] + '.5'
};
});
}, [data]);
return (
<ResponsiveContainer width="100%" height={500}>
<PieChart>
<Pie
data={data}
dataKey="total_price_min"
nameKey="name"
innerRadius={20}
outerRadius={100}
>
{data.map((_entry, index) => (
<Cell
key={`cell-${index}`}
fill={CHART_COLORS[index % CHART_COLORS.length]}
/>
))}
</Pie>
<Pie
data={data}
dataKey="total_price_max"
nameKey="name"
innerRadius={120}
outerRadius={240}
>
{data.map((_entry, index) => (
<Cell
key={`cell-${index}`}
fill={CHART_COLORS[index % CHART_COLORS.length]}
/>
))}
</Pie>
<Tooltip
formatter={(label, payload) => tooltipFormatter(label, currency)}
/>
</PieChart>
</ResponsiveContainer>
<Center>
<DonutChart
data={maxPricing}
size={500}
thickness={80}
withLabels={false}
withLabelsLine={false}
tooltipDataSource="segment"
chartLabel={t`Total Price`}
valueFormatter={(value) => tooltipFormatter(value, currency)}
/>
</Center>
);
}
@ -92,32 +70,18 @@ function BomBarChart({
readonly currency: string;
}) {
return (
<ResponsiveContainer width="100%" height={500}>
<BarChart data={data}>
<XAxis dataKey="name" />
<YAxis
tickFormatter={(value, index) =>
formatCurrency(value, {
currency: currency
})?.toString() ?? ''
}
/>
<Tooltip
formatter={(label, payload) => tooltipFormatter(label, currency)}
/>
<Legend />
<Bar
dataKey="total_price_min"
fill={CHART_COLORS[0]}
label={t`Minimum Total Price`}
/>
<Bar
dataKey="total_price_max"
fill={CHART_COLORS[1]}
label={t`Maximum Total Price`}
/>
</BarChart>
</ResponsiveContainer>
<BarChart
h={500}
dataKey="name"
data={data}
xAxisLabel={t`Component`}
yAxisLabel={t`Price Range`}
series={[
{ name: 'total_price_min', label: t`Minimum Price`, color: 'blue.6' },
{ name: 'total_price_max', label: t`Maximum Price`, color: 'teal.6' }
]}
valueFormatter={(value) => tooltipFormatter(value, currency)}
/>
);
}

View File

@ -1,18 +1,9 @@
import { t } from '@lingui/macro';
import { BarChart } from '@mantine/charts';
import { SimpleGrid } from '@mantine/core';
import { useCallback, useMemo, useState } from 'react';
import {
Bar,
BarChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import { AddItemButton } from '../../../components/buttons/AddItemButton';
import { CHART_COLORS } from '../../../components/charts/colors';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
import { ApiFormFieldSet } from '../../../components/forms/fields/ApiFormField';
import { formatCurrency } from '../../../defaults/formatters';
@ -169,29 +160,14 @@ export default function PriceBreakPanel({
}}
/>
{table.records.length > 0 ? (
<ResponsiveContainer width="100%" height={500}>
<BarChart data={table.records}>
<XAxis dataKey="quantity" />
<YAxis
tickFormatter={(value, index) =>
formatCurrency(value, {
currency: currency
})?.toString() ?? ''
}
/>
<Tooltip
formatter={(label, payload) =>
tooltipFormatter(label, currency)
}
/>
<Legend />
<Bar
dataKey="price"
fill={CHART_COLORS[0]}
label={t`Price Break`}
/>
</BarChart>
</ResponsiveContainer>
<BarChart
dataKey="quantity"
data={table.records}
series={[{ name: 'price', label: t`Price`, color: 'blue.6' }]}
xAxisLabel={t`Quantity`}
yAxisLabel={t`Unit Price`}
valueFormatter={(value) => tooltipFormatter(value, currency)}
/>
) : (
<NoPricingData />
)}

View File

@ -1,4 +1,5 @@
import { t } from '@lingui/macro';
import { BarChart } from '@mantine/charts';
import {
Alert,
Anchor,
@ -19,17 +20,7 @@ import {
} from '@tabler/icons-react';
import { DataTable } from 'mantine-datatable';
import { ReactNode, useMemo } from 'react';
import {
Bar,
BarChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import { CHART_COLORS } from '../../../components/charts/colors';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
import { formatCurrency, renderDate } from '../../../defaults/formatters';
import { panelOptions } from '../PartPricingPanel';
@ -192,34 +183,17 @@ export default function PricingOverviewPanel({
columns={columns}
/>
</Stack>
<ResponsiveContainer width="100%" height={500}>
<BarChart data={overviewData} id="pricing-overview-chart">
<XAxis dataKey="title" />
<YAxis
tickFormatter={(value, index) =>
formatCurrency(value, {
currency: pricing?.currency
})?.toString() ?? ''
}
/>
<Tooltip
formatter={(label, payload) =>
tooltipFormatter(label, pricing?.currency)
}
/>
<Legend />
<Bar
dataKey="min_value"
fill={CHART_COLORS[0]}
label={t`Minimum Price`}
/>
<Bar
dataKey="max_value"
fill={CHART_COLORS[1]}
label={t`Maximum Price`}
/>
</BarChart>
</ResponsiveContainer>
<BarChart
aria-label="pricing-overview-chart"
dataKey="title"
data={overviewData}
title={t`Pricing Overview`}
series={[
{ name: 'min_value', label: t`Minimum Value`, color: 'blue.6' },
{ name: 'max_value', label: t`Maximum Value`, color: 'teal.6' }
]}
valueFormatter={(value) => tooltipFormatter(value, pricing?.currency)}
/>
</SimpleGrid>
</Stack>
);

View File

@ -1,18 +1,8 @@
import { t } from '@lingui/macro';
import { BarChart } from '@mantine/charts';
import { Group, SimpleGrid, Text } from '@mantine/core';
import { ReactNode, useCallback, useMemo } from 'react';
import {
Bar,
BarChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import { CHART_COLORS } from '../../../components/charts/colors';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
import { formatCurrency, renderDate } from '../../../defaults/formatters';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { useTable } from '../../../hooks/UseTable';
@ -131,32 +121,13 @@ export default function PurchaseHistoryPanel({
}}
/>
{purchaseHistoryData.length > 0 ? (
<ResponsiveContainer width="100%" height={500}>
<BarChart data={purchaseHistoryData}>
<XAxis dataKey="name" />
<YAxis
tickFormatter={(value, index) =>
formatCurrency(value, {
currency: currency
})?.toString() ?? ''
}
/>
<Tooltip
formatter={(label, payload) => tooltipFormatter(label, currency)}
/>
<Legend />
<Bar
dataKey="unit_price"
fill={CHART_COLORS[0]}
label={t`Unit Price`}
/>
<Bar
dataKey="purchase_price"
fill={CHART_COLORS[1]}
label={t`Purchase Price`}
/>
</BarChart>
</ResponsiveContainer>
<BarChart
data={purchaseHistoryData}
dataKey="name"
series={[
{ name: 'unit_price', label: t`Unit Price`, color: 'blue.5' }
]}
/>
) : (
<NoPricingData />
)}

View File

@ -1,18 +1,8 @@
import { t } from '@lingui/macro';
import { BarChart } from '@mantine/charts';
import { SimpleGrid } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import {
Bar,
BarChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import { CHART_COLORS } from '../../../components/charts/colors';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
import { formatCurrency } from '../../../defaults/formatters';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { useTable } from '../../../hooks/UseTable';
@ -95,27 +85,13 @@ export default function SaleHistoryPanel({ part }: { part: any }): ReactNode {
}}
/>
{saleHistoryData.length > 0 ? (
<ResponsiveContainer width="100%" height={500}>
<BarChart data={saleHistoryData}>
<XAxis dataKey="name" />
<YAxis
tickFormatter={(value, index) =>
formatCurrency(value, {
currency: currency
})?.toString() ?? ''
}
/>
<Tooltip
formatter={(label, payload) => tooltipFormatter(label, currency)}
/>
<Legend />
<Bar
dataKey="sale_price"
fill={CHART_COLORS[0]}
label={t`Sale Price`}
/>
</BarChart>
</ResponsiveContainer>
<BarChart
data={saleHistoryData}
dataKey="name"
series={[
{ name: 'sale_price', label: t`Sale Price`, color: 'blue.6' }
]}
/>
) : (
<NoPricingData />
)}

View File

@ -1,18 +1,9 @@
import { t } from '@lingui/macro';
import { BarChart } from '@mantine/charts';
import { SimpleGrid } from '@mantine/core';
import { useMemo } from 'react';
import {
Bar,
BarChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import { CHART_COLORS } from '../../../components/charts/colors';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
import { formatCurrency } from '../../../defaults/formatters';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
@ -64,31 +55,19 @@ export default function SupplierPricingPanel({ part }: { part: any }) {
}}
/>
{supplierPricingData.length > 0 ? (
<ResponsiveContainer width="100%" height={500}>
<BarChart data={supplierPricingData}>
<XAxis dataKey="name" />
<YAxis
tickFormatter={(value, index) =>
formatCurrency(value, {
currency: currency
})?.toString() ?? ''
}
/>
<Tooltip
formatter={(label, payload) => tooltipFormatter(label, currency)}
/>
<Bar
dataKey="unit_price"
fill={CHART_COLORS[0]}
label={t`Unit Price`}
/>
<Bar
dataKey="supplier_price"
fill="#82ca9d"
label={t`Supplier Price`}
/>
</BarChart>
</ResponsiveContainer>
<BarChart
data={supplierPricingData}
dataKey="name"
series={[
{ name: 'unit_price', label: t`Unit Price`, color: 'blue.6' },
{
name: 'supplier_price',
label: t`Supplier Price`,
color: 'teal.6'
}
]}
valueFormatter={(value) => tooltipFormatter(value, currency)}
/>
) : (
<NoPricingData />
)}

View File

@ -1,17 +1,8 @@
import { t } from '@lingui/macro';
import { BarChart } from '@mantine/charts';
import { SimpleGrid, Stack } from '@mantine/core';
import { ReactNode, useMemo } from 'react';
import {
Bar,
BarChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from 'recharts';
import { CHART_COLORS } from '../../../components/charts/colors';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
import { formatCurrency } from '../../../defaults/formatters';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
@ -97,34 +88,19 @@ export default function VariantPricingPanel({
}}
/>
{variantPricingData.length > 0 ? (
<ResponsiveContainer width="100%" height={500}>
<BarChart data={variantPricingData}>
<XAxis dataKey="name" />
<YAxis
tickFormatter={(value, index) =>
formatCurrency(value, {
currency: pricing?.currency
})?.toString() ?? ''
}
/>
<Tooltip
formatter={(label, payload) =>
tooltipFormatter(label, pricing?.currency)
}
/>
<Legend />
<Bar
dataKey="pmin"
fill={CHART_COLORS[0]}
label={t`Minimum Price`}
/>
<Bar
dataKey="pmax"
fill={CHART_COLORS[1]}
label={t`Maximum Price`}
/>
</BarChart>
</ResponsiveContainer>
<BarChart
dataKey="name"
data={variantPricingData}
xAxisLabel={t`Variant Part`}
yAxisLabel={t`Price Range`}
series={[
{ name: 'pmin', label: t`Minimum Price`, color: 'blue.6' },
{ name: 'pmax', label: t`Maximum Price`, color: 'teal.6' }
]}
valueFormatter={(value) =>
tooltipFormatter(value, pricing?.currency)
}
/>
) : (
<NoPricingData />
)}

View File

@ -580,8 +580,8 @@ export function InvenTreeTable<T = any>({
)}
<Boundary label={`InvenTreeTable-${tableState.tableKey}`}>
<Stack gap="sm">
<Group justify="apart" grow>
<Group justify="left" key="custom-actions" gap={5}>
<Group justify="apart" grow wrap="nowrap">
<Group justify="left" key="custom-actions" gap={5} wrap="nowrap">
{tableProps.enableDownload && (
<DownloadAction
key="download-action"
@ -617,7 +617,7 @@ export function InvenTreeTable<T = any>({
))}
</Group>
<Space />
<Group justify="right" gap={5}>
<Group justify="right" gap={5} wrap="nowrap">
{tableProps.enableSearch && (
<TableSearchInput
searchCallback={(term: string) =>

View File

@ -29,25 +29,25 @@ test('PUI - Pages - Part - Pricing (Nothing, BOM)', async ({ page }) => {
await page.getByRole('button', { name: 'BOM Pricing' }).isEnabled();
// Overview Graph
let graph = page.locator('#pricing-overview-chart');
let graph = page.getByLabel('pricing-overview-chart');
await graph.waitFor();
await graph.getByText('$60').waitFor();
await graph.getByText('BOM Pricing').waitFor();
await graph.getByText('Overall Pricing').waitFor();
await graph.locator('path').nth(1).hover();
await page.getByText('min_value : $50').waitFor();
await graph.locator('tspan').filter({ hasText: 'BOM Pricing' }).waitFor();
await graph.locator('tspan').filter({ hasText: 'Overall Pricing' }).waitFor();
// BOM Pricing
await page.getByRole('button', { name: 'BOM Pricing' }).click();
await page.getByText('Bar Chart').click();
await page.getByText('total_price_min').waitFor();
await page.getByText('Pie Chart').click();
await page.getByRole('button', { name: 'Quantity Not sorted' }).waitFor();
await page.getByRole('button', { name: 'Unit Price Not sorted' }).waitFor();
// BOM Pricing - linkjumping
await page.getByText('Wood Screw').waitFor();
await page.getByText('Wood Screw').click();
await page
.getByLabel('BOM Pricing')
.getByRole('table')
.getByText('Wood Screw')
.click();
await page.waitForURL('**/part/98/pricing');
});

View File

@ -1809,6 +1809,11 @@
resolved "https://registry.yarnpkg.com/@mantine/carousel/-/carousel-7.10.0.tgz#ee750504814ec4eff7523e17a0052dc86bec45c8"
integrity sha512-+fP/hyHpXoK5nHR5mgEjPhPqjiurwMDP+aLseaE/mvmoBZEFF3vOPAqYOekNDrdyaqvtHkM82T9mnR4M3BAK0w==
"@mantine/charts@^7.10.1":
version "7.10.1"
resolved "https://registry.yarnpkg.com/@mantine/charts/-/charts-7.10.1.tgz#c1b92ccac16d05b87cc103adcd79f0e0c2f87c62"
integrity sha512-fMy2EmgegdHVkrtnRO8ync8kqOJoXqixxc1JDmhn9tks4lSvKAKB9ui8OyaTUOWzls/Cs0VfyW51sB/HKe5faw==
"@mantine/core@^7.10.0":
version "7.10.0"
resolved "https://registry.yarnpkg.com/@mantine/core/-/core-7.10.0.tgz#bfaafc92cf2346e5a6cbb49289f577ce3f7c05f7"