frappe-ui 1.0.0-beta.63 → 1.0.0-beta.65
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/experimental.ts +23 -0
- package/package.json +1 -1
- package/src/charts/AreaChart.cy.ts +2 -4
- package/src/charts/AreaChart.vue +7 -8
- package/src/charts/BarChart.cy.ts +17 -9
- package/src/charts/BarChart.vue +9 -10
- package/src/charts/DonutChart.cy.ts +68 -6
- package/src/charts/DonutChart.vue +32 -25
- package/src/charts/FunnelChart.cy.ts +38 -10
- package/src/charts/FunnelChart.vue +39 -26
- package/src/charts/HeatmapChart.cy.ts +16 -2
- package/src/charts/HeatmapChart.vue +7 -3
- package/src/charts/LineChart.cy.ts +3 -5
- package/src/charts/LineChart.vue +7 -8
- package/src/charts/NumberCard.cy.ts +21 -5
- package/src/charts/NumberCard.vue +11 -7
- package/src/charts/SankeyChart.cy.ts +20 -1
- package/src/charts/SankeyChart.vue +29 -18
- package/src/charts/ScatterChart.vue +10 -6
- package/src/charts/areaChartOptions.test.ts +12 -18
- package/src/charts/axisChartCommon.ts +25 -17
- package/src/charts/axisChartOptions.ts +9 -23
- package/src/charts/axisFormat.test.ts +1 -1
- package/src/charts/barChartOptions.test.ts +11 -11
- package/src/charts/categoryAxisFit.test.ts +1 -1
- package/src/charts/comboChartOptions.test.ts +10 -23
- package/src/charts/components/ChartContainer.cy.ts +44 -4
- package/src/charts/components/ChartContainer.vue +22 -17
- package/src/charts/components/ChartTooltip.cy.ts +13 -2
- package/src/charts/components/ChartTooltip.vue +9 -11
- package/src/charts/core/useAxisChart.test.ts +127 -0
- package/src/charts/core/useAxisChart.ts +64 -37
- package/src/charts/core/usePlotKeyboard.test.ts +7 -3
- package/src/charts/docs/AreaChart.api.md +15 -15
- package/src/charts/docs/AreaChart.md +2 -2
- package/src/charts/docs/BarChart.api.md +15 -15
- package/src/charts/docs/BarChart.md +4 -2
- package/src/charts/docs/ChartCard.md +2 -2
- package/src/charts/docs/ChartContainer.api.md +5 -5
- package/src/charts/docs/ChartContainer.md +7 -5
- package/src/charts/docs/ChartTooltip.api.md +5 -5
- package/src/charts/docs/ChartTooltip.md +6 -2
- package/src/charts/docs/DonutChart.api.md +20 -8
- package/src/charts/docs/DonutChart.md +7 -1
- package/src/charts/docs/FunnelChart.api.md +4 -11
- package/src/charts/docs/FunnelChart.md +9 -5
- package/src/charts/docs/HeatmapChart.api.md +4 -4
- package/src/charts/docs/HeatmapChart.md +1 -1
- package/src/charts/docs/LineChart.api.md +15 -15
- package/src/charts/docs/LineChart.md +7 -7
- package/src/charts/docs/NumberCard.api.md +7 -7
- package/src/charts/docs/NumberCard.md +6 -5
- package/src/charts/docs/Overview.md +69 -10
- package/src/charts/docs/SankeyChart.api.md +6 -6
- package/src/charts/docs/SankeyChart.md +1 -1
- package/src/charts/docs/ScatterChart.api.md +5 -5
- package/src/charts/docs/States.md +3 -1
- package/src/charts/donutChartOptions.test.ts +5 -5
- package/src/charts/donutChartOptions.ts +11 -9
- package/src/charts/format.test.ts +3 -0
- package/src/charts/format.ts +3 -2
- package/src/charts/funnelGeometry.test.ts +33 -2
- package/src/charts/funnelGeometry.ts +32 -11
- package/src/charts/heatmapOptions.test.ts +14 -18
- package/src/charts/heatmapOptions.ts +12 -10
- package/src/charts/index.ts +6 -9
- package/src/charts/lineChartOptions.test.ts +16 -15
- package/src/charts/normalizedStackOptions.test.ts +6 -6
- package/src/charts/numberCardFormat.test.ts +21 -9
- package/src/charts/numberCardFormat.ts +8 -8
- package/src/charts/paletteColors.test.ts +123 -0
- package/src/charts/referenceLines.test.ts +25 -13
- package/src/charts/sankeyOptions.test.ts +2 -2
- package/src/charts/sankeyOptions.ts +8 -6
- package/src/charts/scatterOptions.test.ts +21 -5
- package/src/charts/scatterOptions.ts +7 -5
- package/src/charts/seriesData.test.ts +20 -1
- package/src/charts/seriesData.ts +1 -0
- package/src/charts/stories/AreaLatency.vue +0 -1
- package/src/charts/stories/BarSelect.vue +1 -1
- package/src/charts/stories/CustomRadar.vue +2 -2
- package/src/charts/stories/Dashboard.vue +10 -4
- package/src/charts/stories/DonutHalf.vue +1 -1
- package/src/charts/stories/DonutOthers.vue +1 -1
- package/src/charts/stories/FunnelDeals.vue +1 -2
- package/src/charts/stories/HeatmapSignups.vue +1 -1
- package/src/charts/stories/HeatmapTickets.vue +1 -1
- package/src/charts/stories/LineDualAxis.vue +1 -1
- package/src/charts/stories/LineFilledSeries.vue +1 -1
- package/src/charts/stories/LineTrend.vue +1 -1
- package/src/charts/stories/NumberKpis.vue +8 -2
- package/src/charts/stories/NumberSparklines.vue +7 -1
- package/src/charts/stories/SankeySpend.vue +1 -1
- package/src/charts/stories/TooltipCustom.vue +5 -0
- package/src/charts/style.css +8 -5
- package/src/charts/tokens.ts +88 -107
- package/src/charts/tooltipItems.test.ts +3 -3
- package/src/charts/tooltipItems.ts +4 -4
- package/src/charts/types.ts +167 -115
- package/src/charts/utils.test.ts +50 -0
- package/src/charts/utils.ts +52 -1
- package/src/charts/valueAxisOptions.test.ts +36 -2
- package/src/components/DatePicker/CalendarPanel.vue +7 -55
- package/src/components/DatePicker/DateCalendar.cy.ts +149 -0
- package/src/components/DatePicker/DateCalendar.vue +114 -0
- package/src/components/DatePicker/DatePicker.cy.ts +42 -6
- package/src/components/DatePicker/DatePicker.md +3 -0
- package/src/components/DatePicker/DatePicker.vue +14 -104
- package/src/components/DatePicker/DateRangeCalendar.cy.ts +186 -0
- package/src/components/DatePicker/DateRangeCalendar.vue +211 -0
- package/src/components/DatePicker/DateRangePicker.vue +50 -265
- package/src/components/DatePicker/DateTimePicker.cy.ts +42 -4
- package/src/components/DatePicker/DateTimePicker.vue +29 -96
- package/src/components/DatePicker/calendarTypes.ts +143 -0
- package/src/components/DatePicker/composables.ts +64 -2
- package/src/components/DatePicker/stories/DateCalendar.vue +18 -0
- package/src/components/DatePicker/stories/DateRangeCalendar.vue +27 -0
- package/src/components/DatePicker/utils.ts +72 -1
- package/src/components/Popover/Popover.api.md +25 -5
- package/src/components/Popover/Popover.cy.ts +116 -2
- package/src/components/Popover/Popover.md +22 -3
- package/src/components/Popover/Popover.vue +43 -7
- package/src/components/Popover/stories/Typeahead.vue +66 -0
- package/src/components/Popover/types.ts +25 -1
- package/src/components/shared/picker/PickerShell.vue +38 -109
- package/src/components/shared/picker/types.ts +81 -0
- package/src/components/shared/slotReactivity.cy.ts +36 -0
- package/src/components/shared/tabs/Pill.vue +2 -3
- package/src/molecules/editor/components/MediaNodeView.test.ts +30 -0
- package/src/molecules/editor/components/MediaNodeView.vue +13 -8
- package/src/molecules/editor/components/VideoControls.test.ts +118 -2
- package/src/molecules/editor/components/VideoControls.vue +89 -11
- package/src/charts/chartColors.test.ts +0 -187
package/experimental.ts
CHANGED
|
@@ -35,12 +35,35 @@ export * from './experimental/Charts'
|
|
|
35
35
|
// rebuilt here as seven composable parts. Unstable: the parts stay here until
|
|
36
36
|
// gameplan, helpdesk and this repo's docs site all run on them.
|
|
37
37
|
export * from './experimental/CommandPalette'
|
|
38
|
+
// Date calendars. `DateCalendar` and `DateRangeCalendar` are the calendars
|
|
39
|
+
// inside the date pickers, as standalone components. Incubating while their
|
|
40
|
+
// props and events settle.
|
|
41
|
+
export { default as DateCalendar } from './src/components/DatePicker/DateCalendar.vue'
|
|
42
|
+
export { default as DateRangeCalendar } from './src/components/DatePicker/DateRangeCalendar.vue'
|
|
43
|
+
export type {
|
|
44
|
+
DateCalendarProps,
|
|
45
|
+
DateCalendarEmits,
|
|
46
|
+
DateCalendarExposed,
|
|
47
|
+
DateRangeCalendarProps,
|
|
48
|
+
DateRangeCalendarEmits,
|
|
49
|
+
DateRangeCalendarExposed,
|
|
50
|
+
} from './src/components/DatePicker/calendarTypes'
|
|
38
51
|
// ListView family. Moved out of root (#985) — `frappe-ui/list` is
|
|
39
52
|
// composition-based by design (P3) and doesn't replicate ListView's
|
|
40
53
|
// config-driven columns (resizable widths, per-column getLabel/prefix
|
|
41
54
|
// functions, tooltips, disabled-row exclusion, the select banner), so this
|
|
42
55
|
// stays here, unstable, until `frappe-ui/list` reaches parity.
|
|
43
56
|
export * from './experimental/ListView'
|
|
57
|
+
// PickerShell. The input half of the date pickers: a `TextInput`, a manual
|
|
58
|
+
// `Popover` and the focus wiring between them. Incubating while its props and
|
|
59
|
+
// slots settle.
|
|
60
|
+
export { default as PickerShell } from './src/components/shared/picker/PickerShell.vue'
|
|
61
|
+
export type {
|
|
62
|
+
PickerShellProps,
|
|
63
|
+
PickerShellTriggerSlotProps,
|
|
64
|
+
PickerShellSlots,
|
|
65
|
+
PickerShellExposed,
|
|
66
|
+
} from './src/components/shared/picker/types'
|
|
44
67
|
// v0 TextEditor family. Moved out of root (#974) — `frappe-ui/editor` is the
|
|
45
68
|
// composition-based replacement. Parked here (#1007), unstable, as an interim
|
|
46
69
|
// import path while apps migrate. Deletion stays human-gated (spec/editor.md
|
package/package.json
CHANGED
|
@@ -210,8 +210,7 @@ describe('AreaChart', () => {
|
|
|
210
210
|
reading().should('contain.text', 'Refunds')
|
|
211
211
|
plot().type('{enter}')
|
|
212
212
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
213
|
-
|
|
214
|
-
dataIndex: 0,
|
|
213
|
+
name: 'refunds',
|
|
215
214
|
value: 4,
|
|
216
215
|
row: { month: 'Jan', refunds: 4 },
|
|
217
216
|
})
|
|
@@ -224,8 +223,7 @@ describe('AreaChart', () => {
|
|
|
224
223
|
plot().type('{rightarrow}')
|
|
225
224
|
plot().type('{enter}')
|
|
226
225
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
227
|
-
|
|
228
|
-
dataIndex: 1,
|
|
226
|
+
name: 'sales',
|
|
229
227
|
value: 20,
|
|
230
228
|
row: { month: 'Feb', sales: 20 },
|
|
231
229
|
})
|
package/src/charts/AreaChart.vue
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
<ChartContainer
|
|
3
3
|
:title="title"
|
|
4
4
|
:subtitle="subtitle"
|
|
5
|
-
:
|
|
6
|
-
:
|
|
5
|
+
:y-axis-title="yAxisTitle"
|
|
6
|
+
:y2-axis-title="y2AxisTitle"
|
|
7
7
|
:loading="loading"
|
|
8
8
|
:error="error || renderError"
|
|
9
9
|
:empty="isEmpty"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
:y="tooltip.y"
|
|
41
41
|
:label="tooltip.label"
|
|
42
42
|
:items="tooltip.items"
|
|
43
|
-
:
|
|
43
|
+
:rows="tooltip.rows"
|
|
44
44
|
:dir="dir"
|
|
45
45
|
>
|
|
46
46
|
<template v-if="$slots.tooltip" #default="slotProps">
|
|
@@ -79,7 +79,7 @@ import type {
|
|
|
79
79
|
AreaChartProps,
|
|
80
80
|
AreaChartSlots,
|
|
81
81
|
AxisChartConfig,
|
|
82
|
-
|
|
82
|
+
ChartExposedRefs,
|
|
83
83
|
} from './types'
|
|
84
84
|
|
|
85
85
|
registerChartModules([
|
|
@@ -107,7 +107,6 @@ const config = computed<AxisChartConfig>(() => ({
|
|
|
107
107
|
type: 'area',
|
|
108
108
|
stacked: props.stacked,
|
|
109
109
|
connectNulls: props.connectNulls,
|
|
110
|
-
fillOpacity: props.fillOpacity,
|
|
111
110
|
}))
|
|
112
111
|
|
|
113
112
|
const {
|
|
@@ -115,8 +114,8 @@ const {
|
|
|
115
114
|
chart,
|
|
116
115
|
dir,
|
|
117
116
|
isEmpty,
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
yAxisTitle,
|
|
118
|
+
y2AxisTitle,
|
|
120
119
|
renderError,
|
|
121
120
|
tooltip,
|
|
122
121
|
legendItems,
|
|
@@ -133,5 +132,5 @@ const {
|
|
|
133
132
|
onSelect: (event) => emit('select', event),
|
|
134
133
|
})
|
|
135
134
|
|
|
136
|
-
defineExpose<
|
|
135
|
+
defineExpose<ChartExposedRefs>({ chart: computed(() => chart.value) })
|
|
137
136
|
</script>
|
|
@@ -81,8 +81,7 @@ describe('BarChart', () => {
|
|
|
81
81
|
mountChart({ onSelect: cy.spy().as('onSelect') })
|
|
82
82
|
bars().first().click()
|
|
83
83
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
84
|
-
|
|
85
|
-
dataIndex: 0,
|
|
84
|
+
name: 'sales',
|
|
86
85
|
value: 10,
|
|
87
86
|
row: { month: 'Jan', sales: 10 },
|
|
88
87
|
})
|
|
@@ -476,6 +475,18 @@ describe('BarChart', () => {
|
|
|
476
475
|
container().should('have.attr', 'data-state', 'empty')
|
|
477
476
|
})
|
|
478
477
|
|
|
478
|
+
it('says so when no row carries a number for a series', () => {
|
|
479
|
+
mountChart({ y: ['typo'] })
|
|
480
|
+
cy.contains('No data to show').should('be.visible')
|
|
481
|
+
container().should('have.attr', 'data-state', 'empty')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('says so once the legend has switched every series off', () => {
|
|
485
|
+
mountChart({ hiddenSeries: ['sales', 'refunds'] })
|
|
486
|
+
cy.contains('No data to show').should('be.visible')
|
|
487
|
+
container().should('have.attr', 'data-state', 'empty')
|
|
488
|
+
})
|
|
489
|
+
|
|
479
490
|
it('reads as ready once the bars are drawn', () => {
|
|
480
491
|
mountChart()
|
|
481
492
|
bars().should('have.length', data.length * 2)
|
|
@@ -621,7 +632,7 @@ describe('BarChart', () => {
|
|
|
621
632
|
// The row carries every column, `tooltipColumns` or not: a tooltip the
|
|
622
633
|
// app draws itself needs no prop to reach one.
|
|
623
634
|
mountChart({ data: data.map((row) => ({ ...row, orders: 1000 })) }, {
|
|
624
|
-
tooltip: ({
|
|
635
|
+
tooltip: ({ rows }: any) => h('span', `${rows[0].orders} orders`),
|
|
625
636
|
} as any)
|
|
626
637
|
bars().should('have.length', data.length * 2)
|
|
627
638
|
plot().focus()
|
|
@@ -658,8 +669,7 @@ describe('BarChart', () => {
|
|
|
658
669
|
plot().focus()
|
|
659
670
|
plot().type('{rightarrow}{enter}')
|
|
660
671
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
661
|
-
|
|
662
|
-
dataIndex: 1,
|
|
672
|
+
name: 'sales',
|
|
663
673
|
value: 20,
|
|
664
674
|
row: { month: 'Feb', sales: 20 },
|
|
665
675
|
})
|
|
@@ -673,8 +683,7 @@ describe('BarChart', () => {
|
|
|
673
683
|
plot().focus()
|
|
674
684
|
plot().type('{downarrow}{enter}')
|
|
675
685
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
676
|
-
|
|
677
|
-
dataIndex: 0,
|
|
686
|
+
name: 'refunds',
|
|
678
687
|
value: 4,
|
|
679
688
|
})
|
|
680
689
|
})
|
|
@@ -685,8 +694,7 @@ describe('BarChart', () => {
|
|
|
685
694
|
plot().focus()
|
|
686
695
|
plot().type('{downarrow}{uparrow}{enter}')
|
|
687
696
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
688
|
-
|
|
689
|
-
dataIndex: 0,
|
|
697
|
+
name: 'sales',
|
|
690
698
|
value: 10,
|
|
691
699
|
})
|
|
692
700
|
})
|
package/src/charts/BarChart.vue
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
<ChartContainer
|
|
3
3
|
:title="title"
|
|
4
4
|
:subtitle="subtitle"
|
|
5
|
-
:
|
|
6
|
-
:
|
|
7
|
-
:
|
|
5
|
+
:y-axis-title="yAxisTitle"
|
|
6
|
+
:y2-axis-title="y2AxisTitle"
|
|
7
|
+
:axis-title-placement="axisTitlePlacement"
|
|
8
8
|
:loading="loading"
|
|
9
9
|
:error="error || renderError"
|
|
10
10
|
:empty="isEmpty"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
:y="tooltip.y"
|
|
42
42
|
:label="tooltip.label"
|
|
43
43
|
:items="tooltip.items"
|
|
44
|
-
:
|
|
44
|
+
:rows="tooltip.rows"
|
|
45
45
|
:dir="dir"
|
|
46
46
|
>
|
|
47
47
|
<template v-if="$slots.tooltip" #default="slotProps">
|
|
@@ -80,7 +80,7 @@ import type {
|
|
|
80
80
|
BarChartEmits,
|
|
81
81
|
BarChartProps,
|
|
82
82
|
BarChartSlots,
|
|
83
|
-
|
|
83
|
+
ChartExposedRefs,
|
|
84
84
|
} from './types'
|
|
85
85
|
|
|
86
86
|
registerChartModules([
|
|
@@ -109,7 +109,6 @@ const config = computed<AxisChartConfig>(() => ({
|
|
|
109
109
|
stacked: props.stacked,
|
|
110
110
|
horizontal: props.horizontal,
|
|
111
111
|
connectNulls: props.connectNulls,
|
|
112
|
-
fillOpacity: props.fillOpacity,
|
|
113
112
|
}))
|
|
114
113
|
|
|
115
114
|
const {
|
|
@@ -117,9 +116,9 @@ const {
|
|
|
117
116
|
chart,
|
|
118
117
|
dir,
|
|
119
118
|
isEmpty,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
yAxisTitle,
|
|
120
|
+
y2AxisTitle,
|
|
121
|
+
axisTitlePlacement,
|
|
123
122
|
renderError,
|
|
124
123
|
tooltip,
|
|
125
124
|
legendItems,
|
|
@@ -137,5 +136,5 @@ const {
|
|
|
137
136
|
onSelect: (event) => emit('select', event),
|
|
138
137
|
})
|
|
139
138
|
|
|
140
|
-
defineExpose<
|
|
139
|
+
defineExpose<ChartExposedRefs>({ chart: computed(() => chart.value) })
|
|
141
140
|
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { defineComponent, h } from 'vue'
|
|
1
|
+
import { defineComponent, h, ref } from 'vue'
|
|
2
2
|
import DonutChart from './DonutChart.vue'
|
|
3
|
+
import { OTHERS_KEY } from './utils'
|
|
3
4
|
import './style.css'
|
|
4
5
|
|
|
5
6
|
const data = [
|
|
@@ -73,6 +74,7 @@ describe('DonutChart', () => {
|
|
|
73
74
|
cy.get('@onSelect')
|
|
74
75
|
.should('have.been.calledWithMatch', {
|
|
75
76
|
name: 'Search',
|
|
77
|
+
label: 'Search',
|
|
76
78
|
value: 50,
|
|
77
79
|
percent: 50,
|
|
78
80
|
})
|
|
@@ -96,8 +98,47 @@ describe('DonutChart', () => {
|
|
|
96
98
|
cy.get('[data-slot="chart-legend"]').should('contain.text', '60%')
|
|
97
99
|
})
|
|
98
100
|
|
|
101
|
+
it('round-trips v-model:hiddenSeries', () => {
|
|
102
|
+
const hidden = ref<string[]>([])
|
|
103
|
+
cy.mount(
|
|
104
|
+
defineComponent({
|
|
105
|
+
setup() {
|
|
106
|
+
return () =>
|
|
107
|
+
h('div', { style: 'width: 480px; height: 300px' }, [
|
|
108
|
+
h(DonutChart, {
|
|
109
|
+
data,
|
|
110
|
+
category: 'source',
|
|
111
|
+
value: 'visits',
|
|
112
|
+
echartOptions: { animation: false },
|
|
113
|
+
hiddenSeries: hidden.value,
|
|
114
|
+
'onUpdate:hiddenSeries': (v: string[]) => (hidden.value = v),
|
|
115
|
+
}),
|
|
116
|
+
])
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
cy.get('[aria-label="Hide Search"]')
|
|
122
|
+
.click()
|
|
123
|
+
.then(() => expect(hidden.value).to.deep.equal(['Search']))
|
|
124
|
+
slices().should('have.length', 2)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// An empty ring reads as a bug, so the last entry left showing does nothing.
|
|
128
|
+
it('refuses to hide the last slice left on the ring', () => {
|
|
129
|
+
mountChart()
|
|
130
|
+
cy.get('[aria-label="Hide Search"]').click()
|
|
131
|
+
cy.get('[aria-label="Hide Direct"]').click()
|
|
132
|
+
cy.get('[aria-label="Hide Email"]').click()
|
|
133
|
+
cy.get('[aria-label="Hide Email"]').should(
|
|
134
|
+
'have.attr',
|
|
135
|
+
'aria-pressed',
|
|
136
|
+
'true',
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
|
|
99
140
|
it('gives up the readout when the labels move onto the ring', () => {
|
|
100
|
-
mountChart({
|
|
141
|
+
mountChart({ showDataLabels: true })
|
|
101
142
|
cy.get('[data-slot="chart-plot"]').should('not.contain.text', 'Visits')
|
|
102
143
|
cy.get('[data-slot="chart-plot"] svg text').should('contain.text', 'Search')
|
|
103
144
|
})
|
|
@@ -114,6 +155,25 @@ describe('DonutChart', () => {
|
|
|
114
155
|
cy.get('[data-slot="chart-legend"]').should('contain.text', 'Others')
|
|
115
156
|
})
|
|
116
157
|
|
|
158
|
+
// The tail is identified by the reserved key, not by what it prints, so a
|
|
159
|
+
// caller can tell it apart from a category that happens to read "Others".
|
|
160
|
+
it('names the tail slice by the reserved key', () => {
|
|
161
|
+
mountChart({ maxSlices: 2, onSelect: cy.spy().as('onSelect') })
|
|
162
|
+
slices().should('have.length', 2)
|
|
163
|
+
cy.get('[data-slot="chart-plot"] [role="img"]')
|
|
164
|
+
.focus()
|
|
165
|
+
.type('{rightarrow}{enter}')
|
|
166
|
+
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
167
|
+
name: OTHERS_KEY,
|
|
168
|
+
label: 'Others',
|
|
169
|
+
value: 50,
|
|
170
|
+
rows: [
|
|
171
|
+
{ source: 'Direct', visits: 30 },
|
|
172
|
+
{ source: 'Email', visits: 20 },
|
|
173
|
+
],
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
117
177
|
describe('states', () => {
|
|
118
178
|
/** The state an app styles the card from, without tracking it itself. */
|
|
119
179
|
const container = () => cy.get('[data-slot="chart-container"]')
|
|
@@ -172,7 +232,8 @@ describe('DonutChart', () => {
|
|
|
172
232
|
mountChart(
|
|
173
233
|
{},
|
|
174
234
|
{
|
|
175
|
-
center: ({
|
|
235
|
+
center: ({ formattedValue, label }: any) =>
|
|
236
|
+
h('span', `${formattedValue} of ${label}`),
|
|
176
237
|
},
|
|
177
238
|
)
|
|
178
239
|
cy.get('[data-slot="chart-plot"]').should('contain.text', '100 of Visits')
|
|
@@ -182,8 +243,8 @@ describe('DonutChart', () => {
|
|
|
182
243
|
mountChart(
|
|
183
244
|
{},
|
|
184
245
|
{
|
|
185
|
-
center: ({ label, percent }: any) =>
|
|
186
|
-
h('span', `${label} ${percent
|
|
246
|
+
center: ({ label, value, percent }: any) =>
|
|
247
|
+
h('span', `${label} ${value} ${Math.round(percent)}%`),
|
|
187
248
|
},
|
|
188
249
|
)
|
|
189
250
|
slices().should('have.length', data.length)
|
|
@@ -194,7 +255,7 @@ describe('DonutChart', () => {
|
|
|
194
255
|
350,
|
|
195
256
|
135,
|
|
196
257
|
)
|
|
197
|
-
cy.get('[data-slot="chart-plot"]').should('contain.text', 'Search 50%')
|
|
258
|
+
cy.get('[data-slot="chart-plot"]').should('contain.text', 'Search 50 50%')
|
|
198
259
|
})
|
|
199
260
|
|
|
200
261
|
it('takes an app’s own tooltip body', () => {
|
|
@@ -244,6 +305,7 @@ describe('DonutChart', () => {
|
|
|
244
305
|
plot().type('{enter}')
|
|
245
306
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
246
307
|
name: 'Direct',
|
|
308
|
+
label: 'Direct',
|
|
247
309
|
value: 30,
|
|
248
310
|
percent: 30,
|
|
249
311
|
rows: [{ source: 'Direct', visits: 30 }],
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<!-- HTML rather than an echarts graphic: swapping the readout on hover
|
|
37
37
|
costs two spans instead of a `setOption` on the ring. -->
|
|
38
38
|
<div
|
|
39
|
-
v-if="!
|
|
39
|
+
v-if="!showDataLabels"
|
|
40
40
|
class="pointer-events-none absolute inset-0 flex items-center justify-center"
|
|
41
41
|
:dir="dir"
|
|
42
42
|
>
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
<div
|
|
61
61
|
class="truncate text-center text-xl font-semibold tabular-nums text-ink-gray-8"
|
|
62
62
|
>
|
|
63
|
-
{{ center.
|
|
63
|
+
{{ center.formattedValue }}
|
|
64
64
|
</div>
|
|
65
65
|
<!-- The share is the point of the readout, so only the name
|
|
66
66
|
gives way when the hole is too narrow for both. -->
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
class="flex items-baseline justify-center gap-1 text-xs text-ink-gray-5"
|
|
69
69
|
>
|
|
70
70
|
<span class="min-w-0 truncate">{{ center.label }}</span>
|
|
71
|
-
<span v-if="center.percent" class="shrink-0">
|
|
72
|
-
{{ center.percent }}
|
|
71
|
+
<span v-if="center.percent !== undefined" class="shrink-0">
|
|
72
|
+
{{ formatPercent(center.percent) }}
|
|
73
73
|
</span>
|
|
74
74
|
</div>
|
|
75
75
|
</slot>
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
:x="tooltip.x"
|
|
84
84
|
:y="tooltip.y"
|
|
85
85
|
:items="tooltip.items"
|
|
86
|
+
:rows="tooltip.rows"
|
|
86
87
|
:dir="dir"
|
|
87
88
|
>
|
|
88
89
|
<template v-if="$slots.tooltip" #default="slotProps">
|
|
@@ -105,6 +106,7 @@
|
|
|
105
106
|
import { computed, reactive, ref, watch } from 'vue'
|
|
106
107
|
import { PieChart as PieSeries } from 'echarts/charts'
|
|
107
108
|
import { registerChartModules, useChart } from './core/useChart'
|
|
109
|
+
import { pruneHiddenSeries, toggleHiddenSeries } from './hiddenSeries'
|
|
108
110
|
import { useTooltipDismiss } from './core/useTooltipDismiss'
|
|
109
111
|
import { usePlotKeyboard } from './core/usePlotKeyboard'
|
|
110
112
|
import {
|
|
@@ -124,7 +126,7 @@ import ChartContainer from './components/ChartContainer.vue'
|
|
|
124
126
|
import ChartLegend from './components/ChartLegend.vue'
|
|
125
127
|
import ChartTooltip from './components/ChartTooltip.vue'
|
|
126
128
|
import type {
|
|
127
|
-
|
|
129
|
+
ChartExposedRefs,
|
|
128
130
|
ChartLegendItem,
|
|
129
131
|
ChartTooltipItem,
|
|
130
132
|
DonutChartConfig,
|
|
@@ -139,12 +141,15 @@ registerChartModules([PieSeries])
|
|
|
139
141
|
|
|
140
142
|
const props = defineProps<DonutChartProps>()
|
|
141
143
|
|
|
144
|
+
const hiddenSlices = defineModel<string[]>('hiddenSeries', {
|
|
145
|
+
default: () => [],
|
|
146
|
+
})
|
|
147
|
+
|
|
142
148
|
const emit = defineEmits<DonutChartEmits>()
|
|
143
149
|
|
|
144
150
|
defineSlots<DonutChartSlots>()
|
|
145
151
|
|
|
146
152
|
const plotEl = ref<HTMLElement>()
|
|
147
|
-
const hiddenSlices = ref<string[]>([])
|
|
148
153
|
|
|
149
154
|
const dir = computed(() => props.dir ?? documentDir())
|
|
150
155
|
const isHalf = computed(() => props.variant === 'half')
|
|
@@ -154,7 +159,7 @@ const config = computed<DonutChartConfig>(() => ({
|
|
|
154
159
|
categoryColumn: props.category,
|
|
155
160
|
valueColumn: props.value,
|
|
156
161
|
maxSlices: props.maxSlices,
|
|
157
|
-
|
|
162
|
+
showDataLabels: props.showDataLabels,
|
|
158
163
|
centerLabel: props.centerLabel,
|
|
159
164
|
variant: props.variant,
|
|
160
165
|
palette: props.palette,
|
|
@@ -226,6 +231,8 @@ const tooltip = reactive({
|
|
|
226
231
|
x: 0,
|
|
227
232
|
y: 0,
|
|
228
233
|
items: [] as ChartTooltipItem[],
|
|
234
|
+
/** Plural: a slice groups, and "Others" groups the whole tail. */
|
|
235
|
+
rows: [] as Record<string, any>[],
|
|
229
236
|
})
|
|
230
237
|
|
|
231
238
|
useTooltipDismiss({
|
|
@@ -255,7 +262,8 @@ const { chart, dispatch } = useChart({
|
|
|
255
262
|
const slice = sliceByName.value.get(params.name)
|
|
256
263
|
if (!slice) return
|
|
257
264
|
emit('select', {
|
|
258
|
-
name: slice.
|
|
265
|
+
name: slice.name,
|
|
266
|
+
label: slice.label,
|
|
259
267
|
value: slice.value,
|
|
260
268
|
percent: slice.percent,
|
|
261
269
|
rows: slice.rows,
|
|
@@ -302,8 +310,10 @@ function showTooltip(name: string) {
|
|
|
302
310
|
value: slice.value,
|
|
303
311
|
formattedValue: formatMeasure(slice.value),
|
|
304
312
|
percent: slice.percent,
|
|
313
|
+
kind: 'series',
|
|
305
314
|
},
|
|
306
315
|
]
|
|
316
|
+
tooltip.rows = slice.rows
|
|
307
317
|
tooltip.x = pointer.x
|
|
308
318
|
tooltip.y = pointer.y
|
|
309
319
|
tooltip.open = true
|
|
@@ -351,7 +361,8 @@ const { attrs: plotAttrs } = usePlotKeyboard({
|
|
|
351
361
|
const slice = visibleSlices.value[index]
|
|
352
362
|
if (!slice) return
|
|
353
363
|
emit('select', {
|
|
354
|
-
name: slice.
|
|
364
|
+
name: slice.name,
|
|
365
|
+
label: slice.label,
|
|
355
366
|
value: slice.value,
|
|
356
367
|
percent: slice.percent,
|
|
357
368
|
rows: slice.rows,
|
|
@@ -365,14 +376,11 @@ const { attrs: plotAttrs } = usePlotKeyboard({
|
|
|
365
376
|
})
|
|
366
377
|
|
|
367
378
|
function toggleSlice(name: string) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
} else {
|
|
374
|
-
hiddenSlices.value = hidden.filter((n) => n !== name)
|
|
375
|
-
}
|
|
379
|
+
hiddenSlices.value = toggleHiddenSeries(
|
|
380
|
+
hiddenSlices.value,
|
|
381
|
+
name,
|
|
382
|
+
slices.value.length,
|
|
383
|
+
)
|
|
376
384
|
}
|
|
377
385
|
|
|
378
386
|
function hoverSlice(name: string | null) {
|
|
@@ -387,14 +395,16 @@ const center = computed(() => {
|
|
|
387
395
|
const slice = hovered.value ? sliceByName.value.get(hovered.value) : undefined
|
|
388
396
|
if (slice && !slice.hidden) {
|
|
389
397
|
return {
|
|
390
|
-
value: shorten(slice.value),
|
|
391
398
|
label: slice.label,
|
|
392
|
-
|
|
399
|
+
value: slice.value,
|
|
400
|
+
formattedValue: shorten(slice.value),
|
|
401
|
+
percent: slice.percent,
|
|
393
402
|
}
|
|
394
403
|
}
|
|
395
404
|
return {
|
|
396
|
-
value: shorten(visibleTotal.value),
|
|
397
405
|
label: props.centerLabel ?? formatLabel(props.value),
|
|
406
|
+
value: visibleTotal.value,
|
|
407
|
+
formattedValue: shorten(visibleTotal.value),
|
|
398
408
|
percent: undefined,
|
|
399
409
|
}
|
|
400
410
|
})
|
|
@@ -414,12 +424,9 @@ function shorten(value: number) {
|
|
|
414
424
|
watch(
|
|
415
425
|
() => slices.value.map((slice) => slice.name),
|
|
416
426
|
(names) => {
|
|
417
|
-
|
|
418
|
-
hiddenSlices.value = hiddenSlices.value.filter((name) =>
|
|
419
|
-
names.includes(name),
|
|
420
|
-
)
|
|
427
|
+
hiddenSlices.value = pruneHiddenSeries(hiddenSlices.value, names)
|
|
421
428
|
},
|
|
422
429
|
)
|
|
423
430
|
|
|
424
|
-
defineExpose<
|
|
431
|
+
defineExpose<ChartExposedRefs>({ chart: computed(() => chart.value) })
|
|
425
432
|
</script>
|
|
@@ -50,6 +50,18 @@ describe('FunnelChart', () => {
|
|
|
50
50
|
columns().should('have.length', data.length)
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
+
it('leaves out a stage whose count it cannot read', () => {
|
|
54
|
+
mountChart({
|
|
55
|
+
data: [
|
|
56
|
+
{ stage: 'Visited', deals: 50 },
|
|
57
|
+
{ stage: 'Demoed', deals: 'n/a' },
|
|
58
|
+
{ stage: 'Won', deals: 20 },
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
columns().should('have.length', 2)
|
|
62
|
+
cy.get('[data-slot="chart-container"]').should('not.contain.text', 'Demoed')
|
|
63
|
+
})
|
|
64
|
+
|
|
53
65
|
it('prints each stage against the first, and nothing against the first', () => {
|
|
54
66
|
mountChart()
|
|
55
67
|
cy.get('[data-slot="chart-container"]')
|
|
@@ -58,11 +70,6 @@ describe('FunnelChart', () => {
|
|
|
58
70
|
.and('not.contain.text', '100%')
|
|
59
71
|
})
|
|
60
72
|
|
|
61
|
-
it('drops the percentages on request', () => {
|
|
62
|
-
mountChart({ showPercentages: false })
|
|
63
|
-
cy.get('[data-slot="chart-container"]').should('not.contain.text', '60%')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
73
|
it('gives every stage a hit area that names it', () => {
|
|
67
74
|
mountChart()
|
|
68
75
|
hitAreas().should('have.length', data.length)
|
|
@@ -73,9 +80,9 @@ describe('FunnelChart', () => {
|
|
|
73
80
|
mountChart({ onSelect: cy.spy().as('onSelect') })
|
|
74
81
|
cy.get('[aria-label="Demoed, 30"]').click()
|
|
75
82
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
83
|
+
name: 'Demoed',
|
|
76
84
|
label: 'Demoed',
|
|
77
85
|
value: 30,
|
|
78
|
-
index: 1,
|
|
79
86
|
row: { stage: 'Demoed', deals: 30 },
|
|
80
87
|
})
|
|
81
88
|
})
|
|
@@ -88,6 +95,16 @@ describe('FunnelChart', () => {
|
|
|
88
95
|
.and('contain.text', 'Won')
|
|
89
96
|
})
|
|
90
97
|
|
|
98
|
+
it('reads both conversion rates in the tooltip, against the first stage and the one before', () => {
|
|
99
|
+
mountChart()
|
|
100
|
+
cy.get('[aria-label="Won, 20"]').trigger('mouseenter')
|
|
101
|
+
cy.get('[data-slot="chart-tooltip"]')
|
|
102
|
+
.should('contain.text', 'of Visited')
|
|
103
|
+
.and('contain.text', '40%')
|
|
104
|
+
.and('contain.text', 'of Demoed')
|
|
105
|
+
.and('contain.text', '67%')
|
|
106
|
+
})
|
|
107
|
+
|
|
91
108
|
describe('keyboard', () => {
|
|
92
109
|
// The funnel draws its own HTML, so each stage is a tab stop of its own —
|
|
93
110
|
// no arrow-key cursor, unlike the echarts plots.
|
|
@@ -122,7 +139,6 @@ describe('FunnelChart', () => {
|
|
|
122
139
|
cy.get('@onSelect').should('have.been.calledWithMatch', {
|
|
123
140
|
label: 'Won',
|
|
124
141
|
value: 20,
|
|
125
|
-
index: 2,
|
|
126
142
|
row: { stage: 'Won', deals: 20 },
|
|
127
143
|
})
|
|
128
144
|
})
|
|
@@ -160,6 +176,12 @@ describe('FunnelChart', () => {
|
|
|
160
176
|
cy.contains('No data to show').should('be.visible')
|
|
161
177
|
})
|
|
162
178
|
|
|
179
|
+
it('says so when no row carries a count', () => {
|
|
180
|
+
mountChart({ data: [{ stage: 'Visited', deals: null }] })
|
|
181
|
+
container().should('have.attr', 'data-state', 'empty')
|
|
182
|
+
cy.contains('No data to show').should('be.visible')
|
|
183
|
+
})
|
|
184
|
+
|
|
163
185
|
it('takes an app’s own placeholder in place of the skeleton', () => {
|
|
164
186
|
mountChart({ loading: true }, { loading: () => h('div', { id: 'own' }) })
|
|
165
187
|
cy.get('#own').should('exist')
|
|
@@ -191,12 +213,18 @@ describe('FunnelChart', () => {
|
|
|
191
213
|
cy.get('[data-slot="chart-header"]').should('contain.text', 'Export')
|
|
192
214
|
})
|
|
193
215
|
|
|
194
|
-
it('takes an app’s own tooltip body, with the
|
|
216
|
+
it('takes an app’s own tooltip body, with the rates among the items', () => {
|
|
195
217
|
mountChart(
|
|
196
218
|
{},
|
|
197
219
|
{
|
|
198
|
-
tooltip: ({
|
|
199
|
-
h(
|
|
220
|
+
tooltip: ({ items, rows }: any) =>
|
|
221
|
+
h(
|
|
222
|
+
'span',
|
|
223
|
+
`${rows[0].stage} at ${
|
|
224
|
+
items.find((item: any) => item.name === 'ofPrevious')
|
|
225
|
+
.formattedValue
|
|
226
|
+
}`,
|
|
227
|
+
),
|
|
200
228
|
},
|
|
201
229
|
)
|
|
202
230
|
cy.get('[aria-label="Won, 20"]').focus()
|