@micha.bigler/ui-core-micha 2.14.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/WORK_ORDERS.md +3 -1
- package/dist/components/charts/BarChart.js +32 -0
- package/dist/components/charts/ChartFrame.js +50 -0
- package/dist/components/charts/LineChart.js +32 -0
- package/dist/components/charts/exportChart.js +67 -0
- package/dist/components/charts/formatters.js +23 -0
- package/dist/components/charts/palette.js +21 -0
- package/dist/i18n/chartsTranslations.js +38 -0
- package/dist/index.js +11 -4
- package/package.json +3 -1
- package/src/components/charts/BarChart.jsx +44 -0
- package/src/components/charts/ChartFrame.jsx +112 -0
- package/src/components/charts/LineChart.jsx +44 -0
- package/src/components/charts/exportChart.js +71 -0
- package/src/components/charts/formatters.js +39 -0
- package/src/components/charts/palette.js +24 -0
- package/src/i18n/chartsTranslations.ts +38 -0
- package/src/index.js +18 -4
- package/tests/BarChart.test.jsx +55 -0
- package/tests/ChartFrame.test.jsx +122 -0
- package/tests/LineChart.test.jsx +33 -0
- package/tests/chartsPalette.test.js +39 -0
- package/work-orders/CHART-1.md +285 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
|
|
3
|
+
export function formatPercentage(value, locale, options = {}) {
|
|
4
|
+
return new Intl.NumberFormat(locale, {
|
|
5
|
+
style: 'percent',
|
|
6
|
+
maximumFractionDigits: 1,
|
|
7
|
+
...options,
|
|
8
|
+
}).format(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatCompact(value, locale, options = {}) {
|
|
12
|
+
return new Intl.NumberFormat(locale, {
|
|
13
|
+
notation: 'compact',
|
|
14
|
+
maximumFractionDigits: 1,
|
|
15
|
+
...options,
|
|
16
|
+
}).format(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatRatio(value, locale, options = {}) {
|
|
20
|
+
return new Intl.NumberFormat(locale, {
|
|
21
|
+
maximumFractionDigits: 2,
|
|
22
|
+
...options,
|
|
23
|
+
}).format(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Creates locale-aware formatter functions for use in chart axes and tooltips. */
|
|
27
|
+
export function createChartFormatters(locale) {
|
|
28
|
+
return {
|
|
29
|
+
percentage: (value, options) => formatPercentage(value, locale, options),
|
|
30
|
+
compact: (value, options) => formatCompact(value, locale, options),
|
|
31
|
+
ratio: (value, options) => formatRatio(value, locale, options),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolves formatter locale from the active react-i18next instance. */
|
|
36
|
+
export function useChartFormatters() {
|
|
37
|
+
const { i18n } = useTranslation();
|
|
38
|
+
return createChartFormatters(i18n.language);
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useTheme } from '@mui/material/styles';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns neutral chart colours derived only from the active MUI theme.
|
|
5
|
+
* Consumers may replace either array with their own domain palette.
|
|
6
|
+
*/
|
|
7
|
+
export function getNeutralChartPalette(theme) {
|
|
8
|
+
const palette = theme.palette;
|
|
9
|
+
const isDark = palette.mode === 'dark';
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
categorical: isDark
|
|
13
|
+
? [palette.primary.light, palette.secondary.light, palette.info.light, palette.success.light, palette.warning.light]
|
|
14
|
+
: [palette.primary.main, palette.secondary.main, palette.info.main, palette.success.main, palette.warning.main],
|
|
15
|
+
sequential: isDark
|
|
16
|
+
? [palette.action.disabled, palette.text.secondary, palette.primary.light, palette.primary.main, palette.primary.dark]
|
|
17
|
+
: [palette.action.disabled, palette.divider, palette.text.secondary, palette.primary.light, palette.primary.main],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolves the kit's neutral palette from the nearest MUI ThemeProvider. */
|
|
22
|
+
export function useNeutralChartPalette() {
|
|
23
|
+
return getNeutralChartPalette(useTheme());
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const chartsTranslations = {
|
|
2
|
+
'ChartFrame.LOADING': {
|
|
3
|
+
de: 'Diagramm wird geladen.',
|
|
4
|
+
fr: 'Chargement du graphique.',
|
|
5
|
+
en: 'Loading chart.',
|
|
6
|
+
sw: 'Inapakia chati.',
|
|
7
|
+
},
|
|
8
|
+
'ChartFrame.EMPTY_DEFAULT': {
|
|
9
|
+
de: 'Keine Daten verfügbar.',
|
|
10
|
+
fr: 'Aucune donnée disponible.',
|
|
11
|
+
en: 'No data available.',
|
|
12
|
+
sw: 'Hakuna data inayopatikana.',
|
|
13
|
+
},
|
|
14
|
+
'ChartFrame.ERROR_DEFAULT': {
|
|
15
|
+
de: 'Das Diagramm konnte nicht geladen werden.',
|
|
16
|
+
fr: 'Impossible de charger le graphique.',
|
|
17
|
+
en: 'The chart could not be loaded.',
|
|
18
|
+
sw: 'Chati haikuweza kupakiwa.',
|
|
19
|
+
},
|
|
20
|
+
'ChartFrame.EXPORT_SVG_LABEL': {
|
|
21
|
+
de: 'SVG exportieren',
|
|
22
|
+
fr: 'Exporter en SVG',
|
|
23
|
+
en: 'Export SVG',
|
|
24
|
+
sw: 'Hamisha SVG',
|
|
25
|
+
},
|
|
26
|
+
'ChartFrame.EXPORT_PNG_LABEL': {
|
|
27
|
+
de: 'PNG exportieren',
|
|
28
|
+
fr: 'Exporter en PNG',
|
|
29
|
+
en: 'Export PNG',
|
|
30
|
+
sw: 'Hamisha PNG',
|
|
31
|
+
},
|
|
32
|
+
'ChartFrame.EXPORT_ERROR': {
|
|
33
|
+
de: 'Das Diagramm konnte nicht exportiert werden.',
|
|
34
|
+
fr: 'Impossible d’exporter le graphique.',
|
|
35
|
+
en: 'The chart could not be exported.',
|
|
36
|
+
sw: 'Chati haikuweza kuhamishwa.',
|
|
37
|
+
},
|
|
38
|
+
};
|
package/src/index.js
CHANGED
|
@@ -39,10 +39,23 @@ export { AuthFactorRequirementCard } from './components/AuthFactorRequirementCar
|
|
|
39
39
|
export { AccessCodeSingleUseToggle } from './components/AccessCodeSingleUseToggle';
|
|
40
40
|
export { QrSignupManager } from './components/QrSignupManager';
|
|
41
41
|
|
|
42
|
-
// --- 6.
|
|
42
|
+
// --- 6. Charts ---
|
|
43
|
+
export { ChartFrame } from './components/charts/ChartFrame';
|
|
44
|
+
export { BarChart } from './components/charts/BarChart';
|
|
45
|
+
export { LineChart } from './components/charts/LineChart';
|
|
46
|
+
export { getNeutralChartPalette, useNeutralChartPalette } from './components/charts/palette';
|
|
47
|
+
export {
|
|
48
|
+
formatPercentage,
|
|
49
|
+
formatCompact,
|
|
50
|
+
formatRatio,
|
|
51
|
+
createChartFormatters,
|
|
52
|
+
useChartFormatters,
|
|
53
|
+
} from './components/charts/formatters';
|
|
54
|
+
|
|
55
|
+
// --- 7. Translations ---
|
|
43
56
|
export { authTranslations } from './i18n/authTranslations';
|
|
44
57
|
|
|
45
|
-
// ---
|
|
58
|
+
// --- 8. Notifications ---
|
|
46
59
|
export { NotificationSettings } from './notifications/NotificationSettings';
|
|
47
60
|
export * from './notifications/api';
|
|
48
61
|
export { NotificationsProvider, useNotifications } from './notifications/NotificationsProvider';
|
|
@@ -51,7 +64,7 @@ export { NotificationBell } from './notifications/NotificationBell';
|
|
|
51
64
|
export { PopupSurface } from './notifications/PopupSurface';
|
|
52
65
|
export { getNotificationFeed, getUnreadCount, markNotifications } from './notifications/feedApi';
|
|
53
66
|
|
|
54
|
-
// ---
|
|
67
|
+
// --- 9. Onboarding ---
|
|
55
68
|
export * from './onboarding/api';
|
|
56
69
|
export { selectActiveSteps } from './onboarding/stepSelection';
|
|
57
70
|
export {
|
|
@@ -66,6 +79,7 @@ export { CompleteNameStep } from './onboarding/steps/CompleteNameStep';
|
|
|
66
79
|
export { BrowserPushStep } from './onboarding/steps/BrowserPushStep';
|
|
67
80
|
export { PwaInstallStep } from './onboarding/steps/PwaInstallStep';
|
|
68
81
|
|
|
69
|
-
// ---
|
|
82
|
+
// --- 10. Translations ---
|
|
70
83
|
export { notificationsTranslations } from './i18n/notificationsTranslations';
|
|
71
84
|
export { onboardingTranslations } from './i18n/onboardingTranslations';
|
|
85
|
+
export { chartsTranslations } from './i18n/chartsTranslations';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
5
|
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
6
|
+
|
|
7
|
+
const chartSpy = vi.hoisted(() => vi.fn((props) => <div data-testid="mui-bar-chart" data-props={JSON.stringify(props)} />));
|
|
8
|
+
vi.mock('@mui/x-charts/BarChart', () => ({ BarChart: chartSpy }));
|
|
9
|
+
|
|
10
|
+
import { BarChart } from '../src/components/charts/BarChart';
|
|
11
|
+
|
|
12
|
+
function renderChart(props) {
|
|
13
|
+
return render(
|
|
14
|
+
<ThemeProvider theme={createTheme({ palette: { primary: { main: 'rgb(1, 2, 3)' } } })}>
|
|
15
|
+
<BarChart xAxisLabel="Year" yAxisLabel="Value (kg)" {...props} />
|
|
16
|
+
</ThemeProvider>,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('BarChart preset', () => {
|
|
21
|
+
afterEach(cleanup);
|
|
22
|
+
|
|
23
|
+
it('passes both axis labels, tooltip, responsive sizing, and hides a single-series legend', () => {
|
|
24
|
+
renderChart({ series: [{ data: [1, 2] }] });
|
|
25
|
+
const props = chartSpy.mock.calls[0][0];
|
|
26
|
+
|
|
27
|
+
expect(props.xAxis[0].label).toBe('Year');
|
|
28
|
+
expect(props.yAxis[0].label).toBe('Value (kg)');
|
|
29
|
+
expect(props.slotProps).toEqual({ tooltip: { trigger: 'axis' } });
|
|
30
|
+
expect(props.hideLegend).toBe(true);
|
|
31
|
+
expect(screen.getByTestId('bar-chart-container').getAttribute('style') || '').not.toContain('px');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it.each([
|
|
35
|
+
['grouped', [{ data: [1] }, { data: [2] }], undefined],
|
|
36
|
+
['stacked', [{ data: [1], stack: 'total' }, { data: [2], stack: 'total' }], undefined],
|
|
37
|
+
['dual axis', [{ data: [1], yAxisKey: 'secondary' }, { data: [2] }], [{ id: 'primary' }, { id: 'secondary' }]],
|
|
38
|
+
])('renders the %s variant with a legend for multiple series', (_name, series, yAxis) => {
|
|
39
|
+
renderChart({ series, yAxis });
|
|
40
|
+
const props = chartSpy.mock.calls.at(-1)[0];
|
|
41
|
+
expect(props.hideLegend).toBe(false);
|
|
42
|
+
expect(props.series).toEqual(series);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('passes a caller palette through without substituting a hardcoded colour', () => {
|
|
46
|
+
renderChart({ series: [{ data: [1] }], palette: ['var(--application-chart-colour)'] });
|
|
47
|
+
expect(chartSpy.mock.calls.at(-1)[0].colors).toEqual(['var(--application-chart-colour)']);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('requires labels for both axes', () => {
|
|
51
|
+
expect(() => render(
|
|
52
|
+
<ThemeProvider theme={createTheme()}><BarChart series={[]} xAxisLabel="Year" /></ThemeProvider>,
|
|
53
|
+
)).toThrow('BarChart requires xAxisLabel and yAxisLabel.');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import i18next from 'i18next';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
6
|
+
import { I18nextProvider } from 'react-i18next';
|
|
7
|
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
8
|
+
|
|
9
|
+
const chartExport = vi.hoisted(() => ({
|
|
10
|
+
exportChartSvg: vi.fn(() => new Blob()),
|
|
11
|
+
exportChartPng: vi.fn(() => Promise.resolve(new Blob())),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('../src/components/charts/exportChart', () => chartExport);
|
|
15
|
+
|
|
16
|
+
import { ChartFrame } from '../src/components/charts/ChartFrame';
|
|
17
|
+
|
|
18
|
+
const resources = {
|
|
19
|
+
en: {
|
|
20
|
+
translation: {
|
|
21
|
+
'ChartFrame.LOADING': 'Loading chart.',
|
|
22
|
+
'ChartFrame.EMPTY_DEFAULT': 'No data available.',
|
|
23
|
+
'ChartFrame.ERROR_DEFAULT': 'The chart could not be loaded.',
|
|
24
|
+
'ChartFrame.EXPORT_SVG_LABEL': 'Export SVG',
|
|
25
|
+
'ChartFrame.EXPORT_PNG_LABEL': 'Export PNG',
|
|
26
|
+
'ChartFrame.EXPORT_ERROR': 'The chart could not be exported.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
fr: {
|
|
30
|
+
translation: {
|
|
31
|
+
'ChartFrame.LOADING': 'Chargement du graphique.',
|
|
32
|
+
'ChartFrame.EMPTY_DEFAULT': 'Aucune donnée disponible.',
|
|
33
|
+
'ChartFrame.ERROR_DEFAULT': 'Impossible de charger le graphique.',
|
|
34
|
+
'ChartFrame.EXPORT_SVG_LABEL': 'Exporter en SVG',
|
|
35
|
+
'ChartFrame.EXPORT_PNG_LABEL': 'Exporter en PNG',
|
|
36
|
+
'ChartFrame.EXPORT_ERROR': 'Impossible d’exporter le graphique.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function renderFrame(props = {}, language = 'en') {
|
|
42
|
+
const i18n = i18next.createInstance();
|
|
43
|
+
i18n.init({ lng: language, resources, interpolation: { escapeValue: false } });
|
|
44
|
+
return render(
|
|
45
|
+
<ThemeProvider theme={createTheme()}>
|
|
46
|
+
<I18nextProvider i18n={i18n}>
|
|
47
|
+
<ChartFrame title="Chart title" subtitle="Chart subtitle" {...props}>
|
|
48
|
+
<svg data-testid="chart-body" />
|
|
49
|
+
</ChartFrame>
|
|
50
|
+
</I18nextProvider>
|
|
51
|
+
</ThemeProvider>,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('ChartFrame', () => {
|
|
56
|
+
afterEach(cleanup);
|
|
57
|
+
|
|
58
|
+
it('renders its title, subtitle, toolbar, and accessible chart region', () => {
|
|
59
|
+
renderFrame({ toolbar: <button type="button">Period selector</button> });
|
|
60
|
+
|
|
61
|
+
expect(screen.getByText('Chart title')).toBeTruthy();
|
|
62
|
+
expect(screen.getByText('Chart subtitle')).toBeTruthy();
|
|
63
|
+
expect(screen.getByRole('button', { name: 'Period selector' })).toBeTruthy();
|
|
64
|
+
expect(screen.getByRole('img', { name: 'Chart title' })).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('lets a custom ariaLabel override the title as the accessible name', () => {
|
|
68
|
+
renderFrame({ ariaLabel: 'Custom accessible description' });
|
|
69
|
+
|
|
70
|
+
expect(screen.getByRole('img', { name: 'Custom accessible description' })).toBeTruthy();
|
|
71
|
+
expect(screen.queryByRole('img', { name: 'Chart title' })).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('enforces loading, error, empty, then children state precedence', () => {
|
|
75
|
+
const loading = renderFrame({ loading: true, error: 'Failure', isEmpty: true });
|
|
76
|
+
expect(screen.getByLabelText('Loading chart.')).toBeTruthy();
|
|
77
|
+
expect(screen.queryByText('Failure')).toBeNull();
|
|
78
|
+
expect(screen.queryByTestId('chart-body')).toBeNull();
|
|
79
|
+
loading.unmount();
|
|
80
|
+
|
|
81
|
+
const failure = renderFrame({ error: 'Failure', isEmpty: true });
|
|
82
|
+
expect(screen.getByText('Failure')).toBeTruthy();
|
|
83
|
+
expect(screen.queryByTestId('chart-body')).toBeNull();
|
|
84
|
+
failure.unmount();
|
|
85
|
+
|
|
86
|
+
renderFrame();
|
|
87
|
+
expect(screen.getByTestId('chart-body')).toBeTruthy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('renders an empty message before children when empty', () => {
|
|
91
|
+
renderFrame({ isEmpty: true, emptyMessage: 'No observations' });
|
|
92
|
+
expect(screen.getByText('No observations')).toBeTruthy();
|
|
93
|
+
expect(screen.queryByTestId('chart-body')).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('shows export controls only when enabled and invokes the SVG callback', () => {
|
|
97
|
+
const onExportSvg = vi.fn();
|
|
98
|
+
const disabled = renderFrame();
|
|
99
|
+
expect(screen.queryByRole('button', { name: 'Export SVG' })).toBeNull();
|
|
100
|
+
disabled.unmount();
|
|
101
|
+
|
|
102
|
+
renderFrame({ exportOptions: { png: false }, onExportSvg });
|
|
103
|
+
fireEvent.click(screen.getByRole('button', { name: 'Export SVG' }));
|
|
104
|
+
expect(chartExport.exportChartSvg).toHaveBeenCalledOnce();
|
|
105
|
+
expect(onExportSvg).toHaveBeenCalledOnce();
|
|
106
|
+
expect(screen.queryByRole('button', { name: 'Export PNG' })).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('invokes the PNG callback when the PNG export control is used', async () => {
|
|
110
|
+
const onExportPng = vi.fn();
|
|
111
|
+
renderFrame({ exportOptions: true, onExportPng });
|
|
112
|
+
|
|
113
|
+
fireEvent.click(screen.getByRole('button', { name: 'Export PNG' }));
|
|
114
|
+
await vi.waitFor(() => expect(chartExport.exportChartPng).toHaveBeenCalledOnce());
|
|
115
|
+
await vi.waitFor(() => expect(onExportPng).toHaveBeenCalledOnce());
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('uses resolved i18n strings for its own empty state', () => {
|
|
119
|
+
renderFrame({ isEmpty: true }, 'fr');
|
|
120
|
+
expect(screen.getByText('Aucune donnée disponible.')).toBeTruthy();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { cleanup, render } from '@testing-library/react';
|
|
5
|
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
6
|
+
|
|
7
|
+
const chartSpy = vi.hoisted(() => vi.fn(() => <div data-testid="mui-line-chart" />));
|
|
8
|
+
vi.mock('@mui/x-charts/LineChart', () => ({ LineChart: chartSpy }));
|
|
9
|
+
|
|
10
|
+
import { LineChart } from '../src/components/charts/LineChart';
|
|
11
|
+
|
|
12
|
+
describe('LineChart preset', () => {
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
it('renders multi-series time data with labelled axes, legend, and tooltip', () => {
|
|
16
|
+
render(
|
|
17
|
+
<ThemeProvider theme={createTheme()}>
|
|
18
|
+
<LineChart
|
|
19
|
+
xAxisLabel="Date"
|
|
20
|
+
yAxisLabel="Rate (%)"
|
|
21
|
+
xAxis={[{ scaleType: 'point', data: ['2026-01', '2026-02'] }]}
|
|
22
|
+
series={[{ data: [1, 2] }, { data: [2, 3] }]}
|
|
23
|
+
/>
|
|
24
|
+
</ThemeProvider>,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const props = chartSpy.mock.calls[0][0];
|
|
28
|
+
expect(props.xAxis[0].label).toBe('Date');
|
|
29
|
+
expect(props.yAxis[0].label).toBe('Rate (%)');
|
|
30
|
+
expect(props.hideLegend).toBe(false);
|
|
31
|
+
expect(props.slotProps).toEqual({ tooltip: { trigger: 'axis' } });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createChartFormatters,
|
|
4
|
+
formatCompact,
|
|
5
|
+
formatPercentage,
|
|
6
|
+
getNeutralChartPalette,
|
|
7
|
+
} from '../src/index';
|
|
8
|
+
|
|
9
|
+
describe('chart palette and formatters', () => {
|
|
10
|
+
it('resolves different neutral palettes from light and dark MUI themes without literal API colours', () => {
|
|
11
|
+
const tokenTheme = (mode) => ({
|
|
12
|
+
palette: {
|
|
13
|
+
mode,
|
|
14
|
+
primary: { main: `${mode}-primary-main`, light: `${mode}-primary-light`, dark: `${mode}-primary-dark` },
|
|
15
|
+
secondary: { main: `${mode}-secondary-main`, light: `${mode}-secondary-light` },
|
|
16
|
+
info: { main: `${mode}-info-main`, light: `${mode}-info-light` },
|
|
17
|
+
success: { main: `${mode}-success-main`, light: `${mode}-success-light` },
|
|
18
|
+
warning: { main: `${mode}-warning-main`, light: `${mode}-warning-light` },
|
|
19
|
+
action: { disabled: `${mode}-action-disabled` },
|
|
20
|
+
text: { secondary: `${mode}-text-secondary` },
|
|
21
|
+
divider: `${mode}-divider`,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const light = getNeutralChartPalette(tokenTheme('light'));
|
|
25
|
+
const dark = getNeutralChartPalette(tokenTheme('dark'));
|
|
26
|
+
|
|
27
|
+
expect(light.categorical).not.toEqual(dark.categorical);
|
|
28
|
+
expect(light.categorical).toHaveLength(5);
|
|
29
|
+
expect(light.categorical).toContain('light-primary-main');
|
|
30
|
+
expect(light.categorical.every((colour) => !colour.includes('#'))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('formats percentage and compact numbers according to the requested locale', () => {
|
|
34
|
+
expect(formatPercentage(0.25, 'en-US')).toBe('25%');
|
|
35
|
+
expect(formatPercentage(0.25, 'de-DE')).toContain('25');
|
|
36
|
+
expect(formatCompact(12500, 'en-US')).toMatch(/12.5K|13K/);
|
|
37
|
+
expect(createChartFormatters('fr-FR').ratio(1.5)).toContain('1');
|
|
38
|
+
});
|
|
39
|
+
});
|