@lightdash/cli 1.101.0 → 1.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,13 +27,13 @@ Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
27
27
  ## Frontend Aesthetics Guidelines
28
28
 
29
29
  Focus on:
30
- - **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
30
+ - **Typography**: In this sandbox, webfonts cannot load — the app iframe's CSP blocks external stylesheets, so a Google Fonts `@import` or injected `<link>` fails at runtime and silently falls back (never emit one). Build characterful typography from the fonts that ARE here: pair personalities from the system font space — `ui-serif`/Georgia display over a plain sans body, `ui-monospace` for data and labels, `ui-rounded` where playful — and push contrast with weight, size jumps, letter-spacing, small-caps, and case. A Georgia-headline-over-mono-labels pairing reads as designed; vary which pairing you choose between generations.
31
31
  - **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
32
32
  - **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
33
33
  - **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
34
34
  - **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
35
35
 
36
- NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
36
+ NEVER use generic AI-generated aesthetics like a plain default sans-serif stack for everything, cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
37
37
 
38
38
  Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
39
39
 
@@ -10,6 +10,7 @@
10
10
  - Example 2 — Sankey (flows between stages)
11
11
  - Example 3 — Sunburst (hierarchical part-of-whole)
12
12
  - Example 4 — Word cloud (term frequency)
13
+ - Example 5 — Geo choropleth & globe (maps from GeoJSON)
13
14
  - Common D3 mistakes
14
15
 
15
16
  The template ships with `d3`, `d3-sankey`, and `d3-cloud` pre-installed. Sunburst, treemap, pack, icicle, force, geo, hexbin, and chord all use d3 core (`d3.hierarchy`, `d3.partition`, `d3.tree`, `d3.forceSimulation`, etc.) — no extra packages required.
