@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.
Files changed (42) hide show
  1. package/README.md +4 -2
  2. package/dist/cli/athena-WROJBSLV.js +136 -0
  3. package/dist/cli/athena-WROJBSLV.js.map +7 -0
  4. package/dist/cli/{bigQuery-OQUNH3VT.js → bigQuery-3A7HPXZS.js} +2 -2
  5. package/dist/cli/{chunk-56K2FF57.js → chunk-KW66YQ62.js} +4 -2
  6. package/dist/cli/chunk-KW66YQ62.js.map +7 -0
  7. package/dist/cli/{chunk-TZTTALAV.js → chunk-KYTXXLSS.js} +5401 -4664
  8. package/dist/cli/chunk-KYTXXLSS.js.map +7 -0
  9. package/dist/cli/chunk-OVF4UUFF.js +28 -0
  10. package/dist/cli/chunk-OVF4UUFF.js.map +7 -0
  11. package/dist/cli/cli.js +249 -20
  12. package/dist/cli/{duckdb-TKVMONRK.js → duckdb-YOIX6QOQ.js} +2 -2
  13. package/dist/cli/installBrowser.js +11 -0
  14. package/dist/cli/installBrowser.js.map +7 -0
  15. package/dist/cli/postgres-NF43BPZY.js +147 -0
  16. package/dist/cli/postgres-NF43BPZY.js.map +7 -0
  17. package/dist/cli/{serve2-S2LL4D4D.js → serve2-XALOUIFB.js} +7 -3
  18. package/dist/cli/{serve2-S2LL4D4D.js.map → serve2-XALOUIFB.js.map} +2 -2
  19. package/dist/cli/{snowflake-3VPDEYYP.js → snowflake-GX4FSSWT.js} +2 -2
  20. package/dist/index.d.ts +4 -4
  21. package/dist/lang/index.d.ts +4 -4
  22. package/dist/skills/graphene/SKILL.md +19 -12
  23. package/dist/skills/graphene/references/dropdown.md +12 -0
  24. package/dist/skills/graphene/references/gsql.md +26 -23
  25. package/dist/ui/component-utilities/enrich.ts +92 -53
  26. package/dist/ui/component-utilities/format.ts +36 -21
  27. package/dist/ui/component-utilities/theme.ts +0 -1
  28. package/dist/ui/components/AreaChart.svelte +4 -3
  29. package/dist/ui/components/BarChart.svelte +5 -4
  30. package/dist/ui/components/LineChart.svelte +4 -3
  31. package/dist/ui/components/ScatterPlot.svelte +4 -3
  32. package/dist/ui/components/_Table.svelte +3 -1
  33. package/dist/ui/internal/LocalApp.svelte +5 -1
  34. package/dist/ui/internal/PageNavGroup.svelte +2 -2
  35. package/dist/ui/internal/Sidebar.svelte +7 -7
  36. package/dist/ui/internal/sidebar.svelte.js +11 -1
  37. package/package.json +13 -4
  38. package/dist/cli/chunk-56K2FF57.js.map +0 -7
  39. package/dist/cli/chunk-TZTTALAV.js.map +0 -7
  40. /package/dist/cli/{bigQuery-OQUNH3VT.js.map → bigQuery-3A7HPXZS.js.map} +0 -0
  41. /package/dist/cli/{duckdb-TKVMONRK.js.map → duckdb-YOIX6QOQ.js.map} +0 -0
  42. /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
- // Ordinal x axes already use labels to communicate the bucket boundaries, so
289
- // the y-axis line reads like an extra vertical grid line at the left edge.
290
- // Hide the paired y-axis line unless the caller explicitly configured it.
291
- for (let [axisIndex, axis] of config.xAxis.entries()) {
292
- if (!axis?.field?.metadata?.timeOrdinal) continue
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
- let yAxisIndexes = config.series.filter(entry => Number(entry?.xAxisIndex ?? 0) === axisIndex).map(entry => Number(entry?.yAxisIndex ?? 0))
295
- for (let yAxisIndex of yAxisIndexes) {
296
- let yAxis = config.yAxis[yAxisIndex]
297
- if (!yAxis || yAxis.axisLine?.show != null) continue
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 y-axis formatter when labels are enabled.
512
- // This keeps label formatting in sync with the y-axis without asking callers to repeat it.
513
- // labelsUseYAxisFormat depends on valueAxisFormatting running first so labels inherit axis formatting.
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 yField = getSeriesValueField(series, fields)?.name
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
- if (typeof axisFormatter !== 'function') continue
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 value the y-axis is formatting.
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(axisFormatter, value)
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?.timeOrdinal) {
682
- axis.minInterval = 1
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. Hide value-axis grid lines by default
686
- // and ask ECharts for denser integer ticks than its generic value defaults.
687
- axis.axisLine = {show: false}
688
- axis.splitLine = {show: false}
689
- axis.axisLabel = {hideOverlap: true, formatter: (value: unknown) => (domain && (Number(value) < domain[0] || Number(value) > domain[1]) ? '' : formatTimeOrdinal(field, value))}
690
-
691
- // splitNumber is a hint rather than an exact spacing, but it works better
692
- // with value-axis bars because interval can fight the half-bucket padding we
693
- // add later. These defaults keep compact ordinals readable without forcing
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
- if (type === 'category' && field.metadata?.timeOrdinal) {
714
- axis.axisLabel = {formatter: (value: unknown) => formatTimeOrdinal(field, value)}
715
- axis.axisPointer = {label: {formatter: (value: unknown) => formatTimeOrdinal(field, value)}}
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
- return axis
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 currencySymbols = {usd: '$', eur: '€', gbp: '£', cad: 'C$', aud: 'A$', jpy: '¥'} as 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 (units, ratio/pct, compact notation).
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 unit = field?.metadata?.units?.toLowerCase() as keyof typeof currencySymbols | undefined
43
- let currencyUnit = unit != null && unit in currencySymbols ? unit : undefined
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}${currencySymbols[currencyUnit]}${formatted}`
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
- 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)}`
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.
@@ -111,7 +111,6 @@ registerTheme('graphene-theme', {
111
111
  containLabel: false,
112
112
  },
113
113
  line: {
114
- smooth: true,
115
114
  symbol: 'emptyCircle',
116
115
  symbolSize: 6,
117
116
  lineStyle: {width: 2},
@@ -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={rowsNum * (currentPage - 1)}
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.label === 'Home') return -1
119
- if (b.label === 'Home') return 1
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. The panel slides in as an overlay when the
3
- // user hovers the paired SidebarToggle button (rendered separately by the host)
4
- // or the panel itself. Styling values are ported from shadcn-svelte's sidebar registry.
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.17",
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
- "sanitize-html": "^2.17.0",
67
- "svelte": "5.55.3",
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.18.0"
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
- }