@ifto/mantine-kit 0.3.0 → 0.5.1
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/README.md +85 -10
- package/dist/component/organism/EChart/chart/BarChart.d.ts +3 -0
- package/dist/component/organism/EChart/chart/BarChart.js +5 -0
- package/dist/component/organism/EChart/chart/BarChart.js.map +1 -1
- package/dist/component/organism/EChart/chart/CartesianChart.d.ts +4 -0
- package/dist/component/organism/EChart/chart/CartesianChart.js +5 -0
- package/dist/component/organism/EChart/chart/CartesianChart.js.map +1 -1
- package/dist/component/organism/EChart/chart/ChartType.d.ts +3 -0
- package/dist/component/organism/EChart/chart/DonutChart.d.ts +4 -0
- package/dist/component/organism/EChart/chart/DonutChart.js +6 -0
- package/dist/component/organism/EChart/chart/DonutChart.js.map +1 -1
- package/dist/component/organism/EChart/chart/LineChart.d.ts +3 -0
- package/dist/component/organism/EChart/chart/LineChart.js +5 -0
- package/dist/component/organism/EChart/chart/LineChart.js.map +1 -1
- package/dist/component/organism/EChart/chart/StackedBarChart.d.ts +3 -0
- package/dist/component/organism/EChart/chart/StackedBarChart.js +5 -0
- package/dist/component/organism/EChart/chart/StackedBarChart.js.map +1 -1
- package/dist/component/organism/Table/Table.d.ts +9 -9
- package/dist/component/organism/Table/Table.js +21 -24
- package/dist/component/organism/Table/Table.js.map +1 -1
- package/dist/component/organism/Table/TableData.d.ts +24 -0
- package/dist/component/organism/Table/TableData.js +1 -0
- package/dist/component/organism/Table/useClientPagination.d.ts +21 -0
- package/dist/component/organism/Table/useClientPagination.js +43 -0
- package/dist/component/organism/Table/useClientPagination.js.map +1 -0
- package/dist/component/organism/Table/useServerPagination.d.ts +24 -0
- package/dist/component/organism/Table/useServerPagination.js +105 -0
- package/dist/component/organism/Table/useServerPagination.js.map +1 -0
- package/dist/component/organism/ViewTable/DataTableView.d.ts +39 -0
- package/dist/component/organism/ViewTable/DataTableView.js +52 -0
- package/dist/component/organism/ViewTable/DataTableView.js.map +1 -0
- package/dist/component/organism/ViewTable/TableState.d.ts +30 -0
- package/dist/component/organism/ViewTable/TableState.js +19 -0
- package/dist/component/organism/ViewTable/TableState.js.map +1 -0
- package/dist/component/organism/ViewTable/ViewTable.d.ts +11 -33
- package/dist/component/organism/ViewTable/ViewTable.js +16 -101
- package/dist/component/organism/ViewTable/ViewTable.js.map +1 -1
- package/dist/component/organism/ViewTable/useClientTableState.d.ts +25 -0
- package/dist/component/organism/ViewTable/useClientTableState.js +68 -0
- package/dist/component/organism/ViewTable/useClientTableState.js.map +1 -0
- package/dist/component/template/ListPage/ListPage.d.ts +9 -19
- package/dist/component/template/ListPage/ListPage.js +5 -14
- package/dist/component/template/ListPage/ListPage.js.map +1 -1
- package/dist/data/PageDataSource.d.ts +10 -0
- package/dist/data/PageDataSource.js +1 -0
- package/dist/data/PageRequest.d.ts +29 -0
- package/dist/data/PageRequest.js +1 -0
- package/dist/hook/useService.d.ts +3 -3
- package/dist/hook/useService.js +2 -0
- package/dist/hook/useService.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Install the optional ones only if you use the components that need them:
|
|
|
49
49
|
| `@mantine/form` | the form templates (`FormPage`, `CreateFormPage`, `EditFormPage`) and `DataForm`, which take a `useForm` form |
|
|
50
50
|
| `@mantine/modals` | the confirmations of `Table` (delete), `ListPage` and `DataForm` |
|
|
51
51
|
| `@mantine/dates` and `dayjs` (its peer dependency) | `DateField` |
|
|
52
|
-
| `mantine-datatable` | `Table`, `ViewTable`, `
|
|
52
|
+
| `mantine-datatable` | `Table`, `ViewTable`, `DataTableView` |
|
|
53
53
|
| `echarts` | `EChart` and the chart types |
|
|
54
54
|
| `exceljs` | Excel export |
|
|
55
55
|
| `axios` | `AxiosHttpClient` |
|
|
@@ -117,11 +117,13 @@ module.exports = {
|
|
|
117
117
|
|
|
118
118
|
### CRUD list page
|
|
119
119
|
|
|
120
|
-
`ListPage`
|
|
121
|
-
object with `
|
|
120
|
+
`ListPage` lays out the header with the "Add" button, optional filters and a table. The `Table` shows the records with
|
|
121
|
+
view, edit and delete actions. Its `service` is any object with `delete`, and its `data` comes from a pagination hook:
|
|
122
122
|
|
|
123
123
|
```tsx
|
|
124
124
|
import ListPage from '@ifto/mantine-kit/component/template/ListPage/ListPage';
|
|
125
|
+
import Table from '@ifto/mantine-kit/component/organism/Table/Table';
|
|
126
|
+
import { useClientPagination } from '@ifto/mantine-kit/component/organism/Table/useClientPagination';
|
|
125
127
|
import type { ListDataSource } from '@ifto/mantine-kit/data/ListDataSource';
|
|
126
128
|
|
|
127
129
|
type Region = { id?: number; name: string };
|
|
@@ -132,20 +134,88 @@ const regions: ListDataSource<Region> = {
|
|
|
132
134
|
};
|
|
133
135
|
|
|
134
136
|
export function RegionsPage() {
|
|
137
|
+
// The object must be stable (here, a module constant), since a new one fetches again.
|
|
138
|
+
const data = useClientPagination(regions);
|
|
139
|
+
|
|
135
140
|
return (
|
|
136
141
|
<ListPage
|
|
137
142
|
title="Regions"
|
|
138
143
|
titlePlural="Regions"
|
|
139
144
|
breadcrumbItems={[{ title: 'Regions', href: '/region' }]}
|
|
140
145
|
addActionRoute="/region/create"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
>
|
|
147
|
+
<Table
|
|
148
|
+
data={data}
|
|
149
|
+
service={regions}
|
|
150
|
+
basePath="/region"
|
|
151
|
+
columns={[{ accessor: 'name', title: 'Name', sortable: true }]}
|
|
152
|
+
/>
|
|
153
|
+
</ListPage>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Client-side or server-side pagination
|
|
159
|
+
|
|
160
|
+
The hook that creates the `data` of the `Table` defines where the records are sorted and paginated:
|
|
161
|
+
|
|
162
|
+
| | `useClientPagination(source, { pagination })` | `useServerPagination(source, { firstPage })` |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| **Fetch** | all records at once | one page, with page, size and sort |
|
|
165
|
+
| **Sorted and paginated by** | browser (sorting follows the locale) | backend |
|
|
166
|
+
| **Total of records** | number of fetched records | `totalRecords` of the response |
|
|
167
|
+
|
|
168
|
+
Both take a `source`, which is either:
|
|
169
|
+
|
|
170
|
+
- an object with the method that fetches the records: `findAll()` (a `ListDataSource`) or `findPage(request)` (a
|
|
171
|
+
`PageDataSource`), such as a service. The hook calls the method on the object, so it keeps its `this`;
|
|
172
|
+
- or a function that fetches them, for fetches with a filter (e.g. `useCallback(request => service.findPageByYear(year,
|
|
173
|
+
request), [year])`).
|
|
174
|
+
|
|
175
|
+
The source must be stable (e.g. a module constant, or a function created with `useCallback`), since a new one fetches
|
|
176
|
+
again: a `new Service()` inside the component would fetch on every render.
|
|
177
|
+
|
|
178
|
+
`findPage` (or the function) receives the page, the size and the sort, and returns the records of that page and the total of records. The
|
|
179
|
+
contract knows nothing about your backend, so convert it to the format of your API (here, Spring Data, returning a
|
|
180
|
+
`PagedModel`, which puts the total of records in `page.totalElements`). The page starts at 1, or at the `firstPage`
|
|
181
|
+
option (e.g. 0 for Spring), which only changes the requests: the table always shows the pages starting at 1.
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
import { useCallback } from 'react';
|
|
185
|
+
import { useServerPagination } from '@ifto/mantine-kit/component/organism/Table/useServerPagination';
|
|
186
|
+
import type { FetchPage } from '@ifto/mantine-kit/data/PageRequest';
|
|
187
|
+
|
|
188
|
+
export function RegionsTable({ name }: { name: string }) {
|
|
189
|
+
// A new function means the filter changed, which goes back to page 1.
|
|
190
|
+
const fetchPage: FetchPage<Region> = useCallback(
|
|
191
|
+
async ({ page, size, sort }) => {
|
|
192
|
+
const params = new URLSearchParams({
|
|
193
|
+
name,
|
|
194
|
+
page: String(page),
|
|
195
|
+
size: String(size),
|
|
196
|
+
sort: `${String(sort.columnAccessor)},${sort.direction}`
|
|
197
|
+
});
|
|
198
|
+
const response = await fetch(`/api/region?${params}`).then(result => result.json());
|
|
199
|
+
|
|
200
|
+
return { records: response.content, totalRecords: response.page.totalElements };
|
|
201
|
+
},
|
|
202
|
+
[name]
|
|
145
203
|
);
|
|
204
|
+
const data = useServerPagination(fetchPage, { firstPage: 0 });
|
|
205
|
+
|
|
206
|
+
return <Table data={data} service={regions} basePath="/region" columns={columns} />;
|
|
146
207
|
}
|
|
147
208
|
```
|
|
148
209
|
|
|
210
|
+
With `useServerPagination`:
|
|
211
|
+
|
|
212
|
+
- `sort.columnAccessor` holds the `sortKey` of the column, if it has one, or else its `accessor`. A calculated column
|
|
213
|
+
needs a `sortKey` or `sortable: false`.
|
|
214
|
+
- The table goes back to page 1 when the page size, the sort or the filter changes, and to the last existing page when
|
|
215
|
+
the current one no longer exists (e.g. its last record was deleted).
|
|
216
|
+
- The order of null values and the text collation are defined by the backend, so they can differ from client-side
|
|
217
|
+
pagination, which follows the locale and always puts null values last.
|
|
218
|
+
|
|
149
219
|
### CRUD form page
|
|
150
220
|
|
|
151
221
|
`FormPage` creates, edits or views a record and saves it with any object that has a `save(id, data)` method:
|
|
@@ -226,6 +296,10 @@ The chart types in `component/organism/EChart/chart` (`LINE_CHART`, `BAR_CHART`,
|
|
|
226
296
|
build a complete option, with one panel per unit, from generic series:
|
|
227
297
|
`option={LINE_CHART.buildOption(data, { title: 'Revenue', locale: 'en-US' })}`.
|
|
228
298
|
|
|
299
|
+
Each of these types has an `icon`, to show in a chart type selector (e.g. `IconChartBar` for `BAR_CHART`). To use another
|
|
300
|
+
one, create the type with it: `new BarChart(IconChartHistogram)`. The `icon` is optional in the `ChartType` contract, so
|
|
301
|
+
a custom chart type does not need one.
|
|
302
|
+
|
|
229
303
|
### Export to Excel or CSV
|
|
230
304
|
|
|
231
305
|
```tsx
|
|
@@ -391,9 +465,10 @@ Every file is imported by its path inside `src`, without the extension, e.g.
|
|
|
391
465
|
| `Navbar` | Side menu with links and collapsible groups, highlighting the current page. |
|
|
392
466
|
| `PageHeader` | Page title with the breadcrumb and, in list pages, the "Add" button. |
|
|
393
467
|
| `StatsGrid` | Responsive grid of `StatsCard`s, with placeholders while loading. |
|
|
394
|
-
| `Table` | Table of
|
|
468
|
+
| `Table` | Table of records with view, edit and delete (with confirmation) actions, paginated by `useClientPagination` or `useServerPagination`. |
|
|
395
469
|
| `RowActions` | View, edit and delete buttons of a `Table` row. |
|
|
396
|
-
| `ViewTable` |
|
|
470
|
+
| `ViewTable` | Table that sorts (locale-aware) and paginates the given records in the browser (on top of mantine-datatable). |
|
|
471
|
+
| `DataTableView` | Table that shows records as received, with the pagination and sort given by the caller. |
|
|
397
472
|
|
|
398
473
|
The `EChart/chart` folder has chart types that build the ECharts option from generic series: `LineChart`, `BarChart`
|
|
399
474
|
(negative values highlighted), `StackedBarChart` and `DonutChart`, with one panel per unit of measure.
|
|
@@ -403,7 +478,7 @@ The `EChart/chart` folder has chart types that build the ECharts option from gen
|
|
|
403
478
|
| Component | What it does |
|
|
404
479
|
|---|---|
|
|
405
480
|
| `Layout` | App shell: header with the brand, color scheme toggle and user menu, collapsible side menu and content area. |
|
|
406
|
-
| `ListPage` | List page of records: header with the "Add" button, optional filters and the
|
|
481
|
+
| `ListPage` | List page of records: header with the "Add" button, optional filters and the table given as children. |
|
|
407
482
|
| `FormPage` | Create, edit or view form page that saves the record in a `SaveDataSource`. |
|
|
408
483
|
| `CreateFormPage` | Form page to create a record with a given function. |
|
|
409
484
|
| `EditFormPage` | Form page to update a record with a given function. |
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Icon } from '@tabler/icons-react';
|
|
1
2
|
import { ChartSeries } from './ChartSeries';
|
|
2
3
|
import { ChartTypeId } from './ChartTypeId';
|
|
3
4
|
import { CartesianChart, CartesianSeries, DataPoint } from './CartesianChart';
|
|
@@ -11,6 +12,8 @@ export declare const NEGATIVE_VALUE_COLOR = "#d9480f";
|
|
|
11
12
|
*/
|
|
12
13
|
export declare class BarChart extends CartesianChart {
|
|
13
14
|
readonly id: ChartTypeId;
|
|
15
|
+
/** @param icon icon that replaces the default one */
|
|
16
|
+
constructor(icon?: Icon);
|
|
14
17
|
protected seriesStyle(): CartesianSeries;
|
|
15
18
|
protected seriesData(serie: ChartSeries): DataPoint[];
|
|
16
19
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { CartesianChart } from "./CartesianChart.js";
|
|
3
|
+
import { IconChartBar } from "@tabler/icons-react";
|
|
3
4
|
// src/component/organism/EChart/chart/BarChart.ts
|
|
4
5
|
/** Color of the columns with a negative value (e.g. a deficit), to set them apart from the positive ones. */
|
|
5
6
|
var NEGATIVE_VALUE_COLOR = "#d9480f";
|
|
@@ -11,6 +12,10 @@ var NEGATIVE_VALUE_COLOR = "#d9480f";
|
|
|
11
12
|
*/
|
|
12
13
|
var BarChart = class extends CartesianChart {
|
|
13
14
|
id = "bar";
|
|
15
|
+
/** @param icon icon that replaces the default one */
|
|
16
|
+
constructor(icon = IconChartBar) {
|
|
17
|
+
super(icon);
|
|
18
|
+
}
|
|
14
19
|
seriesStyle() {
|
|
15
20
|
return { type: "bar" };
|
|
16
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/BarChart.ts"],"sourcesContent":["import { ChartSeries } from './ChartSeries';\nimport { ChartTypeId } from './ChartTypeId';\nimport { CartesianChart, CartesianSeries, DataPoint } from './CartesianChart';\n\n/** Color of the columns with a negative value (e.g. a deficit), to set them apart from the positive ones. */\nexport const NEGATIVE_VALUE_COLOR = '#d9480f';\n\n/**\n * Grouped column chart: compares the series category by category.\n * Suited to flows (e.g. yearly values) and to balances that may be negative (deficit/surplus): the negative columns\n * get the {@link NEGATIVE_VALUE_COLOR}, without changing the color of the series in the legend.\n * @author Manoel Campos\n */\nexport class BarChart extends CartesianChart {\n readonly id: ChartTypeId = 'bar';\n\n protected seriesStyle(): CartesianSeries {\n return { type: 'bar' };\n }\n\n protected seriesData(serie: ChartSeries): DataPoint[] {\n return highlightNegativeValues(serie.values);\n }\n}\n\n/**\n * Gives the {@link NEGATIVE_VALUE_COLOR} only to the negative values. The others keep the color of the series.\n * @param values values of a series\n * @return the values in the `series[i].data` format, with their own style in the negative ones\n */\nexport function highlightNegativeValues(values: (number | null)[]): DataPoint[] {\n return values.map(value =>\n value !== null && value < 0 ? { value, itemStyle: { color: NEGATIVE_VALUE_COLOR } } : value\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"BarChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/BarChart.ts"],"sourcesContent":["import { IconChartBar, type Icon } from '@tabler/icons-react';\n\nimport { ChartSeries } from './ChartSeries';\nimport { ChartTypeId } from './ChartTypeId';\nimport { CartesianChart, CartesianSeries, DataPoint } from './CartesianChart';\n\n/** Color of the columns with a negative value (e.g. a deficit), to set them apart from the positive ones. */\nexport const NEGATIVE_VALUE_COLOR = '#d9480f';\n\n/**\n * Grouped column chart: compares the series category by category.\n * Suited to flows (e.g. yearly values) and to balances that may be negative (deficit/surplus): the negative columns\n * get the {@link NEGATIVE_VALUE_COLOR}, without changing the color of the series in the legend.\n * @author Manoel Campos\n */\nexport class BarChart extends CartesianChart {\n readonly id: ChartTypeId = 'bar';\n\n /** @param icon icon that replaces the default one */\n constructor(icon: Icon = IconChartBar) {\n super(icon);\n }\n\n protected seriesStyle(): CartesianSeries {\n return { type: 'bar' };\n }\n\n protected seriesData(serie: ChartSeries): DataPoint[] {\n return highlightNegativeValues(serie.values);\n }\n}\n\n/**\n * Gives the {@link NEGATIVE_VALUE_COLOR} only to the negative values. The others keep the color of the series.\n * @param values values of a series\n * @return the values in the `series[i].data` format, with their own style in the negative ones\n */\nexport function highlightNegativeValues(values: (number | null)[]): DataPoint[] {\n return values.map(value =>\n value !== null && value < 0 ? { value, itemStyle: { color: NEGATIVE_VALUE_COLOR } } : value\n );\n}\n"],"mappings":";;;;;AAOA,IAAa,uBAAuB;;;;;;;AAQpC,IAAa,WAAb,cAA8B,eAAe;CACzC,KAA2B;;CAG3B,YAAY,OAAa,cAAc;EACnC,MAAM,IAAI;CACd;CAEA,cAAyC;EACrC,OAAO,EAAE,MAAM,MAAM;CACzB;CAEA,WAAqB,OAAiC;EAClD,OAAO,wBAAwB,MAAM,MAAM;CAC/C;AACJ;;;;;;AAOA,SAAgB,wBAAwB,QAAwC;CAC5E,OAAO,OAAO,KAAI,UACd,UAAU,QAAQ,QAAQ,IAAI;EAAE;EAAO,WAAW,EAAE,OAAO,qBAAqB;CAAE,IAAI,KAC1F;AACJ"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Icon } from '@tabler/icons-react';
|
|
1
2
|
import type { BarSeriesOption, EChartsOption, LineSeriesOption } from 'echarts';
|
|
2
3
|
import { ChartData, ChartSeries } from './ChartSeries';
|
|
3
4
|
import { ChartContext, ChartType } from './ChartType';
|
|
@@ -17,8 +18,11 @@ export type DataPoint = number | null | {
|
|
|
17
18
|
* @author Manoel Campos
|
|
18
19
|
*/
|
|
19
20
|
export declare abstract class CartesianChart implements ChartType {
|
|
21
|
+
readonly icon?: Icon | undefined;
|
|
20
22
|
abstract readonly id: ChartTypeId;
|
|
21
23
|
readonly singleCategory = false;
|
|
24
|
+
/** @param icon icon of the type (see {@link ChartType#icon}) */
|
|
25
|
+
constructor(icon?: Icon | undefined);
|
|
22
26
|
/** Types with axes work for any set of series, except the ones that restrict it (see the subclasses). */
|
|
23
27
|
isAvailable(series: ChartSeries[]): boolean;
|
|
24
28
|
buildOption(data: ChartData, context: ChartContext): EChartsOption;
|
|
@@ -9,7 +9,12 @@ import { buildChartHeader, legendExtraHeight, tooltipFormatter } from "./ChartHe
|
|
|
9
9
|
* @author Manoel Campos
|
|
10
10
|
*/
|
|
11
11
|
var CartesianChart = class {
|
|
12
|
+
icon;
|
|
12
13
|
singleCategory = false;
|
|
14
|
+
/** @param icon icon of the type (see {@link ChartType#icon}) */
|
|
15
|
+
constructor(icon) {
|
|
16
|
+
this.icon = icon;
|
|
17
|
+
}
|
|
13
18
|
/** Types with axes work for any set of series, except the ones that restrict it (see the subclasses). */
|
|
14
19
|
isAvailable(series) {
|
|
15
20
|
return series.length > 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CartesianChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/CartesianChart.ts"],"sourcesContent":["import type { BarSeriesOption, EChartsOption, LineSeriesOption } from 'echarts';\n\nimport { ChartData, ChartSeries } from './ChartSeries';\nimport { ChartContext, ChartType } from './ChartType';\nimport { ChartTypeId } from './ChartTypeId';\nimport { chartLocale, chartMessages } from './ChartTexts';\nimport { buildPanelAxes, groupByUnit, panelsHeight } from './UnitPanels';\nimport { buildChartHeader, legendExtraHeight, tooltipFormatter } from './ChartHeader';\n\n/** Column or line series, the two formats used in the charts with axes (cartesian). */\nexport type CartesianSeries = BarSeriesOption | LineSeriesOption;\n\n/** Value of a category in a series, in the format accepted by `series[i].data` (with or without its own style). */\nexport type DataPoint = number | null | { value: number; itemStyle: { color: string } };\n\n/**\n * Base of the chart types with an X axis of categories (line, columns, stacked columns).\n * It builds the panels per unit, the header and the tooltip, leaving to each type just the style of each series.\n * @author Manoel Campos\n */\nexport abstract class CartesianChart implements ChartType {\n abstract readonly id: ChartTypeId;\n readonly singleCategory = false;\n\n /** Types with axes work for any set of series, except the ones that restrict it (see the subclasses). */\n isAvailable(series: ChartSeries[]): boolean {\n return series.length > 0;\n }\n\n buildOption(data: ChartData, context: ChartContext): EChartsOption {\n const panels = groupByUnit(data.series);\n const series = panels.flatMap((panel, index) =>\n panel.series.map(serie => this.buildSeries(serie, data.series, index))\n );\n const names = data.series.map(serie => serie.name);\n const extraTop = legendExtraHeight(names, context.width);\n\n return {\n ...buildChartHeader(context.title, names, chartMessages(context)),\n tooltip: { trigger: 'axis' },\n ...buildPanelAxes(panels, data.categories, extraTop, chartLocale(context)),\n series\n };\n }\n\n height(data: ChartData, context: ChartContext): number {\n const extraTop = legendExtraHeight(\n data.series.map(serie => serie.name),\n context.width\n );\n return panelsHeight(groupByUnit(data.series).length, data.categories.length, extraTop);\n }\n\n /**\n * Defines how a series is drawn (type, stacking etc.).\n * @param serie series to draw\n * @param all all series of the chart, for types that depend on the whole set (e.g. stacking)\n * @return the style of the series in the ECharts format, without name, data and axes (filled by the base)\n */\n protected abstract seriesStyle(serie: ChartSeries, all: ChartSeries[]): CartesianSeries;\n\n /**\n * Values of the series in the `series[i].data` format. By default, the values themselves; subclasses may give\n * their own style to some values (e.g. negative values).\n * @param serie series of the chart\n * @return one value per category\n */\n protected seriesData(serie: ChartSeries): DataPoint[] {\n return serie.values;\n }\n\n /**\n * Creates the complete ECharts series: style of the type, name, values, axes of the panel and tooltip format.\n * @param serie series of the chart\n * @param all all series of the chart\n * @param panelIndex index of the panel (and of the axes) of the unit of the series\n * @return the series in the ECharts format\n */\n private buildSeries(serie: ChartSeries, all: ChartSeries[], panelIndex: number): CartesianSeries {\n return {\n ...this.seriesStyle(serie, all),\n id: serie.id,\n name: serie.name,\n data: this.seriesData(serie),\n xAxisIndex: panelIndex,\n yAxisIndex: panelIndex,\n tooltip: { valueFormatter: tooltipFormatter(serie.unit) }\n } as CartesianSeries;\n }\n}\n"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"CartesianChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/CartesianChart.ts"],"sourcesContent":["import type { Icon } from '@tabler/icons-react';\nimport type { BarSeriesOption, EChartsOption, LineSeriesOption } from 'echarts';\n\nimport { ChartData, ChartSeries } from './ChartSeries';\nimport { ChartContext, ChartType } from './ChartType';\nimport { ChartTypeId } from './ChartTypeId';\nimport { chartLocale, chartMessages } from './ChartTexts';\nimport { buildPanelAxes, groupByUnit, panelsHeight } from './UnitPanels';\nimport { buildChartHeader, legendExtraHeight, tooltipFormatter } from './ChartHeader';\n\n/** Column or line series, the two formats used in the charts with axes (cartesian). */\nexport type CartesianSeries = BarSeriesOption | LineSeriesOption;\n\n/** Value of a category in a series, in the format accepted by `series[i].data` (with or without its own style). */\nexport type DataPoint = number | null | { value: number; itemStyle: { color: string } };\n\n/**\n * Base of the chart types with an X axis of categories (line, columns, stacked columns).\n * It builds the panels per unit, the header and the tooltip, leaving to each type just the style of each series.\n * @author Manoel Campos\n */\nexport abstract class CartesianChart implements ChartType {\n abstract readonly id: ChartTypeId;\n readonly singleCategory = false;\n\n /** @param icon icon of the type (see {@link ChartType#icon}) */\n constructor(readonly icon?: Icon) {}\n\n /** Types with axes work for any set of series, except the ones that restrict it (see the subclasses). */\n isAvailable(series: ChartSeries[]): boolean {\n return series.length > 0;\n }\n\n buildOption(data: ChartData, context: ChartContext): EChartsOption {\n const panels = groupByUnit(data.series);\n const series = panels.flatMap((panel, index) =>\n panel.series.map(serie => this.buildSeries(serie, data.series, index))\n );\n const names = data.series.map(serie => serie.name);\n const extraTop = legendExtraHeight(names, context.width);\n\n return {\n ...buildChartHeader(context.title, names, chartMessages(context)),\n tooltip: { trigger: 'axis' },\n ...buildPanelAxes(panels, data.categories, extraTop, chartLocale(context)),\n series\n };\n }\n\n height(data: ChartData, context: ChartContext): number {\n const extraTop = legendExtraHeight(\n data.series.map(serie => serie.name),\n context.width\n );\n return panelsHeight(groupByUnit(data.series).length, data.categories.length, extraTop);\n }\n\n /**\n * Defines how a series is drawn (type, stacking etc.).\n * @param serie series to draw\n * @param all all series of the chart, for types that depend on the whole set (e.g. stacking)\n * @return the style of the series in the ECharts format, without name, data and axes (filled by the base)\n */\n protected abstract seriesStyle(serie: ChartSeries, all: ChartSeries[]): CartesianSeries;\n\n /**\n * Values of the series in the `series[i].data` format. By default, the values themselves; subclasses may give\n * their own style to some values (e.g. negative values).\n * @param serie series of the chart\n * @return one value per category\n */\n protected seriesData(serie: ChartSeries): DataPoint[] {\n return serie.values;\n }\n\n /**\n * Creates the complete ECharts series: style of the type, name, values, axes of the panel and tooltip format.\n * @param serie series of the chart\n * @param all all series of the chart\n * @param panelIndex index of the panel (and of the axes) of the unit of the series\n * @return the series in the ECharts format\n */\n private buildSeries(serie: ChartSeries, all: ChartSeries[], panelIndex: number): CartesianSeries {\n return {\n ...this.seriesStyle(serie, all),\n id: serie.id,\n name: serie.name,\n data: this.seriesData(serie),\n xAxisIndex: panelIndex,\n yAxisIndex: panelIndex,\n tooltip: { valueFormatter: tooltipFormatter(serie.unit) }\n } as CartesianSeries;\n }\n}\n"],"mappings":";;;;;;;;;;AAqBA,IAAsB,iBAAtB,MAA0D;CAKjC;CAHrB,iBAA0B;;CAG1B,YAAY,MAAsB;EAAb,KAAA,OAAA;CAAc;;CAGnC,YAAY,QAAgC;EACxC,OAAO,OAAO,SAAS;CAC3B;CAEA,YAAY,MAAiB,SAAsC;EAC/D,MAAM,SAAS,YAAY,KAAK,MAAM;EACtC,MAAM,SAAS,OAAO,SAAS,OAAO,UAClC,MAAM,OAAO,KAAI,UAAS,KAAK,YAAY,OAAO,KAAK,QAAQ,KAAK,CAAC,CACzE;EACA,MAAM,QAAQ,KAAK,OAAO,KAAI,UAAS,MAAM,IAAI;EACjD,MAAM,WAAW,kBAAkB,OAAO,QAAQ,KAAK;EAEvD,OAAO;GACH,GAAG,iBAAiB,QAAQ,OAAO,OAAO,cAAc,OAAO,CAAC;GAChE,SAAS,EAAE,SAAS,OAAO;GAC3B,GAAG,eAAe,QAAQ,KAAK,YAAY,UAAU,YAAY,OAAO,CAAC;GACzE;EACJ;CACJ;CAEA,OAAO,MAAiB,SAA+B;EACnD,MAAM,WAAW,kBACb,KAAK,OAAO,KAAI,UAAS,MAAM,IAAI,GACnC,QAAQ,KACZ;EACA,OAAO,aAAa,YAAY,KAAK,MAAM,CAAC,CAAC,QAAQ,KAAK,WAAW,QAAQ,QAAQ;CACzF;;;;;;;CAgBA,WAAqB,OAAiC;EAClD,OAAO,MAAM;CACjB;;;;;;;;CASA,YAAoB,OAAoB,KAAoB,YAAqC;EAC7F,OAAO;GACH,GAAG,KAAK,YAAY,OAAO,GAAG;GAC9B,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,MAAM,KAAK,WAAW,KAAK;GAC3B,YAAY;GACZ,YAAY;GACZ,SAAS,EAAE,gBAAgB,iBAAiB,MAAM,IAAI,EAAE;EAC5D;CACJ;AACJ"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Icon } from '@tabler/icons-react';
|
|
1
2
|
import type { EChartsOption } from 'echarts';
|
|
2
3
|
import { KitLocale } from '../../../../i18n/KitLocale';
|
|
3
4
|
import { ChartMessages } from '../../../../i18n/KitMessages';
|
|
@@ -25,6 +26,8 @@ export type ChartContext = {
|
|
|
25
26
|
};
|
|
26
27
|
export interface ChartType {
|
|
27
28
|
readonly id: ChartTypeId;
|
|
29
|
+
/** Icon that represents the type, such as in a chart type selector; absent if the type has none. */
|
|
30
|
+
readonly icon?: Icon;
|
|
28
31
|
/** Whether the type shows a single category (so the screen should show a category selector, such as the year). */
|
|
29
32
|
readonly singleCategory: boolean;
|
|
30
33
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Icon } from '@tabler/icons-react';
|
|
1
2
|
import type { EChartsOption } from 'echarts';
|
|
2
3
|
import { ChartData, ChartSeries } from './ChartSeries';
|
|
3
4
|
import { ChartContext, ChartType } from './ChartType';
|
|
@@ -16,8 +17,11 @@ export declare const DONUT_HEIGHT = 520;
|
|
|
16
17
|
* @author Manoel Campos
|
|
17
18
|
*/
|
|
18
19
|
export declare class DonutChart implements ChartType {
|
|
20
|
+
readonly icon: Icon;
|
|
19
21
|
readonly id: ChartTypeId;
|
|
20
22
|
readonly singleCategory = true;
|
|
23
|
+
/** @param icon icon that replaces the default one */
|
|
24
|
+
constructor(icon?: Icon);
|
|
21
25
|
isAvailable(series: ChartSeries[]): boolean;
|
|
22
26
|
buildOption(data: ChartData, context: ChartContext): EChartsOption;
|
|
23
27
|
height(data: ChartData, context: ChartContext): number;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { groupByStack } from "./ChartSeries.js";
|
|
3
3
|
import { chartMessages } from "./ChartTexts.js";
|
|
4
4
|
import { buildChartHeader, legendExtraHeight, tooltipFormatter } from "./ChartHeader.js";
|
|
5
|
+
import { IconChartDonut } from "@tabler/icons-react";
|
|
5
6
|
// src/component/organism/EChart/chart/DonutChart.ts
|
|
6
7
|
/**
|
|
7
8
|
* Height of the donut chart with the legend in one row (px). It does not depend on the number of categories; it only
|
|
@@ -21,8 +22,13 @@ var DONUT_CENTER = ["50%", "58%"];
|
|
|
21
22
|
* @author Manoel Campos
|
|
22
23
|
*/
|
|
23
24
|
var DonutChart = class {
|
|
25
|
+
icon;
|
|
24
26
|
id = "donut";
|
|
25
27
|
singleCategory = true;
|
|
28
|
+
/** @param icon icon that replaces the default one */
|
|
29
|
+
constructor(icon = IconChartDonut) {
|
|
30
|
+
this.icon = icon;
|
|
31
|
+
}
|
|
26
32
|
isAvailable(series) {
|
|
27
33
|
const stacks = [...groupByStack(series).entries()];
|
|
28
34
|
if (stacks.length !== 1) return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DonutChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/DonutChart.ts"],"sourcesContent":["import type { EChartsOption } from 'echarts';\n\nimport { ChartData, ChartSeries, groupByStack } from './ChartSeries';\nimport { ChartContext, ChartType } from './ChartType';\nimport { ChartTypeId } from './ChartTypeId';\nimport { chartMessages } from './ChartTexts';\nimport { buildChartHeader, legendExtraHeight, tooltipFormatter } from './ChartHeader';\n\n/**\n * Height of the donut chart with the legend in one row (px). It does not depend on the number of categories; it only\n * grows when the legend wraps in more rows.\n */\nexport const DONUT_HEIGHT = 520;\n\n/** Inner and outer radius of the donut, relative to the area of the donut ({@link DONUT_HEIGHT} high). */\nconst DONUT_RADIUS = ['40%', '68%'];\n\n/** Center of the donut: moved down so it is not under the title. */\nconst DONUT_CENTER = ['50%', '58%'];\n\n/**\n * Donut chart: shows how the total of a stack ({@link ChartSeries#stack}) splits among its series in a single\n * category (e.g. \"how the revenue splits by category in 2040\").\n * It is available only when there is exactly one stack, none of its series has a negative value (negative slices make\n * no sense) and all series of the chart are in the stack or are its total ({@link ChartSeries#totalOf}).\n * So choosing the donut never stops showing, without warning, a series the user selected.\n * @author Manoel Campos\n */\nexport class DonutChart implements ChartType {\n readonly id: ChartTypeId = 'donut';\n readonly singleCategory = true;\n\n isAvailable(series: ChartSeries[]): boolean {\n const stacks = [...groupByStack(series).entries()];\n if (stacks.length !== 1) return false;\n\n const [stack, slicesSeries] = stacks[0];\n const belongsToStack = (serie: ChartSeries) => serie.stack === stack || serie.totalOf === stack;\n return series.every(belongsToStack) && slicesSeries.every(hasNoNegativeValues);\n }\n\n buildOption(data: ChartData, context: ChartContext): EChartsOption {\n const messages = chartMessages(context);\n const category = shownCategory(data.categories, context.category);\n const slices = buildSlices(data, context);\n const unit = stackSeries(data)[0]?.unit;\n const total = slices.reduce((sum, slice) => sum + slice.value, 0);\n const header = buildChartHeader(\n `${context.title} - ${category}`,\n slices.map(slice => slice.name),\n messages\n );\n\n return {\n ...header,\n title: { ...header.title, subtext: unit && messages.total(unit.format(total)) },\n // The legend goes to the bottom, so it is not under the subtitle with the total.\n legend: { ...header.legend, top: undefined, bottom: 0 },\n tooltip: { trigger: 'item', valueFormatter: unit && tooltipFormatter(unit) },\n series: [\n {\n type: 'pie',\n // The area of the donut is fixed at the top; the extra legend rows stay below it.\n top: 0,\n height: DONUT_HEIGHT,\n radius: DONUT_RADIUS,\n center: DONUT_CENTER,\n data: slices,\n label: { formatter: '{b}: {d}%' }\n }\n ]\n } as EChartsOption;\n }\n\n height(data: ChartData, context: ChartContext): number {\n return (\n DONUT_HEIGHT +\n legendExtraHeight(\n buildSlices(data, context).map(slice => slice.name),\n context.width\n )\n );\n }\n}\n\n/** Slice of the donut: the value of a series of the stack in the category shown. */\ntype Slice = { name: string; value: number };\n\n/**\n * Returns the series of the only stack of the chart, which become the slices of the donut.\n * @param data series and categories of the chart\n * @return the series of the stack (empty if there is no stack)\n */\nfunction stackSeries(data: ChartData): ChartSeries[] {\n return [...groupByStack(data.series).values()][0] ?? [];\n}\n\n/**\n * Builds the slices of the donut in the category shown, without the series that have no value in it.\n * @param data series and categories of the chart\n * @param context category chosen on the screen\n * @return a slice per series of the stack with a value in the category\n */\nfunction buildSlices(data: ChartData, context: ChartContext): Slice[] {\n const categoryIndex = data.categories.indexOf(shownCategory(data.categories, context.category));\n return stackSeries(data)\n .map(serie => ({ name: serie.name, value: serie.values[categoryIndex] }))\n .filter((slice): slice is Slice => slice.value !== null);\n}\n\n/**\n * Checks whether a series has no negative values, which make no sense as slices.\n * @param serie series of a stack\n * @return true if the series has no negative value (categories without a value are ignored)\n */\nfunction hasNoNegativeValues(serie: ChartSeries): boolean {\n return serie.values.every(value => value === null || value >= 0);\n}\n\n/**\n * Returns the category a single-category chart should show: the chosen one, if it is in the chart; otherwise, the last.\n * @template C type of the categories (e.g. the years, as numbers)\n * @param categories categories of the chart\n * @param chosen category chosen on the screen; absent if none was chosen\n * @return the category to show\n */\nexport function shownCategory<C extends string | number>(categories: C[], chosen?: C): C {\n return chosen !== undefined && categories.includes(chosen) ? chosen : categories[categories.length - 1];\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"DonutChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/DonutChart.ts"],"sourcesContent":["import { IconChartDonut, type Icon } from '@tabler/icons-react';\nimport type { EChartsOption } from 'echarts';\n\nimport { ChartData, ChartSeries, groupByStack } from './ChartSeries';\nimport { ChartContext, ChartType } from './ChartType';\nimport { ChartTypeId } from './ChartTypeId';\nimport { chartMessages } from './ChartTexts';\nimport { buildChartHeader, legendExtraHeight, tooltipFormatter } from './ChartHeader';\n\n/**\n * Height of the donut chart with the legend in one row (px). It does not depend on the number of categories; it only\n * grows when the legend wraps in more rows.\n */\nexport const DONUT_HEIGHT = 520;\n\n/** Inner and outer radius of the donut, relative to the area of the donut ({@link DONUT_HEIGHT} high). */\nconst DONUT_RADIUS = ['40%', '68%'];\n\n/** Center of the donut: moved down so it is not under the title. */\nconst DONUT_CENTER = ['50%', '58%'];\n\n/**\n * Donut chart: shows how the total of a stack ({@link ChartSeries#stack}) splits among its series in a single\n * category (e.g. \"how the revenue splits by category in 2040\").\n * It is available only when there is exactly one stack, none of its series has a negative value (negative slices make\n * no sense) and all series of the chart are in the stack or are its total ({@link ChartSeries#totalOf}).\n * So choosing the donut never stops showing, without warning, a series the user selected.\n * @author Manoel Campos\n */\nexport class DonutChart implements ChartType {\n readonly id: ChartTypeId = 'donut';\n readonly singleCategory = true;\n\n /** @param icon icon that replaces the default one */\n constructor(readonly icon: Icon = IconChartDonut) {}\n\n isAvailable(series: ChartSeries[]): boolean {\n const stacks = [...groupByStack(series).entries()];\n if (stacks.length !== 1) return false;\n\n const [stack, slicesSeries] = stacks[0];\n const belongsToStack = (serie: ChartSeries) => serie.stack === stack || serie.totalOf === stack;\n return series.every(belongsToStack) && slicesSeries.every(hasNoNegativeValues);\n }\n\n buildOption(data: ChartData, context: ChartContext): EChartsOption {\n const messages = chartMessages(context);\n const category = shownCategory(data.categories, context.category);\n const slices = buildSlices(data, context);\n const unit = stackSeries(data)[0]?.unit;\n const total = slices.reduce((sum, slice) => sum + slice.value, 0);\n const header = buildChartHeader(\n `${context.title} - ${category}`,\n slices.map(slice => slice.name),\n messages\n );\n\n return {\n ...header,\n title: { ...header.title, subtext: unit && messages.total(unit.format(total)) },\n // The legend goes to the bottom, so it is not under the subtitle with the total.\n legend: { ...header.legend, top: undefined, bottom: 0 },\n tooltip: { trigger: 'item', valueFormatter: unit && tooltipFormatter(unit) },\n series: [\n {\n type: 'pie',\n // The area of the donut is fixed at the top; the extra legend rows stay below it.\n top: 0,\n height: DONUT_HEIGHT,\n radius: DONUT_RADIUS,\n center: DONUT_CENTER,\n data: slices,\n label: { formatter: '{b}: {d}%' }\n }\n ]\n } as EChartsOption;\n }\n\n height(data: ChartData, context: ChartContext): number {\n return (\n DONUT_HEIGHT +\n legendExtraHeight(\n buildSlices(data, context).map(slice => slice.name),\n context.width\n )\n );\n }\n}\n\n/** Slice of the donut: the value of a series of the stack in the category shown. */\ntype Slice = { name: string; value: number };\n\n/**\n * Returns the series of the only stack of the chart, which become the slices of the donut.\n * @param data series and categories of the chart\n * @return the series of the stack (empty if there is no stack)\n */\nfunction stackSeries(data: ChartData): ChartSeries[] {\n return [...groupByStack(data.series).values()][0] ?? [];\n}\n\n/**\n * Builds the slices of the donut in the category shown, without the series that have no value in it.\n * @param data series and categories of the chart\n * @param context category chosen on the screen\n * @return a slice per series of the stack with a value in the category\n */\nfunction buildSlices(data: ChartData, context: ChartContext): Slice[] {\n const categoryIndex = data.categories.indexOf(shownCategory(data.categories, context.category));\n return stackSeries(data)\n .map(serie => ({ name: serie.name, value: serie.values[categoryIndex] }))\n .filter((slice): slice is Slice => slice.value !== null);\n}\n\n/**\n * Checks whether a series has no negative values, which make no sense as slices.\n * @param serie series of a stack\n * @return true if the series has no negative value (categories without a value are ignored)\n */\nfunction hasNoNegativeValues(serie: ChartSeries): boolean {\n return serie.values.every(value => value === null || value >= 0);\n}\n\n/**\n * Returns the category a single-category chart should show: the chosen one, if it is in the chart; otherwise, the last.\n * @template C type of the categories (e.g. the years, as numbers)\n * @param categories categories of the chart\n * @param chosen category chosen on the screen; absent if none was chosen\n * @return the category to show\n */\nexport function shownCategory<C extends string | number>(categories: C[], chosen?: C): C {\n return chosen !== undefined && categories.includes(chosen) ? chosen : categories[categories.length - 1];\n}\n"],"mappings":";;;;;;;;;;AAaA,IAAa,eAAe;;AAG5B,IAAM,eAAe,CAAC,OAAO,KAAK;;AAGlC,IAAM,eAAe,CAAC,OAAO,KAAK;;;;;;;;;AAUlC,IAAa,aAAb,MAA6C;CAKpB;CAJrB,KAA2B;CAC3B,iBAA0B;;CAG1B,YAAY,OAAsB,gBAAgB;EAA7B,KAAA,OAAA;CAA8B;CAEnD,YAAY,QAAgC;EACxC,MAAM,SAAS,CAAC,GAAG,aAAa,MAAM,CAAC,CAAC,QAAQ,CAAC;EACjD,IAAI,OAAO,WAAW,GAAG,OAAO;EAEhC,MAAM,CAAC,OAAO,gBAAgB,OAAO;EACrC,MAAM,kBAAkB,UAAuB,MAAM,UAAU,SAAS,MAAM,YAAY;EAC1F,OAAO,OAAO,MAAM,cAAc,KAAK,aAAa,MAAM,mBAAmB;CACjF;CAEA,YAAY,MAAiB,SAAsC;EAC/D,MAAM,WAAW,cAAc,OAAO;EACtC,MAAM,WAAW,cAAc,KAAK,YAAY,QAAQ,QAAQ;EAChE,MAAM,SAAS,YAAY,MAAM,OAAO;EACxC,MAAM,OAAO,YAAY,IAAI,CAAC,CAAC,EAAE,EAAE;EACnC,MAAM,QAAQ,OAAO,QAAQ,KAAK,UAAU,MAAM,MAAM,OAAO,CAAC;EAChE,MAAM,SAAS,iBACX,GAAG,QAAQ,MAAM,KAAK,YACtB,OAAO,KAAI,UAAS,MAAM,IAAI,GAC9B,QACJ;EAEA,OAAO;GACH,GAAG;GACH,OAAO;IAAE,GAAG,OAAO;IAAO,SAAS,QAAQ,SAAS,MAAM,KAAK,OAAO,KAAK,CAAC;GAAE;GAE9E,QAAQ;IAAE,GAAG,OAAO;IAAQ,KAAK,KAAA;IAAW,QAAQ;GAAE;GACtD,SAAS;IAAE,SAAS;IAAQ,gBAAgB,QAAQ,iBAAiB,IAAI;GAAE;GAC3E,QAAQ,CACJ;IACI,MAAM;IAEN,KAAK;IACL,QAAA;IACA,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,OAAO,EAAE,WAAW,YAAY;GACpC,CACJ;EACJ;CACJ;CAEA,OAAO,MAAiB,SAA+B;EACnD,OAAA,MAEI,kBACI,YAAY,MAAM,OAAO,CAAC,CAAC,KAAI,UAAS,MAAM,IAAI,GAClD,QAAQ,KACZ;CAER;AACJ;;;;;;AAUA,SAAS,YAAY,MAAgC;CACjD,OAAO,CAAC,GAAG,aAAa,KAAK,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AAC1D;;;;;;;AAQA,SAAS,YAAY,MAAiB,SAAgC;CAClE,MAAM,gBAAgB,KAAK,WAAW,QAAQ,cAAc,KAAK,YAAY,QAAQ,QAAQ,CAAC;CAC9F,OAAO,YAAY,IAAI,CAAC,CACnB,KAAI,WAAU;EAAE,MAAM,MAAM;EAAM,OAAO,MAAM,OAAO;CAAe,EAAE,CAAC,CACxE,QAAQ,UAA0B,MAAM,UAAU,IAAI;AAC/D;;;;;;AAOA,SAAS,oBAAoB,OAA6B;CACtD,OAAO,MAAM,OAAO,OAAM,UAAS,UAAU,QAAQ,SAAS,CAAC;AACnE;;;;;;;;AASA,SAAgB,cAAyC,YAAiB,QAAe;CACrF,OAAO,WAAW,KAAA,KAAa,WAAW,SAAS,MAAM,IAAI,SAAS,WAAW,WAAW,SAAS;AACzG"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Icon } from '@tabler/icons-react';
|
|
1
2
|
import { ChartTypeId } from './ChartTypeId';
|
|
2
3
|
import { CartesianChart, CartesianSeries } from './CartesianChart';
|
|
3
4
|
/**
|
|
@@ -7,5 +8,7 @@ import { CartesianChart, CartesianSeries } from './CartesianChart';
|
|
|
7
8
|
*/
|
|
8
9
|
export declare class LineChart extends CartesianChart {
|
|
9
10
|
readonly id: ChartTypeId;
|
|
11
|
+
/** @param icon icon that replaces the default one */
|
|
12
|
+
constructor(icon?: Icon);
|
|
10
13
|
protected seriesStyle(): CartesianSeries;
|
|
11
14
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { CartesianChart } from "./CartesianChart.js";
|
|
3
|
+
import { IconChartLine } from "@tabler/icons-react";
|
|
3
4
|
// src/component/organism/EChart/chart/LineChart.ts
|
|
4
5
|
/**
|
|
5
6
|
* Line chart: shows the trend of each series along the categories.
|
|
@@ -8,6 +9,10 @@ import { CartesianChart } from "./CartesianChart.js";
|
|
|
8
9
|
*/
|
|
9
10
|
var LineChart = class extends CartesianChart {
|
|
10
11
|
id = "line";
|
|
12
|
+
/** @param icon icon that replaces the default one */
|
|
13
|
+
constructor(icon = IconChartLine) {
|
|
14
|
+
super(icon);
|
|
15
|
+
}
|
|
11
16
|
seriesStyle() {
|
|
12
17
|
return {
|
|
13
18
|
type: "line",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LineChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/LineChart.ts"],"sourcesContent":["import { ChartTypeId } from './ChartTypeId';\nimport { CartesianChart, CartesianSeries } from './CartesianChart';\n\n/**\n * Line chart: shows the trend of each series along the categories.\n * Suited to rates and levels (e.g. percentages, coefficients, population).\n * @author Manoel Campos\n */\nexport class LineChart extends CartesianChart {\n readonly id: ChartTypeId = 'line';\n\n protected seriesStyle(): CartesianSeries {\n // connectNulls false: a category without a value is a gap in the line, instead of joining the neighbors.\n return { type: 'line', connectNulls: false };\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"LineChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/LineChart.ts"],"sourcesContent":["import { IconChartLine, type Icon } from '@tabler/icons-react';\n\nimport { ChartTypeId } from './ChartTypeId';\nimport { CartesianChart, CartesianSeries } from './CartesianChart';\n\n/**\n * Line chart: shows the trend of each series along the categories.\n * Suited to rates and levels (e.g. percentages, coefficients, population).\n * @author Manoel Campos\n */\nexport class LineChart extends CartesianChart {\n readonly id: ChartTypeId = 'line';\n\n /** @param icon icon that replaces the default one */\n constructor(icon: Icon = IconChartLine) {\n super(icon);\n }\n\n protected seriesStyle(): CartesianSeries {\n // connectNulls false: a category without a value is a gap in the line, instead of joining the neighbors.\n return { type: 'line', connectNulls: false };\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAa,YAAb,cAA+B,eAAe;CAC1C,KAA2B;;CAG3B,YAAY,OAAa,eAAe;EACpC,MAAM,IAAI;CACd;CAEA,cAAyC;EAErC,OAAO;GAAE,MAAM;GAAQ,cAAc;EAAM;CAC/C;AACJ"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Icon } from '@tabler/icons-react';
|
|
1
2
|
import { ChartSeries } from './ChartSeries';
|
|
2
3
|
import { ChartTypeId } from './ChartTypeId';
|
|
3
4
|
import { CartesianChart, CartesianSeries } from './CartesianChart';
|
|
@@ -10,6 +11,8 @@ import { CartesianChart, CartesianSeries } from './CartesianChart';
|
|
|
10
11
|
*/
|
|
11
12
|
export declare class StackedBarChart extends CartesianChart {
|
|
12
13
|
readonly id: ChartTypeId;
|
|
14
|
+
/** @param icon icon that replaces the default one */
|
|
15
|
+
constructor(icon?: Icon);
|
|
13
16
|
isAvailable(series: ChartSeries[]): boolean;
|
|
14
17
|
protected seriesStyle(serie: ChartSeries, all: ChartSeries[]): CartesianSeries;
|
|
15
18
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { groupByStack } from "./ChartSeries.js";
|
|
3
3
|
import { CartesianChart } from "./CartesianChart.js";
|
|
4
|
+
import { IconChartColumn } from "@tabler/icons-react";
|
|
4
5
|
// src/component/organism/EChart/chart/StackedBarChart.ts
|
|
5
6
|
/**
|
|
6
7
|
* Stacked column chart with lines: the series of the same stack ({@link ChartSeries#stack}) are stacked, showing how
|
|
@@ -11,6 +12,10 @@ import { CartesianChart } from "./CartesianChart.js";
|
|
|
11
12
|
*/
|
|
12
13
|
var StackedBarChart = class extends CartesianChart {
|
|
13
14
|
id = "stacked-bar";
|
|
15
|
+
/** @param icon icon that replaces the default one */
|
|
16
|
+
constructor(icon = IconChartColumn) {
|
|
17
|
+
super(icon);
|
|
18
|
+
}
|
|
14
19
|
isAvailable(series) {
|
|
15
20
|
return groupByStack(series).size > 0;
|
|
16
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StackedBarChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/StackedBarChart.ts"],"sourcesContent":["import { ChartSeries, groupByStack } from './ChartSeries';\nimport { ChartTypeId } from './ChartTypeId';\nimport { CartesianChart, CartesianSeries } from './CartesianChart';\n\n/**\n * Stacked column chart with lines: the series of the same stack ({@link ChartSeries#stack}) are stacked, showing how\n * much each one contributes to each category, and the other series (such as a total) become lines over the columns.\n * It is available only when there is a stack with enough series (see `groupByStack`). Whoever builds the series\n * decides which ones can be stacked, so series whose sum makes no sense are never stacked.\n * @author Manoel Campos\n */\nexport class StackedBarChart extends CartesianChart {\n readonly id: ChartTypeId = 'stacked-bar';\n\n isAvailable(series: ChartSeries[]): boolean {\n return groupByStack(series).size > 0;\n }\n\n protected seriesStyle(serie: ChartSeries, all: ChartSeries[]): CartesianSeries {\n // A series with a stack, but alone in it, has nothing to be stacked with: it becomes a line.\n if (!serie.stack || !groupByStack(all).has(serie.stack)) return { type: 'line', connectNulls: false };\n\n // Each stack is stacked separately, so series of different wholes are not added up.\n return { type: 'bar', stack: serie.stack };\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"StackedBarChart.js","names":[],"sources":["../../../../../src/component/organism/EChart/chart/StackedBarChart.ts"],"sourcesContent":["import { IconChartColumn, type Icon } from '@tabler/icons-react';\n\nimport { ChartSeries, groupByStack } from './ChartSeries';\nimport { ChartTypeId } from './ChartTypeId';\nimport { CartesianChart, CartesianSeries } from './CartesianChart';\n\n/**\n * Stacked column chart with lines: the series of the same stack ({@link ChartSeries#stack}) are stacked, showing how\n * much each one contributes to each category, and the other series (such as a total) become lines over the columns.\n * It is available only when there is a stack with enough series (see `groupByStack`). Whoever builds the series\n * decides which ones can be stacked, so series whose sum makes no sense are never stacked.\n * @author Manoel Campos\n */\nexport class StackedBarChart extends CartesianChart {\n readonly id: ChartTypeId = 'stacked-bar';\n\n /** @param icon icon that replaces the default one */\n constructor(icon: Icon = IconChartColumn) {\n super(icon);\n }\n\n isAvailable(series: ChartSeries[]): boolean {\n return groupByStack(series).size > 0;\n }\n\n protected seriesStyle(serie: ChartSeries, all: ChartSeries[]): CartesianSeries {\n // A series with a stack, but alone in it, has nothing to be stacked with: it becomes a line.\n if (!serie.stack || !groupByStack(all).has(serie.stack)) return { type: 'line', connectNulls: false };\n\n // Each stack is stacked separately, so series of different wholes are not added up.\n return { type: 'bar', stack: serie.stack };\n }\n}\n"],"mappings":";;;;;;;;;;;;AAaA,IAAa,kBAAb,cAAqC,eAAe;CAChD,KAA2B;;CAG3B,YAAY,OAAa,iBAAiB;EACtC,MAAM,IAAI;CACd;CAEA,YAAY,QAAgC;EACxC,OAAO,aAAa,MAAM,CAAC,CAAC,OAAO;CACvC;CAEA,YAAsB,OAAoB,KAAqC;EAE3E,IAAI,CAAC,MAAM,SAAS,CAAC,aAAa,GAAG,CAAC,CAAC,IAAI,MAAM,KAAK,GAAG,OAAO;GAAE,MAAM;GAAQ,cAAc;EAAM;EAGpG,OAAO;GAAE,MAAM;GAAO,OAAO,MAAM;EAAM;CAC7C;AACJ"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
2
|
import { DataTableColumn } from 'mantine-datatable';
|
|
3
|
+
import { TableData } from './TableData';
|
|
3
4
|
import { ListDataSource } from '../../../data/ListDataSource';
|
|
4
5
|
import { Identifiable } from '../../../model/Identifiable';
|
|
5
6
|
/** Which actions the table shows for each record. */
|
|
@@ -15,21 +16,19 @@ export type ActionsSettings = {
|
|
|
15
16
|
};
|
|
16
17
|
/** Props of the {@link Table}. */
|
|
17
18
|
type TableProps<M extends Identifiable, S extends ListDataSource<M>> = {
|
|
18
|
-
/** Source of the records and of their deletion (e.g. a service that accesses an API). */
|
|
19
|
-
service: S;
|
|
20
19
|
/**
|
|
21
|
-
*
|
|
22
|
-
* `(
|
|
20
|
+
* Records and their pagination and sort, given by `useClientPagination` (sorted and paginated in the browser) or
|
|
21
|
+
* `useServerPagination` (sorted and paginated by the backend).
|
|
23
22
|
*/
|
|
24
|
-
|
|
23
|
+
data: TableData<M>;
|
|
24
|
+
/** Source of the records, which deletes them (e.g. a service that accesses an API). */
|
|
25
|
+
service: S;
|
|
25
26
|
/** Columns of the table. */
|
|
26
27
|
columns: DataTableColumn<M>[];
|
|
27
28
|
/** Base path of the pages of the records (e.g. `/region`), used to build the view and edit URLs. */
|
|
28
29
|
basePath: string;
|
|
29
30
|
/** Which actions are shown; the ones not given are shown. */
|
|
30
31
|
actionsSettings?: Partial<ActionsSettings>;
|
|
31
|
-
/** Whether the table is paginated. */
|
|
32
|
-
pagination?: boolean;
|
|
33
32
|
/**
|
|
34
33
|
* Called by the edit action instead of opening the edit page of the record.
|
|
35
34
|
* @param itemId id of the record to edit
|
|
@@ -42,8 +41,9 @@ type TableProps<M extends Identifiable, S extends ListDataSource<M>> = {
|
|
|
42
41
|
getViewUrl?: (item: M) => string;
|
|
43
42
|
};
|
|
44
43
|
/**
|
|
45
|
-
* A table that shows the records of a data source, with the view, edit and delete actions
|
|
46
|
-
* record.
|
|
44
|
+
* A table that shows the records of a data source, with the view, edit and delete actions, or an alert when they cannot
|
|
45
|
+
* be fetched. Clicking a row opens the record. Its `data` comes from `useClientPagination` or `useServerPagination`,
|
|
46
|
+
* which define where the records are sorted and paginated.
|
|
47
47
|
* @template M type of the records, shown one per row
|
|
48
48
|
* @template S source of the records (e.g. a service that accesses an API)
|
|
49
49
|
*/
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import
|
|
2
|
+
import DataTableView from "../ViewTable/DataTableView.js";
|
|
3
3
|
import RowActions from "./RowActions.js";
|
|
4
4
|
import { useKitConfig } from "../../../provider/useKit.js";
|
|
5
5
|
import ErrorAlert from "../../atom/ErrorAlert/ErrorAlert.js";
|
|
6
|
-
import useService from "../../../hook/useService.js";
|
|
7
6
|
import { showErrorNotification, showSuccessNotification } from "../../../util/Notifications.js";
|
|
8
|
-
import {
|
|
7
|
+
import { useMemo } from "react";
|
|
9
8
|
import { jsx } from "react/jsx-runtime";
|
|
10
9
|
import { Box } from "@mantine/core";
|
|
11
10
|
// src/component/organism/Table/Table.tsx
|
|
12
11
|
var ROW_HOVER_COLOR = "var(--mantine-primary-color-light)";
|
|
13
|
-
/** Initial records of the table, while they are fetched; a single constant, so it is stable across renders. */
|
|
14
|
-
var NO_RECORDS = [];
|
|
15
12
|
/** Every action shown, used for the settings the caller does not give. */
|
|
16
13
|
var DEFAULT_ACTIONS_SETTINGS = {
|
|
17
14
|
canShowActions: true,
|
|
@@ -30,13 +27,13 @@ function viewUrl(props, item, segments) {
|
|
|
30
27
|
}
|
|
31
28
|
/**
|
|
32
29
|
* Creates the function that deletes a record and reloads the table.
|
|
33
|
-
* @param setData function that sets the records of the table
|
|
30
|
+
* @param setData function that sets the records of the table
|
|
34
31
|
* @param service source of the records, which deletes them
|
|
35
|
-
* @param
|
|
32
|
+
* @param reload fetches the records again after the deletion
|
|
36
33
|
* @param messages texts of the locale
|
|
37
34
|
* @return a function that receives the record to delete and returns a promise resolved when it is deleted
|
|
38
35
|
*/
|
|
39
|
-
function getDeleteRecordFn(setData, service,
|
|
36
|
+
function getDeleteRecordFn(setData, service, reload, messages) {
|
|
40
37
|
return async (item) => {
|
|
41
38
|
const id = item?.id ?? 0;
|
|
42
39
|
if (!id) {
|
|
@@ -46,33 +43,32 @@ function getDeleteRecordFn(setData, service, fetchMethod, messages) {
|
|
|
46
43
|
try {
|
|
47
44
|
await service.delete(id);
|
|
48
45
|
showSuccessNotification(messages.table.deleteSuccess, messages.common.success);
|
|
49
|
-
|
|
46
|
+
await reload();
|
|
50
47
|
} catch (e) {
|
|
51
48
|
showErrorNotification(e, void 0, messages.common.error);
|
|
52
49
|
}
|
|
53
50
|
};
|
|
54
51
|
}
|
|
55
52
|
/**
|
|
56
|
-
* A table that shows the records of a data source, with the view, edit and delete actions
|
|
57
|
-
* record.
|
|
53
|
+
* A table that shows the records of a data source, with the view, edit and delete actions, or an alert when they cannot
|
|
54
|
+
* be fetched. Clicking a row opens the record. Its `data` comes from `useClientPagination` or `useServerPagination`,
|
|
55
|
+
* which define where the records are sorted and paginated.
|
|
58
56
|
* @template M type of the records, shown one per row
|
|
59
57
|
* @template S source of the records (e.g. a service that accesses an API)
|
|
60
58
|
*/
|
|
61
59
|
var Table = (props) => {
|
|
62
|
-
const {
|
|
63
|
-
const
|
|
60
|
+
const { service, columns, basePath, customEditFunction, data } = props;
|
|
61
|
+
const { setRecords, reload } = data;
|
|
64
62
|
const { router, messages, formRouteSegments } = useKitConfig();
|
|
65
63
|
const navigate = router.useNavigate();
|
|
66
64
|
const settings = {
|
|
67
65
|
...DEFAULT_ACTIONS_SETTINGS,
|
|
68
66
|
...props.actionsSettings
|
|
69
67
|
};
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
useEffect(() => setData(fetchedData), [fetchedData]);
|
|
73
|
-
const deleteRecord = useMemo(() => getDeleteRecordFn(setData, service, fetchMethod, messages), [
|
|
68
|
+
const deleteRecord = useMemo(() => getDeleteRecordFn(setRecords, service, reload, messages), [
|
|
69
|
+
setRecords,
|
|
74
70
|
service,
|
|
75
|
-
|
|
71
|
+
reload,
|
|
76
72
|
messages
|
|
77
73
|
]);
|
|
78
74
|
const actionsColumn = {
|
|
@@ -91,15 +87,16 @@ var Table = (props) => {
|
|
|
91
87
|
})
|
|
92
88
|
};
|
|
93
89
|
const effectiveColumns = settings.canShowActions ? [...columns, actionsColumn] : columns;
|
|
94
|
-
if (error) return /* @__PURE__ */ jsx(ErrorAlert, {
|
|
90
|
+
if (data.error) return /* @__PURE__ */ jsx(ErrorAlert, {
|
|
95
91
|
title: messages.common.loadError,
|
|
96
|
-
message: String(error)
|
|
92
|
+
message: String(data.error)
|
|
97
93
|
});
|
|
98
|
-
return /* @__PURE__ */ jsx(
|
|
99
|
-
data,
|
|
94
|
+
return /* @__PURE__ */ jsx(DataTableView, {
|
|
95
|
+
records: data.records,
|
|
96
|
+
state: data.tableState,
|
|
97
|
+
paginated: data.paginated,
|
|
100
98
|
columns: effectiveColumns,
|
|
101
|
-
loading,
|
|
102
|
-
pagination: props.pagination,
|
|
99
|
+
loading: data.loading,
|
|
103
100
|
highlightOnHover: true,
|
|
104
101
|
highlightOnHoverColor: ROW_HOVER_COLOR,
|
|
105
102
|
onRowClick: ({ record }) => navigate(viewUrl(props, record, formRouteSegments))
|