@carto/ps-react-ui 4.11.3 → 4.12.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/dist/chat.js +962 -733
- package/dist/chat.js.map +1 -1
- package/dist/csv-item-hH_Gt7ur.js +32 -0
- package/dist/csv-item-hH_Gt7ur.js.map +1 -0
- package/dist/{echart-BMPpj7n_.js → echart-Bdvbfx9s.js} +2 -2
- package/dist/echart-Bdvbfx9s.js.map +1 -0
- package/dist/{option-builders-F-c9ELi1.js → option-builders-DPeoyQaM.js} +41 -33
- package/dist/option-builders-DPeoyQaM.js.map +1 -0
- package/dist/png-item-9dNbB37T.js +57 -0
- package/dist/png-item-9dNbB37T.js.map +1 -0
- package/dist/table-B3ZWWhJt.js +383 -0
- package/dist/table-B3ZWWhJt.js.map +1 -0
- package/dist/types/chat/containers/chat-footer.d.ts +1 -1
- package/dist/types/chat/containers/styles.d.ts +79 -12
- package/dist/types/chat/index.d.ts +1 -1
- package/dist/types/chat/types.d.ts +21 -0
- package/dist/types/chat/use-typewriter.d.ts +5 -3
- package/dist/types/widgets/utils/chart-config/index.d.ts +1 -1
- package/dist/types/widgets-v2/actions/download/constants.d.ts +12 -0
- package/dist/types/widgets-v2/actions/download/csv-item.d.ts +38 -0
- package/dist/types/widgets-v2/actions/download/icons.d.ts +6 -0
- package/dist/types/widgets-v2/actions/download/index.d.ts +3 -1
- package/dist/types/widgets-v2/actions/index.d.ts +1 -1
- package/dist/types/widgets-v2/pie/skeleton.d.ts +9 -0
- package/dist/widgets/bar.js +1 -1
- package/dist/widgets/histogram.js +1 -1
- package/dist/widgets/pie.js +1 -1
- package/dist/widgets/scatterplot.js +5 -5
- package/dist/widgets/timeseries.js +1 -1
- package/dist/widgets/utils.js +1 -1
- package/dist/widgets-v2/actions.js +40 -36
- package/dist/widgets-v2/actions.js.map +1 -1
- package/dist/widgets-v2/bar.js +69 -76
- package/dist/widgets-v2/bar.js.map +1 -1
- package/dist/widgets-v2/category.js +50 -55
- package/dist/widgets-v2/category.js.map +1 -1
- package/dist/widgets-v2/echart.js +1 -1
- package/dist/widgets-v2/formula.js +37 -43
- package/dist/widgets-v2/formula.js.map +1 -1
- package/dist/widgets-v2/histogram.js +141 -147
- package/dist/widgets-v2/histogram.js.map +1 -1
- package/dist/widgets-v2/markdown.js +18 -17
- package/dist/widgets-v2/markdown.js.map +1 -1
- package/dist/widgets-v2/pie.js +174 -126
- package/dist/widgets-v2/pie.js.map +1 -1
- package/dist/widgets-v2/scatterplot.js +156 -166
- package/dist/widgets-v2/scatterplot.js.map +1 -1
- package/dist/widgets-v2/spread.js +36 -41
- package/dist/widgets-v2/spread.js.map +1 -1
- package/dist/widgets-v2/table.js +46 -55
- package/dist/widgets-v2/table.js.map +1 -1
- package/dist/widgets-v2/timeseries.js +83 -89
- package/dist/widgets-v2/timeseries.js.map +1 -1
- package/dist/widgets-v2.js +3 -3
- package/package.json +1 -1
- package/src/chat/bubbles/styles.ts +5 -1
- package/src/chat/containers/chat-content.tsx +4 -1
- package/src/chat/containers/chat-footer.test.tsx +59 -0
- package/src/chat/containers/chat-footer.tsx +124 -36
- package/src/chat/containers/styles.ts +107 -16
- package/src/chat/feedback/styles.ts +11 -4
- package/src/chat/index.ts +1 -0
- package/src/chat/types.ts +22 -0
- package/src/chat/use-typewriter.ts +32 -24
- package/src/widgets/utils/chart-config/index.ts +1 -0
- package/src/widgets/utils/chart-config/option-builders.test.ts +34 -0
- package/src/widgets/utils/chart-config/option-builders.ts +21 -0
- package/src/widgets-v2/actions/download/constants.ts +14 -0
- package/src/widgets-v2/actions/download/csv-item.test.tsx +77 -0
- package/src/widgets-v2/actions/download/csv-item.tsx +71 -0
- package/src/widgets-v2/actions/download/icons.tsx +10 -1
- package/src/widgets-v2/actions/download/index.ts +3 -1
- package/src/widgets-v2/actions/download/png-item.tsx +2 -1
- package/src/widgets-v2/actions/index.ts +5 -0
- package/src/widgets-v2/bar/download.tsx +16 -22
- package/src/widgets-v2/bar/options.ts +3 -2
- package/src/widgets-v2/category/download.test.ts +9 -0
- package/src/widgets-v2/category/download.ts +16 -20
- package/src/widgets-v2/echart/edge-label-clamp.ts +7 -4
- package/src/widgets-v2/formula/download.tsx +23 -29
- package/src/widgets-v2/histogram/download.ts +22 -26
- package/src/widgets-v2/histogram/options.ts +3 -2
- package/src/widgets-v2/markdown/{download.ts → download.tsx} +5 -2
- package/src/widgets-v2/pie/download.ts +16 -20
- package/src/widgets-v2/pie/skeleton.test.tsx +6 -3
- package/src/widgets-v2/pie/skeleton.tsx +69 -7
- package/src/widgets-v2/scatterplot/download.ts +16 -20
- package/src/widgets-v2/scatterplot/options.ts +3 -6
- package/src/widgets-v2/spread/download.ts +23 -27
- package/src/widgets-v2/table/download.test.ts +10 -0
- package/src/widgets-v2/table/download.ts +11 -15
- package/src/widgets-v2/table/helpers.test.ts +19 -0
- package/src/widgets-v2/table/helpers.ts +7 -12
- package/src/widgets-v2/timeseries/download.ts +36 -40
- package/src/widgets-v2/timeseries/options.ts +3 -2
- package/dist/echart-BMPpj7n_.js.map +0 -1
- package/dist/option-builders-F-c9ELi1.js.map +0 -1
- package/dist/png-item-BE9uEqlD.js +0 -45
- package/dist/png-item-BE9uEqlD.js.map +0 -1
- package/dist/table-C9IMbTr0.js +0 -385
- package/dist/table-C9IMbTr0.js.map +0 -1
- package/dist/types/chat/feedback/styles.d.ts +0 -211
- package/dist/types/widgets/utils/chart-config/option-builders.d.ts +0 -124
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DOWNLOAD_ITEM_IDS } from './constants'
|
|
2
|
+
import { downloadToCSV } from './exports'
|
|
3
|
+
import { CSVIcon } from './icons'
|
|
4
|
+
import type { DownloadItem } from './types'
|
|
5
|
+
|
|
6
|
+
interface BuildCsvDownloadItemBase {
|
|
7
|
+
/** Base filename (without extension). The item appends `.csv`. */
|
|
8
|
+
filename: string
|
|
9
|
+
/** Override the menu label. Default `'CSV'`. */
|
|
10
|
+
label?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Args for {@link buildCsvDownloadItem}. A discriminated union: a caller must
|
|
15
|
+
* supply exactly one content source — `getRows` (rows serialised through the
|
|
16
|
+
* shared `toCsvString`) or `getCsv` (a pre-built CSV string). The `?: never`
|
|
17
|
+
* arms make passing both — or neither — a compile-time error.
|
|
18
|
+
*/
|
|
19
|
+
export type BuildCsvDownloadItemArgs =
|
|
20
|
+
| (BuildCsvDownloadItemBase & {
|
|
21
|
+
/**
|
|
22
|
+
* Builds the CSV rows at click time. Used by most widgets — rows are run
|
|
23
|
+
* through `toCsvString` so escaping (incl. the spreadsheet
|
|
24
|
+
* formula-injection guard) stays consistent across widgets.
|
|
25
|
+
*/
|
|
26
|
+
getRows: () => readonly (readonly unknown[])[]
|
|
27
|
+
getCsv?: never
|
|
28
|
+
})
|
|
29
|
+
| (BuildCsvDownloadItemBase & {
|
|
30
|
+
/**
|
|
31
|
+
* Returns a pre-built CSV string at click time. Escape hatch for widgets
|
|
32
|
+
* (e.g. Table) that already serialise their own CSV with bespoke
|
|
33
|
+
* header/cell handling.
|
|
34
|
+
*/
|
|
35
|
+
getCsv: () => string
|
|
36
|
+
getRows?: never
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds the standard CSV `DownloadItem` used by every per-widget download
|
|
41
|
+
* config. Centralised so the menu label, icon, and `.csv` filename suffix stay
|
|
42
|
+
* consistent across widgets — mirrors {@link buildPngDownloadItem} so neither
|
|
43
|
+
* format can drift again.
|
|
44
|
+
*/
|
|
45
|
+
export function buildCsvDownloadItem(
|
|
46
|
+
args: BuildCsvDownloadItemArgs,
|
|
47
|
+
): DownloadItem {
|
|
48
|
+
return {
|
|
49
|
+
id: DOWNLOAD_ITEM_IDS.csv,
|
|
50
|
+
label: args.label ?? 'CSV',
|
|
51
|
+
icon: <CSVIcon fontSize='small' />,
|
|
52
|
+
resolve: () => {
|
|
53
|
+
if (args.getCsv) {
|
|
54
|
+
const csv = args.getCsv()
|
|
55
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
|
56
|
+
const url = URL.createObjectURL(blob)
|
|
57
|
+
return Promise.resolve({
|
|
58
|
+
url,
|
|
59
|
+
filename: `${args.filename}.csv`,
|
|
60
|
+
revoke: () => URL.revokeObjectURL(url),
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
const handle = downloadToCSV(args.getRows())
|
|
64
|
+
return Promise.resolve({
|
|
65
|
+
url: handle.url,
|
|
66
|
+
filename: `${args.filename}.csv`,
|
|
67
|
+
revoke: handle.revoke,
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SvgIcon, type SvgIconProps } from '@mui/material'
|
|
2
|
-
import { ImageOutlined } from '@mui/icons-material'
|
|
2
|
+
import { ArticleOutlined, ImageOutlined } from '@mui/icons-material'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Generic "image" glyph used for the PNG download item. Wraps MUI's
|
|
@@ -10,6 +10,15 @@ export function PNGIcon(props: SvgIconProps) {
|
|
|
10
10
|
return <ImageOutlined {...props} />
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Generic "document" glyph used for the Markdown download item. Wraps MUI's
|
|
15
|
+
* `ArticleOutlined`, mirroring {@link PNGIcon}, so the Markdown menu item
|
|
16
|
+
* carries an icon consistent with the other download formats.
|
|
17
|
+
*/
|
|
18
|
+
export function MarkdownIcon(props: SvgIconProps) {
|
|
19
|
+
return <ArticleOutlined {...props} />
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* "CSV" rectangle with the letters spelled inside — matches v1 visual and is
|
|
15
24
|
* easier to recognise in a download menu than a generic table glyph.
|
|
@@ -7,7 +7,9 @@ export {
|
|
|
7
7
|
type DownloadHandle,
|
|
8
8
|
type DownloadDOMToPNGOptions,
|
|
9
9
|
} from './exports'
|
|
10
|
-
export { CSVIcon, PNGIcon } from './icons'
|
|
10
|
+
export { CSVIcon, PNGIcon, MarkdownIcon } from './icons'
|
|
11
11
|
export { buildPngDownloadItem, type BuildPngDownloadItemArgs } from './png-item'
|
|
12
|
+
export { buildCsvDownloadItem, type BuildCsvDownloadItemArgs } from './csv-item'
|
|
13
|
+
export { DOWNLOAD_ITEM_IDS, type DownloadItemId } from './constants'
|
|
12
14
|
export type { DownloadItem } from './types'
|
|
13
15
|
export { DEFAULT_DOWNLOAD_LABELS, type DownloadLabels } from './labels'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DOWNLOAD_ITEM_IDS } from './constants'
|
|
1
2
|
import { downloadDOMToPNG } from './exports'
|
|
2
3
|
import { PNGIcon } from './icons'
|
|
3
4
|
import type { DownloadItem } from './types'
|
|
@@ -29,7 +30,7 @@ export function buildPngDownloadItem(
|
|
|
29
30
|
args: BuildPngDownloadItemArgs,
|
|
30
31
|
): DownloadItem {
|
|
31
32
|
return {
|
|
32
|
-
id:
|
|
33
|
+
id: DOWNLOAD_ITEM_IDS.png,
|
|
33
34
|
label: args.label ?? 'PNG',
|
|
34
35
|
icon: <PNGIcon fontSize='small' />,
|
|
35
36
|
resolve: async () => {
|
|
@@ -54,14 +54,19 @@ export {
|
|
|
54
54
|
toCsvString,
|
|
55
55
|
triggerLinkDownload,
|
|
56
56
|
buildPngDownloadItem,
|
|
57
|
+
buildCsvDownloadItem,
|
|
57
58
|
CSVIcon,
|
|
58
59
|
PNGIcon,
|
|
60
|
+
MarkdownIcon,
|
|
59
61
|
DEFAULT_DOWNLOAD_LABELS,
|
|
62
|
+
DOWNLOAD_ITEM_IDS,
|
|
60
63
|
type DownloadProps,
|
|
61
64
|
type DownloadItem,
|
|
65
|
+
type DownloadItemId,
|
|
62
66
|
type DownloadHandle,
|
|
63
67
|
type DownloadDOMToPNGOptions,
|
|
64
68
|
type BuildPngDownloadItemArgs,
|
|
69
|
+
type BuildCsvDownloadItemArgs,
|
|
65
70
|
type DownloadLabels,
|
|
66
71
|
} from './download'
|
|
67
72
|
export {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
buildCsvDownloadItem,
|
|
3
3
|
buildPngDownloadItem,
|
|
4
|
-
downloadToCSV,
|
|
5
4
|
type DownloadItem,
|
|
6
5
|
} from '../actions/download'
|
|
7
6
|
import type { BarWidgetData } from './types'
|
|
@@ -42,25 +41,20 @@ export function createBarDownloadConfig(
|
|
|
42
41
|
}),
|
|
43
42
|
)
|
|
44
43
|
}
|
|
45
|
-
items.push(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
filename: `${args.filename}.csv`,
|
|
61
|
-
revoke: handle.revoke,
|
|
62
|
-
})
|
|
63
|
-
},
|
|
64
|
-
})
|
|
44
|
+
items.push(
|
|
45
|
+
buildCsvDownloadItem({
|
|
46
|
+
filename: args.filename,
|
|
47
|
+
getRows: () => {
|
|
48
|
+
const data = args.getData()
|
|
49
|
+
const rows: unknown[][] = []
|
|
50
|
+
for (const [i, series] of data.entries()) {
|
|
51
|
+
if (i > 0) rows.push([])
|
|
52
|
+
rows.push(['name', 'value'])
|
|
53
|
+
for (const d of series) rows.push([d.name, d.value])
|
|
54
|
+
}
|
|
55
|
+
return rows
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
)
|
|
65
59
|
return items
|
|
66
60
|
}
|
|
@@ -2,6 +2,7 @@ import type { EChartsOption } from 'echarts'
|
|
|
2
2
|
import * as echarts from 'echarts'
|
|
3
3
|
import type { CallbackDataParams } from 'echarts/types/dist/shared'
|
|
4
4
|
import {
|
|
5
|
+
buildAxisLabelStyle,
|
|
5
6
|
buildGridConfig,
|
|
6
7
|
buildLegendConfig,
|
|
7
8
|
createTooltipFormatter,
|
|
@@ -86,6 +87,7 @@ export function barOptions({
|
|
|
86
87
|
axisLine: { show: false },
|
|
87
88
|
axisTick: { show: false },
|
|
88
89
|
axisLabel: {
|
|
90
|
+
...buildAxisLabelStyle(theme),
|
|
89
91
|
padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
|
|
90
92
|
margin: 0,
|
|
91
93
|
hideOverlap: true,
|
|
@@ -111,8 +113,7 @@ export function barOptions({
|
|
|
111
113
|
lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },
|
|
112
114
|
},
|
|
113
115
|
axisLabel: {
|
|
114
|
-
|
|
115
|
-
fontFamily: theme.typography.overlineDelicate?.fontFamily,
|
|
116
|
+
...buildAxisLabelStyle(theme),
|
|
116
117
|
margin: parseInt(theme.spacing(1)),
|
|
117
118
|
show: true,
|
|
118
119
|
showMaxLabel: true,
|
|
@@ -37,6 +37,15 @@ describe('createCategoryDownloadConfig', () => {
|
|
|
37
37
|
).toEqual(['csv'])
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
it('CSV item carries the canonical icon and short label', () => {
|
|
41
|
+
const item = createCategoryDownloadConfig({
|
|
42
|
+
filename: 'c',
|
|
43
|
+
getData: () => data,
|
|
44
|
+
}).find((i) => i.id === 'csv')!
|
|
45
|
+
expect(item.label).toBe('CSV')
|
|
46
|
+
expect(item.icon).toBeTruthy()
|
|
47
|
+
})
|
|
48
|
+
|
|
40
49
|
it('prepends PNG when getCaptureEl is provided', () => {
|
|
41
50
|
const items = createCategoryDownloadConfig({
|
|
42
51
|
filename: 'c',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildCsvDownloadItem,
|
|
2
3
|
buildPngDownloadItem,
|
|
3
|
-
downloadToCSV,
|
|
4
4
|
type DownloadItem,
|
|
5
5
|
} from '../actions/download'
|
|
6
6
|
import type { CategoryWidgetData } from './types'
|
|
@@ -30,25 +30,21 @@ export function createCategoryDownloadConfig(args: {
|
|
|
30
30
|
}),
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
|
-
items.push(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
items.push(
|
|
34
|
+
buildCsvDownloadItem({
|
|
35
|
+
filename: args.filename,
|
|
36
|
+
getRows: () => {
|
|
37
|
+
const data = args.getData()
|
|
38
|
+
const rows: unknown[][] = [['series', 'name', 'value']]
|
|
39
|
+
for (const [i, series] of data.entries()) {
|
|
40
|
+
const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`
|
|
41
|
+
for (const item of series) {
|
|
42
|
+
rows.push([seriesName, item.name, item.value])
|
|
43
|
+
}
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
filename: `${args.filename}.csv`,
|
|
49
|
-
revoke: handle.revoke,
|
|
50
|
-
})
|
|
51
|
-
},
|
|
52
|
-
})
|
|
45
|
+
return rows
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
53
49
|
return items
|
|
54
50
|
}
|
|
@@ -28,10 +28,13 @@ export const CENTERED: EdgeAlignment = {
|
|
|
28
28
|
alignMaxLabel: null,
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
//
|
|
32
|
-
// off-centering, and the
|
|
33
|
-
//
|
|
34
|
-
|
|
31
|
+
// Small cushion biasing toward anchoring when borderline: clipping is worse
|
|
32
|
+
// than a hair of off-centering, and the measured width can differ slightly
|
|
33
|
+
// from the final render (mainly when the web font isn't loaded yet at measure
|
|
34
|
+
// time). Kept to 1px — a wider margin anchored labels that only just clear the
|
|
35
|
+
// edge, and an anchored edge label can then be dropped by the axis
|
|
36
|
+
// `hideOverlap`, leaving a gap. 1px guards measurement error without that.
|
|
37
|
+
const SAFETY_MARGIN_PX = 1
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* Pure overflow decision. Inputs are anchor-independent (tick centers and text
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
buildCsvDownloadItem,
|
|
3
3
|
buildPngDownloadItem,
|
|
4
|
-
downloadToCSV,
|
|
5
4
|
type DownloadItem,
|
|
6
5
|
} from '../actions/download'
|
|
7
6
|
import type { FormulaWidgetData } from './types'
|
|
@@ -38,32 +37,27 @@ export function createFormulaDownloadConfig(
|
|
|
38
37
|
}),
|
|
39
38
|
)
|
|
40
39
|
}
|
|
41
|
-
items.push(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
filename: `${args.filename}.csv`,
|
|
64
|
-
revoke: handle.revoke,
|
|
65
|
-
})
|
|
66
|
-
},
|
|
67
|
-
})
|
|
40
|
+
items.push(
|
|
41
|
+
buildCsvDownloadItem({
|
|
42
|
+
filename: args.filename,
|
|
43
|
+
getRows: () => {
|
|
44
|
+
const data = args.getData()
|
|
45
|
+
const rows: unknown[][] = [
|
|
46
|
+
['series', 'prefix', 'value', 'suffix', 'note', 'delta'],
|
|
47
|
+
]
|
|
48
|
+
for (const d of data) {
|
|
49
|
+
rows.push([
|
|
50
|
+
d.series?.name ?? '',
|
|
51
|
+
d.prefix ?? '',
|
|
52
|
+
d.value,
|
|
53
|
+
d.suffix ?? '',
|
|
54
|
+
d.note ?? '',
|
|
55
|
+
d.delta?.value ?? '',
|
|
56
|
+
])
|
|
57
|
+
}
|
|
58
|
+
return rows
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
68
62
|
return items
|
|
69
63
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildCsvDownloadItem,
|
|
2
3
|
buildPngDownloadItem,
|
|
3
|
-
downloadToCSV,
|
|
4
4
|
type DownloadItem,
|
|
5
5
|
} from '../actions/download'
|
|
6
6
|
import type { HistogramWidgetData } from './types'
|
|
@@ -31,30 +31,26 @@ export function createHistogramDownloadConfig(args: {
|
|
|
31
31
|
}),
|
|
32
32
|
)
|
|
33
33
|
}
|
|
34
|
-
items.push(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
revoke: handle.revoke,
|
|
56
|
-
})
|
|
57
|
-
},
|
|
58
|
-
})
|
|
34
|
+
items.push(
|
|
35
|
+
buildCsvDownloadItem({
|
|
36
|
+
filename: args.filename,
|
|
37
|
+
getRows: () => {
|
|
38
|
+
const data = args.getData()
|
|
39
|
+
const ticks = args.getTicks()
|
|
40
|
+
const seriesCount = data.length
|
|
41
|
+
const header: unknown[] = ['bin_low', 'bin_high']
|
|
42
|
+
for (let i = 0; i < seriesCount; i++) {
|
|
43
|
+
header.push(args.seriesNames?.[i] ?? `series_${i + 1}`)
|
|
44
|
+
}
|
|
45
|
+
const rows: unknown[][] = [header]
|
|
46
|
+
for (let bin = 0; bin < Math.max(0, ticks.length - 1); bin++) {
|
|
47
|
+
const row: unknown[] = [ticks[bin], ticks[bin + 1]]
|
|
48
|
+
for (let s = 0; s < seriesCount; s++) row.push(data[s]?.[bin] ?? 0)
|
|
49
|
+
rows.push(row)
|
|
50
|
+
}
|
|
51
|
+
return rows
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
59
55
|
return items
|
|
60
56
|
}
|
|
@@ -2,6 +2,7 @@ import type { EChartsOption } from 'echarts'
|
|
|
2
2
|
import * as echarts from 'echarts'
|
|
3
3
|
import type { CallbackDataParams } from 'echarts/types/dist/shared'
|
|
4
4
|
import {
|
|
5
|
+
buildAxisLabelStyle,
|
|
5
6
|
buildGridConfig,
|
|
6
7
|
buildLegendConfig,
|
|
7
8
|
createTooltipFormatter,
|
|
@@ -92,6 +93,7 @@ export function histogramOptions({
|
|
|
92
93
|
axisLine: { show: false },
|
|
93
94
|
axisTick: { show: false },
|
|
94
95
|
axisLabel: {
|
|
96
|
+
...buildAxisLabelStyle(theme),
|
|
95
97
|
padding: [parseInt(theme.spacing(0.5)), 0, 0, 0],
|
|
96
98
|
margin: 0,
|
|
97
99
|
hideOverlap: true,
|
|
@@ -114,8 +116,7 @@ export function histogramOptions({
|
|
|
114
116
|
lineStyle: { color: theme.palette.black?.[4] ?? theme.palette.divider },
|
|
115
117
|
},
|
|
116
118
|
axisLabel: {
|
|
117
|
-
|
|
118
|
-
fontFamily: theme.typography.overlineDelicate?.fontFamily,
|
|
119
|
+
...buildAxisLabelStyle(theme),
|
|
119
120
|
margin: parseInt(theme.spacing(1)),
|
|
120
121
|
show: true,
|
|
121
122
|
showMaxLabel: true,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DOWNLOAD_ITEM_IDS,
|
|
3
|
+
MarkdownIcon,
|
|
2
4
|
buildPngDownloadItem,
|
|
3
5
|
toCsvString,
|
|
4
6
|
triggerLinkDownload,
|
|
@@ -31,8 +33,9 @@ export function createMarkdownDownloadConfig(args: {
|
|
|
31
33
|
)
|
|
32
34
|
}
|
|
33
35
|
items.push({
|
|
34
|
-
id:
|
|
35
|
-
label: '
|
|
36
|
+
id: DOWNLOAD_ITEM_IDS.markdown,
|
|
37
|
+
label: 'Markdown',
|
|
38
|
+
icon: <MarkdownIcon fontSize='small' />,
|
|
36
39
|
resolve: () => {
|
|
37
40
|
const data = args.getData()
|
|
38
41
|
const blob = new Blob([data.content ?? ''], {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildCsvDownloadItem,
|
|
2
3
|
buildPngDownloadItem,
|
|
3
|
-
downloadToCSV,
|
|
4
4
|
type DownloadItem,
|
|
5
5
|
} from '../actions/download'
|
|
6
6
|
import type { PieWidgetData } from './types'
|
|
@@ -31,25 +31,21 @@ export function createPieDownloadConfig(args: {
|
|
|
31
31
|
}),
|
|
32
32
|
)
|
|
33
33
|
}
|
|
34
|
-
items.push(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
items.push(
|
|
35
|
+
buildCsvDownloadItem({
|
|
36
|
+
filename: args.filename,
|
|
37
|
+
getRows: () => {
|
|
38
|
+
const data = args.getData()
|
|
39
|
+
const rows: unknown[][] = [['series', 'name', 'value']]
|
|
40
|
+
for (const [i, series] of data.entries()) {
|
|
41
|
+
const seriesName = args.seriesNames?.[i] ?? `series_${i + 1}`
|
|
42
|
+
for (const slice of series) {
|
|
43
|
+
rows.push([seriesName, slice.name, slice.value])
|
|
44
|
+
}
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
filename: `${args.filename}.csv`,
|
|
50
|
-
revoke: handle.revoke,
|
|
51
|
-
})
|
|
52
|
-
},
|
|
53
|
-
})
|
|
46
|
+
return rows
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
54
50
|
return items
|
|
55
51
|
}
|
|
@@ -10,8 +10,11 @@ describe('<PieSkeleton>', () => {
|
|
|
10
10
|
).toBeGreaterThan(0)
|
|
11
11
|
})
|
|
12
12
|
|
|
13
|
-
it('
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
it('accepts count but always renders a single donut', () => {
|
|
14
|
+
const single = render(<PieSkeleton count={1} />)
|
|
15
|
+
const many = render(<PieSkeleton count={3} />)
|
|
16
|
+
const skeletonCount = (c: HTMLElement) =>
|
|
17
|
+
c.querySelectorAll('.MuiSkeleton-root').length
|
|
18
|
+
expect(skeletonCount(many.container)).toBe(skeletonCount(single.container))
|
|
16
19
|
})
|
|
17
20
|
})
|
|
@@ -1,19 +1,55 @@
|
|
|
1
1
|
import { Box, Skeleton } from '@mui/material'
|
|
2
2
|
import type { SxProps, Theme } from '@mui/material'
|
|
3
3
|
|
|
4
|
+
const OUTER_SIZE = 160
|
|
5
|
+
const HOLE_SIZE = 96
|
|
6
|
+
|
|
4
7
|
const styles = {
|
|
5
8
|
root: {
|
|
6
9
|
display: 'flex',
|
|
10
|
+
flexDirection: 'column',
|
|
7
11
|
alignItems: 'center',
|
|
8
12
|
justifyContent: 'center',
|
|
9
13
|
minHeight: 200,
|
|
10
14
|
py: 1,
|
|
11
|
-
gap: 2,
|
|
15
|
+
gap: ({ spacing }) => spacing(2),
|
|
12
16
|
},
|
|
13
17
|
donut: {
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
position: 'relative',
|
|
19
|
+
display: 'flex',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
},
|
|
23
|
+
ring: {
|
|
24
|
+
width: OUTER_SIZE,
|
|
25
|
+
height: OUTER_SIZE,
|
|
26
|
+
},
|
|
27
|
+
hole: {
|
|
28
|
+
position: 'absolute',
|
|
29
|
+
zIndex: 1,
|
|
30
|
+
width: HOLE_SIZE,
|
|
31
|
+
height: HOLE_SIZE,
|
|
16
32
|
borderRadius: '50%',
|
|
33
|
+
bgcolor: 'background.paper',
|
|
34
|
+
},
|
|
35
|
+
label: {
|
|
36
|
+
position: 'absolute',
|
|
37
|
+
zIndex: 2,
|
|
38
|
+
display: 'flex',
|
|
39
|
+
flexDirection: 'column',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
justifyContent: 'center',
|
|
42
|
+
gap: ({ spacing }) => spacing(0.5),
|
|
43
|
+
},
|
|
44
|
+
legend: {
|
|
45
|
+
display: 'flex',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
gap: ({ spacing }) => spacing(2),
|
|
48
|
+
},
|
|
49
|
+
legendItem: {
|
|
50
|
+
display: 'flex',
|
|
51
|
+
alignItems: 'center',
|
|
52
|
+
gap: ({ spacing }) => spacing(1.5),
|
|
17
53
|
},
|
|
18
54
|
} satisfies Record<string, SxProps<Theme>>
|
|
19
55
|
|
|
@@ -21,12 +57,38 @@ export interface PieSkeletonProps {
|
|
|
21
57
|
count?: number
|
|
22
58
|
}
|
|
23
59
|
|
|
24
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Loading state for the Pie widget. Mirrors the donut silhouette — a ring
|
|
62
|
+
* (gray circle with a background-coloured hole punched out), a stacked
|
|
63
|
+
* value/name stub in the centre, and a centered legend stub below — so the
|
|
64
|
+
* skeleton reads as "a donut chart" rather than a solid disc.
|
|
65
|
+
*
|
|
66
|
+
* Single donut only: multi-series pie loads as a horizontal bar chart, so
|
|
67
|
+
* rendering one ring stays honest regardless of `count`.
|
|
68
|
+
*/
|
|
69
|
+
export function PieSkeleton({ count }: PieSkeletonProps) {
|
|
70
|
+
// `count` is accepted for API compatibility but intentionally unused: the
|
|
71
|
+
// skeleton always renders a single donut, since multi-series pie loads as a
|
|
72
|
+
// horizontal bar chart rather than multiple donuts.
|
|
73
|
+
void count
|
|
25
74
|
return (
|
|
26
75
|
<Box sx={styles.root}>
|
|
27
|
-
{
|
|
28
|
-
<Skeleton
|
|
29
|
-
|
|
76
|
+
<Box sx={styles.donut}>
|
|
77
|
+
<Skeleton variant='circular' sx={styles.ring} />
|
|
78
|
+
<Box sx={styles.hole} />
|
|
79
|
+
<Box sx={styles.label}>
|
|
80
|
+
<Skeleton width={56} height={18} />
|
|
81
|
+
<Skeleton width={36} height={8} />
|
|
82
|
+
</Box>
|
|
83
|
+
</Box>
|
|
84
|
+
<Box sx={styles.legend}>
|
|
85
|
+
{[0, 1].map((i) => (
|
|
86
|
+
<Box key={`legend-${i}`} sx={styles.legendItem}>
|
|
87
|
+
<Skeleton variant='circular' width={8} height={8} />
|
|
88
|
+
<Skeleton width={48} height={8} />
|
|
89
|
+
</Box>
|
|
90
|
+
))}
|
|
91
|
+
</Box>
|
|
30
92
|
</Box>
|
|
31
93
|
)
|
|
32
94
|
}
|