@adminforth/dashboard 1.2.0 → 1.4.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 +116 -39
- package/custom/api/dashboardApi.ts +4 -0
- package/custom/composables/useElementSize.ts +17 -2
- package/custom/model/dashboard.types.ts +337 -236
- package/custom/skills/adminforth-dashboard/SKILL.md +113 -2
- package/custom/widgets/chart/ChartWidget.vue +38 -53
- package/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/custom/widgets/chart/chart.types.ts +17 -66
- 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 -12
- package/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
- package/custom/widgets/table/TableWidget.vue +8 -3
- package/dist/custom/api/dashboardApi.d.ts +1 -0
- package/dist/custom/api/dashboardApi.js +5 -0
- package/dist/custom/api/dashboardApi.ts +4 -0
- package/dist/custom/composables/useElementSize.js +14 -2
- package/dist/custom/composables/useElementSize.ts +17 -2
- package/dist/custom/model/dashboard.types.d.ts +181 -61
- package/dist/custom/model/dashboard.types.js +82 -93
- package/dist/custom/model/dashboard.types.ts +337 -236
- package/dist/custom/queries/useDashboardConfig.d.ts +852 -66
- package/dist/custom/queries/useWidgetData.d.ts +848 -62
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +113 -2
- package/dist/custom/widgets/chart/ChartWidget.vue +38 -53
- package/dist/custom/widgets/chart/bar/BarChart.vue +20 -12
- package/dist/custom/widgets/chart/chart.types.d.ts +13 -22
- package/dist/custom/widgets/chart/chart.types.js +2 -25
- package/dist/custom/widgets/chart/chart.types.ts +17 -66
- 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 -12
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +6 -8
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +8 -7
- package/dist/custom/widgets/table/TableWidget.vue +8 -3
- package/dist/endpoint/dashboard.d.ts +7 -2
- package/dist/endpoint/dashboard.js +45 -1
- package/dist/endpoint/widgets.d.ts +2 -1
- package/dist/endpoint/widgets.js +6 -2
- package/dist/schema/api.d.ts +2773 -736
- package/dist/schema/api.js +5 -0
- package/dist/schema/widget.d.ts +1648 -476
- package/dist/schema/widget.js +208 -139
- package/dist/services/widgetConfigValidator.js +16 -40
- package/dist/services/widgetDataService.d.ts +2 -1
- package/dist/services/widgetDataService.js +389 -82
- package/endpoint/dashboard.ts +77 -4
- package/endpoint/widgets.ts +11 -4
- package/package.json +1 -1
- package/schema/api.ts +6 -0
- package/schema/widget.ts +225 -139
- package/services/widgetConfigValidator.ts +29 -53
- package/services/widgetDataService.ts +522 -100
|
@@ -41,6 +41,7 @@ If the user asks how the schema works, how to implement the API, or how to chang
|
|
|
41
41
|
Use these tools whenever available:
|
|
42
42
|
|
|
43
43
|
- `dashboard_get_config`
|
|
44
|
+
- `dashboard_set_dashboard_config`
|
|
44
45
|
- `dashboard_add_dashboard_group`
|
|
45
46
|
- `dashboard_set_dashboard_group_config`
|
|
46
47
|
- `dashboard_move_dashboard_group`
|
|
@@ -58,6 +59,7 @@ If a dashboard tool is known by name but its argument schema is not loaded, call
|
|
|
58
59
|
Do not pass fields between dashboard tools by analogy. Use each tool's schema.
|
|
59
60
|
|
|
60
61
|
- `dashboard_add_dashboard_group` creates a new group. It accepts the dashboard slug only. Never pass `groupId` to this tool.
|
|
62
|
+
- `dashboard_set_dashboard_config` replaces the full dashboard config. Use it when the user explicitly asks to edit the whole dashboard config.
|
|
61
63
|
- `dashboard_add_dashboard_widget` creates a widget inside an existing group. Use it when you already have a `groupId`.
|
|
62
64
|
- `dashboard_set_dashboard_group_config`, `dashboard_move_dashboard_group`, and `dashboard_remove_dashboard_group` operate on an existing group and need `groupId`.
|
|
63
65
|
- `dashboard_set_widget_config`, `dashboard_move_dashboard_widget`, `dashboard_remove_dashboard_widget`, and `dashboard_get_dashboard_widget_data` operate on an existing widget and need `widgetId`.
|
|
@@ -113,13 +115,122 @@ For group requests:
|
|
|
113
115
|
|
|
114
116
|
If slug is missing, use `default`.
|
|
115
117
|
|
|
118
|
+
## Dashboard Config Workflow
|
|
119
|
+
|
|
120
|
+
Use `dashboard_set_dashboard_config` only when the user explicitly asks to edit the whole dashboard root config.
|
|
121
|
+
|
|
122
|
+
For requests like:
|
|
123
|
+
|
|
124
|
+
- "update root dashboard config"
|
|
125
|
+
- "replace the whole dashboard config"
|
|
126
|
+
|
|
127
|
+
do this:
|
|
128
|
+
|
|
129
|
+
1. Call `dashboard_get_config`.
|
|
130
|
+
2. Modify the returned root config, preserving existing `version`, `groups`, and `widgets` unless the user asked to change them.
|
|
131
|
+
3. Call `dashboard_set_dashboard_config` with the full updated config.
|
|
132
|
+
4. Return a short summary of the root-level fields changed.
|
|
133
|
+
|
|
134
|
+
Do not use `dashboard_set_dashboard_config` to store reusable widget variables.
|
|
135
|
+
|
|
116
136
|
## Widget Config Rules
|
|
117
137
|
|
|
118
138
|
Use the current schema keys exactly:
|
|
119
139
|
|
|
120
140
|
- Use `target`, not `type`.
|
|
121
141
|
- Use `label`, not `title`.
|
|
122
|
-
- Use `
|
|
123
|
-
- Use `
|
|
142
|
+
- Use `query`, not `data_source`.
|
|
143
|
+
- Use `resource`, not `resource_id`.
|
|
124
144
|
- Use `group_by`, not `groupBy`.
|
|
145
|
+
- Use `order_by`, not `orderBy`.
|
|
125
146
|
- Use `page_size`, not `pageSize`.
|
|
147
|
+
- For step-based chart queries, use `query.steps` as an ordered array of `{ name, resource, metric, filters }` steps and add `query.calcs` when derived fields are needed.
|
|
148
|
+
- Use `card` for KPI and gauge widget view config.
|
|
149
|
+
- Use `pivot` for pivot table view config.
|
|
150
|
+
- Use `variables` for reusable static maps or constants at widget level.
|
|
151
|
+
- In `query.calcs`, use `lookup($variables.some.map, row_field, default_number)` to read a numeric value from a variable map by the current row/group field.
|
|
152
|
+
|
|
153
|
+
## Variables And Lookup Calcs
|
|
154
|
+
|
|
155
|
+
Widget config can define variables:
|
|
156
|
+
|
|
157
|
+
```yaml
|
|
158
|
+
variables:
|
|
159
|
+
token_prices_per_1m:
|
|
160
|
+
input:
|
|
161
|
+
gpt-4.1: 2.00
|
|
162
|
+
gpt-4.1-mini: 0.40
|
|
163
|
+
gpt-4o-mini: 0.15
|
|
164
|
+
output:
|
|
165
|
+
gpt-4.1: 8.00
|
|
166
|
+
gpt-4.1-mini: 1.60
|
|
167
|
+
gpt-4o-mini: 0.60
|
|
168
|
+
cached:
|
|
169
|
+
gpt-4.1: 0.50
|
|
170
|
+
gpt-4.1-mini: 0.10
|
|
171
|
+
gpt-4o-mini: 0.075
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Use variables when a calculation needs a static rate table, threshold table, coefficient map, or other reusable constants. In calcs, `lookup($variables.path.to.map, field_name, 0)` returns the value from the map using `field_name` from the current row/group. The third argument is the numeric fallback when the key is missing.
|
|
175
|
+
|
|
176
|
+
Example widget:
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
target: chart
|
|
180
|
+
label: Model costs
|
|
181
|
+
size: large
|
|
182
|
+
variables:
|
|
183
|
+
token_prices_per_1m:
|
|
184
|
+
input:
|
|
185
|
+
gpt-4.1: 2.00
|
|
186
|
+
gpt-4.1-mini: 0.40
|
|
187
|
+
gpt-4o-mini: 0.15
|
|
188
|
+
output:
|
|
189
|
+
gpt-4.1: 8.00
|
|
190
|
+
gpt-4.1-mini: 1.60
|
|
191
|
+
gpt-4o-mini: 0.60
|
|
192
|
+
cached:
|
|
193
|
+
gpt-4.1: 0.50
|
|
194
|
+
gpt-4.1-mini: 0.10
|
|
195
|
+
gpt-4o-mini: 0.075
|
|
196
|
+
|
|
197
|
+
chart:
|
|
198
|
+
type: stacked_bar
|
|
199
|
+
title: LLM costs by model
|
|
200
|
+
x:
|
|
201
|
+
field: model
|
|
202
|
+
label: Model
|
|
203
|
+
y:
|
|
204
|
+
- field: input_cost
|
|
205
|
+
label: Input
|
|
206
|
+
format: currency
|
|
207
|
+
- field: output_cost
|
|
208
|
+
label: Output
|
|
209
|
+
format: currency
|
|
210
|
+
- field: cached_cost
|
|
211
|
+
label: Cached
|
|
212
|
+
format: currency
|
|
213
|
+
|
|
214
|
+
query:
|
|
215
|
+
resource: model_usage
|
|
216
|
+
select:
|
|
217
|
+
- field: model
|
|
218
|
+
- agg: sum
|
|
219
|
+
field: input_tokens
|
|
220
|
+
as: input_tokens
|
|
221
|
+
- agg: sum
|
|
222
|
+
field: output_tokens
|
|
223
|
+
as: output_tokens
|
|
224
|
+
- agg: sum
|
|
225
|
+
field: cached_tokens
|
|
226
|
+
as: cached_tokens
|
|
227
|
+
group_by:
|
|
228
|
+
- model
|
|
229
|
+
calcs:
|
|
230
|
+
- calc: input_tokens / 1000000 * lookup($variables.token_prices_per_1m.input, model, 0)
|
|
231
|
+
as: input_cost
|
|
232
|
+
- calc: output_tokens / 1000000 * lookup($variables.token_prices_per_1m.output, model, 0)
|
|
233
|
+
as: output_cost
|
|
234
|
+
- calc: cached_tokens / 1000000 * lookup($variables.token_prices_per_1m.cached, model, 0)
|
|
235
|
+
as: cached_cost
|
|
236
|
+
```
|
|
@@ -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
|
/>
|
|
@@ -69,9 +69,10 @@
|
|
|
69
69
|
|
|
70
70
|
<StackedBarChart
|
|
71
71
|
v-else-if="chartConfig?.type === 'stacked_bar'"
|
|
72
|
-
:rows="
|
|
72
|
+
:rows="stackedBarRows"
|
|
73
73
|
:x-field="xField"
|
|
74
|
-
:
|
|
74
|
+
:y-field="stackedBarYField"
|
|
75
|
+
:series-field="stackedBarSeriesField"
|
|
75
76
|
:colors="chartConfig.colors"
|
|
76
77
|
:height="chartHeight"
|
|
77
78
|
/>
|
|
@@ -98,7 +99,7 @@ import HistogramChart from './histogram/HistogramChart.vue'
|
|
|
98
99
|
import LineChart from './line/LineChart.vue'
|
|
99
100
|
import PieChart from './pie/PieChart.vue'
|
|
100
101
|
import StackedBarChart from './stacked-bar/StackedBarChart.vue'
|
|
101
|
-
import {
|
|
102
|
+
import { toFiniteNumber } from './chart.utils.js'
|
|
102
103
|
|
|
103
104
|
const DEFAULT_WIDGET_HEIGHT = 500
|
|
104
105
|
|
|
@@ -128,15 +129,6 @@ const chartData = computed(() => data.value?.data as DashboardWidgetTableData |
|
|
|
128
129
|
const rows = computed(() => chartData.value?.rows ?? [])
|
|
129
130
|
const columns = computed(() => chartData.value?.columns ?? [])
|
|
130
131
|
const chartConfig = computed(() => normalizeChartWidgetConfig(props.widget.chart))
|
|
131
|
-
const aggregateGroupField = computed(() => {
|
|
132
|
-
const dataSource = props.widget.dataSource
|
|
133
|
-
|
|
134
|
-
if (dataSource?.type !== 'aggregate' || !dataSource.groupBy) {
|
|
135
|
-
return undefined
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return dataSource.groupBy.field
|
|
139
|
-
})
|
|
140
132
|
|
|
141
133
|
function resolveChartDimensionField(field: string | undefined, fallbackField: string | undefined) {
|
|
142
134
|
const resolvedField = field ?? fallbackField
|
|
@@ -149,41 +141,39 @@ function resolveChartDimensionField(field: string | undefined, fallbackField: st
|
|
|
149
141
|
return resolvedField
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
if (
|
|
153
|
-
aggregateGroupField.value
|
|
154
|
-
&& resolvedField === aggregateGroupField.value
|
|
155
|
-
&& columns.value.includes('group')
|
|
156
|
-
) {
|
|
157
|
-
return 'group'
|
|
158
|
-
}
|
|
159
|
-
|
|
160
144
|
return resolvedField
|
|
161
145
|
}
|
|
162
146
|
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
147
|
+
const firstYField = computed(() => {
|
|
148
|
+
const y = chartConfig.value?.y
|
|
149
|
+
return Array.isArray(y) ? y[0]?.field : y?.field
|
|
150
|
+
})
|
|
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)
|
|
158
|
+
const stackedBarYItems = computed(() => {
|
|
159
|
+
const y = chartConfig.value?.y
|
|
160
|
+
return Array.isArray(y) ? y : []
|
|
161
|
+
})
|
|
162
|
+
const stackedBarRows = computed(() => {
|
|
163
|
+
if (chartConfig.value?.type !== 'stacked_bar' || !stackedBarYItems.value.length) {
|
|
169
164
|
return rows.value
|
|
170
165
|
}
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
item.value += 1
|
|
178
|
-
groupedRows.set(label, item)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return Array.from(groupedRows.values())
|
|
167
|
+
return rows.value.flatMap((row) => stackedBarYItems.value.map((item) => ({
|
|
168
|
+
[xField.value]: row[xField.value],
|
|
169
|
+
__series: item.label ?? item.field,
|
|
170
|
+
__value: row[item.field],
|
|
171
|
+
})))
|
|
182
172
|
})
|
|
183
|
-
const pieLabelField = computed(() => chartConfig.value?.valueField ? labelField.value : 'label')
|
|
184
|
-
const pieValueField = computed(() => chartConfig.value?.valueField ? valueField.value : 'value')
|
|
185
173
|
const barRows = computed(() => {
|
|
186
|
-
const bucketField = chartConfig.value?.
|
|
174
|
+
const bucketField = chartConfig.value?.type === 'histogram'
|
|
175
|
+
? chartConfig.value.x?.field
|
|
176
|
+
: undefined
|
|
187
177
|
|
|
188
178
|
if (!bucketField) {
|
|
189
179
|
return rows.value
|
|
@@ -200,19 +190,14 @@ const barRows = computed(() => {
|
|
|
200
190
|
}).length,
|
|
201
191
|
}))
|
|
202
192
|
})
|
|
203
|
-
const barLabelField = computed(() => chartConfig.value?.
|
|
204
|
-
const barValueField = computed(() => chartConfig.value?.
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
211
|
-
.filter((column) => column !== xField.value)
|
|
212
|
-
.map((column) => ({
|
|
213
|
-
name: column,
|
|
214
|
-
field: column,
|
|
215
|
-
}))
|
|
193
|
+
const barLabelField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'label' : xField.value)
|
|
194
|
+
const barValueField = computed(() => chartConfig.value?.type === 'histogram' && chartConfig.value.buckets ? 'count' : yField.value)
|
|
195
|
+
const seriesField = computed(() => chartConfig.value?.series?.field || columns.value[2] || '')
|
|
196
|
+
const stackedBarYField = computed(() => stackedBarYItems.value.length ? '__value' : yField.value)
|
|
197
|
+
const stackedBarSeriesField = computed(() => stackedBarYItems.value.length ? '__series' : seriesField.value)
|
|
198
|
+
const lineSeriesName = computed(() => {
|
|
199
|
+
const y = chartConfig.value?.y
|
|
200
|
+
return Array.isArray(y) ? y[0]?.label : undefined
|
|
216
201
|
})
|
|
217
202
|
|
|
218
203
|
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,40 +1,31 @@
|
|
|
1
|
+
import type { ValueFormat } from '../../model/dashboard.types.js';
|
|
1
2
|
export type ChartWidgetType = 'line' | 'pie' | 'bar' | 'stacked_bar' | 'funnel' | 'histogram';
|
|
2
3
|
export type ChartWidgetBucketConfig = {
|
|
3
4
|
label: string;
|
|
4
5
|
min?: number;
|
|
5
6
|
max?: number;
|
|
6
7
|
};
|
|
8
|
+
export type ChartFieldRef = {
|
|
9
|
+
field: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
format?: ValueFormat;
|
|
12
|
+
};
|
|
7
13
|
export type ChartWidgetSeriesConfig = {
|
|
8
|
-
name: string;
|
|
9
14
|
field: string;
|
|
15
|
+
label?: string;
|
|
10
16
|
color?: string;
|
|
11
17
|
};
|
|
12
18
|
export type ChartWidgetConfig = {
|
|
13
19
|
type: ChartWidgetType;
|
|
14
20
|
title?: string;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
bucket_field?: string;
|
|
20
|
-
buckets?: ChartWidgetBucketConfig[];
|
|
21
|
-
series?: ChartWidgetSeriesConfig[];
|
|
22
|
-
series_name?: string;
|
|
23
|
-
color?: string;
|
|
24
|
-
colors?: string[];
|
|
25
|
-
};
|
|
26
|
-
export type NormalizedChartWidgetConfig = {
|
|
27
|
-
type: ChartWidgetType;
|
|
28
|
-
title?: string;
|
|
29
|
-
xField?: string;
|
|
30
|
-
yField?: string;
|
|
31
|
-
labelField?: string;
|
|
32
|
-
valueField?: string;
|
|
33
|
-
bucketField?: string;
|
|
21
|
+
x?: ChartFieldRef;
|
|
22
|
+
y?: ChartFieldRef | ChartFieldRef[];
|
|
23
|
+
label?: ChartFieldRef;
|
|
24
|
+
value?: ChartFieldRef;
|
|
34
25
|
buckets?: ChartWidgetBucketConfig[];
|
|
35
|
-
series?: ChartWidgetSeriesConfig
|
|
36
|
-
seriesName?: string;
|
|
26
|
+
series?: ChartWidgetSeriesConfig;
|
|
37
27
|
color?: string;
|
|
38
28
|
colors?: string[];
|
|
39
29
|
};
|
|
30
|
+
export type NormalizedChartWidgetConfig = ChartWidgetConfig;
|
|
40
31
|
export declare function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined;
|
|
@@ -2,26 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeChartWidgetConfig = normalizeChartWidgetConfig;
|
|
4
4
|
function normalizeChartWidgetConfig(value) {
|
|
5
|
-
|
|
6
|
-
if (!config) {
|
|
5
|
+
if (!isRecord(value) || !normalizeChartWidgetType(value.type)) {
|
|
7
6
|
return undefined;
|
|
8
7
|
}
|
|
9
|
-
|
|
10
|
-
if (!type) {
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
const xField = getStringField(config, 'x_field');
|
|
14
|
-
const yField = getStringField(config, 'y_field');
|
|
15
|
-
const labelField = getStringField(config, 'label_field');
|
|
16
|
-
const valueField = getStringField(config, 'value_field');
|
|
17
|
-
const bucketField = getStringField(config, 'bucket_field');
|
|
18
|
-
const seriesName = getStringField(config, 'series_name');
|
|
19
|
-
const title = getStringField(config, 'title');
|
|
20
|
-
const color = getStringField(config, 'color');
|
|
21
|
-
const colors = Array.isArray(config.colors) ? config.colors : undefined;
|
|
22
|
-
const buckets = Array.isArray(config.buckets) ? config.buckets : undefined;
|
|
23
|
-
const series = Array.isArray(config.series) ? config.series : undefined;
|
|
24
|
-
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ type }, (title !== undefined ? { title } : {})), (xField !== undefined ? { xField } : {})), (yField !== undefined ? { yField } : {})), (labelField !== undefined ? { labelField } : {})), (valueField !== undefined ? { valueField } : {})), (bucketField !== undefined ? { bucketField } : {})), (buckets !== undefined ? { buckets } : {})), (series !== undefined ? { series } : {})), (seriesName !== undefined ? { seriesName } : {})), (color !== undefined ? { color } : {})), (colors !== undefined ? { colors } : {}));
|
|
8
|
+
return value;
|
|
25
9
|
}
|
|
26
10
|
function normalizeChartWidgetType(value) {
|
|
27
11
|
switch (value) {
|
|
@@ -36,13 +20,6 @@ function normalizeChartWidgetType(value) {
|
|
|
36
20
|
return undefined;
|
|
37
21
|
}
|
|
38
22
|
}
|
|
39
|
-
function asChartWidgetConfigRecord(value) {
|
|
40
|
-
return isRecord(value) ? value : undefined;
|
|
41
|
-
}
|
|
42
|
-
function getStringField(record, key) {
|
|
43
|
-
const value = record[key];
|
|
44
|
-
return typeof value === 'string' ? value : undefined;
|
|
45
|
-
}
|
|
46
23
|
function isRecord(value) {
|
|
47
24
|
return typeof value === 'object' && value !== null;
|
|
48
25
|
}
|
|
@@ -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,81 +14,39 @@ 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
|
}
|
|
35
41
|
|
|
36
|
-
export type NormalizedChartWidgetConfig =
|
|
37
|
-
type: ChartWidgetType
|
|
38
|
-
title?: string
|
|
39
|
-
xField?: string
|
|
40
|
-
yField?: string
|
|
41
|
-
labelField?: string
|
|
42
|
-
valueField?: string
|
|
43
|
-
bucketField?: string
|
|
44
|
-
buckets?: ChartWidgetBucketConfig[]
|
|
45
|
-
series?: ChartWidgetSeriesConfig[]
|
|
46
|
-
seriesName?: string
|
|
47
|
-
color?: string
|
|
48
|
-
colors?: string[]
|
|
49
|
-
}
|
|
42
|
+
export type NormalizedChartWidgetConfig = ChartWidgetConfig
|
|
50
43
|
|
|
51
44
|
export function normalizeChartWidgetConfig(value: unknown): NormalizedChartWidgetConfig | undefined {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!config) {
|
|
45
|
+
if (!isRecord(value) || !normalizeChartWidgetType(value.type)) {
|
|
55
46
|
return undefined
|
|
56
47
|
}
|
|
57
48
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!type) {
|
|
61
|
-
return undefined
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const xField = getStringField(config, 'x_field')
|
|
65
|
-
const yField = getStringField(config, 'y_field')
|
|
66
|
-
const labelField = getStringField(config, 'label_field')
|
|
67
|
-
const valueField = getStringField(config, 'value_field')
|
|
68
|
-
const bucketField = getStringField(config, 'bucket_field')
|
|
69
|
-
const seriesName = getStringField(config, 'series_name')
|
|
70
|
-
const title = getStringField(config, 'title')
|
|
71
|
-
const color = getStringField(config, 'color')
|
|
72
|
-
const colors = Array.isArray(config.colors) ? config.colors as string[] : undefined
|
|
73
|
-
const buckets = Array.isArray(config.buckets) ? config.buckets as ChartWidgetBucketConfig[] : undefined
|
|
74
|
-
const series = Array.isArray(config.series) ? config.series as ChartWidgetSeriesConfig[] : undefined
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
type,
|
|
78
|
-
...(title !== undefined ? { title } : {}),
|
|
79
|
-
...(xField !== undefined ? { xField } : {}),
|
|
80
|
-
...(yField !== undefined ? { yField } : {}),
|
|
81
|
-
...(labelField !== undefined ? { labelField } : {}),
|
|
82
|
-
...(valueField !== undefined ? { valueField } : {}),
|
|
83
|
-
...(bucketField !== undefined ? { bucketField } : {}),
|
|
84
|
-
...(buckets !== undefined ? { buckets } : {}),
|
|
85
|
-
...(series !== undefined ? { series } : {}),
|
|
86
|
-
...(seriesName !== undefined ? { seriesName } : {}),
|
|
87
|
-
...(color !== undefined ? { color } : {}),
|
|
88
|
-
...(colors !== undefined ? { colors } : {}),
|
|
89
|
-
}
|
|
49
|
+
return value as ChartWidgetConfig
|
|
90
50
|
}
|
|
91
51
|
|
|
92
52
|
function normalizeChartWidgetType(value: unknown): ChartWidgetType | undefined {
|
|
@@ -103,15 +63,6 @@ function normalizeChartWidgetType(value: unknown): ChartWidgetType | undefined {
|
|
|
103
63
|
}
|
|
104
64
|
}
|
|
105
65
|
|
|
106
|
-
function asChartWidgetConfigRecord(value: unknown): Record<string, unknown> | undefined {
|
|
107
|
-
return isRecord(value) ? value : undefined
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function getStringField(record: Record<string, unknown>, key: string) {
|
|
111
|
-
const value = record[key]
|
|
112
|
-
return typeof value === 'string' ? value : undefined
|
|
113
|
-
}
|
|
114
|
-
|
|
115
66
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
116
67
|
return typeof value === 'object' && value !== null
|
|
117
68
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const CHART_COLORS: string[];
|
|
2
2
|
export declare function toFiniteNumber(value: unknown): number;
|
|
3
3
|
export declare function formatChartValue(value: number, options?: Intl.NumberFormatOptions): string;
|
|
4
|
+
export declare function getChartYAxisWidth(values: number[], chartWidth: number): number;
|
|
4
5
|
export declare function formatChartLabel(value: unknown): string;
|
|
5
6
|
export declare function formatChartAxisLabel(value: unknown, maxLength?: number): string;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CHART_COLORS = void 0;
|
|
4
4
|
exports.toFiniteNumber = toFiniteNumber;
|
|
5
5
|
exports.formatChartValue = formatChartValue;
|
|
6
|
+
exports.getChartYAxisWidth = getChartYAxisWidth;
|
|
6
7
|
exports.formatChartLabel = formatChartLabel;
|
|
7
8
|
exports.formatChartAxisLabel = formatChartAxisLabel;
|
|
8
9
|
exports.CHART_COLORS = [
|
|
@@ -22,6 +23,12 @@ function toFiniteNumber(value) {
|
|
|
22
23
|
function formatChartValue(value, options = {}) {
|
|
23
24
|
return new Intl.NumberFormat(undefined, options).format(value);
|
|
24
25
|
}
|
|
26
|
+
function getChartYAxisWidth(values, chartWidth) {
|
|
27
|
+
const maxLabelLength = Math.max(...values.map((value) => formatChartValue(value).length), 1);
|
|
28
|
+
const estimatedWidth = Math.ceil(maxLabelLength * 6.5) + 18;
|
|
29
|
+
const responsiveMaxWidth = Math.max(Math.floor(chartWidth * 0.36), 38);
|
|
30
|
+
return Math.min(Math.max(estimatedWidth, 38), responsiveMaxWidth, 120);
|
|
31
|
+
}
|
|
25
32
|
function formatChartLabel(value) {
|
|
26
33
|
if (typeof value !== 'string') {
|
|
27
34
|
return String(value);
|
|
@@ -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>
|