@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.
Files changed (107) hide show
  1. package/.woodpecker/buildRelease.sh +13 -0
  2. package/.woodpecker/buildSlackNotify.sh +46 -0
  3. package/.woodpecker/release.yml +57 -0
  4. package/README.md +59 -0
  5. package/custom/api/dashboardApi.ts +213 -0
  6. package/custom/composables/useElementSize.ts +41 -0
  7. package/custom/model/dashboard.types.ts +73 -0
  8. package/custom/package.json +9 -0
  9. package/custom/pnpm-lock.yaml +24 -0
  10. package/custom/queries/useDashboardConfig.ts +51 -0
  11. package/custom/queries/useWidgetData.ts +51 -0
  12. package/custom/runtime/DashboardGroup.vue +185 -0
  13. package/custom/runtime/DashboardPage.vue +122 -0
  14. package/custom/runtime/DashboardRuntime.vue +435 -0
  15. package/custom/runtime/WidgetRenderer.vue +60 -0
  16. package/custom/runtime/WidgetShell.vue +152 -0
  17. package/custom/skills/adminforth-dashboard/SKILL.md +125 -0
  18. package/custom/widgets/chart/ChartWidget.vue +188 -0
  19. package/custom/widgets/chart/bar/BarChart.vue +167 -0
  20. package/custom/widgets/chart/chart.types.ts +34 -0
  21. package/custom/widgets/chart/chart.utils.ts +54 -0
  22. package/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
  23. package/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
  24. package/custom/widgets/chart/line/LineChart.vue +175 -0
  25. package/custom/widgets/chart/pie/PieChart.vue +161 -0
  26. package/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
  27. package/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
  28. package/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
  29. package/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
  30. package/custom/widgets/registry.ts +51 -0
  31. package/custom/widgets/table/TableWidget.vue +110 -0
  32. package/dist/custom/api/dashboardApi.d.ts +32 -0
  33. package/dist/custom/api/dashboardApi.js +179 -0
  34. package/dist/custom/api/dashboardApi.ts +213 -0
  35. package/dist/custom/composables/useElementSize.d.ts +8 -0
  36. package/dist/custom/composables/useElementSize.js +30 -0
  37. package/dist/custom/composables/useElementSize.ts +41 -0
  38. package/dist/custom/model/dashboard.types.d.ts +45 -0
  39. package/dist/custom/model/dashboard.types.js +14 -0
  40. package/dist/custom/model/dashboard.types.ts +73 -0
  41. package/dist/custom/package.json +9 -0
  42. package/dist/custom/pnpm-lock.yaml +24 -0
  43. package/dist/custom/queries/useDashboardConfig.d.ts +112 -0
  44. package/dist/custom/queries/useDashboardConfig.js +57 -0
  45. package/dist/custom/queries/useDashboardConfig.ts +51 -0
  46. package/dist/custom/queries/useWidgetData.d.ts +90 -0
  47. package/dist/custom/queries/useWidgetData.js +57 -0
  48. package/dist/custom/queries/useWidgetData.ts +51 -0
  49. package/dist/custom/runtime/DashboardGroup.vue +185 -0
  50. package/dist/custom/runtime/DashboardPage.vue +122 -0
  51. package/dist/custom/runtime/DashboardRuntime.vue +435 -0
  52. package/dist/custom/runtime/WidgetRenderer.vue +60 -0
  53. package/dist/custom/runtime/WidgetShell.vue +152 -0
  54. package/dist/custom/skills/adminforth-dashboard/SKILL.md +125 -0
  55. package/dist/custom/widgets/chart/ChartWidget.vue +188 -0
  56. package/dist/custom/widgets/chart/bar/BarChart.vue +167 -0
  57. package/dist/custom/widgets/chart/chart.types.d.ts +25 -0
  58. package/dist/custom/widgets/chart/chart.types.js +2 -0
  59. package/dist/custom/widgets/chart/chart.types.ts +34 -0
  60. package/dist/custom/widgets/chart/chart.utils.d.ts +5 -0
  61. package/dist/custom/widgets/chart/chart.utils.js +52 -0
  62. package/dist/custom/widgets/chart/chart.utils.ts +54 -0
  63. package/dist/custom/widgets/chart/funnel/FunnelChart.vue +197 -0
  64. package/dist/custom/widgets/chart/histogram/HistogramChart.vue +21 -0
  65. package/dist/custom/widgets/chart/line/LineChart.vue +175 -0
  66. package/dist/custom/widgets/chart/pie/PieChart.vue +161 -0
  67. package/dist/custom/widgets/chart/stacked-bar/StackedBarChart.vue +256 -0
  68. package/dist/custom/widgets/gauge-card/GaugeCardWidget.vue +107 -0
  69. package/dist/custom/widgets/kpi-card/KpiCardWidget.vue +73 -0
  70. package/dist/custom/widgets/pivot-table/PivotTableWidget.vue +122 -0
  71. package/dist/custom/widgets/registry.d.ts +11 -0
  72. package/dist/custom/widgets/registry.js +47 -0
  73. package/dist/custom/widgets/registry.ts +51 -0
  74. package/dist/custom/widgets/table/TableWidget.vue +110 -0
  75. package/dist/endpoint/dashboard.d.ts +7 -0
  76. package/dist/endpoint/dashboard.js +29 -0
  77. package/dist/endpoint/groups.d.ts +30 -0
  78. package/dist/endpoint/groups.js +131 -0
  79. package/dist/endpoint/widgets.d.ts +15 -0
  80. package/dist/endpoint/widgets.js +182 -0
  81. package/dist/index.d.ts +13 -0
  82. package/dist/index.js +124 -0
  83. package/dist/schema/api.d.ts +1205 -0
  84. package/dist/schema/api.js +84 -0
  85. package/dist/schema/widget.d.ts +514 -0
  86. package/dist/schema/widget.js +133 -0
  87. package/dist/services/dashboardConfigService.d.ts +35 -0
  88. package/dist/services/dashboardConfigService.js +79 -0
  89. package/dist/services/widgetConfigValidator.d.ts +8 -0
  90. package/dist/services/widgetConfigValidator.js +65 -0
  91. package/dist/services/widgetDataService.d.ts +20 -0
  92. package/dist/services/widgetDataService.js +32 -0
  93. package/dist/types.d.ts +8 -0
  94. package/dist/types.js +1 -0
  95. package/endpoint/dashboard.ts +32 -0
  96. package/endpoint/groups.ts +213 -0
  97. package/endpoint/widgets.ts +255 -0
  98. package/index.ts +141 -0
  99. package/package.json +64 -0
  100. package/schema/api.ts +99 -0
  101. package/schema/widget.ts +159 -0
  102. package/services/dashboardConfigService.ts +136 -0
  103. package/services/widgetConfigValidator.ts +93 -0
  104. package/services/widgetDataService.ts +57 -0
  105. package/shims-vue.d.ts +5 -0
  106. package/tsconfig.json +18 -0
  107. 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,25 @@