@@ -528,6 +529,201 @@ export function FeedbackWordCloud() {
528
529
  - Cap input rows (`.limit(80)`-ish). The layout is `O(n²)` in placement attempts; a 500-word cloud will hang the iframe for several seconds.
529
530
  - Don't rotate to arbitrary angles. Stick to `0` and `90` (or `0`, `-30`, `30`). Mixed-angle clouds look chaotic and labels become unreadable.
530
531
 
532
+ ## Example 5 — Geo choropleth & globe (maps from GeoJSON)
533
+
534
+ Maps in data apps are SVG projections drawn with `d3-geo` (in d3 core). There are no tile basemaps — the iframe CSP blocks tile servers and external stylesheets — and that's fine: `d3.geoPath` renders crisp vector maps without them. Shapes come as **GeoJSON through a linked external connection**: the prompt lists it as a file under `/tmp/external-data/{alias}.json` — read that file for the alias, origin, and exact path, and see `/app/references/external-apis.md` for the `externalFetch` contract. Use plain GeoJSON sources only — TopoJSON needs `topojson-client`, which is **not installed**.
535
+
536
+ The pattern: fetch the FeatureCollection once via `externalFetch`, run the metric query with `useLightdash`, join rows onto features by a stable key, and paint one `<path>` per feature.
537
+
538
+ ```tsx
539
+ import { useEffect, useMemo, useRef, useState } from 'react';
540
+ import * as d3 from 'd3';
541
+ import { Loader2 } from 'lucide-react';
542
+ import { query, useLightdash, useLightdashClient } from '@lightdash/query-sdk';
543
+ import { useGlobalFilters } from '@/lib/filters';
544
+ import { CHART_COLORS } from '@/lib/theme';
545
+
546
+ const EXPLORE = 'orders';
547
+ const COUNTRY_FIELD = 'customer_country_code'; // adjust: must return ISO-3 codes
548
+ const GEO_ALIAS = 'world_geojson'; // adjust: the linked connection's alias
549
+ const GEO_PATH =
550
+ '/gh/nvkelso/natural-earth-vector/geojson/ne_110m_admin_0_countries.geojson';
551
+
552
+ type Feature = GeoJSON.Feature<GeoJSON.Geometry, Record<string, unknown>>;
553
+
554
+ const baseQuery = query(EXPLORE)
555
+ .label('Revenue by country (choropleth)')
556
+ .dimensions([COUNTRY_FIELD])
557
+ .metrics(['total_revenue'])
558
+ .limit(500);
559
+
560
+ export function WorldChoropleth() {
561
+ const client = useLightdashClient();
562
+ const { filtersFor, addFilter } = useGlobalFilters();
563
+ const q = useMemo(
564
+ () => baseQuery.filters(filtersFor(EXPLORE)),
565
+ [filtersFor],
566
+ );
567
+ const { data, loading } = useLightdash(q);
568
+
569
+ const [geo, setGeo] = useState<GeoJSON.FeatureCollection | null>(null);
570
+ const [geoError, setGeoError] = useState<string | null>(null);
571
+ const svgRef = useRef<SVGSVGElement | null>(null);
572
+
573
+ // Fetch shapes once. externalFetch parses JSON — res.body IS the FeatureCollection.
574
+ useEffect(() => {
575
+ let cancelled = false;
576
+ client
577
+ .externalFetch(GEO_ALIAS, { method: 'GET', path: GEO_PATH })
578
+ .then((res) => {
579
+ if (!cancelled) setGeo(res.body as GeoJSON.FeatureCollection);
580
+ })
581
+ .catch((err) => {
582
+ if (!cancelled) setGeoError(err.message);
583
+ });
584
+ return () => {
585
+ cancelled = true;
586
+ };
587
+ }, [client]);
588
+
589
+ useEffect(() => {
590
+ if (!svgRef.current || !geo || !data) return;
591
+ const svg = d3.select(svgRef.current);
592
+ svg.selectAll('*').remove();
593
+
594
+ const width = 720;
595
+ const height = 400;
596
+ svg.attr('viewBox', `0 0 ${width} ${height}`);
597
+
598
+ // Stable ISO-3 key per feature. Natural Earth's ISO_A3 is the
599
+ // placeholder '-99' (a truthy string!) for France, Norway and others —
600
+ // `p.ISO_A3 || p.ADM0_A3` returns '-99' for them. ADM0_A3 comes FIRST.
601
+ const countryKey = (f: Feature) =>
602
+ String(f.properties.ADM0_A3 ?? f.properties.ISO_A3_EH);
603
+
604
+ const byCountry = new Map(
605
+ data.map((d) => [String(d[COUNTRY_FIELD]), Number(d.total_revenue)]),
606
+ );
607
+ const valueFor = (f: Feature) => byCountry.get(countryKey(f)) ?? null;
608
+
609
+ const max = d3.max(data, (d) => Number(d.total_revenue)) ?? 1;
610
+ const opacity = d3.scaleSqrt().domain([0, max]).range([0.15, 1]);
611
+
612
+ const projection = d3.geoNaturalEarth1().fitSize([width, height], geo);
613
+ const path = d3.geoPath(projection);
614
+
615
+ svg.append('g')
616
+ .selectAll('path')
617
+ .data(geo.features as Feature[])
618
+ .join('path')
619
+ .attr('d', path)
620
+ .attr('fill', CHART_COLORS[0])
621
+ .attr('fill-opacity', (f) => {
622
+ const v = valueFor(f);
623
+ return v == null ? 0.06 : opacity(v); // near-invisible = no data
624
+ })
625
+ .attr('stroke', 'currentColor')
626
+ .attr('stroke-opacity', 0.2)
627
+ .attr('stroke-width', 0.5)
628
+ .style('cursor', 'pointer')
629
+ .on('click', (_event, f) => {
630
+ addFilter({
631
+ field: COUNTRY_FIELD,
632
+ operator: 'equals',
633
+ value: countryKey(f),
634
+ explore: EXPLORE,
635
+ });
636
+ })
637
+ .append('title')
638
+ .text((f) => String(f.properties.NAME));
639
+ }, [geo, data, addFilter]);
640
+
641
+ if (geoError) {
642
+ return (
643
+ <div className="flex items-center justify-center h-[400px] text-sm text-muted-foreground">
644
+ Could not load map shapes: {geoError}
645
+ </div>
646
+ );
647
+ }
648
+ if (loading || !geo) {
649
+ return (
650
+ <div className="flex items-center justify-center h-[400px]">
651
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
652
+ </div>
653
+ );
654
+ }
655
+ return <svg ref={svgRef} className="w-full h-auto" />;
656
+ }
657
+ ```
658
+
659
+ **Globe variant.** Swap the projection for `d3.geoOrthographic()` and add drag-to-rotate. Tag every projected path with a shared class so one redraw updates them all, and always cancel the animation frame on cleanup:
660
+
661
+ ```tsx
662
+ const projection = d3
663
+ .geoOrthographic()
664
+ .scale(180)
665
+ .translate([width / 2, height / 2])
666
+ .clipAngle(90);
667
+ const path = d3.geoPath(projection);
668
+
669
+ // Ocean + countries all get class 'geo' so redraws hit everything projected
670
+ svg.append('path')
671
+ .datum({ type: 'Sphere' })
672
+ .attr('class', 'geo')
673
+ .attr('d', path)
674
+ .attr('fill', 'currentColor')
675
+ .attr('fill-opacity', 0.05);
676
+ // ... country paths as above, plus .attr('class', 'geo') ...
677
+
678
+ const redraw = () => svg.selectAll<SVGPathElement, unknown>('.geo').attr('d', path);
679
+
680
+ svg.call(
681
+ d3.drag<SVGSVGElement, unknown>().on('drag', (event) => {
682
+ const [lambda, phi, gamma] = projection.rotate();
683
+ projection.rotate([
684
+ lambda + event.dx * 0.3,
685
+ Math.max(-70, Math.min(70, phi - event.dy * 0.3)),
686
+ gamma,
687
+ ]);
688
+ redraw();
689
+ }),
690
+ );
691
+
692
+ // Optional auto-rotation — MUST be cancelled in the effect cleanup
693
+ let raf = 0;
694
+ const spin = () => {
695
+ const [lambda, phi, gamma] = projection.rotate();
696
+ projection.rotate([lambda + 0.1, phi, gamma]);
697
+ redraw();
698
+ raf = requestAnimationFrame(spin);
699
+ };
700
+ raf = requestAnimationFrame(spin);
701
+ return () => cancelAnimationFrame(raf);
702
+ ```
703
+
704
+ **Region focus.** To map one region, filter the features and fit the projection to the subset:
705
+
706
+ ```tsx
707
+ const europe = {
708
+ type: 'FeatureCollection' as const,
709
+ features: geo.features.filter((f) => f.properties.CONTINENT === 'Europe'),
710
+ };
711
+ const projection = d3.geoMercator().fitExtent(
712
+ [[16, 16], [width - 16, height - 16]],
713
+ europe,
714
+ );
715
+ ```
716
+
717
+ **Geo-specific notes:**
718
+ - **Join on `ADM0_A3`, never `ISO_A3` — and never `p.ISO_A3 || p.ADM0_A3`.** Natural Earth's `ISO_A3` is the placeholder `'-99'` for France, Norway, N. Cyprus, Somaliland, and Kosovo, and `'-99'` is truthy, so an ISO_A3-first fallback chain silently returns it. Use the `countryKey` helper above. If the query dimension returns ISO alpha-2 (`FR`), map it to alpha-3 before joining.
719
+ - **GeoJSON only.** `topojson-client` is not installed — request `.geojson` sources through the connection; never try to decode TopoJSON by hand.
720
+ - **The choropleth ramp is opacity on `CHART_COLORS[0]`**, not a hand-picked palette. Use `scaleSqrt` (or `scaleSequentialLog` for heavy-tailed metrics like population/GDP). Give no-data features near-zero opacity rather than a special color.
721
+ - **System fonts only.** Loading Google Fonts (or any external stylesheet) is CSP-blocked — it fails at the console and falls back anyway. Don't emit `<link>` or `@import` for fonts.
722
+ - **Rotation cleanup is mandatory.** An un-cancelled `requestAnimationFrame` loop keeps mutating a detached SVG after refetch/unmount. Return `() => cancelAnimationFrame(raf)` from the effect.
723
+ - **Redraw by class, not rebuild.** Drag/spin only changes the projection — update with `selectAll('.geo').attr('d', path)`. Rebuilding the DOM per frame will not hold 60fps with 177 country paths.
724
+ - **Overlays don't intercept clicks.** Anything painted above the countries (graticule, atmosphere gradient, sphere outline) needs `.attr('pointer-events', 'none')` or country click-to-filter breaks.
725
+ - The 1:110m world file (~0.8 MB, 177 features) is the right resolution for a world view; only reach for 1:50m when zoomed into a region, and expect the admin to raise the connection's response size for it.
726
+
531
727
  ## Common D3 mistakes
532
728
 
533
729
  | Mistake | Fix |
@@ -543,3 +739,6 @@ export function FeedbackWordCloud() {
543
739
  | Word cloud: painting synchronously | `d3-cloud` is async — paint inside `.on('end', ...)` only. Return `() => layout.stop()` from the effect for cleanup. |
544
740
  | Word cloud: not capping rows | `O(n²)` layout. Cap with `.limit(80)`-ish on the query. |
545
741
  | Measuring container width with `getBoundingClientRect` | Set `viewBox` once and let CSS scale via `className="w-full h-auto"`. |
742
+ | Geo: joining rows on `ISO_A3` (or `p.ISO_A3 \|\| p.ADM0_A3` fallback chains) | `ISO_A3` is the truthy placeholder `'-99'` for France, Norway and others — `ADM0_A3` comes first: `p.ADM0_A3 ?? p.ISO_A3_EH`. |
743
+ | Geo: fetching shapes with raw `fetch()` or a tile server | CSP blocks both. GeoJSON comes through `externalFetch` on a linked connection; maps are SVG projections, no tiles. |
744
+ | Geo: auto-rotate loop without cleanup | Return `() => cancelAnimationFrame(raf)` from the effect or the loop outlives the chart. |
@@ -695,6 +695,8 @@ For any external HTTP API call, read `/app/references/external-apis.md` and use
695
695
 
696
696
  **Use the `frontend-design` skill before writing any UI code.** It drives the aesthetic direction — pick a distinctive look for *this* app rather than defaulting to generic shadcn-on-dark-mode. This guide does not prescribe layout, typography, color, or composition; that's `frontend-design`'s job.
697
697
 
698
+ One environment constraint overrides `frontend-design`'s typography advice: **webfonts cannot load in the app iframe.** The runtime CSP blocks external stylesheets — a Google Fonts `@import` or `<link>` fails at the console and silently falls back, so it only adds noise. Never emit one. Distinctive typography here comes from expressive *system-stack* pairings (`ui-serif`/Georgia display over a sans body, `ui-monospace` for data, small-caps, letter-spacing, weight contrast) — a Georgia-headline-over-monospace-labels pairing reads as designed, not generic.
699
+
698
700
  Express that direction **through the theme tokens, in both modes**. "Distinctive" is never a licence to pin one colour scheme: the viewer's Lightdash decides light or dark, and an app that ignores it reads as broken for half its audience. If a look only works on a dark background, it is not a look you can ship — rebuild it so the same design resolves correctly in both.
699
701
 
700
702
  Lightdash-specific constraints that apply on top of `frontend-design`'s direction:
@@ -1228,7 +1230,7 @@ Only when the user asks for adjustable panel sizing (or two sibling areas genuin
1228
1230
 
1229
1231
  If a Recharts component covers it, **use Recharts** — even if a D3 version would be marginally prettier. The cost of D3 is more code, more chances for memory leaks, and harder integration with the action menu.
1230
1232
 
1231
- When you do need D3, **read `/app/references/d3.md` first.** It contains the React-19 + D3 integration pattern, four worked examples (bar, sankey, sunburst, word cloud), the cross-cutting rules (`CHART_COLORS`, `filtersFor`, action menu, no-cross-refetch animation), and a common-mistakes table. Don't try to wire D3 from memory — load the reference.
1233
+ When you do need D3, **read `/app/references/d3.md` first.** It contains the React-19 + D3 integration pattern, five worked examples (bar, sankey, sunburst, word cloud, geo choropleth/globe), the cross-cutting rules (`CHART_COLORS`, `filtersFor`, action menu, no-cross-refetch animation), and a common-mistakes table. Don't try to wire D3 from memory — load the reference.
1232
1234
 
1233
1235
  ## `drillDown()` Reference
1234
1236
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "1.101.0",
3
+ "version": "1.103.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -41,8 +41,8 @@
41
41
  "unique-names-generator": "4.7.1",
42
42
  "uuid": "11.1.1",
43
43
  "yaml": "2.8.3",
44
- "@lightdash/common": "1.101.0",
45
- "@lightdash/warehouses": "1.101.0"
44
+ "@lightdash/common": "1.103.0",
45
+ "@lightdash/warehouses": "1.103.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/inquirer": "8.2.4",