@graphenedata/cli 0.0.17 → 0.0.19
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 +4 -2
- package/dist/cli/athena-WROJBSLV.js +136 -0
- package/dist/cli/athena-WROJBSLV.js.map +7 -0
- package/dist/cli/{bigQuery-OQUNH3VT.js → bigQuery-3A7HPXZS.js} +2 -2
- package/dist/cli/{chunk-56K2FF57.js → chunk-KW66YQ62.js} +4 -2
- package/dist/cli/chunk-KW66YQ62.js.map +7 -0
- package/dist/cli/{chunk-TZTTALAV.js → chunk-KYTXXLSS.js} +5401 -4664
- package/dist/cli/chunk-KYTXXLSS.js.map +7 -0
- package/dist/cli/chunk-OVF4UUFF.js +28 -0
- package/dist/cli/chunk-OVF4UUFF.js.map +7 -0
- package/dist/cli/cli.js +249 -20
- package/dist/cli/{duckdb-TKVMONRK.js → duckdb-YOIX6QOQ.js} +2 -2
- package/dist/cli/installBrowser.js +11 -0
- package/dist/cli/installBrowser.js.map +7 -0
- package/dist/cli/postgres-NF43BPZY.js +147 -0
- package/dist/cli/postgres-NF43BPZY.js.map +7 -0
- package/dist/cli/{serve2-S2LL4D4D.js → serve2-XALOUIFB.js} +7 -3
- package/dist/cli/{serve2-S2LL4D4D.js.map → serve2-XALOUIFB.js.map} +2 -2
- package/dist/cli/{snowflake-3VPDEYYP.js → snowflake-GX4FSSWT.js} +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/lang/index.d.ts +4 -4
- package/dist/skills/graphene/SKILL.md +19 -12
- package/dist/skills/graphene/references/dropdown.md +12 -0
- package/dist/skills/graphene/references/gsql.md +26 -23
- package/dist/ui/component-utilities/enrich.ts +92 -53
- package/dist/ui/component-utilities/format.ts +36 -21
- package/dist/ui/component-utilities/theme.ts +0 -1
- package/dist/ui/components/AreaChart.svelte +4 -3
- package/dist/ui/components/BarChart.svelte +5 -4
- package/dist/ui/components/LineChart.svelte +4 -3
- package/dist/ui/components/ScatterPlot.svelte +4 -3
- package/dist/ui/components/_Table.svelte +3 -1
- package/dist/ui/internal/LocalApp.svelte +5 -1
- package/dist/ui/internal/PageNavGroup.svelte +2 -2
- package/dist/ui/internal/Sidebar.svelte +7 -7
- package/dist/ui/internal/sidebar.svelte.js +11 -1
- package/package.json +13 -4
- package/dist/cli/chunk-56K2FF57.js.map +0 -7
- package/dist/cli/chunk-TZTTALAV.js.map +0 -7
- /package/dist/cli/{bigQuery-OQUNH3VT.js.map → bigQuery-3A7HPXZS.js.map} +0 -0
- /package/dist/cli/{duckdb-TKVMONRK.js.map → duckdb-YOIX6QOQ.js.map} +0 -0
- /package/dist/cli/{snowflake-3VPDEYYP.js.map → snowflake-GX4FSSWT.js.map} +0 -0
|
@@ -22,6 +22,7 @@ export function enrich(config: EChartsConfig, rows: Record<string, any>[], field
|
|
|
22
22
|
|
|
23
23
|
// Resolve axis metadata up front so row shaping (like explicit sorting) can use it.
|
|
24
24
|
inferAxesFromEncodedFields(normalized, fields, rows)
|
|
25
|
+
hideDimensionAxisChrome(normalized)
|
|
25
26
|
extendValueAxisDomainsForBars(normalized)
|
|
26
27
|
|
|
27
28
|
// Mutate row/field data before dataset creation so synthesized fields are reflected in dataset dimensions.
|
|
@@ -284,18 +285,31 @@ function inferAxesFromEncodedFields(config: NormalConfig, fields: Field[], rows:
|
|
|
284
285
|
|
|
285
286
|
config.yAxis[axisIndex] = {...inferred, ...axis, axisLabel: {...inferred.axisLabel, ...axis.axisLabel}, axisPointer: {...inferred.axisPointer, ...axis.axisPointer}}
|
|
286
287
|
}
|
|
288
|
+
}
|
|
287
289
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
for (let
|
|
292
|
-
|
|
290
|
+
// Hide vertical grid lines for line/bar charts. For horizontal bar charts, hide the horizontal lines.
|
|
291
|
+
// Other chart types (like scatter) keep the grid lines as they're helpful.
|
|
292
|
+
function hideDimensionAxisChrome(config: NormalConfig) {
|
|
293
|
+
for (let series of config.series) {
|
|
294
|
+
let xAxis = config.xAxis[Number(series?.xAxisIndex ?? 0)]
|
|
295
|
+
let yAxis = config.yAxis[Number(series?.yAxisIndex ?? 0)]
|
|
296
|
+
|
|
297
|
+
let isLineOrArea = series?.type === 'line'
|
|
298
|
+
let isVerticalBar = series?.type === 'bar' && yAxis?.type !== 'category'
|
|
299
|
+
let isHorizontalBar = series?.type === 'bar' && yAxis?.type === 'category'
|
|
300
|
+
|
|
301
|
+
if (isLineOrArea || isVerticalBar) {
|
|
302
|
+
if (xAxis?.type !== 'value') continue
|
|
303
|
+
if (xAxis.splitLine?.show == null) xAxis.splitLine = {...xAxis.splitLine, show: false}
|
|
304
|
+
if (xAxis.axisLine?.show == null) xAxis.axisLine = {...xAxis.axisLine, show: false}
|
|
305
|
+
if (xAxis.axisTick?.show == null) xAxis.axisTick = {...xAxis.axisTick, show: false}
|
|
306
|
+
if (yAxis && yAxis.axisLine?.show == null) yAxis.axisLine = {...yAxis.axisLine, show: false}
|
|
307
|
+
}
|
|
293
308
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
yAxis.axisLine = {...yAxis.axisLine, show: false}
|
|
309
|
+
if (isHorizontalBar) {
|
|
310
|
+
if (yAxis.splitLine?.show == null) yAxis.splitLine = {...yAxis.splitLine, show: false}
|
|
311
|
+
if (yAxis.axisLine?.show == null) yAxis.axisLine = {...yAxis.axisLine, show: false}
|
|
312
|
+
if (yAxis.axisTick?.show == null) yAxis.axisTick = {...yAxis.axisTick, show: false}
|
|
299
313
|
}
|
|
300
314
|
}
|
|
301
315
|
}
|
|
@@ -406,7 +420,7 @@ function valueFormatting(config: NormalConfig, fields: Field[]) {
|
|
|
406
420
|
let valueAxes = [...config.xAxis, ...config.yAxis].filter(axis => axis?.type === 'value' && !axis.field?.metadata?.timeOrdinal)
|
|
407
421
|
for (let axis of valueAxes) {
|
|
408
422
|
if (axis.axisLabel?.formatter != null) continue
|
|
409
|
-
axis.axisLabel = {...axis.axisLabel, formatter: makeValueFormatter(axis.field ? [axis.field] : [])}
|
|
423
|
+
axis.axisLabel = {...axis.axisLabel, formatter: makeValueFormatter(axis.field ? [axis.field] : [], {unitStyle: 'axis'})}
|
|
410
424
|
}
|
|
411
425
|
|
|
412
426
|
for (let series of config.series) {
|
|
@@ -508,22 +522,24 @@ function barLabelPositioning(config: NormalConfig) {
|
|
|
508
522
|
}
|
|
509
523
|
}
|
|
510
524
|
|
|
511
|
-
// Match series data labels to the assigned
|
|
512
|
-
// This keeps label formatting in sync with
|
|
513
|
-
// labelsUseYAxisFormat depends on
|
|
525
|
+
// Match series data labels to the assigned value field when labels are enabled.
|
|
526
|
+
// This keeps label formatting in sync with tooltips without asking callers to repeat it.
|
|
527
|
+
// labelsUseYAxisFormat depends on valueFormatting running first so labels inherit axis formatting.
|
|
514
528
|
function labelsUseYAxisFormat(config: NormalConfig, fields: Field[]) {
|
|
515
529
|
for (let series of config.series) {
|
|
516
530
|
// No-op when labels are off or already explicitly formatted.
|
|
517
531
|
if (!series?.label || series.label.show !== true || series.label.formatter != null) continue
|
|
518
532
|
|
|
519
|
-
let
|
|
533
|
+
let valueField = getSeriesValueField(series, fields)
|
|
534
|
+
let yField = valueField?.name
|
|
520
535
|
let axisIndex = Number(series.yAxisIndex ?? 0)
|
|
521
536
|
let axisFormatter = config.yAxis[axisIndex]?.axisLabel?.formatter
|
|
522
|
-
|
|
537
|
+
let labelFormatter = valueField ? makeValueFormatter([valueField]) : axisFormatter
|
|
538
|
+
if (typeof labelFormatter !== 'function') continue
|
|
523
539
|
|
|
524
540
|
// ECharts can pass different value shapes depending on series/transform shape.
|
|
525
541
|
// We resolve the numeric value in a few fallback steps so labels always use the
|
|
526
|
-
// same
|
|
542
|
+
// same field that tooltips format.
|
|
527
543
|
series.label.formatter = (params: unknown) => {
|
|
528
544
|
let typed = params as {value?: unknown; data?: Record<string, unknown>}
|
|
529
545
|
let value = typed?.value
|
|
@@ -535,7 +551,7 @@ function labelsUseYAxisFormat(config: NormalConfig, fields: Field[]) {
|
|
|
535
551
|
}
|
|
536
552
|
}
|
|
537
553
|
|
|
538
|
-
return formatAxisValue(
|
|
554
|
+
return formatAxisValue(labelFormatter, value)
|
|
539
555
|
}
|
|
540
556
|
}
|
|
541
557
|
}
|
|
@@ -678,49 +694,78 @@ function inferAxisFromField(field: Field | undefined, rows: Record<string, any>[
|
|
|
678
694
|
axis.max = domain[1]
|
|
679
695
|
}
|
|
680
696
|
|
|
681
|
-
if (field.metadata?.
|
|
682
|
-
|
|
697
|
+
if (field.metadata?.timeGrain === 'year') {
|
|
698
|
+
// Pin year ticks to evenly-spaced integers so a domain like [2000, 2005]
|
|
699
|
+
// doesn't end up with the 2000/2002/2004/2005 stub-label pattern.
|
|
700
|
+
let ticks = domain ? niceIntegerTicks(domain[0], domain[1]) : []
|
|
701
|
+
axis.axisLabel = {customValues: ticks, formatter: (value: unknown) => (Number.isInteger(Number(value)) ? String(Number(value)) : '')}
|
|
702
|
+
axis.axisTick = {customValues: ticks}
|
|
703
|
+
return axis
|
|
704
|
+
}
|
|
683
705
|
|
|
706
|
+
if (field.metadata?.timeOrdinal) {
|
|
684
707
|
// Ordinal values are numeric so we use a value axis with a fixed domain, but
|
|
685
|
-
// visually they are discrete buckets.
|
|
686
|
-
//
|
|
687
|
-
|
|
688
|
-
axis.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// every possible day/week label.
|
|
695
|
-
let ordinal = field.metadata.timeOrdinal
|
|
696
|
-
if (ordinal === 'month_of_year' || ordinal === 'quarter_of_year' || ordinal === 'dow_0s' || ordinal === 'dow_1s' || ordinal === 'dow_1m')
|
|
697
|
-
axis.splitNumber = domain ? domain[1] - domain[0] + 1 : 5
|
|
698
|
-
if (ordinal === 'hour_of_day') axis.splitNumber = 8
|
|
699
|
-
if (ordinal === 'day_of_month') axis.splitNumber = 6
|
|
700
|
-
if (ordinal === 'week_of_year') axis.splitNumber = 13
|
|
701
|
-
if (ordinal === 'day_of_year') axis.splitNumber = 12
|
|
702
|
-
|
|
708
|
+
// visually they are discrete buckets. Pin tick positions to evenly-spaced
|
|
709
|
+
// integers so we never get a stub boundary label (e.g. weeks 1, 14, 27, 40, 53).
|
|
710
|
+
let ticks = domain ? niceIntegerTicks(domain[0], domain[1]) : []
|
|
711
|
+
axis.axisLabel = {
|
|
712
|
+
hideOverlap: true,
|
|
713
|
+
customValues: ticks,
|
|
714
|
+
formatter: (value: unknown) => (domain && (Number(value) < domain[0] || Number(value) > domain[1]) ? '' : formatTimeOrdinal(field, value)),
|
|
715
|
+
}
|
|
716
|
+
axis.axisTick = {customValues: ticks}
|
|
703
717
|
axis.axisPointer = {label: {formatter: (value: unknown) => formatTimeOrdinal(field, value)}}
|
|
704
718
|
return axis
|
|
705
719
|
}
|
|
706
|
-
|
|
707
|
-
if (field.metadata?.timePart === 'year') {
|
|
708
|
-
axis.minInterval = 1
|
|
709
|
-
axis.axisLabel = {formatter: (value: unknown) => (Number.isInteger(Number(value)) ? String(Number(value)) : '')}
|
|
710
|
-
}
|
|
711
720
|
}
|
|
712
721
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
722
|
+
return axis
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Pick evenly-spaced integer tick positions across [min, max] for ordinal/year value axes.
|
|
726
|
+
// Strategy:
|
|
727
|
+
// 1. If the range is small enough that labeling every value is readable
|
|
728
|
+
// (≤ denseLimit, sized to cover the 12-month ordinal), label every value.
|
|
729
|
+
// 2. Otherwise prefer step sizes that divide the range exactly so the last tick
|
|
730
|
+
// lands on max (avoiding the stub-label problem where ECharts' auto-picked step
|
|
731
|
+
// doesn't reach max).
|
|
732
|
+
// 3. If no candidate divides the range cleanly, fall back to the smallest step that
|
|
733
|
+
// fits inside targetMax ticks; the final tick may fall short of max, but the chart's
|
|
734
|
+
// domain still extends visually via half-bucket padding.
|
|
735
|
+
function niceIntegerTicks(min: number, max: number, targetMin = 4, targetMax = 8, denseLimit = 13): number[] {
|
|
736
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || max < min) return []
|
|
737
|
+
let range = max - min
|
|
738
|
+
if (range === 0) return [min]
|
|
739
|
+
if (range + 1 <= denseLimit) return tickRange(min, max, 1)
|
|
740
|
+
|
|
741
|
+
let candidates = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 5000]
|
|
742
|
+
for (let step of candidates) {
|
|
743
|
+
if (range % step !== 0) continue
|
|
744
|
+
let count = range / step + 1
|
|
745
|
+
if (count >= targetMin && count <= targetMax) return tickRange(min, max, step)
|
|
716
746
|
}
|
|
747
|
+
for (let step of candidates) {
|
|
748
|
+
let count = Math.floor(range / step) + 1
|
|
749
|
+
if (count >= targetMin && count <= targetMax) return tickRange(min, max, step)
|
|
750
|
+
}
|
|
751
|
+
return [min, max]
|
|
752
|
+
}
|
|
717
753
|
|
|
718
|
-
|
|
754
|
+
function tickRange(min: number, max: number, step: number): number[] {
|
|
755
|
+
let values: number[] = []
|
|
756
|
+
for (let v = min; v <= max + 1e-9; v += step) values.push(Math.round(v))
|
|
757
|
+
return values
|
|
719
758
|
}
|
|
720
759
|
|
|
721
760
|
// Return the natural numeric domain for temporal values that are encoded as numbers.
|
|
722
761
|
function temporalValueDomain(field: Field, rows: Record<string, any>[]): [number, number] | undefined {
|
|
723
762
|
let ordinal = field.metadata?.timeOrdinal
|
|
763
|
+
if (field.metadata?.timeGrain === 'year') {
|
|
764
|
+
let values = rows.map(row => Number(row?.[field.name])).filter(value => Number.isFinite(value))
|
|
765
|
+
if (values.length === 0) return undefined
|
|
766
|
+
return [Math.min(...values), Math.max(...values)]
|
|
767
|
+
}
|
|
768
|
+
|
|
724
769
|
if (ordinal === 'hour_of_day') return [0, 23]
|
|
725
770
|
if (ordinal === 'day_of_month') return [1, 31]
|
|
726
771
|
if (ordinal === 'day_of_year') return [1, 366]
|
|
@@ -729,12 +774,6 @@ function temporalValueDomain(field: Field, rows: Record<string, any>[]): [number
|
|
|
729
774
|
if (ordinal === 'quarter_of_year') return [1, 4]
|
|
730
775
|
if (ordinal === 'dow_0s') return [0, 6]
|
|
731
776
|
if (ordinal === 'dow_1s' || ordinal === 'dow_1m') return [1, 7]
|
|
732
|
-
|
|
733
|
-
if (field.metadata?.timePart == 'year') {
|
|
734
|
-
let values = rows.map(row => Number(row?.[field.name])).filter(value => Number.isFinite(value))
|
|
735
|
-
if (values.length === 0) return undefined
|
|
736
|
-
return [Math.min(...values), Math.max(...values)]
|
|
737
|
-
}
|
|
738
777
|
}
|
|
739
778
|
|
|
740
779
|
// Series sometimes encode their value field as `y` and sometimes as `value` (pie, funnel, etc).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type {Field} from './types.ts'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const supportedCurrencyCodes = new Set(Intl.supportedValuesOf('currency'))
|
|
4
4
|
const percent = new Intl.NumberFormat('en-US', {maximumFractionDigits: 0})
|
|
5
5
|
const currencyCompact = new Intl.NumberFormat('en-US', {notation: 'compact', maximumFractionDigits: 1})
|
|
6
6
|
const monthYearFormatter = new Intl.DateTimeFormat('en-US', {month: 'long', year: 'numeric'})
|
|
@@ -11,6 +11,8 @@ const yearMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep
|
|
|
11
11
|
const titleCaseAcronyms = ['id', 'gdp']
|
|
12
12
|
const titleCaseLowerWords = ['of', 'the', 'and', 'in', 'on']
|
|
13
13
|
|
|
14
|
+
type ValueFormatterOptions = {unitStyle?: 'label' | 'axis'}
|
|
15
|
+
|
|
14
16
|
// Formats a raw column name into a readable title.
|
|
15
17
|
export function formatTitle(column: string) {
|
|
16
18
|
let cleaned = column.replace(/"/g, '').replace(/_/g, ' ')
|
|
@@ -24,43 +26,56 @@ export function formatTitle(column: string) {
|
|
|
24
26
|
// ECharts valueFormatter will take different arguments depending on the chart type.
|
|
25
27
|
// For bar/line/area it's just a number
|
|
26
28
|
// for scatter, it's [x,y], for candlestick [open, close, low, high], etc
|
|
27
|
-
export function makeValueFormatter(fields: Field[] = []) {
|
|
29
|
+
export function makeValueFormatter(fields: Field[] = [], options: ValueFormatterOptions = {}) {
|
|
28
30
|
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
|
+
if (Array.isArray(value)) return value.map((entry, index) => formatSingleValue(entry, fields[index] || fields[0], options)).join(', ')
|
|
32
|
+
return formatSingleValue(value, fields[0], options)
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
// Formats one numeric value with field metadata (
|
|
35
|
-
export function formatSingleValue(value: any, field?: Field) {
|
|
36
|
+
// Formats one numeric value with field metadata (currency, ratio/pct, compact notation).
|
|
37
|
+
export function formatSingleValue(value: any, field?: Field, options: ValueFormatterOptions = {}) {
|
|
36
38
|
let amount = Number(value)
|
|
37
39
|
if (!Number.isFinite(amount)) return String(value ?? '')
|
|
38
40
|
|
|
39
41
|
if (field?.metadata?.ratio) return `${percent.format(amount * 100)}%`
|
|
40
42
|
if (field?.metadata?.pct) return `${percent.format(amount)}%`
|
|
41
43
|
|
|
42
|
-
let
|
|
43
|
-
|
|
44
|
-
if (currencyUnit) {
|
|
44
|
+
let currency = field?.metadata?.currency?.toUpperCase()
|
|
45
|
+
if (currency && supportedCurrencyCodes.has(currency)) {
|
|
45
46
|
let sign = amount < 0 ? '-' : ''
|
|
46
47
|
let formatted = currencyCompact.format(Math.abs(amount)).replace('K', 'k').replace('M', 'm').replace('B', 'b')
|
|
47
|
-
return `${sign}${
|
|
48
|
+
return `${sign}${formatCurrencySymbol(currency)}${formatted}`
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
if (amount === 0) return '0'
|
|
51
|
+
if (amount === 0) return addUnit('0', field, options)
|
|
51
52
|
let sign = amount < 0 ? '-' : ''
|
|
52
53
|
let absolute = Math.abs(amount)
|
|
54
|
+
let formatted = ''
|
|
55
|
+
|
|
56
|
+
if (absolute >= 1e12) formatted = `${compactValue(absolute / 1e12)}T`
|
|
57
|
+
else if (absolute >= 1e9) formatted = `${compactValue(absolute / 1e9)}B`
|
|
58
|
+
else if (absolute >= 1e6) formatted = `${compactValue(absolute / 1e6)}M`
|
|
59
|
+
else if (absolute >= 1e3) formatted = `${compactValue(absolute / 1e3)}k`
|
|
60
|
+
else if (absolute >= 1) formatted = compactValue(absolute)
|
|
61
|
+
else if (absolute >= 1e-3) formatted = compactValue(absolute)
|
|
62
|
+
else if (absolute >= 1e-6) formatted = `${compactValue(absolute * 1e3)}m`
|
|
63
|
+
else if (absolute >= 1e-9) formatted = `${compactValue(absolute * 1e6)}u`
|
|
64
|
+
else if (absolute >= 1e-12) formatted = `${compactValue(absolute * 1e9)}n`
|
|
65
|
+
else formatted = compactValue(absolute)
|
|
66
|
+
|
|
67
|
+
return addUnit(`${sign}${formatted}`, field, options)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatCurrencySymbol(currency: string) {
|
|
71
|
+
let parts = new Intl.NumberFormat('en-US', {style: 'currency', currency, currencyDisplay: 'symbol', maximumFractionDigits: 0}).formatToParts(0)
|
|
72
|
+
return parts.find(part => part.type === 'currency')?.value || currency
|
|
73
|
+
}
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
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)}`
|
|
75
|
+
function addUnit(value: string, field: Field | undefined, options: ValueFormatterOptions) {
|
|
76
|
+
let unit = field?.metadata?.unit?.trim()
|
|
77
|
+
if (!unit) return value
|
|
78
|
+
return options.unitStyle === 'axis' ? `${value} (${unit})` : `${value} ${unit}`
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
// Creates a formatter function that renders date/timestamp values based on field metadata.timeGrain.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import ECharts from './ECharts.svelte'
|
|
4
4
|
import type {EChartsConfig, QueryResult} from '../component-utilities/types.ts'
|
|
5
5
|
import {componentLogger, logExtraProps} from '../internal/telemetry.ts'
|
|
6
|
+
import {formatTitle} from '../component-utilities/format.ts'
|
|
6
7
|
import {parseCommaList} from '../component-utilities/inputUtils.ts'
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
@@ -49,17 +50,17 @@
|
|
|
49
50
|
series = [{type: 'line' as const, areaStyle: {opacity: 0.2}, stack, stackPercentage, encode: {x, y: yFields[0], splitBy, ...sortHint}}]
|
|
50
51
|
} else {
|
|
51
52
|
// "wide" data, one area series per field listed in y
|
|
52
|
-
series = yFields.map(field => ({type: 'line' as const, name: field, areaStyle: {opacity: 0.2}, encode: {x, y: field, ...sortHint}}))
|
|
53
|
+
series = yFields.map(field => ({type: 'line' as const, name: formatTitle(field), areaStyle: {opacity: 0.2}, encode: {x, y: field, ...sortHint}}))
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
if (y2) series.push({type: 'line' as const, name: y2, yAxisIndex: 1, encode: {x, y: y2, ...sortHint}})
|
|
56
|
+
if (y2) series.push({type: 'line' as const, name: formatTitle(y2), yAxisIndex: 1, encode: {x, y: y2, ...sortHint}})
|
|
56
57
|
|
|
57
58
|
return {
|
|
58
59
|
title: title ? {text: title} : undefined,
|
|
59
60
|
tooltip: {trigger: 'axis'},
|
|
60
61
|
legend: {show: Boolean(splitBy || y2 || yFields.length > 1)},
|
|
61
62
|
xAxis: {},
|
|
62
|
-
yAxis: [{max: stackPercentage ? 1 : undefined}, ...(y2 ? [{}] : [])],
|
|
63
|
+
yAxis: [{max: stackPercentage ? 1 : undefined}, ...(y2 ? [{alignTicks: true}] : [])],
|
|
63
64
|
series,
|
|
64
65
|
}
|
|
65
66
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import ECharts from './ECharts.svelte'
|
|
4
4
|
import type {EChartsConfig, QueryResult, SeriesWithGroupingHint} from '../component-utilities/types.ts'
|
|
5
5
|
import {componentLogger, logExtraProps} from '../internal/telemetry.ts'
|
|
6
|
+
import {formatTitle} from '../component-utilities/format.ts'
|
|
6
7
|
import {parseCommaList} from '../component-utilities/inputUtils.ts'
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
@@ -61,21 +62,21 @@
|
|
|
61
62
|
} else {
|
|
62
63
|
// "wide" data, series are created for field listed in the y (or x, for horizontal) attribute
|
|
63
64
|
if (horizontal) {
|
|
64
|
-
series = xFields.map(field => ({type: 'bar' as const, name: field, encode: {x: field, y, ...sortHint}, label: barLabel}))
|
|
65
|
+
series = xFields.map(field => ({type: 'bar' as const, name: formatTitle(field), encode: {x: field, y, ...sortHint}, label: barLabel}))
|
|
65
66
|
} else {
|
|
66
|
-
series = yFields.map(field => ({type: 'bar' as const, name: field, encode: {x, y: field, ...sortHint}, label: barLabel}))
|
|
67
|
+
series = yFields.map(field => ({type: 'bar' as const, name: formatTitle(field), encode: {x, y: field, ...sortHint}, label: barLabel}))
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// y2 is a special shortcut for adding a line on top of a bar chart
|
|
71
|
-
if (y2) series.push({type: 'line' as const, name: y2, yAxisIndex: 1, encode: {x, y: y2, ...sortHint}})
|
|
72
|
+
if (y2) series.push({type: 'line' as const, name: formatTitle(y2), yAxisIndex: 1, encode: {x, y: y2, ...sortHint}})
|
|
72
73
|
|
|
73
74
|
return {
|
|
74
75
|
title: title ? {text: title} : undefined,
|
|
75
76
|
tooltip: {trigger: 'axis'},
|
|
76
77
|
legend: {show: Boolean(splitBy || y2 || (!horizontal && yFields.length > 1) || (horizontal && xFields.length > 1))},
|
|
77
78
|
xAxis: {},
|
|
78
|
-
yAxis: [{max: stackPercentage ? 1 : undefined}, ...(y2 ? [{}] : [])],
|
|
79
|
+
yAxis: [{max: stackPercentage ? 1 : undefined}, ...(y2 ? [{alignTicks: true}] : [])],
|
|
79
80
|
series,
|
|
80
81
|
}
|
|
81
82
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {untrack} from 'svelte'
|
|
3
3
|
import ECharts from './ECharts.svelte'
|
|
4
4
|
import {componentLogger, logExtraProps} from '../internal/telemetry.ts'
|
|
5
|
+
import {formatTitle} from '../component-utilities/format.ts'
|
|
5
6
|
import {parseCommaList} from '../component-utilities/inputUtils.ts'
|
|
6
7
|
import type {EChartsConfig, QueryResult, SeriesWithGroupingHint} from '../component-utilities/types.ts'
|
|
7
8
|
|
|
@@ -45,17 +46,17 @@
|
|
|
45
46
|
series = [{type: 'line' as const, encode: {x, y: yFields[0], splitBy, ...sortHint}}]
|
|
46
47
|
} else {
|
|
47
48
|
// "wide" data, one line per field listed in y
|
|
48
|
-
series = yFields.map(field => ({type: 'line' as const, name: field, encode: {x, y: field, ...sortHint}}))
|
|
49
|
+
series = yFields.map(field => ({type: 'line' as const, name: formatTitle(field), encode: {x, y: field, ...sortHint}}))
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
if (y2) series.push({type: 'line' as const, name: y2, yAxisIndex: 1, encode: {x, y: y2, ...sortHint}})
|
|
52
|
+
if (y2) series.push({type: 'line' as const, name: formatTitle(y2), yAxisIndex: 1, encode: {x, y: y2, ...sortHint}})
|
|
52
53
|
|
|
53
54
|
return {
|
|
54
55
|
title: title ? {text: title} : undefined,
|
|
55
56
|
tooltip: {trigger: 'axis'},
|
|
56
57
|
legend: {show: Boolean(splitBy || y2 || yFields.length > 1)},
|
|
57
58
|
xAxis: {},
|
|
58
|
-
yAxis: [{}, ...(y2 ? [{}] : [])],
|
|
59
|
+
yAxis: [{}, ...(y2 ? [{alignTicks: true}] : [])],
|
|
59
60
|
series,
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ECharts from './ECharts.svelte'
|
|
3
|
+
import {formatTitle} from '../component-utilities/format.ts'
|
|
3
4
|
import {parseCommaList} from '../component-utilities/inputUtils.ts'
|
|
4
5
|
import type {EChartsConfig, QueryResult, SeriesWithGroupingHint} from '../component-utilities/types.ts'
|
|
5
6
|
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
series = [{type: 'scatter' as const, encode: {x, y: yFields[0], splitBy}}]
|
|
35
36
|
} else {
|
|
36
37
|
// "wide" data, one scatter series per field listed in y
|
|
37
|
-
series = yFields.map(field => ({type: 'scatter' as const, name: field, encode: {x, y: field}}))
|
|
38
|
+
series = yFields.map(field => ({type: 'scatter' as const, name: formatTitle(field), encode: {x, y: field}}))
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
return {
|
|
@@ -42,8 +43,8 @@
|
|
|
42
43
|
tooltip: {trigger: 'item'},
|
|
43
44
|
legend: {show: Boolean(splitBy || yFields.length > 1)},
|
|
44
45
|
grid: {left: 56, bottom: 52},
|
|
45
|
-
xAxis: {name: x, nameLocation: 'middle', nameGap: 28},
|
|
46
|
-
yAxis: {name: y, nameLocation: 'middle', nameGap: 40},
|
|
46
|
+
xAxis: {name: formatTitle(x), nameLocation: 'middle', nameGap: 28},
|
|
47
|
+
yAxis: {name: formatTitle(y), nameLocation: 'middle', nameGap: 40},
|
|
47
48
|
series,
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
}: Props = $props()
|
|
42
42
|
|
|
43
43
|
let rowsNum = $derived.by(() => {
|
|
44
|
+
if (String(rows).toLowerCase() === 'all') return Infinity
|
|
44
45
|
let parsed = Number.parseInt(String(rows), 10)
|
|
45
46
|
return (!Number.isFinite(parsed) || parsed <= 0) ? 10 : parsed
|
|
46
47
|
})
|
|
@@ -269,6 +270,7 @@
|
|
|
269
270
|
let displayedPageLength = $derived(paginated
|
|
270
271
|
? Math.min(rowsNum, (dataForDisplay?.length ?? 0) - rowsNum * (currentPage - 1))
|
|
271
272
|
: dataForDisplay?.length ?? 0)
|
|
273
|
+
let rowNumberOffset = $derived(paginated ? rowsNum * (currentPage - 1) : 0)
|
|
272
274
|
|
|
273
275
|
const goToPage = (page: number) => {
|
|
274
276
|
if (!paginated) return
|
|
@@ -457,7 +459,7 @@
|
|
|
457
459
|
groupNamePosition={groupNamePosition}
|
|
458
460
|
orderedColumns={orderedColumns}
|
|
459
461
|
columnLookup={columnLookup}
|
|
460
|
-
index={
|
|
462
|
+
index={rowNumberOffset}
|
|
461
463
|
/>
|
|
462
464
|
{/if}
|
|
463
465
|
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
let navData = $state(navFiles)
|
|
20
20
|
import.meta.hot?.accept('virtual:nav', mod => navData = mod.default)
|
|
21
21
|
|
|
22
|
-
let pathName = window.location.pathname.replace(/^\//, '') || 'index'
|
|
22
|
+
let pathName = window.location.pathname.replace(/^\//, '').replace(/\/$/, '') || 'index'
|
|
23
|
+
// Mirror the server-side folder redirect: if /foo.md doesn't exist but /foo/index.md does, load that.
|
|
24
|
+
if (pathName != 'index' && !navFiles.some(f => f.path == pathName + '.md') && navFiles.some(f => f.path == pathName + '/index.md')) {
|
|
25
|
+
pathName += '/index'
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
// Track compile errors from both initial load and subsequent HMR failures.
|
|
25
29
|
let compileError = $state<GrapheneError | null>(null)
|
|
@@ -115,8 +115,8 @@
|
|
|
115
115
|
return nodes
|
|
116
116
|
.map(n => n.type === 'folder' && n.children?.length ? {...n, children: sortNodes(n.children)} : n)
|
|
117
117
|
.sort((a, b) => {
|
|
118
|
-
if (a.
|
|
119
|
-
if (b.
|
|
118
|
+
if (a.path?.toLowerCase() === 'index.md') return -1
|
|
119
|
+
if (b.path?.toLowerCase() === 'index.md') return 1
|
|
120
120
|
if (a.type !== b.type) return a.type === 'folder' ? -1 : 1
|
|
121
121
|
return a.label.localeCompare(b.label)
|
|
122
122
|
})
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
// Content-agnostic sidebar shell.
|
|
3
|
-
// user hovers the paired SidebarToggle button
|
|
4
|
-
//
|
|
2
|
+
// Content-agnostic sidebar shell. By default the panel slides in as an overlay
|
|
3
|
+
// when the user hovers the paired SidebarToggle button or the panel itself;
|
|
4
|
+
// alwaysOpen keeps the same shell visible for persistent navigation.
|
|
5
5
|
import {sidebar} from './sidebar.svelte.js'
|
|
6
|
-
let {children, width = '16rem'} = $props()
|
|
6
|
+
let {children, width = '16rem', alwaysOpen = false} = $props()
|
|
7
7
|
</script>
|
|
8
8
|
|
|
9
9
|
<nav
|
|
10
10
|
id="nav"
|
|
11
11
|
class="sb-panel pretty-scrollbar"
|
|
12
12
|
style="--sb-w:{width}"
|
|
13
|
-
data-open={sidebar.open}
|
|
14
|
-
onmouseenter={sidebar.enter}
|
|
15
|
-
onmouseleave={sidebar.leave}
|
|
13
|
+
data-open={alwaysOpen || sidebar.open}
|
|
14
|
+
onmouseenter={() => { if (!alwaysOpen) sidebar.enter() }}
|
|
15
|
+
onmouseleave={() => { if (!alwaysOpen) sidebar.leave() }}
|
|
16
16
|
>
|
|
17
17
|
<div class="sb-inner">
|
|
18
18
|
{@render children?.()}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
let state = $state({ open: false });
|
|
1
|
+
let state = $state({ open: false, pinned: false });
|
|
2
2
|
let closeTimer;
|
|
3
3
|
const sidebar = {
|
|
4
4
|
get open() {
|
|
@@ -10,7 +10,17 @@ const sidebar = {
|
|
|
10
10
|
},
|
|
11
11
|
leave() {
|
|
12
12
|
clearTimeout(closeTimer);
|
|
13
|
+
if (state.pinned) return;
|
|
13
14
|
closeTimer = setTimeout(() => state.open = false, 120);
|
|
15
|
+
},
|
|
16
|
+
pin() {
|
|
17
|
+
clearTimeout(closeTimer);
|
|
18
|
+
state.pinned = true;
|
|
19
|
+
state.open = true;
|
|
20
|
+
},
|
|
21
|
+
unpin() {
|
|
22
|
+
state.pinned = false;
|
|
23
|
+
this.leave();
|
|
14
24
|
}
|
|
15
25
|
};
|
|
16
26
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphenedata/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"author": "Graphene Systems Inc",
|
|
6
6
|
"repository": {
|
|
@@ -19,12 +19,17 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
+
"@aws-sdk/client-athena": "^3.1049.0",
|
|
22
23
|
"@clickhouse/client": "^1.18.2",
|
|
23
24
|
"@duckdb/node-api": "1.3.2-alpha.26",
|
|
24
25
|
"@google-cloud/bigquery": "^8.2.0",
|
|
26
|
+
"pg": "8.13.3",
|
|
25
27
|
"snowflake-sdk": "2.4.0"
|
|
26
28
|
},
|
|
27
29
|
"peerDependenciesMeta": {
|
|
30
|
+
"@aws-sdk/client-athena": {
|
|
31
|
+
"optional": true
|
|
32
|
+
},
|
|
28
33
|
"@clickhouse/client": {
|
|
29
34
|
"optional": true
|
|
30
35
|
},
|
|
@@ -34,6 +39,9 @@
|
|
|
34
39
|
"@google-cloud/bigquery": {
|
|
35
40
|
"optional": true
|
|
36
41
|
},
|
|
42
|
+
"pg": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
37
45
|
"snowflake-sdk": {
|
|
38
46
|
"optional": true
|
|
39
47
|
}
|
|
@@ -63,11 +71,12 @@
|
|
|
63
71
|
"js-yaml": "^3.14.2",
|
|
64
72
|
"json5": "^2.2.3",
|
|
65
73
|
"mdsvex": "^0.12.6",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
74
|
+
"playwright-core": "1.58.2",
|
|
75
|
+
"sanitize-html": "^2.17.4",
|
|
76
|
+
"svelte": "5.55.7",
|
|
68
77
|
"unified": "^11.0.5",
|
|
69
78
|
"unist-util-visit": "4.1.2",
|
|
70
79
|
"vite": "7.3.2",
|
|
71
|
-
"ws": "^8.
|
|
80
|
+
"ws": "^8.20.1"
|
|
72
81
|
}
|
|
73
82
|
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../lang/config.ts"],
|
|
4
|
-
"sourcesContent": ["import {existsSync} from 'node:fs'\nimport {readFile} from 'node:fs/promises'\nimport path from 'path'\n\nexport interface Config {\n root: string\n dialect: string\n defaultNamespace?: string\n ignoredFiles: string[]\n telemetry?: boolean\n port?: number\n host?: string\n envFile: string[] // array of paths where we can look for the env file\n\n bigquery?: {\n projectId?: string\n keyPath?: string\n }\n\n snowflake?: {\n account: string\n username: string\n privateKeyPath: string\n schema?: string\n database?: string\n }\n\n clickhouse?: {\n url?: string\n username?: string\n database?: string\n requestTimeout?: number\n }\n\n duckdb?: {\n path?: string\n }\n}\n\nexport type ConfigInput = Omit<Config, 'root' | 'dialect' | 'ignoredFiles' | 'envFile'> & {\n root?: string\n dialect?: Config['dialect']\n ignoredFiles?: Config['ignoredFiles']\n envFile?: string | string[]\n namespace?: string\n}\n\nexport let config: Config = {dialect: 'duckdb', root: ''} as Config\n\nexport function setGlobalConfig(cfg: ConfigInput) {\n Object.keys(config).forEach(key => delete config[key])\n Object.assign(config, normalizeConfig(cfg))\n}\n\nexport function normalizeConfig(input: ConfigInput, defaultRoot = process.cwd()): Config {\n let cfg = {...input}\n if (cfg.namespace && !cfg.defaultNamespace) cfg.defaultNamespace = cfg.namespace\n\n let dialect = cfg.dialect || 'duckdb'\n if (cfg.bigquery) dialect = 'bigquery'\n else if (cfg.snowflake) dialect = 'snowflake'\n else if (cfg.clickhouse) dialect = 'clickhouse'\n else if (cfg.duckdb) dialect = 'duckdb'\n let envFile = ['.env']\n if (Array.isArray(cfg.envFile)) envFile = cfg.envFile\n else if (cfg.envFile) envFile = [cfg.envFile]\n\n return {\n ...cfg,\n dialect,\n root: path.resolve(cfg.root || defaultRoot),\n port: cfg.port || Number(process.env.GRAPHENE_PORT) || 4000,\n ignoredFiles: cfg.ignoredFiles || ['**/agents.md', '**/claude.md'],\n envFile,\n } as Config\n}\n\n// Read graphene config from the nearest parent package.json.\nexport async function loadConfig(dir: string, envLoader: (envFiles: string[]) => void): Promise<Config> {\n // seek upwards from dir looking for package.json\n let configDir = path.resolve(dir)\n while (!existsSync(path.join(configDir, 'package.json'))) {\n let parent = path.dirname(configDir)\n if (parent == configDir) throw new Error(`No package.json found in ${path.resolve(dir)} or its parents`)\n configDir = parent\n }\n\n let txt = await readFile(path.join(configDir, 'package.json'), 'utf8')\n let graphene = JSON.parse(txt).graphene\n if (!graphene || typeof graphene != 'object' || Array.isArray(graphene)) {\n throw new Error(`No graphene config found in ${path.join(configDir, 'package.json')}`)\n }\n\n // config can provide 1 or more env files that Graphene should load. Default to just `.env`\n let envFiles = Array.isArray(graphene.envFile) ? graphene.envFile : [graphene.envFile || '.env']\n envLoader(envFiles.map(file => path.resolve(configDir, file)))\n\n let cfg = normalizeConfig({...graphene, root: configDir}, configDir)\n return cfg\n}\n"],
|
|
5
|
-
"mappings": ";AAAA,SAAQ,kBAAiB;AACzB,SAAQ,gBAAe;AACvB,OAAO,UAAU;AA6CV,IAAI,SAAiB,EAAC,SAAS,UAAU,MAAM,GAAE;AAEjD,SAAS,gBAAgB,KAAkB;AAChD,SAAO,KAAK,MAAM,EAAE,QAAQ,SAAO,OAAO,OAAO,GAAG,CAAC;AACrD,SAAO,OAAO,QAAQ,gBAAgB,GAAG,CAAC;AAC5C;AAEO,SAAS,gBAAgB,OAAoB,cAAc,QAAQ,IAAI,GAAW;AACvF,MAAI,MAAM,EAAC,GAAG,MAAK;AACnB,MAAI,IAAI,aAAa,CAAC,IAAI,iBAAkB,KAAI,mBAAmB,IAAI;AAEvE,MAAI,UAAU,IAAI,WAAW;AAC7B,MAAI,IAAI,SAAU,WAAU;AAAA,WACnB,IAAI,UAAW,WAAU;AAAA,WACzB,IAAI,WAAY,WAAU;AAAA,WAC1B,IAAI,OAAQ,WAAU;AAC/B,MAAI,UAAU,CAAC,MAAM;AACrB,MAAI,MAAM,QAAQ,IAAI,OAAO,EAAG,WAAU,IAAI;AAAA,WACrC,IAAI,QAAS,WAAU,CAAC,IAAI,OAAO;AAE5C,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,MAAM,KAAK,QAAQ,IAAI,QAAQ,WAAW;AAAA,IAC1C,MAAM,IAAI,QAAQ,OAAO,QAAQ,IAAI,aAAa,KAAK;AAAA,IACvD,cAAc,IAAI,gBAAgB,CAAC,gBAAgB,cAAc;AAAA,IACjE;AAAA,EACF;AACF;AAGA,eAAsB,WAAW,KAAa,WAA0D;AAEtG,MAAI,YAAY,KAAK,QAAQ,GAAG;AAChC,SAAO,CAAC,WAAW,KAAK,KAAK,WAAW,cAAc,CAAC,GAAG;AACxD,QAAI,SAAS,KAAK,QAAQ,SAAS;AACnC,QAAI,UAAU,UAAW,OAAM,IAAI,MAAM,4BAA4B,KAAK,QAAQ,GAAG,CAAC,iBAAiB;AACvG,gBAAY;AAAA,EACd;AAEA,MAAI,MAAM,MAAM,SAAS,KAAK,KAAK,WAAW,cAAc,GAAG,MAAM;AACrE,MAAI,WAAW,KAAK,MAAM,GAAG,EAAE;AAC/B,MAAI,CAAC,YAAY,OAAO,YAAY,YAAY,MAAM,QAAQ,QAAQ,GAAG;AACvE,UAAM,IAAI,MAAM,+BAA+B,KAAK,KAAK,WAAW,cAAc,CAAC,EAAE;AAAA,EACvF;AAGA,MAAI,WAAW,MAAM,QAAQ,SAAS,OAAO,IAAI,SAAS,UAAU,CAAC,SAAS,WAAW,MAAM;AAC/F,YAAU,SAAS,IAAI,UAAQ,KAAK,QAAQ,WAAW,IAAI,CAAC,CAAC;AAE7D,MAAI,MAAM,gBAAgB,EAAC,GAAG,UAAU,MAAM,UAAS,GAAG,SAAS;AACnE,SAAO;AACT;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|