1
+ export type ChartWidgetType = 'line' | 'pie' | 'bar' | 'stacked_bar' | 'funnel' | 'histogram';
2
+ export type ChartWidgetBucketConfig = {
3
+ label: string;
4
+ min?: number;
5
+ max?: number;
6
+ };
7
+ export type ChartWidgetSeriesConfig = {
8
+ name: string;
9
+ field: string;
10
+ color?: string;
11
+ };
12
+ export type ChartWidgetConfig = {
13
+ type: ChartWidgetType;
14
+ title?: string;
15
+ x_field?: string;
16
+ y_field?: string;
17
+ label_field?: string;
18
+ value_field?: string;
19
+ bucket_field?: string;
20
+ buckets?: ChartWidgetBucketConfig[];
21
+ series?: ChartWidgetSeriesConfig[];
22
+ series_name?: string;
23
+ color?: string;
24
+ colors?: string[];
25
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,5 @@
1
+ export declare const CHART_COLORS: string[];
2
+ export declare function toFiniteNumber(value: unknown): number;
3
+ export declare function formatChartValue(value: number): string;
4
+ export declare function formatChartLabel(value: unknown): string;
5
+ export declare function formatChartAxisLabel(value: unknown, maxLength?: number): string;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHART_COLORS = void 0;
4
+ exports.toFiniteNumber = toFiniteNumber;
5
+ exports.formatChartValue = formatChartValue;
6
+ exports.formatChartLabel = formatChartLabel;
7
+ exports.formatChartAxisLabel = formatChartAxisLabel;
8
+ exports.CHART_COLORS = [
9
+ '#2563eb',
10
+ '#16a34a',
11
+ '#f97316',
12
+ '#dc2626',
13
+ '#7c3aed',
14
+ '#0891b2',
15
+ '#ca8a04',
16
+ '#db2777',
17
+ ];
18
+ function toFiniteNumber(value) {
19
+ const numberValue = typeof value === 'number' ? value : Number(value);
20
+ return Number.isFinite(numberValue) ? numberValue : 0;
21
+ }
22
+ function formatChartValue(value) {
23
+ return new Intl.NumberFormat().format(value);
24
+ }
25
+ function formatChartLabel(value) {
26
+ if (typeof value !== 'string') {
27
+ return String(value);
28
+ }
29
+ const timestamp = Date.parse(value);
30
+ if (!Number.isFinite(timestamp)) {
31
+ return value;
32
+ }
33
+ return new Intl.DateTimeFormat(undefined, {
34
+ year: 'numeric',
35
+ month: 'short',
36
+ day: '2-digit',
37
+ }).format(new Date(timestamp));
38
+ }
39
+ function formatChartAxisLabel(value, 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
+ if (label.length <= maxLength) {
49
+ return label;
50
+ }
51
+ return `${label.slice(0, Math.max(maxLength - 1, 1))}…`;
52
+ }
@@ -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
+ }