@graphenedata/cli 0.0.14 → 0.0.16
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/LICENSE.md +3 -3
- package/README.md +138 -0
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/bin.js +2 -2
- package/dist/cli/bigQuery-I3F46SC6.js +75 -0
- package/dist/cli/bigQuery-I3F46SC6.js.map +7 -0
- package/dist/cli/chunk-OVWODUTJ.js +12849 -0
- package/dist/cli/chunk-OVWODUTJ.js.map +7 -0
- package/dist/cli/chunk-QAXEOZ43.js +53 -0
- package/dist/cli/chunk-QAXEOZ43.js.map +7 -0
- package/dist/cli/cli.js +245 -10290
- package/dist/cli/clickhouse-ZN5AN2UL.js +64 -0
- package/dist/cli/clickhouse-ZN5AN2UL.js.map +7 -0
- package/dist/cli/duckdb-IYBIO5KJ.js +87 -0
- package/dist/cli/duckdb-IYBIO5KJ.js.map +7 -0
- package/dist/cli/serve2-TNN5EROW.js +447 -0
- package/dist/cli/serve2-TNN5EROW.js.map +7 -0
- package/dist/cli/snowflake-MOQB5GA4.js +128 -0
- package/dist/cli/snowflake-MOQB5GA4.js.map +7 -0
- package/dist/index.d.ts +63 -0
- package/dist/lang/index.d.ts +63 -0
- package/dist/skills/graphene/SKILL.md +235 -0
- package/dist/skills/graphene/references/big-value.md +20 -0
- package/dist/skills/graphene/references/date-range.md +64 -0
- package/dist/skills/graphene/references/dropdown.md +62 -0
- package/dist/skills/graphene/references/echarts.md +162 -0
- package/dist/skills/graphene/references/gsql.md +393 -0
- package/dist/skills/graphene/references/model-gsql.md +72 -0
- package/dist/skills/graphene/references/table.md +143 -0
- package/dist/skills/graphene/references/text-input.md +29 -0
- package/dist/ui/app.css +263 -299
- package/dist/ui/component-utilities/dataShaping.ts +484 -0
- package/dist/ui/component-utilities/dataSummary.ts +57 -0
- package/dist/ui/component-utilities/enrich.ts +763 -0
- package/dist/ui/component-utilities/format.ts +177 -0
- package/dist/ui/component-utilities/inputUtils.ts +48 -9
- package/dist/ui/component-utilities/theme.ts +200 -0
- package/dist/ui/component-utilities/themeStores.ts +26 -21
- package/dist/ui/component-utilities/types.ts +70 -0
- package/dist/ui/components/AreaChart.svelte +57 -105
- package/dist/ui/components/BarChart.svelte +71 -129
- package/dist/ui/components/BigValue.svelte +24 -40
- package/dist/ui/components/Column.svelte +11 -19
- package/dist/ui/components/DateRange.svelte +71 -34
- package/dist/ui/components/Dropdown.svelte +82 -49
- package/dist/ui/components/DropdownOption.svelte +1 -2
- package/dist/ui/components/ECharts.svelte +179 -60
- package/dist/ui/components/InlineDelta.svelte +51 -32
- package/dist/ui/components/LineChart.svelte +54 -125
- package/dist/ui/components/PieChart.svelte +27 -37
- package/dist/ui/components/QueryLoad.svelte +78 -44
- package/dist/ui/components/Row.svelte +2 -1
- package/dist/ui/components/ScatterPlot.svelte +52 -0
- package/dist/ui/components/Skeleton.svelte +32 -0
- package/dist/ui/components/Table.svelte +3 -2
- package/dist/ui/components/TableGroupRow.svelte +28 -36
- package/dist/ui/components/TableHarness.svelte +32 -0
- package/dist/ui/components/TableHeader.svelte +34 -59
- package/dist/ui/components/TableRow.svelte +15 -39
- package/dist/ui/components/TableSubtotalRow.svelte +26 -21
- package/dist/ui/components/TableTotalRow.svelte +27 -37
- package/dist/ui/components/TextInput.svelte +17 -14
- package/dist/ui/components/Value.svelte +25 -0
- package/dist/ui/components/_Table.svelte +80 -76
- package/dist/ui/internal/ChartGallery.svelte +527 -0
- package/dist/ui/internal/ErrorDisplay.svelte +60 -0
- package/dist/ui/internal/LocalApp.svelte +87 -19
- package/dist/ui/internal/PageNavGroup.svelte +269 -0
- package/dist/ui/internal/Sidebar.svelte +178 -0
- package/dist/ui/internal/SidebarToggle.svelte +47 -0
- package/dist/ui/internal/StyleGallery.svelte +244 -0
- package/dist/ui/internal/clientCache.ts +15 -13
- package/dist/ui/internal/pageInputs.svelte.js +292 -0
- package/dist/ui/internal/queryEngine.ts +124 -132
- package/dist/ui/internal/runSocket.ts +59 -0
- package/dist/ui/internal/sidebar.svelte.js +18 -0
- package/dist/ui/internal/telemetry.ts +52 -17
- package/dist/ui/internal/types.d.ts +7 -0
- package/dist/ui/web.js +55 -13
- package/package.json +40 -41
- package/dist/docs/agent-instructions.md +0 -18
- package/dist/docs/base.md +0 -98
- package/dist/docs/cli.md +0 -22
- package/dist/docs/graphene.md +0 -1462
- package/dist/ui/component-utilities/autoFormatting.js +0 -301
- package/dist/ui/component-utilities/builtInFormats.js +0 -482
- package/dist/ui/component-utilities/chartContext.js +0 -12
- package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
- package/dist/ui/component-utilities/checkInputs.js +0 -95
- package/dist/ui/component-utilities/convert.js +0 -15
- package/dist/ui/component-utilities/dateParsing.js +0 -57
- package/dist/ui/component-utilities/dropdownContext.ts +0 -1
- package/dist/ui/component-utilities/echarts.js +0 -272
- package/dist/ui/component-utilities/echartsThemes.js +0 -453
- package/dist/ui/component-utilities/formatTitle.js +0 -24
- package/dist/ui/component-utilities/formatting.js +0 -250
- package/dist/ui/component-utilities/getColumnExtents.js +0 -79
- package/dist/ui/component-utilities/getColumnSummary.js +0 -67
- package/dist/ui/component-utilities/getCompletedData.js +0 -114
- package/dist/ui/component-utilities/getDistinctCount.js +0 -7
- package/dist/ui/component-utilities/getDistinctValues.js +0 -15
- package/dist/ui/component-utilities/getSeriesConfig.js +0 -237
- package/dist/ui/component-utilities/getSortedData.js +0 -7
- package/dist/ui/component-utilities/getStackPercentages.js +0 -43
- package/dist/ui/component-utilities/getStackedData.js +0 -17
- package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
- package/dist/ui/component-utilities/globalContexts.js +0 -1
- package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
- package/dist/ui/component-utilities/replaceNulls.js +0 -14
- package/dist/ui/component-utilities/tableUtils.ts +0 -120
- package/dist/ui/components/Area.svelte +0 -214
- package/dist/ui/components/Bar.svelte +0 -350
- package/dist/ui/components/Chart.svelte +0 -989
- package/dist/ui/components/ErrorChart.svelte +0 -118
- package/dist/ui/components/Line.svelte +0 -227
- package/dist/ui/internal/NavSidebar.svelte +0 -396
- package/dist/ui/internal/PageError.svelte +0 -23
- package/dist/ui/internal/checkSocket.ts +0 -48
- package/dist/ui/internal/theme.ts +0 -88
- package/dist/ui/public/inter-latin-ext.woff2 +0 -0
- package/dist/ui/public/inter-latin.woff2 +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type {Field} from './types.ts'
|
|
2
|
+
|
|
3
|
+
const currencySymbols = {usd: '$', eur: '€', gbp: '£', cad: 'C$', aud: 'A$', jpy: '¥'} as const
|
|
4
|
+
const percent = new Intl.NumberFormat('en-US', {maximumFractionDigits: 0})
|
|
5
|
+
const currencyCompact = new Intl.NumberFormat('en-US', {notation: 'compact', maximumFractionDigits: 1})
|
|
6
|
+
const monthYearFormatter = new Intl.DateTimeFormat('en-US', {month: 'long', year: 'numeric'})
|
|
7
|
+
const monthDayYearFormatter = new Intl.DateTimeFormat('en-US', {month: 'short', day: 'numeric', year: 'numeric'})
|
|
8
|
+
const sundayWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const
|
|
9
|
+
const mondayWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] as const
|
|
10
|
+
const yearMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] as const
|
|
11
|
+
const titleCaseAcronyms = ['id', 'gdp']
|
|
12
|
+
const titleCaseLowerWords = ['of', 'the', 'and', 'in', 'on']
|
|
13
|
+
|
|
14
|
+
// Formats a raw column name into a readable title.
|
|
15
|
+
export function formatTitle(column: string) {
|
|
16
|
+
let cleaned = column.replace(/"/g, '').replace(/_/g, ' ')
|
|
17
|
+
return cleaned.replace(/\S*/g, token => {
|
|
18
|
+
if (titleCaseAcronyms.includes(token)) return token.toUpperCase()
|
|
19
|
+
if (titleCaseLowerWords.includes(token)) return token.toLowerCase()
|
|
20
|
+
return token.charAt(0).toUpperCase() + token.substr(1).toLowerCase()
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ECharts valueFormatter will take different arguments depending on the chart type.
|
|
25
|
+
// For bar/line/area it's just a number
|
|
26
|
+
// for scatter, it's [x,y], for candlestick [open, close, low, high], etc
|
|
27
|
+
export function makeValueFormatter(fields: Field[] = []) {
|
|
28
|
+
return (value: unknown) => {
|
|
29
|
+
if (Array.isArray(value)) return value.map((entry, index) => formatSingleValue(entry, fields[index] || fields[0])).join(', ')
|
|
30
|
+
return formatSingleValue(value, fields[0])
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Formats one numeric value with field metadata (units, ratio/pct, compact notation).
|
|
35
|
+
export function formatSingleValue(value: any, field?: Field) {
|
|
36
|
+
let amount = Number(value)
|
|
37
|
+
if (!Number.isFinite(amount)) return String(value ?? '')
|
|
38
|
+
|
|
39
|
+
if (field?.metadata?.ratio) return `${percent.format(amount * 100)}%`
|
|
40
|
+
if (field?.metadata?.pct) return `${percent.format(amount)}%`
|
|
41
|
+
|
|
42
|
+
let unit = field?.metadata?.units?.toLowerCase() as keyof typeof currencySymbols | undefined
|
|
43
|
+
let currencyUnit = unit != null && unit in currencySymbols ? unit : undefined
|
|
44
|
+
if (currencyUnit) {
|
|
45
|
+
let sign = amount < 0 ? '-' : ''
|
|
46
|
+
let formatted = currencyCompact.format(Math.abs(amount)).replace('K', 'k').replace('M', 'm').replace('B', 'b')
|
|
47
|
+
return `${sign}${currencySymbols[currencyUnit]}${formatted}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (amount === 0) return '0'
|
|
51
|
+
let sign = amount < 0 ? '-' : ''
|
|
52
|
+
let absolute = Math.abs(amount)
|
|
53
|
+
|
|
54
|
+
if (absolute >= 1e12) return `${sign}${compactValue(absolute / 1e12)}T`
|
|
55
|
+
if (absolute >= 1e9) return `${sign}${compactValue(absolute / 1e9)}B`
|
|
56
|
+
if (absolute >= 1e6) return `${sign}${compactValue(absolute / 1e6)}M`
|
|
57
|
+
if (absolute >= 1e3) return `${sign}${compactValue(absolute / 1e3)}k`
|
|
58
|
+
if (absolute >= 1) return `${sign}${compactValue(absolute)}`
|
|
59
|
+
if (absolute >= 1e-3) return `${sign}${compactValue(absolute)}`
|
|
60
|
+
if (absolute >= 1e-6) return `${sign}${compactValue(absolute * 1e3)}m`
|
|
61
|
+
if (absolute >= 1e-9) return `${sign}${compactValue(absolute * 1e6)}u`
|
|
62
|
+
if (absolute >= 1e-12) return `${sign}${compactValue(absolute * 1e9)}n`
|
|
63
|
+
return `${sign}${compactValue(absolute)}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Creates a formatter function that renders date/timestamp values based on field metadata.timeGrain.
|
|
67
|
+
export function makeTimeFormatter(field?: Field) {
|
|
68
|
+
let timeGrain = String(field?.metadata?.timeGrain || '').toLowerCase()
|
|
69
|
+
|
|
70
|
+
return (input: unknown) => {
|
|
71
|
+
let value = input
|
|
72
|
+
if (value && typeof value === 'object' && 'value' in (value as Record<string, unknown>)) {
|
|
73
|
+
value = (value as Record<string, unknown>).value
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let date = value instanceof Date ? value : new Date(Number(value))
|
|
77
|
+
if (!Number.isFinite(date.getTime())) return String(value ?? '')
|
|
78
|
+
|
|
79
|
+
let y = date.getFullYear()
|
|
80
|
+
let m = pad2(date.getMonth() + 1)
|
|
81
|
+
let d = pad2(date.getDate())
|
|
82
|
+
let h = pad2(date.getHours())
|
|
83
|
+
let min = pad2(date.getMinutes())
|
|
84
|
+
let s = pad2(date.getSeconds())
|
|
85
|
+
|
|
86
|
+
if (timeGrain === 'year') return String(y)
|
|
87
|
+
if (timeGrain === 'quarter') return `Q${Math.floor(date.getMonth() / 3) + 1} ${y}`
|
|
88
|
+
if (timeGrain === 'month') return monthYearFormatter.format(date)
|
|
89
|
+
if (timeGrain === 'week' || timeGrain === 'day') return monthDayYearFormatter.format(date)
|
|
90
|
+
if (timeGrain === 'hour') return `${y}-${m}-${d} ${h}:00`
|
|
91
|
+
if (timeGrain === 'minute') return `${y}-${m}-${d} ${h}:${min}`
|
|
92
|
+
if (timeGrain === 'second') return `${y}-${m}-${d} ${h}:${min}:${s}`
|
|
93
|
+
|
|
94
|
+
return monthDayYearFormatter.format(date)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Formats one value by selecting the right formatter from the field type.
|
|
99
|
+
export function formatFromField(field: Field | undefined, value: unknown) {
|
|
100
|
+
if (value === null || value === undefined) return '-'
|
|
101
|
+
|
|
102
|
+
let type = String(field?.type || '').toLowerCase()
|
|
103
|
+
if (type === 'number') return formatSingleValue(value, field)
|
|
104
|
+
if (type === 'date' || type === 'timestamp') return makeTimeFormatter(field)(value)
|
|
105
|
+
return String(value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Formats ordinal time buckets like hour_of_day and day_of_week variants.
|
|
109
|
+
export function formatTimeOrdinal(field: Field | undefined, input: unknown) {
|
|
110
|
+
let value = extractFormatterValue(input)
|
|
111
|
+
let ordinal = String(field?.metadata?.timeOrdinal || '').toLowerCase()
|
|
112
|
+
if (!ordinal) return String(value ?? '')
|
|
113
|
+
|
|
114
|
+
if (ordinal === 'hour_of_day') {
|
|
115
|
+
let hour = Number(value)
|
|
116
|
+
if (!Number.isInteger(hour) || hour < 0 || hour > 23) return String(value ?? '')
|
|
117
|
+
let normalized = hour % 12 || 12
|
|
118
|
+
return `${normalized}${hour < 12 ? 'am' : 'pm'}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (ordinal === 'dow_1s') {
|
|
122
|
+
let day = Number(value)
|
|
123
|
+
if (!Number.isInteger(day) || day < 1 || day > 7) return String(value ?? '')
|
|
124
|
+
return sundayWeek[day - 1]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (ordinal === 'dow_0s') {
|
|
128
|
+
let day = Number(value)
|
|
129
|
+
if (!Number.isInteger(day) || day < 0 || day > 6) return String(value ?? '')
|
|
130
|
+
return sundayWeek[day]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ordinal === 'dow_1m') {
|
|
134
|
+
let day = Number(value)
|
|
135
|
+
if (!Number.isInteger(day) || day < 1 || day > 7) return String(value ?? '')
|
|
136
|
+
return mondayWeek[day - 1]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (ordinal === 'month_of_year') {
|
|
140
|
+
let month = Number(value)
|
|
141
|
+
if (!Number.isInteger(month) || month < 1 || month > 12) return String(value ?? '')
|
|
142
|
+
return yearMonths[month - 1]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (ordinal === 'quarter_of_year') {
|
|
146
|
+
let quarter = Number(value)
|
|
147
|
+
if (!Number.isInteger(quarter) || quarter < 1 || quarter > 4) return String(value ?? '')
|
|
148
|
+
return `Q${quarter}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return String(value ?? '')
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function extractFormatterValue(input: unknown) {
|
|
155
|
+
if (input && typeof input === 'object' && 'value' in (input as Record<string, unknown>)) {
|
|
156
|
+
return (input as Record<string, unknown>).value
|
|
157
|
+
}
|
|
158
|
+
return input
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function pad2(value: number) {
|
|
162
|
+
return String(value).padStart(2, '0')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function compactValue(num: number) {
|
|
166
|
+
let exponent = Math.floor(Math.log10(Math.abs(num)))
|
|
167
|
+
let scale = Math.pow(10, exponent - 1)
|
|
168
|
+
let rounded = Math.round(num / scale) * scale
|
|
169
|
+
if (!Number.isFinite(rounded)) return String(num)
|
|
170
|
+
let magnitude = Math.floor(Math.log10(rounded))
|
|
171
|
+
let decimals = Math.max(0, 1 - magnitude)
|
|
172
|
+
return rounded
|
|
173
|
+
.toFixed(decimals)
|
|
174
|
+
.replace(/\.0+$/, '')
|
|
175
|
+
.replace(/(\.[0-9]*[1-9])0+$/, '$1')
|
|
176
|
+
.replace(/\.$/, '')
|
|
177
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export function toBoolean(value: any): boolean | undefined {
|
|
2
|
+
if (value === undefined || value === null) return undefined
|
|
3
3
|
if (typeof value === 'boolean') return value
|
|
4
4
|
if (typeof value === 'number') return value !== 0
|
|
5
5
|
if (typeof value === 'string') {
|
|
@@ -10,13 +10,13 @@ export function toBoolean (value: any): boolean {
|
|
|
10
10
|
return Boolean(value)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function ensureArray<T>
|
|
13
|
+
export function ensureArray<T>(value: T | T[] | undefined | null): T[] {
|
|
14
14
|
if (Array.isArray(value)) return value
|
|
15
15
|
if (value === undefined || value === null) return []
|
|
16
16
|
return [value]
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function serializeValue
|
|
19
|
+
export function serializeValue(value: unknown): string {
|
|
20
20
|
if (value === null || value === undefined) return 'NULL'
|
|
21
21
|
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
22
22
|
if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'
|
|
@@ -25,12 +25,51 @@ export function serializeValue (value: unknown): string {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Parse a comma-separated list into an array of trimmed strings.
|
|
28
|
-
// - Strings are split on commas
|
|
28
|
+
// - Strings are split on top-level commas, ignoring commas inside calls and quoted strings.
|
|
29
29
|
// - Arrays are normalized by trimming string items and String()-ing non-strings.
|
|
30
30
|
// - null/undefined -> []
|
|
31
|
-
export function parseCommaList
|
|
31
|
+
export function parseCommaList(value: unknown): string[] {
|
|
32
32
|
if (value === undefined || value === null) return []
|
|
33
|
-
if (Array.isArray(value)) return value.map(v => typeof v === 'string' ? v.trim() : String(v)).filter(v => v.length > 0)
|
|
34
|
-
if (typeof value
|
|
35
|
-
|
|
33
|
+
if (Array.isArray(value)) return value.map(v => (typeof v === 'string' ? v.trim() : String(v))).filter(v => v.length > 0)
|
|
34
|
+
if (typeof value !== 'string') return [String(value).trim()].filter(v => v.length > 0)
|
|
35
|
+
|
|
36
|
+
let parts: string[] = []
|
|
37
|
+
let current = ''
|
|
38
|
+
let quote: string | undefined
|
|
39
|
+
let depth = 0
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < value.length; i++) {
|
|
42
|
+
let char = value[i]
|
|
43
|
+
|
|
44
|
+
if (quote) {
|
|
45
|
+
current += char
|
|
46
|
+
if (char === quote) {
|
|
47
|
+
if (quote === "'" && value[i + 1] === "'") current += value[++i]
|
|
48
|
+
else quote = undefined
|
|
49
|
+
}
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
54
|
+
quote = char
|
|
55
|
+
current += char
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (char === '(' || char === '[' || char === '{') depth++
|
|
60
|
+
if ((char === ')' || char === ']' || char === '}') && depth > 0) depth--
|
|
61
|
+
|
|
62
|
+
if (char === ',' && depth === 0) {
|
|
63
|
+
let trimmed = current.trim()
|
|
64
|
+
if (trimmed) parts.push(trimmed)
|
|
65
|
+
current = ''
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
current += char
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let trimmed = current.trim()
|
|
73
|
+
if (trimmed) parts.push(trimmed)
|
|
74
|
+
return parts
|
|
36
75
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// @ts-expect-error ECharts CJS typings don't expose named exports cleanly, but bundling works.
|
|
2
|
+
import {registerTheme} from 'echarts'
|
|
3
|
+
|
|
4
|
+
// ── Color tokens ────────────────────────────────────────────────────────
|
|
5
|
+
// Palette C · Fjord Dusk
|
|
6
|
+
export const colorPalette = [
|
|
7
|
+
'#3D6B7E', // deep teal — Scandinavian twilight anchor
|
|
8
|
+
'#C87F5A', // warm amber — accent warmth, kiln-fired
|
|
9
|
+
'#87A68C', // sage green — twilight foliage
|
|
10
|
+
'#8E7AA0', // muted mauve — dusk shadow
|
|
11
|
+
'#D4A94C', // amber gold — last light on water
|
|
12
|
+
'#5B8F9E', // fjord blue — mid-tone companion
|
|
13
|
+
'#C4868E', // dusty rose — fading alpine glow
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
// Rotate a palette by a deterministic page offset so pages don't always start on the same color.
|
|
17
|
+
// We keep this pure and reusable so enrichments can apply it onto config.color.
|
|
18
|
+
export function paletteForPath(pathname?: string) {
|
|
19
|
+
if (import.meta.env.VITE_TEST) return [...colorPalette] // Keep screenshot baselines stable in UI tests.
|
|
20
|
+
|
|
21
|
+
let rawPath = pathname ?? location.pathname
|
|
22
|
+
let key = String(rawPath)
|
|
23
|
+
.split('?')[0]
|
|
24
|
+
.split('#')[0]
|
|
25
|
+
.replace(/\\/g, '/')
|
|
26
|
+
.replace(/^\/+|\/+$/g, '')
|
|
27
|
+
key = key.replace(/\/index$/i, '') || 'index'
|
|
28
|
+
|
|
29
|
+
let hash = 2166136261
|
|
30
|
+
for (let i = 0; i < key.length; i++) {
|
|
31
|
+
hash ^= key.charCodeAt(i)
|
|
32
|
+
hash = Math.imul(hash, 16777619)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let offset = (hash >>> 0) % colorPalette.length
|
|
36
|
+
if (offset === 0) return [...colorPalette]
|
|
37
|
+
return [...colorPalette.slice(offset), ...colorPalette.slice(0, offset)]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let clr = {
|
|
41
|
+
white: '#ffffff',
|
|
42
|
+
tooltipTxt: '#111827',
|
|
43
|
+
textDark: '#374151',
|
|
44
|
+
textMid: '#6b7280',
|
|
45
|
+
textLight: '#9ca3af',
|
|
46
|
+
lineSubtle: '#d1d5db',
|
|
47
|
+
splitLine: '#dde0e4',
|
|
48
|
+
border: '#e5e7eb',
|
|
49
|
+
seqStart: '#e4eff3',
|
|
50
|
+
statusBad: '#B87470', // muted brick red — in-theme with Fjord Dusk's cool desaturation
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let axisCommon = {
|
|
54
|
+
axisLine: {lineStyle: {color: clr.border}},
|
|
55
|
+
axisLabel: {color: clr.textLight},
|
|
56
|
+
axisTick: {show: false},
|
|
57
|
+
splitLine: {show: false, lineStyle: {color: clr.splitLine, type: 'dashed'}},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
registerTheme('graphene-theme', {
|
|
61
|
+
color: colorPalette,
|
|
62
|
+
backgroundColor: 'transparent',
|
|
63
|
+
textStyle: {
|
|
64
|
+
fontFamily: "'Source Sans 3', sans-serif",
|
|
65
|
+
color: clr.textMid,
|
|
66
|
+
fontSize: 13,
|
|
67
|
+
},
|
|
68
|
+
title: {
|
|
69
|
+
left: 'left',
|
|
70
|
+
padding: 0,
|
|
71
|
+
textStyle: {color: clr.textDark, fontSize: 15},
|
|
72
|
+
},
|
|
73
|
+
categoryAxis: {
|
|
74
|
+
...axisCommon,
|
|
75
|
+
},
|
|
76
|
+
valueAxis: {
|
|
77
|
+
...axisCommon,
|
|
78
|
+
splitLine: {lineStyle: {color: clr.splitLine}},
|
|
79
|
+
splitNumber: 3,
|
|
80
|
+
},
|
|
81
|
+
timeAxis: {
|
|
82
|
+
...axisCommon,
|
|
83
|
+
},
|
|
84
|
+
logAxis: {
|
|
85
|
+
...axisCommon,
|
|
86
|
+
},
|
|
87
|
+
tooltip: {
|
|
88
|
+
backgroundColor: clr.white,
|
|
89
|
+
borderColor: clr.border,
|
|
90
|
+
textStyle: {color: clr.tooltipTxt},
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
visualMap: {
|
|
94
|
+
show: false,
|
|
95
|
+
textStyle: {color: clr.textLight},
|
|
96
|
+
},
|
|
97
|
+
legend: {
|
|
98
|
+
type: 'scroll',
|
|
99
|
+
icon: 'circle',
|
|
100
|
+
itemWidth: 8,
|
|
101
|
+
itemHeight: 8,
|
|
102
|
+
top: 24,
|
|
103
|
+
left: 0,
|
|
104
|
+
textStyle: {color: clr.textMid},
|
|
105
|
+
},
|
|
106
|
+
grid: {
|
|
107
|
+
top: 75,
|
|
108
|
+
left: 40,
|
|
109
|
+
right: 16,
|
|
110
|
+
bottom: 36,
|
|
111
|
+
containLabel: false,
|
|
112
|
+
},
|
|
113
|
+
line: {
|
|
114
|
+
smooth: true,
|
|
115
|
+
symbol: 'emptyCircle',
|
|
116
|
+
symbolSize: 6,
|
|
117
|
+
lineStyle: {width: 2},
|
|
118
|
+
},
|
|
119
|
+
bar: {},
|
|
120
|
+
pie: {
|
|
121
|
+
radius: ['30%', '58%'],
|
|
122
|
+
label: {color: clr.textMid},
|
|
123
|
+
itemStyle: {borderColor: 'var(--color-bg)', borderWidth: 1},
|
|
124
|
+
},
|
|
125
|
+
scatter: {
|
|
126
|
+
symbolSize: 8,
|
|
127
|
+
itemStyle: {opacity: 0.8},
|
|
128
|
+
},
|
|
129
|
+
radar: {
|
|
130
|
+
axisName: {color: clr.textLight},
|
|
131
|
+
splitLine: {lineStyle: {color: clr.border}},
|
|
132
|
+
splitArea: {show: false},
|
|
133
|
+
axisLine: {lineStyle: {color: clr.border}},
|
|
134
|
+
areaStyle: {opacity: 0.15},
|
|
135
|
+
lineStyle: {width: 1.5},
|
|
136
|
+
},
|
|
137
|
+
boxplot: {
|
|
138
|
+
itemStyle: {
|
|
139
|
+
color: clr.seqStart,
|
|
140
|
+
borderColor: colorPalette[0],
|
|
141
|
+
borderWidth: 1.5,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
candlestick: {
|
|
145
|
+
itemStyle: {
|
|
146
|
+
color: colorPalette[2], // up candle — sage green
|
|
147
|
+
color0: clr.statusBad, // down candle — brick red
|
|
148
|
+
borderColor: colorPalette[2],
|
|
149
|
+
borderColor0: clr.statusBad,
|
|
150
|
+
borderWidth: 1.5,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
gauge: {
|
|
154
|
+
progress: {show: true, width: 14, roundCap: true},
|
|
155
|
+
axisLine: {roundCap: true, lineStyle: {width: 14, color: [[1, clr.border]]}},
|
|
156
|
+
axisTick: {show: false},
|
|
157
|
+
splitLine: {show: false},
|
|
158
|
+
axisLabel: {show: false},
|
|
159
|
+
pointer: {show: false},
|
|
160
|
+
detail: {valueAnimation: true, fontSize: 24, color: clr.textDark, offsetCenter: [0, '0%']},
|
|
161
|
+
},
|
|
162
|
+
funnel: {
|
|
163
|
+
left: '10%',
|
|
164
|
+
width: '80%',
|
|
165
|
+
label: {position: 'inside', color: clr.white, fontSize: 12},
|
|
166
|
+
itemStyle: {borderColor: clr.white, borderWidth: 1},
|
|
167
|
+
},
|
|
168
|
+
heatmap: {
|
|
169
|
+
label: {show: true, color: clr.textDark, fontSize: 11},
|
|
170
|
+
},
|
|
171
|
+
graph: {
|
|
172
|
+
lineStyle: {color: clr.lineSubtle, width: 1.5, opacity: 1},
|
|
173
|
+
label: {show: true, color: clr.textDark, fontSize: 11, position: 'right'},
|
|
174
|
+
},
|
|
175
|
+
tree: {
|
|
176
|
+
orient: 'LR',
|
|
177
|
+
symbolSize: 10,
|
|
178
|
+
lineStyle: {color: colorPalette[0], width: 2},
|
|
179
|
+
itemStyle: {color: colorPalette[0], borderColor: colorPalette[0]},
|
|
180
|
+
label: {color: clr.textDark, fontSize: 11, position: 'top', verticalAlign: 'middle', align: 'center'},
|
|
181
|
+
leaves: {label: {position: 'right', verticalAlign: 'middle', align: 'left'}},
|
|
182
|
+
emphasis: {focus: 'descendant'},
|
|
183
|
+
},
|
|
184
|
+
treemap: {
|
|
185
|
+
roam: false,
|
|
186
|
+
breadcrumb: {show: false},
|
|
187
|
+
label: {color: clr.white, fontSize: 12},
|
|
188
|
+
itemStyle: {borderColor: clr.white, borderWidth: 0, gapWidth: 1},
|
|
189
|
+
},
|
|
190
|
+
sunburst: {
|
|
191
|
+
label: {color: clr.textDark, fontSize: 10, rotateLabel: true},
|
|
192
|
+
itemStyle: {borderColor: clr.white, borderWidth: 1},
|
|
193
|
+
},
|
|
194
|
+
sankey: {
|
|
195
|
+
nodeWidth: 8,
|
|
196
|
+
nodeGap: 12,
|
|
197
|
+
lineStyle: {color: 'gradient', opacity: 0.3},
|
|
198
|
+
label: {color: clr.textDark, fontSize: 11},
|
|
199
|
+
},
|
|
200
|
+
})
|
|
@@ -18,18 +18,7 @@ type ThemeStores = {
|
|
|
18
18
|
|
|
19
19
|
const THEME_CONTEXT = Symbol('GrapheneThemeStores')
|
|
20
20
|
|
|
21
|
-
const DEFAULT_PALETTE = [
|
|
22
|
-
'#4C78A8',
|
|
23
|
-
'#F58518',
|
|
24
|
-
'#E45756',
|
|
25
|
-
'#72B7B2',
|
|
26
|
-
'#54A24B',
|
|
27
|
-
'#EECA3B',
|
|
28
|
-
'#B279A2',
|
|
29
|
-
'#FF9DA6',
|
|
30
|
-
'#9D755D',
|
|
31
|
-
'#BAB0AC',
|
|
32
|
-
]
|
|
21
|
+
const DEFAULT_PALETTE = ['#4C78A8', '#F58518', '#E45756', '#72B7B2', '#54A24B', '#EECA3B', '#B279A2', '#FF9DA6', '#9D755D', '#BAB0AC']
|
|
33
22
|
|
|
34
23
|
const DEFAULT_THEME: Theme = {
|
|
35
24
|
colors: {
|
|
@@ -38,10 +27,10 @@ const DEFAULT_THEME: Theme = {
|
|
|
38
27
|
'base-300': '#e5e7eb',
|
|
39
28
|
'base-content': '#1f2937',
|
|
40
29
|
'base-content-muted': '#6b7280',
|
|
41
|
-
primary: '#
|
|
42
|
-
positive: '#
|
|
43
|
-
negative: '#
|
|
44
|
-
warning: '#
|
|
30
|
+
primary: '#3D6B7E',
|
|
31
|
+
positive: '#87A68C',
|
|
32
|
+
negative: '#B87470',
|
|
33
|
+
warning: '#D4A94C',
|
|
45
34
|
},
|
|
46
35
|
colorPalettes: {
|
|
47
36
|
default: DEFAULT_PALETTE,
|
|
@@ -103,6 +92,19 @@ export const resolveColorsObject = (input: Record<string, unknown> | string | un
|
|
|
103
92
|
return readable(Object.fromEntries(entries))
|
|
104
93
|
}
|
|
105
94
|
|
|
95
|
+
const cssVar = (name: string): string | undefined => {
|
|
96
|
+
if (typeof document === 'undefined' || typeof getComputedStyle === 'undefined') return undefined
|
|
97
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A single color makes a flat scale; expand to a bg→color gradient so heatmaps work.
|
|
101
|
+
// chroma.scale needs a real color, so resolve the CSS var before building the palette.
|
|
102
|
+
const finalizePalette = (values: string[]): string[] => {
|
|
103
|
+
if (values.length === 0) return DEFAULT_PALETTE
|
|
104
|
+
if (values.length === 1) return [cssVar('--color-bg') ?? DEFAULT_THEME.colors['base-100'], values[0]]
|
|
105
|
+
return values
|
|
106
|
+
}
|
|
107
|
+
|
|
106
108
|
export const resolveColorPalette = (input: unknown): Readable<string[] | undefined> => {
|
|
107
109
|
if (isReadable<string[] | undefined>(input)) return input
|
|
108
110
|
if (input == null) return readable(DEFAULT_PALETTE)
|
|
@@ -115,20 +117,23 @@ export const resolveColorPalette = (input: unknown): Readable<string[] | undefin
|
|
|
115
117
|
let parsed = parseJson(key)
|
|
116
118
|
if (Array.isArray(parsed)) {
|
|
117
119
|
let values = parsed.map(item => normalizeColor(item)).filter(Boolean) as string[]
|
|
118
|
-
return readable(values
|
|
120
|
+
return readable(finalizePalette(values))
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
if (key.includes(',')) {
|
|
122
|
-
let values = key
|
|
123
|
-
|
|
124
|
+
let values = key
|
|
125
|
+
.split(',')
|
|
126
|
+
.map(part => normalizeColor(part))
|
|
127
|
+
.filter(Boolean) as string[]
|
|
128
|
+
return readable(finalizePalette(values))
|
|
124
129
|
}
|
|
125
130
|
let normalized = normalizeColor(key)
|
|
126
|
-
return readable(normalized ? [normalized] : DEFAULT_PALETTE)
|
|
131
|
+
return readable(normalized ? finalizePalette([normalized]) : DEFAULT_PALETTE)
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
if (Array.isArray(input)) {
|
|
130
135
|
let values = input.map(item => normalizeColor(item)).filter(Boolean) as string[]
|
|
131
|
-
return readable(values
|
|
136
|
+
return readable(finalizePalette(values))
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
return readable(DEFAULT_PALETTE)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DatasetComponentOption,
|
|
3
|
+
EChartsOption,
|
|
4
|
+
GridComponentOption,
|
|
5
|
+
LegendComponentOption,
|
|
6
|
+
SeriesOption,
|
|
7
|
+
TitleComponentOption,
|
|
8
|
+
TooltipComponentOption,
|
|
9
|
+
XAXisComponentOption,
|
|
10
|
+
YAXisComponentOption,
|
|
11
|
+
} from 'echarts/types/dist/echarts'
|
|
12
|
+
|
|
13
|
+
import type {Field as ApiField, QueryResult as ApiQueryResult} from '../../lang/index.d.ts'
|
|
14
|
+
|
|
15
|
+
type SingleOrArray<T> = T | T[]
|
|
16
|
+
type SeriesEncode = Record<string, unknown>
|
|
17
|
+
|
|
18
|
+
export type Field = ApiField
|
|
19
|
+
export type QueryResult = ApiQueryResult
|
|
20
|
+
|
|
21
|
+
type CommonSeriesFields = {
|
|
22
|
+
type?: string
|
|
23
|
+
name?: string
|
|
24
|
+
color?: string
|
|
25
|
+
stack?: string
|
|
26
|
+
datasetId?: string
|
|
27
|
+
data?: unknown
|
|
28
|
+
links?: unknown
|
|
29
|
+
xAxisIndex?: number
|
|
30
|
+
yAxisIndex?: number
|
|
31
|
+
label?: Record<string, any>
|
|
32
|
+
labelLayout?: Record<string, any> | ((...args: any[]) => any)
|
|
33
|
+
itemStyle?: Record<string, any>
|
|
34
|
+
lineStyle?: Record<string, any>
|
|
35
|
+
areaStyle?: Record<string, any>
|
|
36
|
+
showSymbol?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ECharts supports a lightweight split hint so configs stay concise.
|
|
40
|
+
// - `encode.splitBy: "field"` splits one template into one series per distinct field value.
|
|
41
|
+
// - `encode.splitBy: ["groupBy", "stackBy"]` is bar-only grouped+stacked shorthand.
|
|
42
|
+
// - with a single split field, use native `series.stack` to choose stacked vs grouped behavior.
|
|
43
|
+
export type SeriesWithGroupingHint = Omit<SeriesOption, 'encode'> &
|
|
44
|
+
CommonSeriesFields & {
|
|
45
|
+
stackPercentage?: boolean
|
|
46
|
+
encode?: SeriesEncode & {
|
|
47
|
+
splitBy?: string | string[]
|
|
48
|
+
sort?: string
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type EChartsConfig = Omit<EChartsOption, 'series'> & {
|
|
53
|
+
series?: SingleOrArray<SeriesWithGroupingHint>
|
|
54
|
+
legendSelection?: any
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type AxisWithField<TAxis> = TAxis & {field?: Field}
|
|
58
|
+
|
|
59
|
+
// Config shape after enrich() normalization runs.
|
|
60
|
+
// We keep this mutable and array-based because enrichments mutate in place.
|
|
61
|
+
export type NormalConfig = Omit<EChartsConfig, 'series' | 'xAxis' | 'yAxis' | 'dataset' | 'grid' | 'legend' | 'title' | 'tooltip'> & {
|
|
62
|
+
series: SeriesWithGroupingHint[]
|
|
63
|
+
xAxis: AxisWithField<XAXisComponentOption>[]
|
|
64
|
+
yAxis: AxisWithField<YAXisComponentOption>[]
|
|
65
|
+
dataset: DatasetComponentOption[]
|
|
66
|
+
grid: GridComponentOption[]
|
|
67
|
+
legend: LegendComponentOption[]
|
|
68
|
+
title: TitleComponentOption[]
|
|
69
|
+
tooltip: TooltipComponentOption[]
|
|
70
|
+
}
|