@graphenedata/cli 0.0.5 → 0.0.6

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.
@@ -21,6 +21,10 @@ Graphene also has a CLI that lets you check syntax, run queries, serve data apps
21
21
  - [Using stored expressions in queries](#using-stored-expressions-in-queries)
22
22
  - [Safe aggregation in fan-outs](#safe-aggregation-in-fan-outs)
23
23
  - [`table as` statements](#table-as-statements)
24
+ - [`extend` statements](#extend-statements)
25
+ - [Working with dates, timestamps, and intervals](#working-with-dates-timestamps-and-intervals)
26
+ - [Date and timestamp literals](#date-and-timestamp-literals)
27
+ - [Interval literals](#interval-literals)
24
28
  - [Other miscellaneous details about GSQL](#other-miscellaneous-details-about-gsql)
25
29
  - [Graphene data apps (dashboards)](#graphene-data-apps-dashboards)
26
30
  - [Visualization components](#visualization-components)
@@ -90,40 +94,30 @@ GSQL is comprised of `table` statements that declare tables and `select` stateme
90
94
 
91
95
  ```sql
92
96
  table orders (
93
-
94
- /* Base columns */
95
-
96
- id BIGINT primary_key,
97
- user_id BIGINT,
98
- created_at DATETIME,
99
- status STRING, -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
100
- amount FLOAT, -- Amount paid by customer
101
- cost FLOAT, -- Cost of materials
102
-
103
- /* Join relationships */
104
-
105
- join_one users on user_id = users.id,
106
-
107
- /* Scalar expressions */
108
-
109
- status in ('Processing', 'Shipped', 'Complete') as revenue_recognized,
110
-
111
- /* Agg expressions */
112
-
113
- sum(case when revenue_recognized then amount else 0 end) as revenue,
114
- sum(case when revenue_recognized then cost else 0 end) as cogs,
115
- revenue - cogs as profit,
116
- profit / revenue as profit_margin
97
+ id BIGINT primary_key
98
+ user_id BIGINT
99
+ created_at DATETIME
100
+ status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
101
+ amount FLOAT -- Amount paid by customer
102
+ cost FLOAT -- Cost of materials
103
+
104
+ join one users on user_id = users.id
105
+
106
+ revenue_recognized: status in ('Processing', 'Shipped', 'Complete')
107
+ revenue: sum(case when revenue_recognized then amount else 0 end)
108
+ cogs: sum(case when revenue_recognized then cost else 0 end)
109
+ profit: revenue - cogs
110
+ profit_margin: profit / revenue
117
111
  )
118
112
 
119
113
  table users (
120
- id BIGINT primary_key,
121
- name VARCHAR,
122
- email VARCHAR,
123
- age INTEGER,
124
- country_code VARCHAR,
114
+ id BIGINT primary_key
115
+ name VARCHAR
116
+ email VARCHAR
117
+ age INTEGER
118
+ country_code VARCHAR
125
119
 
126
- join_many orders on id = orders.user_id
120
+ join many orders on id = orders.user_id
127
121
  )
128
122
  ```
129
123
 
@@ -154,19 +148,19 @@ Sometimes there are multiple valid ways to join two tables together. You can mod
154
148
  ```sql
155
149
  table projects (
156
150
  ...
157
- owner_id BIGINT,
158
- viewer_id BIGINT,
151
+ owner_id BIGINT
152
+ viewer_id BIGINT
159
153
 
160
- join_one users as project_owner on owner_id = project_owner.id,
161
- join_one users as project_viewer on viewer_id = project_viewer.id
154
+ join one users as project_owner on owner_id = project_owner.id
155
+ join one users as project_viewer on viewer_id = project_viewer.id
162
156
  )
163
157
 
164
158
  table users (
165
159
  ...
166
- id BIGINT,
160
+ id BIGINT
167
161
 
168
- join_many projects as projects_as_owner on id = projects_as_owner.owner_id,
169
- join_many projects as projects_as_viewer on id = projects_as_viewer.viewer_id
162
+ join many projects as projects_as_owner on id = projects_as_owner.owner_id
163
+ join many projects as projects_as_viewer on id = projects_as_viewer.viewer_id
170
164
  )
171
165
  ```
172
166
 
@@ -179,7 +173,7 @@ table users (
179
173
 
180
174
  **Stored expressions** are GSQL expressions (ie. any arbitrary combination of functions, operators, and column references) that you want to make reusable to queries. Stored expressions are great for canonizing metrics, segments, and other important business definitions.
181
175
 
182
- A stored expression must be given a name via `as`. It can then be referenced by name in queries that use the parent table. See [Using stored expressions in queries](#using-stored-expressions-in-queries) below for how to use stored expressions in queries.
176
+ A stored expression must be given a name via `name: expression`. It can then be referenced by name in queries that use the parent table. See [Using stored expressions in queries](#using-stored-expressions-in-queries) below for how to use stored expressions in queries.
183
177
 
184
178
  Like expressions in regular SQL, expressions in GSQL are either scalar or aggregative. In BI parlance, these would be called dimensions and measures, respectively.
185
179
 
@@ -190,15 +184,13 @@ table orders (
190
184
  ...
191
185
 
192
186
  /* Scalar expressions */
193
-
194
- status in ('Processing', 'Shipped', 'Complete') as revenue_recognized,
187
+ revenue_recognized: status in ('Processing', 'Shipped', 'Complete')
195
188
 
196
189
  /* Agg expressions */
197
-
198
- sum(case when revenue_recognized then amount else 0 end) as revenue,
199
- sum(case when revenue_recognized then cost else 0 end) as cogs,
200
- revenue - cogs as profit, -- even though there are no agg functions here, this is still aggregative as it references other aggregative expressions
201
- profit / revenue as profit_margin
190
+ revenue: sum(case when revenue_recognized then amount else 0 end)
191
+ cogs: sum(case when revenue_recognized then cost else 0 end)
192
+ profit: revenue - cogs -- even though there are no agg functions here, this is still aggregative as it references other aggregative expressions
193
+ profit_margin: profit / revenue
202
194
  )
203
195
  ```
204
196
 
@@ -221,7 +213,7 @@ If you recall the model from before:
221
213
  table orders (
222
214
  ...
223
215
  user_id BIGINT,
224
- join_one users on user_id = users.id
216
+ join one users on user_id = users.id
225
217
  )
226
218
 
227
219
  table users (
@@ -253,23 +245,23 @@ Sometimes you need to access columns or stored expressions in a table that is tw
253
245
  table orders (
254
246
  ...
255
247
 
256
- join_one users on user_id = users.id
248
+ join one users on user_id = users.id
257
249
  )
258
250
 
259
251
  table users (
260
252
  ...
261
253
 
262
- join_many orders on id = orders.user_id,
263
- join_one country on country_code = countries.code
254
+ join many orders on id = orders.user_id
255
+ join one country on country_code = countries.code
264
256
  )
265
257
 
266
258
  table countries (
267
- code VARCHAR primary_key,
268
- name VARCHAR,
269
- currency VARCHAR,
270
- free_shipping BOOLEAN,
259
+ code VARCHAR primary_key
260
+ name VARCHAR
261
+ currency VARCHAR
262
+ free_shipping BOOLEAN
271
263
 
272
- join_many users on code = users.country_code
264
+ join many users on code = users.country_code
273
265
  )
274
266
  ```
275
267
 
@@ -294,21 +286,20 @@ Again, using the orders table from before:
294
286
 
295
287
  ```sql
296
288
  table orders (
297
- id BIGINT primary_key,
298
- user_id BIGINT,
299
- created_at DATETIME,
300
- status STRING, -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
301
- amount FLOAT, -- Amount paid by customer
302
- cost FLOAT, -- Cost of materials
303
-
304
- join_one users on user_id = users.id,
305
-
306
- status in ('Processing', 'Shipped', 'Complete') as revenue_recognized,
307
-
308
- sum(case when revenue_recognized then amount else 0 end) as revenue,
309
- sum(case when revenue_recognized then cost else 0 end) as cogs,
310
- revenue - cogs as profit,
311
- profit / revenue as profit_margin
289
+ id BIGINT primary_key
290
+ user_id BIGINT
291
+ created_at DATETIME
292
+ status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
293
+ amount FLOAT -- Amount paid by customer
294
+ cost FLOAT -- Cost of materials
295
+
296
+ join one users on user_id = users.id
297
+
298
+ revenue_recognized: status in ('Processing', 'Shipped', 'Complete')
299
+ revenue: sum(case when revenue_recognized then amount else 0 end)
300
+ cogs: sum(case when revenue_recognized then cost else 0 end)
301
+ profit: revenue - cogs
302
+ profit_margin: profit / revenue
312
303
  )
313
304
  ```
314
305
 
@@ -330,7 +321,7 @@ select
330
321
  status in ('Processing', 'Shipped', 'Complete') as revenue_recognized,
331
322
  count(*)
332
323
  from orders
333
- group by 1
324
+ group by 1
334
325
  ```
335
326
 
336
327
  You can see that invoking a stored expression is like using a macro: the definition for the stored expression is effectively expanded in-line by Graphene when it runs the query.
@@ -371,7 +362,7 @@ GSQL aims to solve this problem. With the additional information provided via `j
371
362
  The query `select avg(users.age) from orders` will be rewritten to the following SQL when Graphene queries the underlying database (this is for BigQuery, specifically):
372
363
 
373
364
  ```sql
374
- SELECT
365
+ SELECT
375
366
  (CAST((
376
367
  (
377
368
  SUM(DISTINCT
@@ -394,45 +385,38 @@ You can turn the output of any `select` statement into a table with `table foo a
394
385
 
395
386
  ```sql
396
387
  table orders (
397
- id BIGINT primary_key,
398
- user_id BIGINT,
399
- created_at DATETIME,
400
- status STRING, -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
401
- amount FLOAT, -- Amount paid by customer
402
- cost FLOAT, -- Cost of materials
403
-
404
- join_one users on user_id = users.id,
405
-
406
- status in ('Processing', 'Shipped', 'Complete') as revenue_recognized,
407
-
408
- sum(case when revenue_recognized then amount else 0 end) as revenue,
409
- sum(case when revenue_recognized then cost else 0 end) as cogs,
410
- revenue - cogs as profit,
411
- profit / revenue as profit_margin
388
+ id BIGINT primary_key
389
+ user_id BIGINT
390
+ created_at DATETIME
391
+ status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
392
+ amount FLOAT -- Amount paid by customer
393
+ cost FLOAT -- Cost of materials
394
+
395
+ join one users on user_id = users.id
396
+
397
+ revenue_recognized: status in ('Processing', 'Shipped', 'Complete')
398
+ revenue: sum(case when revenue_recognized then amount else 0 end)
399
+ cogs: sum(case when revenue_recognized then cost else 0 end)
400
+ profit: revenue - cogs
401
+ profit_margin: profit / revenue
412
402
  )
413
403
 
414
404
  table users (
415
- id BIGINT primary_key,
416
- name VARCHAR,
417
- email VARCHAR,
418
- age INTEGER,
405
+ id BIGINT primary_key
406
+ name VARCHAR
407
+ email VARCHAR
408
+ age INTEGER
419
409
 
420
- join_many orders on id = orders.user_id,
421
- join_one user_facts on id = user_facts.id,
410
+ join many orders on id = orders.user_id
411
+ join one user_facts on id = user_facts.id
422
412
 
423
- /* Scalar expressions */
424
-
425
- user_facts.ltv as ltv,
426
- user_facts.lifetime_orders as lifetime_orders
413
+ ltv: user_facts.ltv
414
+ lifetime_orders: user_facts.lifetime_orders
427
415
  )
428
416
 
429
417
  table user_facts as (
430
- select
431
- id,
432
- orders.revenue as ltv,
433
- count(orders.id) as lifetime_orders,
434
- from users
435
- group by id
418
+ select id, orders.revenue as ltv, count(orders.id) as lifetime_orders,
419
+ from users group by id
436
420
  )
437
421
  ```
438
422
 
@@ -440,6 +424,61 @@ table user_facts as (
440
424
  - You cannot yet declare join relationships or stored expressions directly in a `table as` statement. Other tables can declare join relationships to it, though, as shown above.
441
425
  - In the example above, the `ltv` and `lifetime_orders` columns from `user_facts` are "hoisted" back into `users` so that they appear as if they are columns from `users`. This is simply a design choice which allows query writers to never need to know about `user_facts`.
442
426
 
427
+ ### `extend` statements
428
+
429
+ `extend` statements allow you to add join relationships or stored expressions to an existing table. This is especially useful for tables created via `table as` statements, which do not support defining these properties directly.
430
+
431
+ For example, if we have a `table as` statement that creates a daily summary of orders:
432
+
433
+ ```sql
434
+ table daily_orders as (
435
+ select
436
+ date_trunc(created_at, day) as day,
437
+ count(*) as num_orders,
438
+ sum(amount) as total_revenue
439
+ from orders
440
+ group by 1
441
+ )
442
+ ```
443
+
444
+ We can extend this table to add measures or joins:
445
+
446
+ ```sql
447
+ extend daily_orders (
448
+ join one calendar on day = calendar.date
449
+
450
+ avg_order_value: total_revenue / num_orders
451
+ )
452
+ ```
453
+
454
+ Note that you cannot add new base columns with `extend`; you can only add joins and stored expressions.
455
+
456
+ ### Working with dates, timestamps, and intervals
457
+
458
+ Graphene understands a handful of common literal formats so you rarely need explicit casts when filtering or doing time math.
459
+
460
+ **Date and timestamp literals**
461
+
462
+ - `YYYY`, `YYYY-MM`, and `YYYY-MM-DD` strings are treated as dates. Leading/trailing spaces are ignored.
463
+ - `YYYY-MM-DD HH[:MM[:SS]]` (with either a space or `T` between the date and time) is treated as a timestamp. Missing minutes or seconds default to `00`.
464
+
465
+ ```sql
466
+ from users select id
467
+ where created_at >= '2024-01-01' and created_at <= '2024-02-01'
468
+ ```
469
+
470
+ **Interval literals**
471
+
472
+ To add or subtract time, provide a quantity followed by a unit inside a string literal. Supported units include `second`, `minute`, `hour`, `day`, `week`, `month`, `quarter`, and `year` (plural forms or shorthands like `secs`, `mins`, `hrs` also work).
473
+
474
+ ```sql
475
+ from users select
476
+ created_at + '5 minutes' as first_seen_plus_5,
477
+ created_at - '2 days' as first_seen_minus_2
478
+ ```
479
+
480
+ Interval literals accept decimals (`'1.5 hours'`) and negative values (`'-7 days'`). Invalid strings produce a diagnostic such as “Could not parse interval literal: "many moons"”.
481
+
443
482
  ### Other miscellaneous details about GSQL
444
483
 
445
484
  - Trailing commas in `table` statements are optional.
@@ -449,6 +488,16 @@ table user_facts as (
449
488
  - Expressions in `group by` are implicitly selected, so `from orders select avg(amount) group by user_id` will return two columns.
450
489
  - `count` is a reserved word. Do not alias your columns as `count`.
451
490
  - Window functions and set operations are not supported.
491
+ - Subqueries are not supported. However, you can accomplish the same functionality by chaining queries:
492
+ ````md
493
+ ```sql my_subquery
494
+ select ...
495
+ ```
496
+
497
+ ```sql my_query
498
+ select ... from my_subquery
499
+ ```
500
+ ````
452
501
 
453
502
  ## Graphene data apps (dashboards)
454
503
 
@@ -493,7 +542,7 @@ Use bar or column charts to compare a metric across categories. Bar charts are b
493
542
  Here's an example:
494
543
 
495
544
  ```markdown
496
- <BarChart
545
+ <BarChart
497
546
  title="Sales by Category"
498
547
  data=orders_by_category_2021
499
548
  x=month
@@ -522,10 +571,10 @@ Here's an example:
522
571
  |----------|-------------|----------|---------|---------|
523
572
  | data | Query name, wrapped in curly braces | true | query name | - |
524
573
  | x | Column or expression to use for the x-axis of the chart | false | column name, stored expression name, GSQL expression | First column |
525
- | y | Column(s) or expression(s) to use for the y-axis of the chart | false | column name, stored expression name, GSQL expression, list of any combination of these | Any non-assigned numeric columns |
526
- | y2 | Column(s) or expression(s) to include on a secondary y-axis | false | column name, stored expression name, GSQL expression, list of any combination of these | - |
574
+ | y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | false | column name, stored expression name, GSQL expression, list of any combination of these | Any non-assigned numeric columns |
575
+ | y2 | Column(s) or expression(s) to include on a secondary y-axis. | false | column name, stored expression name, GSQL expression, list of any combination of these | - |
527
576
  | y2SeriesType | Chart type to apply to the series on the y2 axis | false | `bar`, `line`, `scatter` | `bar` |
528
- | series | Column or expression to use as the series (groups) in a multi-series chart | false | column name, stored expression name, GSQL expression | - |
577
+ | series | Column or expression to use to define the series (groups) in a multi-series chart. Use when values of a particular column dictate the multiple series to plot, eg. `country` would create a series for every distinct country in the column. | false | column name, stored expression name, GSQL expression | - |
529
578
  | sort | Whether to apply default sort to your data. Default sort is x ascending for number and date x-axes, and y descending for category x-axes | false | `true`, `false` | `true` |
530
579
  | type | Grouping method to use for multi-series charts | false | `stacked`, `grouped`, `stacked100` | `stacked` |
531
580
  | stackName | Name for an individual stack. If separate Bar components are used with different stackNames, the chart will show multiple stacks | false | string | - |
@@ -608,7 +657,7 @@ Use a pie chart to show part-to-whole relationships across categories. Best for
608
657
  Here's an example:
609
658
 
610
659
  ```markdown
611
- <PieChart
660
+ <PieChart
612
661
  title="Sales share by category"
613
662
  data=orders_by_category_2021
614
663
  category=category
@@ -640,12 +689,12 @@ Use line charts to display how one or more metrics vary over time. Line charts a
640
689
  Here's an example:
641
690
 
642
691
  ```markdown
643
- <LineChart
692
+ <LineChart
644
693
  title="Monthly Sales"
645
694
  subtitle="Includes all categories"
646
695
  data=orders_by_month
647
696
  x=month
648
- y=sales_usd0k
697
+ y=sales_usd0k
649
698
  yAxisTitle="Sales per Month"
650
699
  />
651
700
  ```
@@ -670,10 +719,10 @@ Here's an example:
670
719
  |------|-------------|----------|---------|---------|
671
720
  | data | Query name, wrapped in curly braces | true | query name | - |
672
721
  | x | Column or expression to use for the x-axis of the chart | true | column name, stored expression name, GSQL expression | - |
673
- | y | Column(s) or expression(s) to use for the y-axis of the chart | true | column name, stored expression name, GSQL expression, list of any combination of these | - |
674
- | y2 | Column(s) or expression(s) to include on a secondary y-axis | false | column name, stored expression name, GSQL expression, list of any combination of these | - |
722
+ | y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | true | column name, stored expression name, GSQL expression, list of any combination of these | - |
723
+ | y2 | Column(s) or expression(s) to include on a secondary y-axis. | false | column name, stored expression name, GSQL expression, list of any combination of these | - |
675
724
  | y2SeriesType | Chart type to apply to the series on the y2 axis | false | `line`, `bar`, `scatter` | `line` |
676
- | series | Column or expression to use as the series (groups) in a multi-series chart | false | column name, stored expression name, GSQL expression | - |
725
+ | series | Column or expression to use to define the series (groups) in a multi-series chart. Use when values of a particular column dictate the multiple series to plot, eg. `country` would create a series for every distinct country in the column. | false | column name, stored expression name, GSQL expression | - |
677
726
  | sort | Whether to apply default sort to your data. Default is x ascending for number and date x-axes, and y descending for category x-axes | false | `true`, `false` | `true` |
678
727
  | handleMissing | Treatment of missing values in the dataset | false | `gap`, `connect`, `zero` | `gap` |
679
728
  | emptySet | Sets behaviour for empty datasets. Can throw an error, a warning, or allow empty. When set to 'error', empty datasets will block builds in `build:strict`. Note this only applies to initial page load - empty datasets caused by input component changes (dropdowns, etc.) are allowed. | false | `error`, `warn`, `pass` | `error` |
@@ -752,7 +801,7 @@ Use area charts to track how a metric with multiple series changes over time, or
752
801
  Here's an example:
753
802
 
754
803
  ```markdown
755
- <AreaChart
804
+ <AreaChart
756
805
  data=orders_by_month
757
806
  x=month
758
807
  y=sales
@@ -779,8 +828,8 @@ Here's an example:
779
828
  |------|-------------|----------|---------|---------|
780
829
  | data | Query name, wrapped in curly braces | true | query name | - |
781
830
  | x | Column or expression to use for the x-axis of the chart | true | column name, stored expression name, GSQL expression | First column |
782
- | y | Column(s) or expression(s) to use for the y-axis of the chart | true | column name, stored expression name, GSQL expression, list of any combination of these | Any non-assigned numeric columns |
783
- | series | Column or expression to use as the series (groups) in a multi-series chart | false | column name, stored expression name, GSQL expression | - |
831
+ | y | Column(s) or expression(s) to use for the y-axis of the chart. Each will create its own series. Consider a split axis with `y2` if there is a difference of scale or unit of measure between the series. | true | column name, stored expression name, GSQL expression, list of any combination of these | Any non-assigned numeric columns |
832
+ | series | Column or expression to use to define the series (groups) in a multi-series chart. Use when values of a particular column dictate the multiple series to plot, eg. `country` would create a series for every distinct country in the column. | false | column name, stored expression name, GSQL expression | - |
784
833
  | sort | Whether to apply default sort to your data. Default sort is x ascending for number and date x-axes, and y descending for category x-axes | false | `true`, `false` | `true` |
785
834
  | type | Grouping method to use for multi-series charts | false | `stacked`, `stacked100` | `stacked` |
786
835
  | handleMissing | Treatment of missing values in the dataset | false | `gap`, `connect`, `zero` | `gap` for single series, `zero` for multi-series |
@@ -851,8 +900,8 @@ Use big values to display a large value standalone, and optionally include a com
851
900
  Here's an example:
852
901
 
853
902
  ```markdown
854
- <BigValue
855
- data=orders_with_comparisons
903
+ <BigValue
904
+ data=orders_with_comparisons
856
905
  value=num_orders
857
906
  sparkline=month
858
907
  comparison=order_growth
@@ -1069,7 +1118,7 @@ The user-inputted text would then be referenced in GSQL via `$name_of_input`. Fo
1069
1118
  ```sql
1070
1119
  select *
1071
1120
  from users
1072
- where email ilike concat('%', $name_of_input, '%')
1121
+ where email ilike concat('%', $name_of_input, '%')
1073
1122
  ```
1074
1123
 
1075
1124
  ##### All text input attributes
@@ -1132,10 +1181,10 @@ Here's an example:
1132
1181
  select distinct status from orders
1133
1182
  ```
1134
1183
 
1135
- <Dropdown
1136
- title="Select Order Status"
1184
+ <Dropdown
1185
+ title="Select Order Status"
1137
1186
  name="status_dropdown"
1138
- data="statuses"
1187
+ data="statuses"
1139
1188
  value="status"
1140
1189
  defaultValue="Complete"
1141
1190
  />
@@ -1201,20 +1250,20 @@ The easiest way to format numbers and dates in Graphene is through component att
1201
1250
  For example, you can use the `fmt` attribute to format values inside a BigValue component:
1202
1251
 
1203
1252
  ```markdown
1204
- <BigValue
1205
- data=sales_data
1206
- value=sales
1207
- fmt="$#,##0"
1253
+ <BigValue
1254
+ data=sales_data
1255
+ value=sales
1256
+ fmt="$#,##0"
1208
1257
  />
1209
1258
  ```
1210
1259
 
1211
1260
  Within charts, you can format individual columns using `xFmt` and `yFmt`:
1212
1261
 
1213
1262
  ```markdown
1214
- <LineChart
1215
- data=sales_data
1216
- x=date
1217
- y=sales
1263
+ <LineChart
1264
+ data=sales_data
1265
+ x=date
1266
+ y=sales
1218
1267
  xFmt="m/d"
1219
1268
  yFmt=eur
1220
1269
  />
@@ -1306,8 +1355,6 @@ These are the available commands:
1306
1355
  - `npm run graphene check <mdPath> --chart "<chartTitle>"` - Same as above, except if the runtime check is successful, only takes a screenshot of the specified chart. `<chartTitle>` must match (case sensitive) the `title` attribute on the chart component. `-c` can be used as shorthand for `--chart`.
1307
1356
  - `npm run graphene compile "<GSQL>"` - Shows how GSQL is translated into the underlying database SQL.
1308
1357
  - `npm run graphene run "<GSQL>"` - Runs a GSQL query. The tables and semantics defined in all .gsql files in the project are available for the query to use.
1309
- - `npm run graphene serve` - Starts (or restarts) the dev server, which allows the user to view their Graphene app on localhost.
1310
- - `npm run graphene stop` - Stops the dev server.
1311
1358
 
1312
1359
  # AGENT INSTRUCTIONS
1313
1360
 
@@ -1318,6 +1365,6 @@ Follow these guidelines when working in a Graphene project.
1318
1365
  - Do not try to search the web for Graphene-specific info; you will not find anything. All the documentation is here in graphene.md.
1319
1366
  - When writing to a .gsql file, check your code with `npm run graphene check`.
1320
1367
  - When writing to a Graphene .md file:
1321
- - Always check your code with `npm run graphene check <mdPath>`.
1368
+ - Always check your code with `npm run graphene check <mdPath>`. Run the command with full permissions because the screenshot may not work in a sandbox.
1322
1369
  - Then do a visual check by either a) looking at the screenshot that `npm run graphene check <mdPath>` creates, or b) using your browser tool to open the .md file at `localhost:<port>/mdPath` (without the .md extension; default port 4000).
1323
- - Critique what you see: Are all the data values formatted in a way that is easy to read? Does the shape of the visualized data require an adjustment to scale, axis min/max, etc.? Are any visualizations missing data altogether? Is that visualization type really the best way to paint the picture? Etc.
1370
+ - Critique what you see: Are all the data values formatted in a way that is easy to read? Does the shape of the visualized data require an adjustment to scale, axis min/max, etc.? Are any visualizations missing data altogether? Is that visualization type really the best way to paint the picture? Etc.
@@ -23,3 +23,14 @@ export function serializeValue (value: unknown): string {
23
23
  let str = String(value)
24
24
  return `'${str.replace(/'/g, "''")}'`
25
25
  }
26
+
27
+ // Parse a comma-separated list into an array of trimmed strings.
28
+ // - Strings are split on commas; whitespace trimmed; empty entries removed.
29
+ // - Arrays are normalized by trimming string items and String()-ing non-strings.
30
+ // - null/undefined -> []
31
+ export function parseCommaList (value: unknown): string[] {
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 === 'string') return value.split(',').map(v => v.trim()).filter(v => v.length > 0)
35
+ return [String(value).trim()].filter(v => v.length > 0)
36
+ }
@@ -10,6 +10,7 @@
10
10
  getFormatObjectFromString,
11
11
  } from '../component-utilities/formatting.js'
12
12
  import {getThemeStores} from '../component-utilities/themeStores'
13
+ import {parseCommaList} from '../component-utilities/inputUtils.ts'
13
14
 
14
15
  const {resolveColor} = getThemeStores()
15
16
  const props = getContext(propKey)
@@ -72,12 +73,14 @@
72
73
  $: xMismatch = $props.xMismatch
73
74
  $: columnSummary = $props.columnSummary
74
75
  $: series = seriesSet ? series : $props.series
75
- $: resolvedY = ySet ? y : $props.y
76
+ $: resolvedY = ySet ? parseCommaList(y) : $props.y
77
+ $: seriesOrder = parseCommaList(seriesOrder)
76
78
 
77
79
  $: {
78
- if (!series && typeof resolvedY !== 'object') {
80
+ if (!series && (!Array.isArray(resolvedY) || resolvedY.length === 1)) {
79
81
  stackName = undefined
80
- if (columnSummary?.[resolvedY]) name = name ?? formatTitle(resolvedY, columnSummary[resolvedY].title)
82
+ let col = Array.isArray(resolvedY) ? resolvedY[0] : resolvedY
83
+ if (columnSummary?.[col]) name = name ?? formatTitle(col, columnSummary[col].title)
81
84
  } else {
82
85
  stackName = 'area'
83
86
  data = getCompletedData(data, x, resolvedY, series, false, xType === 'value')
@@ -3,6 +3,7 @@
3
3
  import Area from './Area.svelte'
4
4
  import QueryLoad from './QueryLoad.svelte'
5
5
  import {getThemeStores} from '../component-utilities/themeStores'
6
+ import {parseCommaList} from '../component-utilities/inputUtils.ts'
6
7
 
7
8
  const {resolveColor, resolveColorsObject, resolveColorPalette} = getThemeStores()
8
9
 
@@ -83,7 +84,7 @@
83
84
  export let xLabelWrap = undefined
84
85
  </script>
85
86
 
86
- <QueryLoad data={data} fields={{x, y, series}} let:loaded>
87
+ <QueryLoad data={data} fields={{x, y: parseCommaList(y), series}} let:loaded>
87
88
  <Chart
88
89
  data={loaded}
89
90
  chartContext={{data, x, y, series}}
@@ -16,6 +16,7 @@
16
16
  getFormatObjectFromString,
17
17
  } from '../component-utilities/formatting.js'
18
18
  import {getThemeStores} from '../component-utilities/themeStores'
19
+ import {parseCommaList} from '../component-utilities/inputUtils.ts'
19
20
 
20
21
  const {resolveColor} = getThemeStores()
21
22
 
@@ -77,8 +78,8 @@
77
78
  // Prop check. If local props supplied, use those. Otherwise fall back to global props.
78
79
  $: data = $props.data
79
80
  $: x = $props.x
80
- $: y = ySet ? y : $props.y
81
- $: y2 = y2Set ? y2 : $props.y2
81
+ $: y = ySet ? parseCommaList(y) : $props.y
82
+ $: y2 = y2Set ? parseCommaList(y2) : $props.y2
82
83
  $: yFormat = $props.yFormat
83
84
  $: y2Format = $props.y2Format
84
85
  $: yCount = $props.yCount
@@ -89,14 +90,18 @@
89
90
  $: columnSummary = $props.columnSummary
90
91
  $: sort = $props.sort
91
92
  $: series = seriesSet ? series : $props.series
93
+ $: seriesOrder = parseCommaList(seriesOrder)
92
94
 
93
95
  let stackedData
94
96
  let sortOrder
95
97
  let defaultLabelPosition
96
98
 
97
- $: if (!series && typeof y !== 'object') {
99
+ $: if (!series && (!Array.isArray(y) || y.length === 1)) {
98
100
  // Single Series
99
- name = name ?? formatTitle(y, columnSummary[y].title)
101
+ {
102
+ let col = Array.isArray(y) ? y[0] : y
103
+ name = name ?? formatTitle(col, columnSummary[col].title)
104
+ }
100
105
 
101
106
  if (swapXY && xType !== 'category') {
102
107
  data = getCompletedData(data, x, y, series, true, xType !== 'time')
@@ -112,10 +117,11 @@
112
117
  if (sort === true && xType === 'category') {
113
118
  stackedData = getStackedData(data, x, y)
114
119
 
115
- if (typeof y === 'object') {
120
+ if (Array.isArray(y) && y.length > 1) {
116
121
  stackedData = getSortedData(stackedData, 'stackTotal', false)
117
122
  } else {
118
- stackedData = getSortedData(stackedData, y, false)
123
+ let col = Array.isArray(y) ? y[0] : y
124
+ stackedData = getSortedData(stackedData, col, false)
119
125
  }
120
126
 
121
127
  sortOrder = stackedData.map((d) => d[x])
@@ -233,7 +239,7 @@
233
239
  if (
234
240
  labels === true &&
235
241
  type === 'stacked' &&
236
- (typeof y === 'object') | (series !== undefined) &&
242
+ ((Array.isArray(y) && y.length > 1) || (series !== undefined)) &&
237
243
  stackTotalLabel === true &&
238
244
  series !== x
239
245
  ) {
@@ -309,7 +315,7 @@
309
315
  } else {
310
316
  d.yAxis[0] = {...d.yAxis[0], ...chartOverrides.yAxis}
311
317
  d.xAxis = {...d.xAxis, ...chartOverrides.xAxis}
312
- if (y2) {
318
+ if (y2Count > 0) {
313
319
  d.yAxis[1] = {...d.yAxis[1], show: true}
314
320
  if (['line', 'bar', 'scatter'].includes(y2SeriesType)) {
315
321
  for (let i = 0; i < y2Count; i++) {