@adminforth/dashboard 1.0.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/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +46 -0
- package/.woodpecker/release.yml +57 -0
- package/README.md +59 -0
- package/custom/api/dashboardApi.ts +213 -0
- package/custom/composables/useElementSize.ts +41 -0
- package/custom/model/dashboard.types.ts +73 -0
- package/custom/package.json +9 -0
- package/custom/pnpm-lock.yaml +24 -0
- package/custom/queries/useDashboardConfig.ts +51 -0
- package/custom/queries/useWidgetData.ts +51 -0
- package/custom/runtime/DashboardGroup.vue +185 -0
- package/custom/runtime/DashboardPage.vue +122 -0
- package/custom/runtime/DashboardRuntime.vue +435 -0
- package/custom/runtime/WidgetRenderer.vue +60 -0
- package/custom/runtime/WidgetShell.vue +152 -0
- package/custom/skills/adminforth-dashboard/SKILL.md +125 -0
- package/custom/widgets/chart/ChartWidget.vue +188 -0
- package/custom/widgets/chart/bar/BarChart.vue +167 -0
- package/custom/widgets/chart/chart.types.ts +34 -0
- package/custom/widgets/chart/chart.utils.ts +54 -0
- package/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
- package/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
- package/custom/widgets/chart/line/LineChart.vue +175 -0
- package/custom/widgets/chart/pie/PieChart.vue +161 -0
- package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
- package/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
- package/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
- package/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
- package/custom/widgets/registry.ts +51 -0
- package/custom/widgets/table/TableWidget.vue +110 -0
- package/dist/custom/api/dashboardApi.d.ts +32 -0
- package/dist/custom/api/dashboardApi.js +179 -0
- package/dist/custom/api/dashboardApi.ts +213 -0
- package/dist/custom/composables/useElementSize.d.ts +8 -0
- package/dist/custom/composables/useElementSize.js +30 -0
- package/dist/custom/composables/useElementSize.ts +41 -0
- package/dist/custom/model/dashboard.types.d.ts +45 -0
- package/dist/custom/model/dashboard.types.js +14 -0
- package/dist/custom/model/dashboard.types.ts +73 -0
- package/dist/custom/package.json +9 -0
- package/dist/custom/pnpm-lock.yaml +24 -0
- package/dist/custom/queries/useDashboardConfig.d.ts +112 -0
- package/dist/custom/queries/useDashboardConfig.js +57 -0
- package/dist/custom/queries/useDashboardConfig.ts +51 -0
- package/dist/custom/queries/useWidgetData.d.ts +90 -0
- package/dist/custom/queries/useWidgetData.js +57 -0
- package/dist/custom/queries/useWidgetData.ts +51 -0
- package/dist/custom/runtime/DashboardGroup.vue +185 -0
- package/dist/custom/runtime/DashboardPage.vue +122 -0
- package/dist/custom/runtime/DashboardRuntime.vue +435 -0
- package/dist/custom/runtime/WidgetRenderer.vue +60 -0
- package/dist/custom/runtime/WidgetShell.vue +152 -0
- package/dist/custom/skills/adminforth-dashboard/SKILL.md +125 -0
- package/dist/custom/widgets/chart/ChartWidget.vue +188 -0
- package/dist/custom/widgets/chart/bar/BarChart.vue +167 -0
- package/dist/custom/widgets/chart/chart.types.d.ts +25 -0
- package/dist/custom/widgets/chart/chart.types.js +2 -0
- package/dist/custom/widgets/chart/chart.types.ts +34 -0
- package/dist/custom/widgets/chart/chart.utils.d.ts +5 -0
- package/dist/custom/widgets/chart/chart.utils.js +52 -0
- package/dist/custom/widgets/chart/chart.utils.ts +54 -0
- package/dist/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
- package/dist/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
- package/dist/custom/widgets/chart/line/LineChart.vue +175 -0
- package/dist/custom/widgets/chart/pie/PieChart.vue +161 -0
- package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
- package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
- package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
- package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
- package/dist/custom/widgets/registry.d.ts +11 -0
- package/dist/custom/widgets/registry.js +47 -0
- package/dist/custom/widgets/registry.ts +51 -0
- package/dist/custom/widgets/table/TableWidget.vue +110 -0
- package/dist/endpoint/dashboard.d.ts +7 -0
- package/dist/endpoint/dashboard.js +29 -0
- package/dist/endpoint/groups.d.ts +30 -0
- package/dist/endpoint/groups.js +131 -0
- package/dist/endpoint/widgets.d.ts +15 -0
- package/dist/endpoint/widgets.js +182 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +124 -0
- package/dist/schema/api.d.ts +1205 -0
- package/dist/schema/api.js +84 -0
- package/dist/schema/widget.d.ts +514 -0
- package/dist/schema/widget.js +133 -0
- package/dist/services/dashboardConfigService.d.ts +35 -0
- package/dist/services/dashboardConfigService.js +79 -0
- package/dist/services/widgetConfigValidator.d.ts +8 -0
- package/dist/services/widgetConfigValidator.js +65 -0
- package/dist/services/widgetDataService.d.ts +20 -0
- package/dist/services/widgetDataService.js +32 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -0
- package/endpoint/dashboard.ts +32 -0
- package/endpoint/groups.ts +213 -0
- package/endpoint/widgets.ts +255 -0
- package/index.ts +141 -0
- package/package.json +64 -0
- package/schema/api.ts +99 -0
- package/schema/widget.ts +159 -0
- package/services/dashboardConfigService.ts +136 -0
- package/services/widgetConfigValidator.ts +93 -0
- package/services/widgetDataService.ts +57 -0
- package/shims-vue.d.ts +5 -0
- package/tsconfig.json +18 -0
- package/types.ts +8 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: adminforth-dashboard
|
|
3
|
+
description: Use when the user wants to view, create, update, move, remove, validate, or load data for AdminForth dashboard groups and widgets.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AdminForth Dashboard Plugin
|
|
7
|
+
|
|
8
|
+
This skill is action-oriented. When the user asks to create, add, edit, update, move, remove, or configure dashboard entities, complete the request by calling the dashboard tools. Do not satisfy dashboard mutation requests by only showing JSON, YAML, JavaScript, TypeScript, Zod schemas, or example config snippets.
|
|
9
|
+
|
|
10
|
+
## Primary Rule
|
|
11
|
+
|
|
12
|
+
If callable dashboard tools are available, use them.
|
|
13
|
+
|
|
14
|
+
A response that only shows a config object, schema, or code snippet is incomplete unless the user explicitly asked for a schema, code example, or explanation.
|
|
15
|
+
|
|
16
|
+
For dashboard mutation requests, the expected flow is:
|
|
17
|
+
|
|
18
|
+
1. Load dashboard state when needed.
|
|
19
|
+
2. Choose or create the target group/widget.
|
|
20
|
+
3. Call the appropriate dashboard mutation tool. (WITHOUT USER CONFIRMATION)
|
|
21
|
+
4. Return a short result summary.
|
|
22
|
+
|
|
23
|
+
Do not print the widget config as the main answer instead of calling tools.
|
|
24
|
+
|
|
25
|
+
## User Intent Mapping
|
|
26
|
+
|
|
27
|
+
Use dashboard tools for these intents:
|
|
28
|
+
|
|
29
|
+
- "add/create/write/make a widget" → create and configure a widget.
|
|
30
|
+
- "change/update/edit this widget" → update widget config.
|
|
31
|
+
- "move this widget/group" → call the move tool.
|
|
32
|
+
- "remove/delete this widget/group" → call the remove tool.
|
|
33
|
+
- "show/load dashboard" → call the dashboard config tool.
|
|
34
|
+
- "load/check widget data" → call the widget data tool.
|
|
35
|
+
- "validate this widget config" → call validation logic/tool if available.
|
|
36
|
+
|
|
37
|
+
If the user asks how the schema works, how to implement the API, or how to change backend code, then answer as a developer/code task instead of mutating the dashboard.
|
|
38
|
+
|
|
39
|
+
## Callable Dashboard Tools
|
|
40
|
+
|
|
41
|
+
Use these tools whenever available:
|
|
42
|
+
|
|
43
|
+
- `dashboard_get_config`
|
|
44
|
+
- `dashboard_add_dashboard_group`
|
|
45
|
+
- `dashboard_set_dashboard_group_config`
|
|
46
|
+
- `dashboard_move_dashboard_group`
|
|
47
|
+
- `dashboard_remove_dashboard_group`
|
|
48
|
+
- `dashboard_add_dashboard_widget`
|
|
49
|
+
- `dashboard_move_dashboard_widget`
|
|
50
|
+
- `dashboard_remove_dashboard_widget`
|
|
51
|
+
- `dashboard_set_widget_config`
|
|
52
|
+
- `dashboard_get_dashboard_widget_data`
|
|
53
|
+
|
|
54
|
+
If a dashboard tool is known by name but its argument schema is not loaded, call `fetch_tool_schema` for that tool first. After the schema is loaded, call the dashboard tool. Do not guess arguments if `fetch_tool_schema` is available.
|
|
55
|
+
|
|
56
|
+
## Tool Argument Rules
|
|
57
|
+
|
|
58
|
+
Do not pass fields between dashboard tools by analogy. Use each tool's schema.
|
|
59
|
+
|
|
60
|
+
- `dashboard_add_dashboard_group` creates a new group. It accepts the dashboard slug only. Never pass `groupId` to this tool.
|
|
61
|
+
- `dashboard_add_dashboard_widget` creates a widget inside an existing group. Use it when you already have a `groupId`.
|
|
62
|
+
- `dashboard_set_dashboard_group_config`, `dashboard_move_dashboard_group`, and `dashboard_remove_dashboard_group` operate on an existing group and need `groupId`.
|
|
63
|
+
- `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`.
|
|
64
|
+
- If a tool call fails with "input did not match expected schema", call `fetch_tool_schema` for that exact tool, remove unsupported arguments, and retry the correct tool.
|
|
65
|
+
|
|
66
|
+
## Widget Creation Workflow
|
|
67
|
+
|
|
68
|
+
For requests like:
|
|
69
|
+
|
|
70
|
+
- "add a table widget"
|
|
71
|
+
- "create a chart"
|
|
72
|
+
- "write a widget showing top 5 orders"
|
|
73
|
+
- "make a widget for revenue by product"
|
|
74
|
+
|
|
75
|
+
do this:
|
|
76
|
+
|
|
77
|
+
1. Call `dashboard_get_config` with the requested slug, or `default` if slug is not specified.
|
|
78
|
+
2. Select the requested group. If the group is not specified, use the first existing group.
|
|
79
|
+
3. If there are no groups, call `dashboard_add_dashboard_group`.
|
|
80
|
+
4. Call `dashboard_add_dashboard_widget` with the selected group id.
|
|
81
|
+
5. Call `dashboard_set_widget_config` with the returned widget id and schema-valid config.
|
|
82
|
+
6. Return a short summary with dashboard slug, group id, widget id, target, label, resource, selected fields, order, and limit.
|
|
83
|
+
|
|
84
|
+
Do not stop after generating config text.
|
|
85
|
+
|
|
86
|
+
## Widget Update Workflow
|
|
87
|
+
|
|
88
|
+
For requests like:
|
|
89
|
+
|
|
90
|
+
- "change this widget"
|
|
91
|
+
- "make the chart use another field"
|
|
92
|
+
- "update the widget config"
|
|
93
|
+
- "turn this widget into a table"
|
|
94
|
+
|
|
95
|
+
do this:
|
|
96
|
+
|
|
97
|
+
1. Call `dashboard_get_config`.
|
|
98
|
+
2. Find the widget by id, label, or clear context.
|
|
99
|
+
3. Build the new config while preserving server-owned fields handled by the API.
|
|
100
|
+
4. Call `dashboard_set_widget_config`.
|
|
101
|
+
5. Return a short summary of what changed.
|
|
102
|
+
|
|
103
|
+
If the widget cannot be identified, ask only for the missing widget id or label.
|
|
104
|
+
|
|
105
|
+
## Group Workflow
|
|
106
|
+
|
|
107
|
+
For group requests:
|
|
108
|
+
|
|
109
|
+
- Add group → `dashboard_add_dashboard_group`
|
|
110
|
+
- Rename/change group config → `dashboard_set_dashboard_group_config`
|
|
111
|
+
- Move group → `dashboard_move_dashboard_group`
|
|
112
|
+
- Remove group → `dashboard_remove_dashboard_group`
|
|
113
|
+
|
|
114
|
+
If slug is missing, use `default`.
|
|
115
|
+
|
|
116
|
+
## Widget Config Rules
|
|
117
|
+
|
|
118
|
+
Use the current schema keys exactly:
|
|
119
|
+
|
|
120
|
+
- Use `target`, not `type`.
|
|
121
|
+
- Use `label`, not `title`.
|
|
122
|
+
- Use `query.resource`, not `resourceId`.
|
|
123
|
+
- Use `query.select`, not `columns`.
|
|
124
|
+
- Use `query.order`, not `sort`.
|
|
125
|
+
- Use `query.limit` for row count.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full min-h-0 overflow-hidden rounded-lg bg-lightTableBackground p-3 dark:bg-darkTableBackground">
|
|
3
|
+
<div
|
|
4
|
+
v-if="isLoading"
|
|
5
|
+
class="text-sm text-lightListTableText dark:text-darkListTableText"
|
|
6
|
+
>
|
|
7
|
+
Loading...
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
v-else-if="error"
|
|
12
|
+
class="text-sm text-lightInputErrorColor"
|
|
13
|
+
>
|
|
14
|
+
Failed to load chart data
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
v-else-if="!rows.length"
|
|
19
|
+
class="text-sm text-lightListTableText dark:text-darkListTableText"
|
|
20
|
+
>
|
|
21
|
+
No data available
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<LineChart
|
|
25
|
+
v-else-if="chartConfig?.type === 'line'"
|
|
26
|
+
:rows="rows"
|
|
27
|
+
:x-field="xField"
|
|
28
|
+
:y-field="yField"
|
|
29
|
+
:series-name="chartConfig.series_name"
|
|
30
|
+
:color="chartConfig.color"
|
|
31
|
+
:height="chartHeight"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<PieChart
|
|
35
|
+
v-else-if="chartConfig?.type === 'pie'"
|
|
36
|
+
:rows="pieRows"
|
|
37
|
+
:label-field="pieLabelField"
|
|
38
|
+
:value-field="pieValueField"
|
|
39
|
+
:colors="chartConfig.colors"
|
|
40
|
+
:height="chartHeight"
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
<BarChart
|
|
44
|
+
v-else-if="chartConfig?.type === 'bar'"
|
|
45
|
+
:rows="barRows"
|
|
46
|
+
:label-field="barLabelField"
|
|
47
|
+
:value-field="barValueField"
|
|
48
|
+
:color="chartConfig.color"
|
|
49
|
+
:height="chartHeight"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<HistogramChart
|
|
53
|
+
v-else-if="chartConfig?.type === 'histogram'"
|
|
54
|
+
:rows="barRows"
|
|
55
|
+
:label-field="barLabelField"
|
|
56
|
+
:value-field="barValueField"
|
|
57
|
+
:color="chartConfig.color"
|
|
58
|
+
:height="chartHeight"
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
<FunnelChart
|
|
62
|
+
v-else-if="chartConfig?.type === 'funnel'"
|
|
63
|
+
:rows="rows"
|
|
64
|
+
:label-field="labelField"
|
|
65
|
+
:value-field="valueField"
|
|
66
|
+
:colors="chartConfig.colors"
|
|
67
|
+
:height="chartHeight"
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<StackedBarChart
|
|
71
|
+
v-else-if="chartConfig?.type === 'stacked_bar'"
|
|
72
|
+
:rows="rows"
|
|
73
|
+
:x-field="xField"
|
|
74
|
+
:series="stackedBarSeries"
|
|
75
|
+
:colors="chartConfig.colors"
|
|
76
|
+
:height="chartHeight"
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
<div
|
|
80
|
+
v-else
|
|
81
|
+
class="text-sm text-lightListTableText dark:text-darkListTableText"
|
|
82
|
+
>
|
|
83
|
+
Unsupported chart type
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
<script setup lang="ts">
|
|
91
|
+
import { computed, watch } from 'vue'
|
|
92
|
+
import { useWidgetData } from '../../queries/useWidgetData.js'
|
|
93
|
+
import type { DashboardWidgetConfig, DashboardWidgetTableData } from '../../model/dashboard.types.js'
|
|
94
|
+
import BarChart from './bar/BarChart.vue'
|
|
95
|
+
import FunnelChart from './funnel/FunnelChart.vue'
|
|
96
|
+
import HistogramChart from './histogram/HistogramChart.vue'
|
|
97
|
+
import LineChart from './line/LineChart.vue'
|
|
98
|
+
import PieChart from './pie/PieChart.vue'
|
|
99
|
+
import StackedBarChart from './stacked-bar/StackedBarChart.vue'
|
|
100
|
+
import { formatChartLabel, toFiniteNumber } from './chart.utils.js'
|
|
101
|
+
|
|
102
|
+
const DEFAULT_WIDGET_HEIGHT = 500
|
|
103
|
+
|
|
104
|
+
const props = defineProps<{
|
|
105
|
+
dashboardSlug: string
|
|
106
|
+
widget: DashboardWidgetConfig
|
|
107
|
+
}>()
|
|
108
|
+
|
|
109
|
+
const dashboardSlugRef = computed(() => props.dashboardSlug)
|
|
110
|
+
const widgetIdRef = computed(() => props.widget.id)
|
|
111
|
+
const {
|
|
112
|
+
data,
|
|
113
|
+
isLoading,
|
|
114
|
+
error,
|
|
115
|
+
refetch,
|
|
116
|
+
} = useWidgetData(dashboardSlugRef, widgetIdRef)
|
|
117
|
+
|
|
118
|
+
watch(
|
|
119
|
+
() => props.widget,
|
|
120
|
+
() => {
|
|
121
|
+
void refetch()
|
|
122
|
+
},
|
|
123
|
+
{ deep: true },
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const chartData = computed(() => data.value?.data as DashboardWidgetTableData | null)
|
|
127
|
+
const rows = computed(() => chartData.value?.rows ?? [])
|
|
128
|
+
const columns = computed(() => chartData.value?.columns ?? [])
|
|
129
|
+
const chartConfig = computed(() => props.widget.chart)
|
|
130
|
+
const xField = computed(() => chartConfig.value?.x_field || columns.value[0])
|
|
131
|
+
const yField = computed(() => chartConfig.value?.y_field || columns.value[1])
|
|
132
|
+
const labelField = computed(() => chartConfig.value?.label_field || columns.value[0])
|
|
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
|
+
}
|
|
138
|
+
|
|
139
|
+
const groupedRows = new Map<string, { label: string, value: number }>()
|
|
140
|
+
|
|
141
|
+
for (const row of rows.value) {
|
|
142
|
+
const label = formatChartLabel(row[labelField.value])
|
|
143
|
+
const item = groupedRows.get(label) ?? { label, value: 0 }
|
|
144
|
+
item.value += 1
|
|
145
|
+
groupedRows.set(label, item)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return Array.from(groupedRows.values())
|
|
149
|
+
})
|
|
150
|
+
const pieLabelField = computed(() => chartConfig.value?.value_field ? labelField.value : 'label')
|
|
151
|
+
const pieValueField = computed(() => chartConfig.value?.value_field ? valueField.value : 'value')
|
|
152
|
+
const barRows = computed(() => {
|
|
153
|
+
const bucketField = chartConfig.value?.bucket_field
|
|
154
|
+
|
|
155
|
+
if (!bucketField) {
|
|
156
|
+
return rows.value
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const buckets = chartConfig.value?.buckets ?? []
|
|
160
|
+
|
|
161
|
+
return buckets.map((bucket) => ({
|
|
162
|
+
label: bucket.label,
|
|
163
|
+
count: rows.value.filter((row) => {
|
|
164
|
+
const value = toFiniteNumber(row[bucketField])
|
|
165
|
+
return (bucket.min === undefined || value >= bucket.min)
|
|
166
|
+
&& (bucket.max === undefined || value < bucket.max)
|
|
167
|
+
}).length,
|
|
168
|
+
}))
|
|
169
|
+
})
|
|
170
|
+
const barLabelField = computed(() => chartConfig.value?.bucket_field ? 'label' : labelField.value)
|
|
171
|
+
const barValueField = computed(() => chartConfig.value?.bucket_field ? 'count' : valueField.value)
|
|
172
|
+
const stackedBarSeries = computed(() => {
|
|
173
|
+
if (chartConfig.value?.series?.length) {
|
|
174
|
+
return chartConfig.value.series
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return columns.value
|
|
178
|
+
.filter((column) => column !== xField.value)
|
|
179
|
+
.map((column) => ({
|
|
180
|
+
name: column,
|
|
181
|
+
field: column,
|
|
182
|
+
}))
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const chartHeight = computed(() => {
|
|
186
|
+
return Math.max((props.widget.height ?? DEFAULT_WIDGET_HEIGHT) - 24, 96)
|
|
187
|
+
})
|
|
188
|
+
</script>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="rootEl"
|
|
4
|
+
class="h-full min-h-0 w-full overflow-hidden"
|
|
5
|
+
>
|
|
6
|
+
<svg
|
|
7
|
+
v-if="chartWidth > 0 && chartHeight > 0"
|
|
8
|
+
class="block h-full w-full"
|
|
9
|
+
:viewBox="`0 0 ${chartWidth} ${chartHeight}`"
|
|
10
|
+
role="img"
|
|
11
|
+
:aria-label="valueField"
|
|
12
|
+
>
|
|
13
|
+
<g class="text-lightListTableText dark:text-darkListTableText">
|
|
14
|
+
<line
|
|
15
|
+
v-for="tick in yTicks"
|
|
16
|
+
:key="tick.y"
|
|
17
|
+
:x1="padding.left"
|
|
18
|
+
:x2="chartWidth - padding.right"
|
|
19
|
+
:y1="tick.y"
|
|
20
|
+
:y2="tick.y"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
stroke-opacity="0.14"
|
|
23
|
+
/>
|
|
24
|
+
<text
|
|
25
|
+
v-for="tick in yTicks"
|
|
26
|
+
:key="`label-${tick.y}`"
|
|
27
|
+
:x="padding.left - 8"
|
|
28
|
+
:y="tick.y + 4"
|
|
29
|
+
fill="currentColor"
|
|
30
|
+
font-size="11"
|
|
31
|
+
text-anchor="end"
|
|
32
|
+
>
|
|
33
|
+
{{ formatChartValue(tick.value) }}
|
|
34
|
+
</text>
|
|
35
|
+
</g>
|
|
36
|
+
|
|
37
|
+
<rect
|
|
38
|
+
v-for="bar in bars"
|
|
39
|
+
:key="bar.label"
|
|
40
|
+
:x="bar.x"
|
|
41
|
+
:y="bar.y"
|
|
42
|
+
:width="barWidth"
|
|
43
|
+
:height="bar.height"
|
|
44
|
+
:fill="chartColor"
|
|
45
|
+
rx="5"
|
|
46
|
+
>
|
|
47
|
+
<title>{{ bar.label }}: {{ formatChartValue(bar.value) }}</title>
|
|
48
|
+
</rect>
|
|
49
|
+
|
|
50
|
+
<g class="text-lightListTableText dark:text-darkListTableText">
|
|
51
|
+
<text
|
|
52
|
+
v-for="(bar, barIndex) in bars"
|
|
53
|
+
v-show="visibleLabelIndexes.has(barIndex)"
|
|
54
|
+
:key="`x-${bar.label}`"
|
|
55
|
+
:x="bar.x + barWidth / 2"
|
|
56
|
+
:y="padding.top + innerHeight + 24"
|
|
57
|
+
fill="currentColor"
|
|
58
|
+
font-size="11"
|
|
59
|
+
text-anchor="middle"
|
|
60
|
+
>
|
|
61
|
+
{{ bar.axisLabel }}
|
|
62
|
+
</text>
|
|
63
|
+
<text
|
|
64
|
+
v-for="bar in bars"
|
|
65
|
+
v-show="barWidth >= 18"
|
|
66
|
+
:key="`value-${bar.label}`"
|
|
67
|
+
:x="bar.x + barWidth / 2"
|
|
68
|
+
:y="Math.max(bar.y - 8, 12)"
|
|
69
|
+
fill="currentColor"
|
|
70
|
+
font-size="11"
|
|
71
|
+
font-weight="600"
|
|
72
|
+
text-anchor="middle"
|
|
73
|
+
>
|
|
74
|
+
{{ formatChartValue(bar.value) }}
|
|
75
|
+
</text>
|
|
76
|
+
</g>
|
|
77
|
+
</svg>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
import { computed } from 'vue'
|
|
85
|
+
import { useElementSize } from '../../../composables/useElementSize.js'
|
|
86
|
+
import { CHART_COLORS, formatChartAxisLabel, formatChartLabel, formatChartValue, toFiniteNumber } from '../chart.utils.js'
|
|
87
|
+
|
|
88
|
+
const props = withDefaults(defineProps<{
|
|
89
|
+
rows: Record<string, unknown>[]
|
|
90
|
+
labelField: string
|
|
91
|
+
valueField: string
|
|
92
|
+
color?: string
|
|
93
|
+
height?: number
|
|
94
|
+
}>(), {
|
|
95
|
+
height: 260,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const { el: rootEl, width: rootWidth, height: rootHeight } = useElementSize<HTMLDivElement>()
|
|
99
|
+
|
|
100
|
+
const padding = {
|
|
101
|
+
top: 20,
|
|
102
|
+
right: 6,
|
|
103
|
+
bottom: 34,
|
|
104
|
+
left: 38,
|
|
105
|
+
}
|
|
106
|
+
const chartWidth = computed(() => Math.max(rootWidth.value, 1))
|
|
107
|
+
const chartHeight = computed(() => {
|
|
108
|
+
if (rootHeight.value > 0) {
|
|
109
|
+
return Math.max(rootHeight.value, 1)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return Math.max(props.height, 1)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const chartColor = computed(() => props.color || CHART_COLORS[0])
|
|
116
|
+
const values = computed(() => props.rows.map((row) => toFiniteNumber(row[props.valueField])))
|
|
117
|
+
const maxValue = computed(() => Math.max(...values.value, 1))
|
|
118
|
+
const innerWidth = computed(() => Math.max(chartWidth.value - padding.left - padding.right, 1))
|
|
119
|
+
const innerHeight = computed(() => Math.max(chartHeight.value - padding.top - padding.bottom, 1))
|
|
120
|
+
const barGap = 12
|
|
121
|
+
const barWidth = computed(() => {
|
|
122
|
+
const count = Math.max(props.rows.length, 1)
|
|
123
|
+
return Math.max(Math.min((innerWidth.value - barGap * (count - 1)) / count, 80), 4)
|
|
124
|
+
})
|
|
125
|
+
const totalChartWidth = computed(() => {
|
|
126
|
+
const count = Math.max(props.rows.length, 1)
|
|
127
|
+
return count * barWidth.value + (count - 1) * barGap
|
|
128
|
+
})
|
|
129
|
+
const chartStartX = computed(() => padding.left + Math.max((innerWidth.value - totalChartWidth.value) / 2, 0))
|
|
130
|
+
const visibleLabelIndexes = computed(() => {
|
|
131
|
+
const count = props.rows.length
|
|
132
|
+
const approxLabelWidth = 52
|
|
133
|
+
const maxLabels = Math.max(2, Math.floor(innerWidth.value / approxLabelWidth))
|
|
134
|
+
|
|
135
|
+
if (count <= maxLabels || barWidth.value >= 44) {
|
|
136
|
+
return new Set(props.rows.map((_, index) => index))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const indexes = new Set<number>()
|
|
140
|
+
const step = (count - 1) / (maxLabels - 1)
|
|
141
|
+
|
|
142
|
+
for (let index = 0; index < maxLabels; index += 1) {
|
|
143
|
+
indexes.add(Math.round(index * step))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return indexes
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const bars = computed(() => props.rows.map((row, index) => {
|
|
150
|
+
const value = values.value[index]
|
|
151
|
+
const height = (value / maxValue.value) * innerHeight.value
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
label: formatChartLabel(row[props.labelField]),
|
|
155
|
+
axisLabel: formatChartAxisLabel(row[props.labelField]),
|
|
156
|
+
value,
|
|
157
|
+
x: chartStartX.value + index * (barWidth.value + barGap),
|
|
158
|
+
y: padding.top + innerHeight.value - height,
|
|
159
|
+
height,
|
|
160
|
+
}
|
|
161
|
+
}))
|
|
162
|
+
|
|
163
|
+
const yTicks = computed(() => [0, 0.5, 1].map((ratio) => ({
|
|
164
|
+
value: maxValue.value * (1 - ratio),
|
|
165
|
+
y: padding.top + innerHeight.value * ratio,
|
|
166
|
+
})))
|
|
167
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ChartWidgetType =
|
|
2
|
+
| 'line'
|
|
3
|
+
| 'pie'
|
|
4
|
+
| 'bar'
|
|
5
|
+
| 'stacked_bar'
|
|
6
|
+
| 'funnel'
|
|
7
|
+
| 'histogram'
|
|
8
|
+
|
|
9
|
+
export type ChartWidgetBucketConfig = {
|
|
10
|
+
label: string
|
|
11
|
+
min?: number
|
|
12
|
+
max?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ChartWidgetSeriesConfig = {
|
|
16
|
+
name: string
|
|
17
|
+
field: string
|
|
18
|
+
color?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ChartWidgetConfig = {
|
|
22
|
+
type: ChartWidgetType
|
|
23
|
+
title?: string
|
|
24
|
+
x_field?: string
|
|
25
|
+
y_field?: string
|
|
26
|
+
label_field?: string
|
|
27
|
+
value_field?: string
|
|
28
|
+
bucket_field?: string
|
|
29
|
+
buckets?: ChartWidgetBucketConfig[]
|
|
30
|
+
series?: ChartWidgetSeriesConfig[]
|
|
31
|
+
series_name?: string
|
|
32
|
+
color?: string
|
|
33
|
+
colors?: string[]
|
|
34
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const CHART_COLORS = [
|
|
2
|
+
'#2563eb',
|
|
3
|
+
'#16a34a',
|
|
4
|
+
'#f97316',
|
|
5
|
+
'#dc2626',
|
|
6
|
+
'#7c3aed',
|
|
7
|
+
'#0891b2',
|
|
8
|
+
'#ca8a04',
|
|
9
|
+
'#db2777',
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export function toFiniteNumber(value: unknown) {
|
|
13
|
+
const numberValue = typeof value === 'number' ? value : Number(value)
|
|
14
|
+
return Number.isFinite(numberValue) ? numberValue : 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function formatChartValue(value: number) {
|
|
18
|
+
return new Intl.NumberFormat().format(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatChartLabel(value: unknown) {
|
|
22
|
+
if (typeof value !== 'string') {
|
|
23
|
+
return String(value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const timestamp = Date.parse(value)
|
|
27
|
+
|
|
28
|
+
if (!Number.isFinite(timestamp)) {
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
33
|
+
year: 'numeric',
|
|
34
|
+
month: 'short',
|
|
35
|
+
day: '2-digit',
|
|
36
|
+
}).format(new Date(timestamp))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatChartAxisLabel(value: unknown, maxLength = 12) {
|
|
40
|
+
const rawLabel = typeof value === 'string' ? value : String(value)
|
|
41
|
+
const timestamp = Date.parse(rawLabel)
|
|
42
|
+
const label = Number.isFinite(timestamp)
|
|
43
|
+
? new Intl.DateTimeFormat(undefined, {
|
|
44
|
+
month: 'short',
|
|
45
|
+
day: 'numeric',
|
|
46
|
+
}).format(new Date(timestamp))
|
|
47
|
+
: rawLabel
|
|
48
|
+
|
|
49
|
+
if (label.length <= maxLength) {
|
|
50
|
+
return label
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `${label.slice(0, Math.max(maxLength - 1, 1))}…`
|
|
54
|
+
}
|