@adminforth/dashboard 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -52
- package/custom/composables/useElementSize.ts +17 -2
- package/custom/model/dashboard.types.ts +385 -98
- package/custom/runtime/DashboardRuntime.vue +2 -1
- package/custom/runtime/WidgetRenderer.vue +2 -1
- package/custom/skills/adminforth-dashboard/SKILL.md +8 -4
- package/custom/widgets/chart/ChartWidget.vue +36 -35
- package/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/custom/widgets/chart/chart.types.ts +42 -8
- package/custom/widgets/chart/chart.utils.ts +11 -0
- package/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
- package/custom/widgets/chart/line/LineChart.vue +23 -15
- package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -43
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
- package/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
- package/custom/widgets/table/TableWidget.vue +9 -4
- package/dist/custom/composables/useElementSize.js +14 -2
- package/dist/custom/composables/useElementSize.ts +17 -2
- package/dist/custom/model/dashboard.types.d.ts +179 -38
- package/dist/custom/model/dashboard.types.js +108 -42
- package/dist/custom/model/dashboard.types.ts +385 -98
- package/dist/custom/queries/useDashboardConfig.d.ts +832 -68
- package/dist/custom/queries/useWidgetData.d.ts +828 -64
- package/dist/custom/runtime/DashboardRuntime.vue +2 -1
- package/dist/custom/runtime/WidgetRenderer.vue +2 -1
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +8 -4
- package/dist/custom/widgets/chart/ChartWidget.vue +36 -35
- package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +14 -8
- package/dist/custom/widgets/chart/chart.types.js +23 -0
- package/dist/custom/widgets/chart/chart.types.ts +42 -8
- package/dist/custom/widgets/chart/chart.utils.d.ts +1 -0
- package/dist/custom/widgets/chart/chart.utils.js +7 -0
- package/dist/custom/widgets/chart/chart.utils.ts +11 -0
- package/dist/custom/widgets/chart/funnel/FunnelChart.vue +6 -4
- package/dist/custom/widgets/chart/line/LineChart.vue +23 -15
- package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +28 -43
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +7 -43
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -10
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +10 -11
- package/dist/custom/widgets/table/TableWidget.vue +9 -4
- package/dist/endpoint/widgets.js +23 -3
- package/dist/schema/api.d.ts +2637 -933
- package/dist/schema/widget.d.ts +1562 -582
- package/dist/schema/widget.js +207 -127
- package/dist/services/widgetConfigValidator.js +16 -80
- package/dist/services/widgetDataService.d.ts +0 -9
- package/dist/services/widgetDataService.js +356 -97
- package/endpoint/dashboard.ts +1 -1
- package/endpoint/widgets.ts +29 -3
- package/package.json +1 -1
- package/schema/widget.ts +221 -121
- package/services/widgetConfigValidator.ts +29 -100
- package/services/widgetDataService.ts +478 -129
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
:rows="rows"
|
|
27
27
|
:x-field="xField"
|
|
28
28
|
:y-field="yField"
|
|
29
|
-
:series-name="
|
|
29
|
+
:series-name="lineSeriesName"
|
|
30
30
|
:color="chartConfig.color"
|
|
31
31
|
:height="chartHeight"
|
|
32
32
|
/>
|
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
v-else-if="chartConfig?.type === 'stacked_bar'"
|
|
72
72
|
:rows="rows"
|
|
73
73
|
:x-field="xField"
|
|
74
|
-
:
|
|
74
|
+
:y-field="yField"
|
|
75
|
+
:series-field="seriesField"
|
|
75
76
|
:colors="chartConfig.colors"
|
|
76
77
|
:height="chartHeight"
|
|
77
78
|
/>
|
|
@@ -91,13 +92,14 @@
|
|
|
91
92
|
import { computed, watch } from 'vue'
|
|
92
93
|
import { useWidgetData } from '../../queries/useWidgetData.js'
|
|
93
94
|
import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
|
|
95
|
+
import { normalizeChartWidgetConfig } from './chart.types.js'
|
|
94
96
|
import BarChart from './bar/BarChart.vue'
|
|
95
97
|
import FunnelChart from './funnel/FunnelChart.vue'
|
|
96
98
|
import HistogramChart from './histogram/HistogramChart.vue'
|
|
97
99
|
import LineChart from './line/LineChart.vue'
|
|
98
100
|
import PieChart from './pie/PieChart.vue'
|
|
99
101
|
import StackedBarChart from './stacked-bar/StackedBarChart.vue'
|
|
100
|
-
import {
|
|
102
|
+
import { toFiniteNumber } from './chart.utils.js'
|
|
101
103
|
|
|
102
104
|
const DEFAULT_WIDGET_HEIGHT = 500
|
|
103
105
|
|
|
@@ -126,31 +128,37 @@ watch(
|
|
|
126
128
|
const chartData = computed(() => data.value?.data as DashboardWidgetTableData | null)
|
|
127
129
|
const rows = computed(() => chartData.value?.rows ?? [])
|
|
128
130
|
const columns = computed(() => chartData.value?.columns ?? [])
|
|
129
|
-
const chartConfig = computed(() => props.widget.chart)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const valueField = computed(() => chartConfig.value?.value_field || columns.value[1])
|
|
134
|
-
const pieRows = computed(() => {
|
|
135
|
-
if (chartConfig.value?.value_field) {
|
|
136
|
-
return rows.value
|
|
137
|
-
}
|
|
131
|
+
const chartConfig = computed(() => normalizeChartWidgetConfig(props.widget.chart))
|
|
132
|
+
|
|
133
|
+
function resolveChartDimensionField(field: string | undefined, fallbackField: string | undefined) {
|
|
134
|
+
const resolvedField = field ?? fallbackField
|
|
138
135
|
|
|
139
|
-
|
|
136
|
+
if (!resolvedField) {
|
|
137
|
+
return ''
|
|
138
|
+
}
|
|
140
139
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const item = groupedRows.get(label) ?? { label, value: 0 }
|
|
144
|
-
item.value += 1
|
|
145
|
-
groupedRows.set(label, item)
|
|
140
|
+
if (columns.value.includes(resolvedField)) {
|
|
141
|
+
return resolvedField
|
|
146
142
|
}
|
|
147
143
|
|
|
148
|
-
return
|
|
144
|
+
return resolvedField
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const firstYField = computed(() => {
|
|
148
|
+
const y = chartConfig.value?.y
|
|
149
|
+
return Array.isArray(y) ? y[0]?.field : y?.field
|
|
149
150
|
})
|
|
150
|
-
const
|
|
151
|
-
const
|
|
151
|
+
const xField = computed(() => resolveChartDimensionField(chartConfig.value?.x?.field, columns.value[0]))
|
|
152
|
+
const yField = computed(() => firstYField.value || columns.value[1])
|
|
153
|
+
const labelField = computed(() => resolveChartDimensionField(chartConfig.value?.label?.field, columns.value[0] || 'name'))
|
|
154
|
+
const valueField = computed(() => chartConfig.value?.value?.field || columns.value[1] || 'value')
|
|
155
|
+
const pieRows = computed(() => rows.value)
|
|
156
|
+
const pieLabelField = computed(() => labelField.value)
|
|
157
|
+
const pieValueField = computed(() => valueField.value)
|
|
152
158
|
const barRows = computed(() => {
|
|
153
|
-
const bucketField = chartConfig.value?.
|
|
159
|
+
const bucketField = chartConfig.value?.type === 'histogram'
|
|
160
|
+
? chartConfig.value.x?.field
|
|
161
|
+
: undefined
|
|
154
162
|
|
|
155
163
|
if (!bucketField) {
|
|
156
164
|
return rows.value
|
|
@@ -167,19 +175,12 @@ const barRows = computed(() => {
|
|
|
167
175
|
}).length,
|
|
168
176
|
}))
|
|
169
177
|
})
|
|
170
|
-
const barLabelField = computed(() => chartConfig.value?.
|
|
171
|
-
const barValueField = computed(() => chartConfig.value?.
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return columns.value
|
|
178
|
-
.filter((column) => column !== xField.value)
|
|
179
|
-
.map((column) => ({
|
|
180
|
-
name: column,
|
|
181
|
-
field: column,
|
|
182
|
-
}))
|
|
178
|
+
const barLabelField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'label' : xField.value)
|
|
179
|
+
const barValueField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'count' : yField.value)
|
|
180
|
+
const seriesField = computed(() => chartConfig.value?.series?.field || columns.value[2] || '')
|
|
181
|
+
const lineSeriesName = computed(() => {
|
|
182
|
+
const y = chartConfig.value?.y
|
|
183
|
+
return Array.isArray(y) ? y[0]?.label : undefined
|
|
183
184
|
})
|
|
184
185
|
|
|
185
186
|
const chartHeight = computed(() => {
|
|
@@ -83,7 +83,14 @@
|
|
|
83
83
|
<script setup lang="ts">
|
|
84
84
|
import { computed } from 'vue'
|
|
85
85
|
import { useElementSize } from '../../../composables/useElementSize.js'
|
|
86
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
CHART_COLORS,
|
|
88
|
+
formatChartAxisLabel,
|
|
89
|
+
formatChartLabel,
|
|
90
|
+
formatChartValue,
|
|
91
|
+
getChartYAxisWidth,
|
|
92
|
+
toFiniteNumber,
|
|
93
|
+
} from '../chart.utils.js'
|
|
87
94
|
|
|
88
95
|
const props = withDefaults(defineProps<{
|
|
89
96
|
rows: Record<string, unknown>[]
|
|
@@ -97,12 +104,6 @@ const props = withDefaults(defineProps<{
|
|
|
97
104
|
|
|
98
105
|
const { el: rootEl, width: rootWidth, height: rootHeight } = useElementSize<HTMLDivElement>()
|
|
99
106
|
|
|
100
|
-
const padding = {
|
|
101
|
-
top: 20,
|
|
102
|
-
right: 6,
|
|
103
|
-
bottom: 34,
|
|
104
|
-
left: 38,
|
|
105
|
-
}
|
|
106
107
|
const chartWidth = computed(() => Math.max(rootWidth.value, 1))
|
|
107
108
|
const chartHeight = computed(() => {
|
|
108
109
|
if (rootHeight.value > 0) {
|
|
@@ -115,8 +116,15 @@ const chartHeight = computed(() => {
|
|
|
115
116
|
const chartColor = computed(() => props.color || CHART_COLORS[0])
|
|
116
117
|
const values = computed(() => props.rows.map((row) => toFiniteNumber(row[props.valueField])))
|
|
117
118
|
const maxValue = computed(() => Math.max(...values.value, 1))
|
|
118
|
-
const
|
|
119
|
-
const
|
|
119
|
+
const yTickValues = computed(() => [maxValue.value, maxValue.value * 0.5, 0])
|
|
120
|
+
const padding = computed(() => ({
|
|
121
|
+
top: 20,
|
|
122
|
+
right: 6,
|
|
123
|
+
bottom: 34,
|
|
124
|
+
left: getChartYAxisWidth(yTickValues.value, chartWidth.value),
|
|
125
|
+
}))
|
|
126
|
+
const innerWidth = computed(() => Math.max(chartWidth.value - padding.value.left - padding.value.right, 1))
|
|
127
|
+
const innerHeight = computed(() => Math.max(chartHeight.value - padding.value.top - padding.value.bottom, 1))
|
|
120
128
|
const barGap = 12
|
|
121
129
|
const barWidth = computed(() => {
|
|
122
130
|
const count = Math.max(props.rows.length, 1)
|
|
@@ -126,7 +134,7 @@ const totalChartWidth = computed(() => {
|
|
|
126
134
|
const count = Math.max(props.rows.length, 1)
|
|
127
135
|
return count * barWidth.value + (count - 1) * barGap
|
|
128
136
|
})
|
|
129
|
-
const chartStartX = computed(() => padding.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
|
|
137
|
+
const chartStartX = computed(() => padding.value.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
|
|
130
138
|
const visibleLabelIndexes = computed(() => {
|
|
131
139
|
const count = props.rows.length
|
|
132
140
|
const approxLabelWidth = 52
|
|
@@ -155,13 +163,13 @@ const bars = computed(() => props.rows.map((row, index) => {
|
|
|
155
163
|
axisLabel: formatChartAxisLabel(row[props.labelField]),
|
|
156
164
|
value,
|
|
157
165
|
x: chartStartX.value + index * (barWidth.value + barGap),
|
|
158
|
-
y: padding.top + innerHeight.value - height,
|
|
166
|
+
y: padding.value.top + innerHeight.value - height,
|
|
159
167
|
height,
|
|
160
168
|
}
|
|
161
169
|
}))
|
|
162
170
|
|
|
163
171
|
const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
|
|
164
172
|
value: maxValue.value * (1 - ratio),
|
|
165
|
-
y: padding.top + innerHeight.value * ratio,
|
|
173
|
+
y: padding.value.top + innerHeight.value * ratio,
|
|
166
174
|
})))
|
|
167
175
|
</script>
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ValueFormat } from '../../model/dashboard.types.js'
|
|
2
|
+
|
|
1
3
|
export type ChartWidgetType =
|
|
2
4
|
| 'line'
|
|
3
5
|
| 'pie'
|
|
@@ -12,23 +14,55 @@ export type ChartWidgetBucketConfig = {
|
|
|
12
14
|
max?: number
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export type ChartFieldRef = {
|
|
18
|
+
field: string
|
|
19
|
+
label?: string
|
|
20
|
+
format?: ValueFormat
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export type ChartWidgetSeriesConfig = {
|
|
16
|
-
name: string
|
|
17
24
|
field: string
|
|
25
|
+
label?: string
|
|
18
26
|
color?: string
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export type ChartWidgetConfig = {
|
|
22
30
|
type: ChartWidgetType
|
|
23
31
|
title?: string
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
bucket_field?: string
|
|
32
|
+
x?: ChartFieldRef
|
|
33
|
+
y?: ChartFieldRef | ChartFieldRef[]
|
|
34
|
+
label?: ChartFieldRef
|
|
35
|
+
value?: ChartFieldRef
|
|
29
36
|
buckets?: ChartWidgetBucketConfig[]
|
|
30
|
-
series?: ChartWidgetSeriesConfig
|
|
31
|
-
series_name?: string
|
|
37
|
+
series?: ChartWidgetSeriesConfig
|
|
32
38
|
color?: string
|
|
33
39
|
colors?: string[]
|
|
34
40
|
}
|
|
41
|
+
|
|
42
|
+
export type NormalizedChartWidgetConfig = ChartWidgetConfig
|
|
43
|
+
|
|
44
|
+
export function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined {
|
|
45
|
+
if (!isRecord(value) || !normalizeChartWidgetType(value.type)) {
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return value as ChartWidgetConfig
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeChartWidgetType(value: unknown): ChartWidgetType | undefined {
|
|
53
|
+
switch (value) {
|
|
54
|
+
case 'line':
|
|
55
|
+
case 'pie':
|
|
56
|
+
case 'bar':
|
|
57
|
+
case 'stacked_bar':
|
|
58
|
+
case 'funnel':
|
|
59
|
+
case 'histogram':
|
|
60
|
+
return value
|
|
61
|
+
default:
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
67
|
+
return typeof value === 'object' && value !== null
|
|
68
|
+
}
|
|
@@ -18,6 +18,17 @@ export function formatChartValue(value: number, options: Intl.NumberFormatOption
|
|
|
18
18
|
return new Intl.NumberFormat(undefined, options).format(value)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function getChartYAxisWidth(values: number[], chartWidth: number) {
|
|
22
|
+
const maxLabelLength = Math.max(
|
|
23
|
+
...values.map((value) => formatChartValue(value).length),
|
|
24
|
+
1,
|
|
25
|
+
)
|
|
26
|
+
const estimatedWidth = Math.ceil(maxLabelLength * 6.5) + 18
|
|
27
|
+
const responsiveMaxWidth = Math.max(Math.floor(chartWidth * 0.36), 38)
|
|
28
|
+
|
|
29
|
+
return Math.min(Math.max(estimatedWidth, 38), responsiveMaxWidth, 120)
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
export function formatChartLabel(value: unknown) {
|
|
22
33
|
if (typeof value !== 'string') {
|
|
23
34
|
return String(value)
|
|
@@ -48,9 +48,11 @@ const funnelRows = computed(() => {
|
|
|
48
48
|
groupedRows.set(label, item)
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
const rows = Array.from(groupedRows.values()).filter((row) => row.value > 0)
|
|
52
|
+
|
|
53
|
+
return props.labelField === 'name'
|
|
54
|
+
? rows
|
|
55
|
+
: rows.sort((left, right) => right.value - left.value)
|
|
54
56
|
})
|
|
55
57
|
|
|
56
58
|
const maxValue = computed(() => {
|
|
@@ -194,4 +196,4 @@ const segments = computed(() => funnelRows.value.map((row, index) => {
|
|
|
194
196
|
</div>
|
|
195
197
|
</div>
|
|
196
198
|
</div>
|
|
197
|
-
</template>
|
|
199
|
+
</template>
|
|
@@ -85,7 +85,14 @@
|
|
|
85
85
|
<script setup lang="ts">
|
|
86
86
|
import { computed } from 'vue'
|
|
87
87
|
import { useElementSize } from '../../../composables/useElementSize.js'
|
|
88
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
CHART_COLORS,
|
|
90
|
+
formatChartAxisLabel,
|
|
91
|
+
formatChartLabel,
|
|
92
|
+
formatChartValue,
|
|
93
|
+
getChartYAxisWidth,
|
|
94
|
+
toFiniteNumber,
|
|
95
|
+
} from '../chart.utils.js'
|
|
89
96
|
|
|
90
97
|
const props = withDefaults(defineProps<{
|
|
91
98
|
rows: Record<string, unknown>[]
|
|
@@ -100,12 +107,6 @@ const props = withDefaults(defineProps<{
|
|
|
100
107
|
|
|
101
108
|
const { el: rootEl, width: rootWidth, height: rootHeight } = useElementSize<HTMLDivElement>()
|
|
102
109
|
|
|
103
|
-
const padding = {
|
|
104
|
-
top: 12,
|
|
105
|
-
right: 6,
|
|
106
|
-
bottom: 24,
|
|
107
|
-
left: 38,
|
|
108
|
-
}
|
|
109
110
|
const chartWidth = computed(() => Math.max(rootWidth.value, 1))
|
|
110
111
|
const chartHeight = computed(() => {
|
|
111
112
|
if (rootHeight.value > 0) {
|
|
@@ -118,14 +119,21 @@ const chartHeight = computed(() => {
|
|
|
118
119
|
const chartColor = computed(() => props.color || CHART_COLORS[0])
|
|
119
120
|
const values = computed(() => props.rows.map((row) => toFiniteNumber(row[props.yField])))
|
|
120
121
|
const maxValue = computed(() => Math.max(...values.value, 1))
|
|
121
|
-
const
|
|
122
|
-
const
|
|
122
|
+
const yTickValues = computed(() => [maxValue.value, maxValue.value * 0.5, 0])
|
|
123
|
+
const padding = computed(() => ({
|
|
124
|
+
top: 12,
|
|
125
|
+
right: 6,
|
|
126
|
+
bottom: 24,
|
|
127
|
+
left: getChartYAxisWidth(yTickValues.value, chartWidth.value),
|
|
128
|
+
}))
|
|
129
|
+
const innerWidth = computed(() => Math.max(chartWidth.value - padding.value.left - padding.value.right, 1))
|
|
130
|
+
const innerHeight = computed(() => Math.max(chartHeight.value - padding.value.top - padding.value.bottom, 1))
|
|
123
131
|
|
|
124
132
|
const points = computed(() => {
|
|
125
133
|
if (props.rows.length === 1) {
|
|
126
134
|
return [{
|
|
127
|
-
x: padding.left + innerWidth.value / 2,
|
|
128
|
-
y: padding.top + innerHeight.value - (values.value[0] / maxValue.value) * innerHeight.value,
|
|
135
|
+
x: padding.value.left + innerWidth.value / 2,
|
|
136
|
+
y: padding.value.top + innerHeight.value - (values.value[0] / maxValue.value) * innerHeight.value,
|
|
129
137
|
label: formatChartLabel(props.rows[0][props.xField]),
|
|
130
138
|
axisLabel: formatChartAxisLabel(props.rows[0][props.xField]),
|
|
131
139
|
value: values.value[0],
|
|
@@ -133,8 +141,8 @@ const points = computed(() => {
|
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
return props.rows.map((row, index) => ({
|
|
136
|
-
x: padding.left + (index / (props.rows.length - 1)) * innerWidth.value,
|
|
137
|
-
y: padding.top + innerHeight.value - (values.value[index] / maxValue.value) * innerHeight.value,
|
|
144
|
+
x: padding.value.left + (index / (props.rows.length - 1)) * innerWidth.value,
|
|
145
|
+
y: padding.value.top + innerHeight.value - (values.value[index] / maxValue.value) * innerHeight.value,
|
|
138
146
|
label: formatChartLabel(row[props.xField]),
|
|
139
147
|
axisLabel: formatChartAxisLabel(row[props.xField]),
|
|
140
148
|
value: values.value[index],
|
|
@@ -152,12 +160,12 @@ const fillPath = computed(() => {
|
|
|
152
160
|
|
|
153
161
|
const first = points.value[0]
|
|
154
162
|
const last = points.value[points.value.length - 1]
|
|
155
|
-
return `${linePath.value} L ${last.x} ${padding.top + innerHeight.value} L ${first.x} ${padding.top + innerHeight.value} Z`
|
|
163
|
+
return `${linePath.value} L ${last.x} ${padding.value.top + innerHeight.value} L ${first.x} ${padding.value.top + innerHeight.value} Z`
|
|
156
164
|
})
|
|
157
165
|
|
|
158
166
|
const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
|
|
159
167
|
value: maxValue.value * (1 - ratio),
|
|
160
|
-
y: padding.top + innerHeight.value * ratio,
|
|
168
|
+
y: padding.value.top + innerHeight.value * ratio,
|
|
161
169
|
})))
|
|
162
170
|
|
|
163
171
|
const xLabels = computed(() => {
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { useElementSize } from '../../../composables/useElementSize.js'
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
CHART_COLORS,
|
|
6
|
+
formatChartAxisLabel,
|
|
7
|
+
formatChartLabel,
|
|
8
|
+
formatChartValue,
|
|
9
|
+
getChartYAxisWidth,
|
|
10
|
+
toFiniteNumber,
|
|
11
|
+
} from '../chart.utils.js'
|
|
6
12
|
|
|
7
13
|
const props = withDefaults(defineProps<{
|
|
8
14
|
rows: Record<string, unknown>[]
|
|
9
15
|
xField: string
|
|
10
|
-
|
|
16
|
+
yField: string
|
|
17
|
+
seriesField: string
|
|
11
18
|
colors?: string[]
|
|
12
19
|
height?: number
|
|
13
20
|
}>(), {
|
|
@@ -17,16 +24,11 @@ const props = withDefaults(defineProps<{
|
|
|
17
24
|
const { el: rootEl, width: rootWidth } = useElementSize<HTMLDivElement>()
|
|
18
25
|
const { el: svgEl, width: svgWidth, height: svgHeight } = useElementSize<HTMLDivElement>()
|
|
19
26
|
|
|
20
|
-
const padding = {
|
|
21
|
-
top: 24,
|
|
22
|
-
right: 6,
|
|
23
|
-
bottom: 34,
|
|
24
|
-
left: 38,
|
|
25
|
-
}
|
|
26
27
|
const barGap = 10
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const seriesNames = computed(() => Array.from(new Set(props.rows.map((row) => formatChartLabel(row[props.seriesField])))))
|
|
29
|
+
const normalizedSeries = computed(() => seriesNames.value.map((name, index) => ({
|
|
30
|
+
name,
|
|
31
|
+
color: props.colors?.[index] || CHART_COLORS[index % CHART_COLORS.length],
|
|
30
32
|
})))
|
|
31
33
|
const showLegend = computed(() => normalizedSeries.value.length > 0)
|
|
32
34
|
const isCompact = computed(() => rootWidth.value > 0 && rootWidth.value < 420)
|
|
@@ -38,19 +40,15 @@ const chartHeight = computed(() => {
|
|
|
38
40
|
|
|
39
41
|
return Math.max(props.height - (showLegend.value ? 28 : 0), 96)
|
|
40
42
|
})
|
|
41
|
-
const innerWidth = computed(() => Math.max(chartWidth.value - padding.left - padding.right, 1))
|
|
42
|
-
const innerHeight = computed(() => Math.max(chartHeight.value - padding.top - padding.bottom, 1))
|
|
43
43
|
const groupedRows = computed(() => {
|
|
44
44
|
const grouped = new Map<string, Record<string, unknown>>()
|
|
45
45
|
|
|
46
46
|
for (const row of props.rows) {
|
|
47
47
|
const label = formatChartLabel(row[props.xField])
|
|
48
48
|
const item = grouped.get(label) ?? { [props.xField]: label }
|
|
49
|
+
const seriesName = formatChartLabel(row[props.seriesField])
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
item[series.name] = toFiniteNumber(item[series.name])
|
|
52
|
-
+ getSeriesContribution(row[series.field], series.name)
|
|
53
|
-
}
|
|
51
|
+
item[seriesName] = toFiniteNumber(item[seriesName]) + toFiniteNumber(row[props.yField])
|
|
54
52
|
|
|
55
53
|
grouped.set(label, item)
|
|
56
54
|
}
|
|
@@ -65,15 +63,24 @@ const totalChartWidth = computed(() => {
|
|
|
65
63
|
const count = Math.max(groupedRows.value.length, 1)
|
|
66
64
|
return count * barWidth.value + (count - 1) * barGap
|
|
67
65
|
})
|
|
68
|
-
const chartStartX = computed(() => padding.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
|
|
66
|
+
const chartStartX = computed(() => padding.value.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
|
|
69
67
|
const totals = computed(() => groupedRows.value.map((row) => normalizedSeries.value.reduce(
|
|
70
68
|
(sum, series) => sum + toFiniteNumber(row[series.name]),
|
|
71
69
|
0,
|
|
72
70
|
)))
|
|
73
71
|
const maxTotal = computed(() => Math.max(...totals.value, 1))
|
|
72
|
+
const yTickValues = computed(() => [maxTotal.value, maxTotal.value * 0.5, 0])
|
|
73
|
+
const padding = computed(() => ({
|
|
74
|
+
top: 24,
|
|
75
|
+
right: 6,
|
|
76
|
+
bottom: 34,
|
|
77
|
+
left: getChartYAxisWidth(yTickValues.value, chartWidth.value),
|
|
78
|
+
}))
|
|
79
|
+
const innerWidth = computed(() => Math.max(chartWidth.value - padding.value.left - padding.value.right, 1))
|
|
80
|
+
const innerHeight = computed(() => Math.max(chartHeight.value - padding.value.top - padding.value.bottom, 1))
|
|
74
81
|
|
|
75
82
|
const bars = computed(() => groupedRows.value.map((row, rowIndex) => {
|
|
76
|
-
let y = padding.top + innerHeight.value
|
|
83
|
+
let y = padding.value.top + innerHeight.value
|
|
77
84
|
|
|
78
85
|
return {
|
|
79
86
|
label: String(row[props.xField]),
|
|
@@ -118,31 +125,9 @@ const visibleLabelIndexes = computed(() => {
|
|
|
118
125
|
|
|
119
126
|
const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
|
|
120
127
|
value: maxTotal.value * (1 - ratio),
|
|
121
|
-
y: padding.top + innerHeight.value * ratio,
|
|
128
|
+
y: padding.value.top + innerHeight.value * ratio,
|
|
122
129
|
})))
|
|
123
130
|
|
|
124
|
-
function getSeriesContribution(value: unknown, seriesName: string) {
|
|
125
|
-
if (typeof value === 'boolean') {
|
|
126
|
-
return value === getBooleanSeriesValue(seriesName) ? 1 : 0
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase())) {
|
|
130
|
-
return (value.toLowerCase() === 'true') === getBooleanSeriesValue(seriesName) ? 1 : 0
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return toFiniteNumber(value)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function getBooleanSeriesValue(seriesName: string) {
|
|
137
|
-
const normalizedName = seriesName.toLowerCase()
|
|
138
|
-
|
|
139
|
-
if (normalizedName.includes('unlisted') || normalizedName.includes('false')) {
|
|
140
|
-
return false
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return true
|
|
144
|
-
}
|
|
145
|
-
|
|
146
131
|
function getBarTooltip(bar: { label: string, total: number, segments: Array<{ name: string, value: number }> }) {
|
|
147
132
|
const percentFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 })
|
|
148
133
|
|
|
@@ -18,40 +18,6 @@ const {
|
|
|
18
18
|
refetch,
|
|
19
19
|
} = useWidgetData(dashboardSlugRef, widgetIdRef)
|
|
20
20
|
|
|
21
|
-
type GaugeCardConfig = {
|
|
22
|
-
value_field?: string
|
|
23
|
-
valueField?: string
|
|
24
|
-
min?: number | string
|
|
25
|
-
max?: number | string
|
|
26
|
-
min_field?: string
|
|
27
|
-
minField?: string
|
|
28
|
-
max_field?: string
|
|
29
|
-
maxField?: string
|
|
30
|
-
suffix?: string
|
|
31
|
-
color?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
|
-
return typeof value === 'object' && value !== null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function parseGaugeCardConfig(value: unknown): GaugeCardConfig | undefined {
|
|
39
|
-
if (isRecord(value)) {
|
|
40
|
-
return value as GaugeCardConfig
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (typeof value !== 'string') {
|
|
44
|
-
return undefined
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
try {
|
|
48
|
-
const parsed = JSON.parse(value) as unknown
|
|
49
|
-
return isRecord(parsed) ? parsed as GaugeCardConfig : undefined
|
|
50
|
-
} catch {
|
|
51
|
-
return undefined
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
21
|
function parseOptionalNumber(value: unknown): number | undefined {
|
|
56
22
|
if (value === null || value === undefined || value === '') {
|
|
57
23
|
return undefined
|
|
@@ -86,20 +52,18 @@ watch(
|
|
|
86
52
|
{ deep: true },
|
|
87
53
|
)
|
|
88
54
|
|
|
89
|
-
const gaugeConfig = computed(() =>
|
|
55
|
+
const gaugeConfig = computed(() => props.widget.target === 'gauge_card' ? props.widget.card : undefined)
|
|
90
56
|
const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
|
|
91
57
|
const columns = computed(() => widgetData.value?.columns ?? [])
|
|
92
58
|
const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
|
|
93
|
-
const valueField = computed(() => gaugeConfig.value?.
|
|
94
|
-
const
|
|
95
|
-
const maxField = computed(() => gaugeConfig.value?.max_field || gaugeConfig.value?.maxField)
|
|
59
|
+
const valueField = computed(() => gaugeConfig.value?.value.field || columns.value[0])
|
|
60
|
+
const targetField = computed(() => gaugeConfig.value?.target?.field ?? gaugeConfig.value?.progress?.targetField)
|
|
96
61
|
const minValue = computed(() => {
|
|
97
|
-
|
|
98
|
-
return dynamicMin ?? parseOptionalNumber(gaugeConfig.value?.min) ?? 0
|
|
62
|
+
return 0
|
|
99
63
|
})
|
|
100
64
|
const maxValue = computed(() => {
|
|
101
|
-
const dynamicMax =
|
|
102
|
-
return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.
|
|
65
|
+
const dynamicMax = targetField.value ? parseOptionalNumber(firstRow.value[targetField.value]) : undefined
|
|
66
|
+
return dynamicMax ?? parseOptionalNumber(gaugeConfig.value?.target?.value ?? gaugeConfig.value?.progress?.targetValue) ?? 100
|
|
103
67
|
})
|
|
104
68
|
const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
|
|
105
69
|
const fractionDigits = computed(() => Math.min([
|
|
@@ -179,7 +143,7 @@ const gaugeColor = computed(() => gaugeConfig.value?.color || CHART_COLORS[0])
|
|
|
179
143
|
</svg>
|
|
180
144
|
|
|
181
145
|
<div class="text-3xl font-bold text-lightNavbarText dark:text-darkNavbarText">
|
|
182
|
-
{{ formattedValue }}{{ gaugeConfig?.suffix ?? '' }}
|
|
146
|
+
{{ gaugeConfig?.value.prefix ?? '' }}{{ formattedValue }}{{ gaugeConfig?.value.suffix ?? '' }}
|
|
183
147
|
</div>
|
|
184
148
|
<div class="text-sm text-lightListTableText dark:text-darkListTableText">
|
|
185
149
|
{{ formattedMinValue }} - {{ formattedMaxValue }}
|
|
@@ -26,20 +26,16 @@ watch(
|
|
|
26
26
|
{ deep: true },
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
const kpiConfig = computed(() => props.widget.kpi_card
|
|
30
|
-
value_field?: string
|
|
31
|
-
label_field?: string
|
|
32
|
-
prefix?: string
|
|
33
|
-
suffix?: string
|
|
34
|
-
} | undefined)
|
|
29
|
+
const kpiConfig = computed(() => props.widget.target === 'kpi_card' ? props.widget.card : undefined)
|
|
35
30
|
const widgetData = computed(() => data.value?.data as DashboardWidgetTableData | null)
|
|
36
31
|
const columns = computed(() => widgetData.value?.columns ?? [])
|
|
37
32
|
const firstRow = computed(() => widgetData.value?.rows[0] ?? {})
|
|
38
|
-
const valueField = computed(() => kpiConfig.value?.
|
|
39
|
-
const labelField = computed(() => kpiConfig.value?.label_field)
|
|
33
|
+
const valueField = computed(() => kpiConfig.value?.value.field || columns.value[0])
|
|
40
34
|
const value = computed(() => toFiniteNumber(firstRow.value[valueField.value]))
|
|
41
|
-
const label = computed(() =>
|
|
42
|
-
|
|
35
|
+
const label = computed(() => kpiConfig.value?.subtitle?.field
|
|
36
|
+
? String(firstRow.value[kpiConfig.value.subtitle.field])
|
|
37
|
+
: kpiConfig.value?.subtitle?.text ?? kpiConfig.value?.title ?? props.widget.label)
|
|
38
|
+
const formattedValue = computed(() => `${kpiConfig.value?.value.prefix ?? ''}${formatChartValue(value.value)}${kpiConfig.value?.value.suffix ?? ''}`)
|
|
43
39
|
</script>
|
|
44
40
|
|
|
45
41
|
<template>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, watch } from 'vue'
|
|
3
3
|
import { useWidgetData } from '../../queries/useWidgetData.js'
|
|
4
|
+
import {
|
|
5
|
+
getFieldRefField,
|
|
6
|
+
} from '../../model/dashboard.types.js'
|
|
4
7
|
import type { DashboardWidgetConfig, DashboardWidgetData } from '../../model/dashboard.types.js'
|
|
5
8
|
import { formatChartLabel, formatChartValue, toFiniteNumber } from '../chart/chart.utils.js'
|
|
6
9
|
|
|
@@ -26,21 +29,17 @@ watch(
|
|
|
26
29
|
{ deep: true },
|
|
27
30
|
)
|
|
28
31
|
|
|
29
|
-
const pivotConfig = computed(() => props.widget.pivot_table
|
|
30
|
-
row_field?: string
|
|
31
|
-
column_field?: string
|
|
32
|
-
value_field?: string
|
|
33
|
-
aggregation?: 'count' | 'sum'
|
|
34
|
-
} | undefined)
|
|
32
|
+
const pivotConfig = computed(() => props.widget.target === 'pivot_table' ? props.widget.pivot : undefined)
|
|
35
33
|
const widgetData = computed(() => data.value?.data as DashboardWidgetData | null)
|
|
36
34
|
const rows = computed(() => widgetData.value?.rows ?? [])
|
|
37
35
|
const columns = computed(() => widgetData.value?.columns ?? [])
|
|
38
36
|
const isAggregateData = computed(() => widgetData.value?.kind === 'aggregate')
|
|
39
|
-
const shouldRenderAggregateMatrix = computed(() => isAggregateData.value && !pivotConfig.value?.
|
|
40
|
-
const rowField = computed(() => pivotConfig.value?.
|
|
41
|
-
const columnField = computed(() => pivotConfig.value?.
|
|
42
|
-
const
|
|
43
|
-
const
|
|
37
|
+
const shouldRenderAggregateMatrix = computed(() => isAggregateData.value && !pivotConfig.value?.columns?.length)
|
|
38
|
+
const rowField = computed(() => getFieldRefField(pivotConfig.value?.rows[0]) || columns.value[0])
|
|
39
|
+
const columnField = computed(() => getFieldRefField(pivotConfig.value?.columns?.[0]) || columns.value[1])
|
|
40
|
+
const valueConfig = computed(() => pivotConfig.value?.values[0])
|
|
41
|
+
const valueField = computed(() => valueConfig.value?.field || columns.value[2] || columns.value[1])
|
|
42
|
+
const aggregation = computed(() => valueConfig.value?.aggregation || (valueField.value ? 'sum' : 'count'))
|
|
44
43
|
const pivotColumnLabels = computed(() => {
|
|
45
44
|
if (shouldRenderAggregateMatrix.value) {
|
|
46
45
|
return columns.value.filter((column) => column !== rowField.value)
|