diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 629542d57a0..cd3f9464c87 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -44,6 +44,10 @@ jobs: - run: bun install --frozen-lockfile + # charts imports the built output of its workspace siblings, so + # they must be built before prepublishOnly compiles charts. + - run: bun run build:colours && bun run build:components + - run: bunx npm publish --access public --provenance ${{ env.DRY_RUN }} working-directory: packages/charts diff --git a/.storybook/decorators/ChartsDecorator.tsx b/.storybook/decorators/ChartsDecorator.tsx deleted file mode 100644 index 9ec35b914c8..00000000000 --- a/.storybook/decorators/ChartsDecorator.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react" - -import { ChartsProvider } from "../../packages/charts/src/config/ChartsProvider" -import type { ChartsConfig } from "../../packages/charts/src/config/ChartsConfig" - -const storybookChartsConfig: ChartsConfig = { - branding: { - poweredByText: "Build Canada Charts", - licenseText: "CC BY 4.0", - licenseUrl: "https://creativecommons.org/licenses/by/4.0/", - }, - dataApi: { - baseUrl: "https://api.example.com/v1/indicators/", - }, - errorReporting: { - enabled: false, - }, - analytics: { - enabled: false, - }, -} - -export const ChartsDecorator = (Story: React.ComponentType) => ( - -
- -
-
-) diff --git a/.storybook/docs/ChartTypes.mdx b/.storybook/docs/ChartTypes.mdx deleted file mode 100644 index a578627bbe9..00000000000 --- a/.storybook/docs/ChartTypes.mdx +++ /dev/null @@ -1,185 +0,0 @@ -import { Meta } from "@storybook/addon-docs/blocks" - - - -# Chart Types - -Configure the chart type using the `chartTypes` property on `GrapherState`. - -## Available Chart Types - -```tsx -import { GRAPHER_CHART_TYPES } from "@buildcanada/charts" - -// All available chart types: -GRAPHER_CHART_TYPES.LineChart // Time series line chart -GRAPHER_CHART_TYPES.DiscreteBar // Horizontal bar chart (single time point) -GRAPHER_CHART_TYPES.StackedArea // Stacked area chart -GRAPHER_CHART_TYPES.StackedBar // Vertical stacked bar chart -GRAPHER_CHART_TYPES.SlopeChart // Slope chart (two time points) -GRAPHER_CHART_TYPES.ScatterPlot // Scatter plot (requires x and y) -GRAPHER_CHART_TYPES.Marimekko // Marimekko chart -``` - -## Line Chart - -Best for showing trends over time for one or more entities. - -```tsx -import { GrapherState, GRAPHER_CHART_TYPES, DimensionProperty } from "@buildcanada/charts" - -const grapherState = new GrapherState({ - title: "Life Expectancy Over Time", - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - dimensions: [{ variableId: 1, property: DimensionProperty.y }], - selectedEntityNames: ["Canada", "United States", "Germany"], -}) -grapherState.inputTable = table -``` - -## Discrete Bar Chart - -Best for comparing values across entities at a single point in time. - -```tsx -const grapherState = new GrapherState({ - title: "GDP by Country (2023)", - chartTypes: [GRAPHER_CHART_TYPES.DiscreteBar], - dimensions: [{ variableId: 1, property: DimensionProperty.y }], -}) -``` - -## Stacked Area Chart - -Best for showing how parts contribute to a whole over time. - -```tsx -const grapherState = new GrapherState({ - title: "Energy Sources Over Time", - chartTypes: [GRAPHER_CHART_TYPES.StackedArea], - dimensions: [{ variableId: 1, property: DimensionProperty.y }], -}) -``` - -## Stacked Bar Chart - -Best for comparing composition across categories. - -```tsx -const grapherState = new GrapherState({ - title: "Population by Age Group", - chartTypes: [GRAPHER_CHART_TYPES.StackedBar], - dimensions: [{ variableId: 1, property: DimensionProperty.y }], -}) -``` - -## Slope Chart - -Best for showing change between two specific time points. - -```tsx -const grapherState = new GrapherState({ - title: "Life Expectancy: 2000 vs 2020", - chartTypes: [GRAPHER_CHART_TYPES.SlopeChart], - dimensions: [{ variableId: 1, property: DimensionProperty.y }], -}) -``` - -## Scatter Plot - -Best for showing relationships between two variables. Requires both X and Y dimensions. - -```tsx -import { createTestDataset, legacyToChartsTableAndDimensionsWithMandatorySlug } from "@buildcanada/charts" - -// Define two variables -const gdpData = [ - { year: 2020, entity: { id: 1, name: "Canada" }, value: 45000 }, - { year: 2020, entity: { id: 2, name: "USA" }, value: 63000 }, -] - -const lifeExpData = [ - { year: 2020, entity: { id: 1, name: "Canada" }, value: 82 }, - { year: 2020, entity: { id: 2, name: "USA" }, value: 78 }, -] - -const dimensions = [ - { variableId: 1, property: DimensionProperty.x }, - { variableId: 2, property: DimensionProperty.y }, -] - -const dataset = createTestDataset([ - { data: gdpData, metadata: { id: 1, display: { name: "GDP per capita" } } }, - { data: lifeExpData, metadata: { id: 2, display: { name: "Life expectancy" } } }, -]) - -const table = legacyToChartsTableAndDimensionsWithMandatorySlug(dataset, dimensions, {}) - -const grapherState = new GrapherState({ - title: "GDP vs Life Expectancy", - chartTypes: [GRAPHER_CHART_TYPES.ScatterPlot], - dimensions, -}) -grapherState.inputTable = table -``` - -## Multiple Chart Types - -You can enable multiple chart types to let users switch between views: - -```tsx -const grapherState = new GrapherState({ - title: "Flexible Chart", - chartTypes: [ - GRAPHER_CHART_TYPES.LineChart, - GRAPHER_CHART_TYPES.DiscreteBar, - GRAPHER_CHART_TYPES.StackedArea, - ], - dimensions: [{ variableId: 1, property: DimensionProperty.y }], -}) -``` - -## Chart Configuration Options - -### Time Range - -```tsx -const grapherState = new GrapherState({ - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - minTime: 2000, // Start year - maxTime: 2020, // End year - // ... -}) -``` - -### Axis Configuration - -```tsx -const grapherState = new GrapherState({ - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - yAxis: { - min: 0, - max: 100, - scaleType: "linear", // or "log" - }, - // ... -}) -``` - -### Hide Controls - -```tsx -const grapherState = new GrapherState({ - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - hideEntityControls: true, // Hide entity picker - hideTimeline: true, // Hide time slider - hideRelativeToggle: true, // Hide relative/absolute toggle - // ... -}) -``` - -## Next Steps - -- [Map Charts](/docs/docs-map-charts--docs) - Geographic visualizations -- [Data Loading](/docs/docs-data-loading--docs) - Load your own data -- [Charts/Grapher](/docs/charts-grapher--docs) - Interactive examples diff --git a/.storybook/docs/Configuration.mdx b/.storybook/docs/Configuration.mdx deleted file mode 100644 index c97f4f7ddb1..00000000000 --- a/.storybook/docs/Configuration.mdx +++ /dev/null @@ -1,218 +0,0 @@ -import { Meta } from "@storybook/addon-docs/blocks" - - - -# Configuration - -The `ChartsConfig` interface allows you to customize branding, data sources, error handling, and analytics. - -## ChartsConfig Interface - -```typescript -interface ChartsConfig { - branding?: ChartsBranding - dataApi: ChartsDataApi - errorReporting?: ChartsErrorReporting - analytics?: ChartsAnalytics -} -``` - -## Branding - -Customize the appearance and attribution of your charts: - -```typescript -interface ChartsBranding { - // License text displayed in chart footer (default: "CC BY") - licenseText?: string - - // URL for license link - licenseUrl?: string - - // "Powered by" attribution text - poweredByText?: string - - // URL for "Powered by" link - poweredByUrl?: string - - // Support email for error messages - supportEmail?: string - - // Custom tooltip for license section - licenseTooltip?: string - - // Custom logos - logos?: Record -} -``` - -### Example - -```tsx -const config = { - branding: { - poweredByText: "Acme Analytics", - poweredByUrl: "https://acme.com", - licenseText: "CC BY 4.0", - licenseUrl: "https://creativecommons.org/licenses/by/4.0/", - supportEmail: "support@acme.com", - }, - dataApi: { baseUrl: "https://api.acme.com/data/" }, -} -``` - -## Data API - -Configure where chart data is fetched from: - -```typescript -interface ChartsDataApi { - // Base URL for data API - baseUrl: string - - // Custom route for variable data (optional) - getVariableDataRoute?: (variableId: number) => string - - // Custom route for variable metadata (optional) - getVariableMetadataRoute?: (variableId: number) => string -} -``` - -### Default Routes - -By default, the library fetches data from: -- Data: `${baseUrl}/${variableId}.data.json` -- Metadata: `${baseUrl}/${variableId}.metadata.json` - -### Custom Routes - -```tsx -const config = { - dataApi: { - baseUrl: "https://api.example.com", - getVariableDataRoute: (id) => `/v2/variables/${id}/values`, - getVariableMetadataRoute: (id) => `/v2/variables/${id}/meta`, - }, -} -``` - -## Error Reporting - -Integrate with your error tracking service: - -```typescript -interface ChartsErrorReporting { - enabled: boolean - handler?: (error: Error, context: Record) => void -} -``` - -### Example with Sentry - -```tsx -import * as Sentry from "@sentry/react" - -const config = { - dataApi: { baseUrl: "..." }, - errorReporting: { - enabled: true, - handler: (error, context) => { - Sentry.captureException(error, { extra: context }) - }, - }, -} -``` - -## Analytics - -Track user interactions with charts: - -```typescript -interface ChartsAnalytics { - enabled: boolean - trackEvent?: (eventName: string, properties: Record) => void -} -``` - -### Example with Custom Analytics - -```tsx -const config = { - dataApi: { baseUrl: "..." }, - analytics: { - enabled: true, - trackEvent: (event, props) => { - // Send to your analytics service - myAnalytics.track(event, props) - }, - }, -} -``` - -## Custom Logos - -Add custom logos to display in charts: - -```typescript -interface LogoConfig { - svg: string // SVG markup as string - width: number // Logo width in pixels - height: number // Logo height in pixels - url?: string // Optional link URL -} -``` - -### Example - -```tsx -const config = { - branding: { - logos: { - primary: { - svg: `...`, - width: 120, - height: 40, - url: "https://example.com", - }, - }, - }, - dataApi: { baseUrl: "..." }, -} -``` - -## Full Example - -```tsx -import { ChartsProvider } from "@buildcanada/charts" - -const config = { - branding: { - poweredByText: "Build Canada", - poweredByUrl: "https://buildcanada.ca", - licenseText: "CC BY 4.0", - licenseUrl: "https://creativecommons.org/licenses/by/4.0/", - supportEmail: "data@buildcanada.ca", - }, - dataApi: { - baseUrl: "https://api.buildcanada.ca/v1/indicators/", - }, - errorReporting: { - enabled: process.env.NODE_ENV === "production", - handler: (error, context) => console.error(error, context), - }, - analytics: { - enabled: true, - trackEvent: (event, props) => { - window.gtag?.("event", event, props) - }, - }, -} - -function App() { - return ( - - {/* Your application */} - - ) -} -``` diff --git a/.storybook/docs/DataLoading.mdx b/.storybook/docs/DataLoading.mdx deleted file mode 100644 index 3da963953c2..00000000000 --- a/.storybook/docs/DataLoading.mdx +++ /dev/null @@ -1,297 +0,0 @@ -import { Meta } from "@storybook/addon-docs/blocks" - - - -# Data Loading Guide - -This guide explains how to use `@buildcanada/charts` with your own CSV or JSON data. - -## Data Format Requirements - -All charts require data with these mandatory columns: -- **Entity Name**: The name of the data series (e.g., country, category) -- **Year**: The time dimension -- **Value column(s)**: Your numeric data - -## Method 1: Using TestData Format (Recommended) - -The simplest approach uses the `createTestDataset` helper with a structured array: - -```tsx -import { - Grapher, - GrapherState, - createTestDataset, - legacyToChartsTableAndDimensionsWithMandatorySlug, - GRAPHER_CHART_TYPES, - DimensionProperty, -} from "@buildcanada/charts" -import type { TestData, TestMetadata } from "@buildcanada/charts" - -// Step 1: Define your data as TestData array -const myData: TestData = [ - { year: 2020, entity: { id: 1, name: "Canada", code: "CAN" }, value: 100 }, - { year: 2021, entity: { id: 1, name: "Canada", code: "CAN" }, value: 120 }, - { year: 2020, entity: { id: 2, name: "USA", code: "USA" }, value: 200 }, - { year: 2021, entity: { id: 2, name: "USA", code: "USA" }, value: 220 }, -] - -// Step 2: Define metadata for your indicator -const myMetadata: TestMetadata = { - id: 1, // unique identifier for this variable - display: { - name: "GDP Growth", - unit: "%", - shortUnit: "%", - numDecimalPlaces: 1, - }, -} - -// Step 3: Create the dataset and table -const variableId = myMetadata.id -const dimensions = [ - { variableId, property: DimensionProperty.y }, -] - -const dataset = createTestDataset([ - { data: myData, metadata: myMetadata }, -]) - -const table = legacyToChartsTableAndDimensionsWithMandatorySlug( - dataset, - dimensions, - {} // optional: color mapping for entities -) - -// Step 4: Create GrapherState -const grapherState = new GrapherState({ - title: "GDP Growth by Country", - dimensions, - chartTypes: [GRAPHER_CHART_TYPES.LineChart], -}) -grapherState.inputTable = table -``` - -## Method 2: Loading from JSON - -If you have JSON data from an API or file: - -```tsx -import { - Grapher, - GrapherState, - createTestDataset, - legacyToChartsTableAndDimensionsWithMandatorySlug, - GRAPHER_CHART_TYPES, - DimensionProperty, -} from "@buildcanada/charts" - -// Your JSON data -const jsonData = [ - { country: "Canada", year: 2020, gdp: 1.8 }, - { country: "Canada", year: 2021, gdp: 5.0 }, - { country: "USA", year: 2020, gdp: -3.4 }, - { country: "USA", year: 2021, gdp: 5.7 }, -] - -// Create entity mapping (id must be unique per entity) -const entityMap = new Map() -let entityId = 1 -jsonData.forEach((row) => { - if (!entityMap.has(row.country)) { - entityMap.set(row.country, { id: entityId++, name: row.country }) - } -}) - -// Transform to TestData format -const testData = jsonData.map((row) => ({ - year: row.year, - entity: entityMap.get(row.country)!, - value: row.gdp, -})) - -// Create metadata -const metadata = { - id: 1, - display: { - name: "GDP Growth Rate", - unit: "%", - numDecimalPlaces: 1, - }, -} - -// Build chart -const dimensions = [{ variableId: 1, property: DimensionProperty.y }] -const dataset = createTestDataset([{ data: testData, metadata }]) -const table = legacyToChartsTableAndDimensionsWithMandatorySlug(dataset, dimensions, {}) - -const grapherState = new GrapherState({ - title: "GDP Growth Rate", - dimensions, - chartTypes: [GRAPHER_CHART_TYPES.LineChart], -}) -grapherState.inputTable = table -``` - -## Method 3: Loading from CSV - -For CSV data, parse it first then convert to the TestData format: - -```tsx -import { parseDelimited } from "@buildcanada/charts" - -// CSV string (could be from file or fetch) -const csvString = `country,year,value -Canada,2020,100 -Canada,2021,120 -USA,2020,200 -USA,2021,220` - -// Parse CSV to rows -const rows = parseDelimited(csvString) as Array<{ - country: string - year: string - value: string -}> - -// Create entity mapping -const entityMap = new Map() -let entityId = 1 -rows.forEach((row) => { - if (!entityMap.has(row.country)) { - entityMap.set(row.country, { id: entityId++, name: row.country }) - } -}) - -// Convert to TestData format -const testData = rows.map((row) => ({ - year: parseInt(row.year, 10), - entity: entityMap.get(row.country)!, - value: parseFloat(row.value), -})) - -// Then create the chart as shown above... -``` - -### Loading CSV from a File (Browser) - -```tsx -async function loadCSVFile(file: File) { - const text = await file.text() - const rows = parseDelimited(text) - // Transform to TestData format... -} - -// Usage with file input - { - if (e.target.files?.[0]) { - loadCSVFile(e.target.files[0]) - } -}} /> -``` - -### Loading CSV from URL - -```tsx -async function loadCSVFromUrl(url: string) { - const response = await fetch(url) - const text = await response.text() - const rows = parseDelimited(text) - // Transform to TestData format... -} -``` - -## Using fakeEntities for Common Countries - -The library provides pre-defined entity mappings for ~200 countries and regions: - -```tsx -import { fakeEntities } from "@buildcanada/charts" - -// fakeEntities contains countries and regions with pre-assigned IDs -const canadaEntity = fakeEntities["Canada"] -const usaEntity = fakeEntities["United States"] - -// Use in your data -const testData = [ - { year: 2020, entity: fakeEntities["Canada"], value: 100 }, - { year: 2020, entity: fakeEntities["United States"], value: 200 }, -] -``` - -## Pre-selecting Entities - -To show specific entities by default: - -```tsx -const grapherState = new GrapherState({ - // ... - selectedEntityNames: ["Canada", "USA", "Germany"], -}) -``` - -## Entity Colors - -Assign custom colors to entities: - -```tsx -const entityColors = { - "Canada": "#ff0000", - "USA": "#0000ff", - "Germany": "#ffcc00", -} - -const table = legacyToChartsTableAndDimensionsWithMandatorySlug( - dataset, - dimensions, - entityColors // Pass colors here -) -``` - -## TypeScript Types - -Key types for working with data: - -```tsx -import type { - TestData, // Array of { year, entity, value } - TestMetadata, // Variable metadata with display options - EntityName, // string - EntityId, // number - EntityCode, // string -} from "@buildcanada/charts" - -// TestData structure -type TestData = Array<{ - year: number - entity: { id: EntityId; code?: EntityCode; name?: EntityName } - value: string | number -}> - -// TestMetadata structure -type TestMetadata = { - id: number - display?: { - name?: string - unit?: string - shortUnit?: string - numDecimalPlaces?: number - } -} -``` - -## Troubleshooting - -### Chart shows no data -- Ensure entity names match between your data and the chart -- Verify the year values are numbers, not strings -- Check that `dimensions` includes the correct `variableId` - -### Colors not applying -- Pass the color map to `legacyToChartsTableAndDimensionsWithMandatorySlug` -- Entity names in the color map must exactly match data entity names - -## Next Steps - -- [Chart Types](/docs/docs-chart-types--docs) - Configure different visualizations -- [Map Charts](/docs/docs-map-charts--docs) - Create geographic visualizations diff --git a/.storybook/docs/GettingStarted.mdx b/.storybook/docs/GettingStarted.mdx deleted file mode 100644 index 04e29503f75..00000000000 --- a/.storybook/docs/GettingStarted.mdx +++ /dev/null @@ -1,124 +0,0 @@ -import { Meta } from "@storybook/addon-docs/blocks" - - - -# Getting Started - -This guide walks you through setting up @buildcanada/charts in your React application. - -## Installation - -Install the package and its peer dependencies: - -```bash -# Using npm -npm install @buildcanada/charts react react-dom mobx mobx-react - -# Using bun -bun add @buildcanada/charts react react-dom mobx mobx-react -``` - -## Basic Setup - -### 1. Import Styles - -Import the chart styles in your application entry point: - -```tsx -import "@buildcanada/charts/styles.css" -``` - -### 2. Configure the Provider - -Wrap your application with `ChartsProvider`: - -```tsx -import { ChartsProvider } from "@buildcanada/charts" - -const config = { - branding: { - poweredByText: "My Organization", - licenseText: "CC BY 4.0", - licenseUrl: "https://creativecommons.org/licenses/by/4.0/", - }, - dataApi: { - baseUrl: "https://api.example.com/v1/indicators/", - }, -} - -function App() { - return ( - - {/* Your app content */} - - ) -} -``` - -### 3. Create a Chart - -Use the `Grapher` component with a `GrapherState`: - -```tsx -import { Grapher, GrapherState, Bounds, GRAPHER_CHART_TYPES } from "@buildcanada/charts" - -function MyChart() { - const grapherState = new GrapherState({ - bounds: new Bounds(0, 0, 800, 600), - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - title: "My Chart", - // ... chart configuration - }) - - return -} -``` - -## Using Sample Data - -For development and testing, you can use the included sample data: - -```tsx -import { Grapher, LifeExpectancyGrapher, Bounds, GRAPHER_CHART_TYPES } from "@buildcanada/charts" - -function DemoChart() { - const grapherState = LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - }) - - return -} -``` - -## Chart Types - -The library supports the following chart types: - -| Type | Value | Description | -|------|-------|-------------| -| Line Chart | `"LineChart"` | Time series with multiple entities | -| Discrete Bar | `"DiscreteBar"` | Horizontal bar chart | -| Stacked Area | `"StackedArea"` | Area chart with stacked series | -| Stacked Bar | `"StackedBar"` | Vertical stacked bar chart | -| Slope Chart | `"SlopeChart"` | Compare values between two time points | -| Scatter Plot | `"ScatterPlot"` | Two-variable comparison | -| Map | (tab: "map") | Geographic visualization | - -## Responsive Charts - -Charts automatically resize to fit their container. Wrap them in a sized container: - -```tsx -
- -
-``` - -## Next Steps - -- [Data Loading](/docs/docs-data-loading--docs) - Load your own CSV/JSON data -- [Chart Types](/docs/docs-chart-types--docs) - Configure different visualizations -- [Map Charts](/docs/docs-map-charts--docs) - Geographic visualizations -- [Configuration](/docs/docs-configuration--docs) - Customize branding and data sources -- [Charts/Grapher](/docs/charts-grapher--docs) - View chart type examples diff --git a/.storybook/docs/Introduction.mdx b/.storybook/docs/Introduction.mdx index 0d31015f81e..416f4b6cb1b 100644 --- a/.storybook/docs/Introduction.mdx +++ b/.storybook/docs/Introduction.mdx @@ -4,15 +4,19 @@ import { Meta } from "@storybook/addon-docs/blocks" # @buildcanada/charts -A configurable data visualization library for creating interactive charts. +Build Canada charts: a pure-function layout core with a single React SVG +renderer, deterministic headless rendering, and a CLI. ## Features -- **Multiple Chart Types**: Line charts, bar charts, stacked area, slope charts, scatter plots, and maps -- **Interactive**: Tooltips, zoom, entity selection, and time sliders -- **Configurable**: Custom branding, colors, and data sources -- **Responsive**: Adapts to container size -- **Accessible**: Keyboard navigation and screen reader support +- **Multiple Chart Types**: Line, slope, discrete bar, stacked area/bar, + dumbbell, scatter, and Marimekko charts, with faceting +- **Pure layout core**: DOM-free, React-free `layoutChart()` produces a + `ChartScene` rendered by a single `SceneSVG` renderer +- **Deterministic headless rendering**: The CLI renders the exact same SVG as + the browser, suitable for golden-corpus testing and static export +- **Interactive chrome**: Tooltips, timeline, entity selector, tabs, settings, + and data table ## Quick Start @@ -23,41 +27,47 @@ bun add @buildcanada/charts ``` ```tsx -import { ChartsProvider, Grapher, GrapherState } from "@buildcanada/charts" +import { buildDataset, parseCsv, parseDefinition, parseManifest } from "@buildcanada/charts/core" +import { Chart } from "@buildcanada/charts" import "@buildcanada/charts/styles.css" -const config = { - branding: { poweredByText: "My Organization" }, - dataApi: { baseUrl: "https://api.example.com/v1/indicators/" } -} - +// 1. Dataset: manifest + CSV → Dataset. +const { manifest } = parseManifest(rawManifestJson) +const { rows } = parseCsv(csvText, manifest!) +const { dataset } = buildDataset(manifest!, rows) + +// 2. Definition: title + data + y is a publishable chart; +// everything else is progressive refinement with documented defaults. +const { definition, diagnostics } = parseDefinition({ + title: "Provincial budget spending", + data: "provincial-budgets", + y: ["total_spending"], + selectedEntities: ["Ontario", "Quebec", "British Columbia"], +}) +if (definition === null) throw new Error(diagnostics.map((d) => d.message).join("; ")) + +// 3. Render. function App() { - return ( - - - - ) + return } ``` +## CLI + +The package ships a `charts` binary for validating and rendering chart +definitions headlessly: + +```bash +charts validate my-chart.json +charts render my-chart.json --out chart.svg +``` + ## Peer Dependencies This library requires the following peer dependencies: - `react` ^19.0.0 - `react-dom` ^19.0.0 -- `mobx` ^6.13.0 -- `mobx-react` ^7.6.0 - -## Browse the Documentation - -- **[Getting Started](/docs/docs-getting-started--docs)** - Setup and basic usage -- **[Data Loading](/docs/docs-data-loading--docs)** - Load CSV/JSON data into charts -- **[Chart Types](/docs/docs-chart-types--docs)** - Line, bar, scatter, and more -- **[Map Charts](/docs/docs-map-charts--docs)** - Geographic visualizations -- **[Configuration](/docs/docs-configuration--docs)** - Customize branding and data sources -- **[Charts/Grapher](/docs/charts-grapher--docs)** - Main chart component examples -- **[Charts/Explorer](/docs/charts-explorer--docs)** - Data explorer with filters ## License diff --git a/.storybook/docs/MapCharts.mdx b/.storybook/docs/MapCharts.mdx deleted file mode 100644 index fc090072bde..00000000000 --- a/.storybook/docs/MapCharts.mdx +++ /dev/null @@ -1,288 +0,0 @@ -import { Meta } from "@storybook/addon-docs/blocks" - - - -# Map Charts - -Create geographic visualizations with choropleth maps. - -## Basic World Map - -To display a world map, set `hasMapTab: true` and `tab: "map"`: - -```tsx -import { - GrapherState, - DimensionProperty, - MapRegionName, - ColorSchemeName, -} from "@buildcanada/charts" - -const grapherState = new GrapherState({ - title: "Life Expectancy by Country", - dimensions: [{ variableId: 1, property: DimensionProperty.y }], - hasMapTab: true, - tab: "map", - map: { - region: MapRegionName.World, - colorScale: { - baseColorScheme: ColorSchemeName.Blues, - }, - }, -}) -grapherState.inputTable = table -``` - -## Map Regions - -The library supports several map regions: - -```tsx -import { MapRegionName } from "@buildcanada/charts" - -MapRegionName.World // Global map -MapRegionName.Africa // African continent -MapRegionName.Asia // Asian continent -MapRegionName.Europe // European continent -MapRegionName.NorthAmerica // North America -MapRegionName.SouthAmerica // South America -MapRegionName.Oceania // Oceania region -MapRegionName.Canada // Canadian provinces/territories -``` - -## Canada Provincial Map - -For Canadian provincial/territorial data: - -```tsx -const grapherState = new GrapherState({ - title: "Population by Province", - dimensions: [{ variableId: 1, property: DimensionProperty.y }], - hasMapTab: true, - tab: "map", - map: { - region: MapRegionName.Canada, - colorScale: { - baseColorScheme: ColorSchemeName.Reds, - }, - }, -}) -grapherState.inputTable = table -``` - -**Important**: For Canada maps, entity names must match province/territory names exactly: -- Ontario -- Quebec -- British Columbia -- Alberta -- Manitoba -- Saskatchewan -- Nova Scotia -- New Brunswick -- Newfoundland and Labrador -- Prince Edward Island -- Northwest Territories -- Yukon -- Nunavut - -## Color Schemes - -Available color schemes for maps: - -```tsx -import { ColorSchemeName } from "@buildcanada/charts" - -// Sequential (single hue) -ColorSchemeName.Blues -ColorSchemeName.Greens -ColorSchemeName.Reds -ColorSchemeName.Oranges -ColorSchemeName.Purples -ColorSchemeName.Greys - -// Sequential (multi-hue) -ColorSchemeName.YlGn // Yellow-Green -ColorSchemeName.YlGnBu // Yellow-Green-Blue -ColorSchemeName.GnBu // Green-Blue -ColorSchemeName.BuGn // Blue-Green -ColorSchemeName.PuBuGn // Purple-Blue-Green -ColorSchemeName.PuBu // Purple-Blue -ColorSchemeName.BuPu // Blue-Purple -ColorSchemeName.RdPu // Red-Purple -ColorSchemeName.PuRd // Purple-Red -ColorSchemeName.OrRd // Orange-Red -ColorSchemeName.YlOrRd // Yellow-Orange-Red -ColorSchemeName.YlOrBr // Yellow-Orange-Brown - -// Diverging -ColorSchemeName.RdYlBu // Red-Yellow-Blue -ColorSchemeName.RdYlGn // Red-Yellow-Green -ColorSchemeName.BrBG // Brown-Blue-Green -ColorSchemeName.PRGn // Purple-Green -ColorSchemeName.PiYG // Pink-Yellow-Green -ColorSchemeName.RdBu // Red-Blue -ColorSchemeName.RdGy // Red-Grey -ColorSchemeName.PuOr // Purple-Orange -``` - -## Map Configuration Options - -### Color Scale - -```tsx -const grapherState = new GrapherState({ - // ... - map: { - region: MapRegionName.World, - colorScale: { - baseColorScheme: ColorSchemeName.Blues, - binningStrategy: "manual", // or "equalInterval", "quantile" - customNumericValues: [10, 25, 50, 75, 100], // Custom bin boundaries - customNumericLabels: ["Low", "Medium-Low", "Medium", "Medium-High", "High"], - }, - }, -}) -``` - -### Time Configuration - -```tsx -const grapherState = new GrapherState({ - // ... - map: { - region: MapRegionName.World, - time: 2020, // Show data for specific year - // Or use timeTolerance to show nearest available data - timeTolerance: 5, - }, -}) -``` - -### Projection - -```tsx -const grapherState = new GrapherState({ - // ... - map: { - region: MapRegionName.World, - projection: "Robinson", // Default world projection - }, -}) -``` - -## Entity Name Requirements - -For maps to display correctly, entity names in your data must match the geographic names expected by the map: - -### World Map Entity Names - -Use standard country names: -- "United States" (not "USA") -- "United Kingdom" (not "UK") -- "South Korea" (not "Korea") -- "Democratic Republic of Congo" (full name) - -### Example: Creating Map Data - -```tsx -import { fakeEntities } from "@buildcanada/charts" - -// Use fakeEntities for correct country names and IDs -const mapData = [ - { year: 2020, entity: fakeEntities["Canada"], value: 82 }, - { year: 2020, entity: fakeEntities["United States"], value: 78 }, - { year: 2020, entity: fakeEntities["Germany"], value: 81 }, - { year: 2020, entity: fakeEntities["Japan"], value: 84 }, -] -``` - -## Complete Map Example - -```tsx -import { - ChartsProvider, - Grapher, - GrapherState, - createTestDataset, - legacyToChartsTableAndDimensionsWithMandatorySlug, - fakeEntities, - DimensionProperty, - MapRegionName, - ColorSchemeName, - Bounds, -} from "@buildcanada/charts" -import "@buildcanada/charts/styles.css" - -// Sample data for a few countries -const lifeExpData = [ - { year: 2020, entity: fakeEntities["Canada"], value: 82.2 }, - { year: 2020, entity: fakeEntities["United States"], value: 78.9 }, - { year: 2020, entity: fakeEntities["Germany"], value: 81.3 }, - { year: 2020, entity: fakeEntities["Japan"], value: 84.6 }, - { year: 2020, entity: fakeEntities["Brazil"], value: 75.9 }, - { year: 2020, entity: fakeEntities["India"], value: 70.4 }, - { year: 2020, entity: fakeEntities["China"], value: 77.1 }, - { year: 2020, entity: fakeEntities["Nigeria"], value: 54.7 }, -] - -const metadata = { - id: 1, - display: { - name: "Life expectancy at birth", - unit: "years", - numDecimalPlaces: 1, - }, -} - -const dimensions = [{ variableId: 1, property: DimensionProperty.y }] -const dataset = createTestDataset([{ data: lifeExpData, metadata }]) -const table = legacyToChartsTableAndDimensionsWithMandatorySlug(dataset, dimensions, {}) - -const grapherState = new GrapherState({ - title: "Life Expectancy by Country (2020)", - dimensions, - bounds: new Bounds(0, 0, 800, 600), - hasMapTab: true, - tab: "map", - map: { - region: MapRegionName.World, - colorScale: { - baseColorScheme: ColorSchemeName.YlGnBu, - }, - }, -}) -grapherState.inputTable = table - -function WorldMapChart() { - return ( - -
- -
-
- ) -} -``` - -## Troubleshooting - -### Map shows blank regions -- Entity names must exactly match geographic region names -- Check for typos or alternative spellings -- Use `fakeEntities` to ensure correct names - -### Colors not showing -- Ensure data values are numeric -- Check that the year in your data matches the map's time setting -- Verify the color scale configuration - -### Wrong projection -- Different regions use different default projections -- World maps use Robinson projection -- Continental maps use appropriate regional projections - -## Next Steps - -- [Data Loading](/docs/docs-data-loading--docs) - Load your own data -- [Chart Types](/docs/docs-chart-types--docs) - Other visualization options -- [Charts/Grapher](/docs/charts-grapher--docs) - Interactive examples diff --git a/.storybook/helpers/grapherStateFactory.ts b/.storybook/helpers/grapherStateFactory.ts deleted file mode 100644 index 2ff327797a5..00000000000 --- a/.storybook/helpers/grapherStateFactory.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { Bounds } from "../../packages/charts/src/utils/index.js" -import { GrapherState } from "../../packages/charts/src/grapher/core/GrapherState.js" -import { - LifeExpectancyGrapher, - CanadaProvinceGrapher, -} from "../../packages/charts/src/grapher/testData/TestData.sample.js" -import type { GrapherProgrammaticInterface } from "../../packages/charts/src/grapher/core/Grapher.js" -import { - ColorSchemeName, - GRAPHER_CHART_TYPES, - MapRegionName, -} from "../../packages/charts/src/types/index.js" - -/** - * Create a GrapherState configured for Storybook stories. - * Uses the LifeExpectancyGrapher sample data. - */ -export function createStorybookGrapherState( - overrides: Partial = {} -): GrapherState { - return LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - selectedEntityNames: ["Canada", "United States", "Germany", "Japan"], - ...overrides, - }) -} - -/** - * Create a GrapherState for a LineChart story - */ -export function createLineChartState( - overrides: Partial = {} -): GrapherState { - return LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - selectedEntityNames: ["Canada", "United States", "Germany", "Japan"], - ...overrides, - }) -} - -/** - * Create a GrapherState for a DiscreteBarChart story - */ -export function createDiscreteBarChartState( - overrides: Partial = {} -): GrapherState { - return LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - chartTypes: [GRAPHER_CHART_TYPES.DiscreteBar], - selectedEntityNames: [ - "Japan", - "Canada", - "Germany", - "United States", - "Brazil", - "China", - "India", - "Nigeria", - ], - ...overrides, - }) -} - -/** - * Create a GrapherState for a StackedArea chart story - */ -export function createStackedAreaChartState( - overrides: Partial = {} -): GrapherState { - return LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - chartTypes: [GRAPHER_CHART_TYPES.StackedArea], - selectedEntityNames: ["Canada", "United States", "Germany", "Japan", "Brazil"], - ...overrides, - }) -} - -/** - * Create a GrapherState for a StackedBar chart story - */ -export function createStackedBarChartState( - overrides: Partial = {} -): GrapherState { - return LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - chartTypes: [GRAPHER_CHART_TYPES.StackedBar], - selectedEntityNames: ["Canada", "United States", "Germany", "Japan", "Brazil"], - ...overrides, - }) -} - -/** - * Create a GrapherState for a SlopeChart story - */ -export function createSlopeChartState( - overrides: Partial = {} -): GrapherState { - return LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - chartTypes: [GRAPHER_CHART_TYPES.SlopeChart], - selectedEntityNames: ["Canada", "United States", "Germany", "Japan", "Brazil", "China"], - ...overrides, - }) -} - -/** - * Create a GrapherState for a ScatterPlot story - */ -export function createScatterPlotState( - overrides: Partial = {} -): GrapherState { - return LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - chartTypes: [GRAPHER_CHART_TYPES.ScatterPlot], - selectedEntityNames: ["Canada", "United States", "Germany", "Japan", "Brazil", "China", "India"], - ...overrides, - }) -} - -/** - * Create a GrapherState for a WorldMap story - * Uses Blues color scheme for standard styling - */ -export function createMapChartState( - overrides: Partial = {} -): GrapherState { - return LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - hasMapTab: true, - tab: "map", - map: { - colorScale: { - baseColorScheme: ColorSchemeName.Blues, - }, - }, - ...overrides, - }) -} - -/** - * Create a GrapherState for a Canada provincial map story - * Shows Canadian provinces/territories with population data - */ -export function createCanadaMapChartState( - overrides: Partial = {} -): GrapherState { - return CanadaProvinceGrapher({ - bounds: new Bounds(0, 0, 1100, 660), - hasMapTab: true, - tab: "map", - map: { - region: MapRegionName.Canada, - colorScale: { - baseColorScheme: ColorSchemeName.Blues, - }, - }, - ...overrides, - }) -} diff --git a/.storybook/main.ts b/.storybook/main.ts index b40bc77e69a..1a57ba190ce 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -5,7 +5,6 @@ const config: StorybookConfig = { stories: [ "./docs/**/*.mdx", "../packages/charts/src/**/*.stories.@(ts|tsx)", - "../packages/charts2/src/**/*.stories.@(ts|tsx)", "../packages/colours/src/**/*.stories.@(ts|tsx)", "../packages/components/src/**/*.stories.@(ts|tsx)", ], @@ -59,20 +58,11 @@ const config: StorybookConfig = { // Include d3 packages to ensure they're properly bundled include: [ ...(config.optimizeDeps?.include || []), - "d3", "d3-array", - "d3-color", - "d3-ease", - "d3-force", + "d3-dsv", "d3-format", - "d3-geo", - "d3-interpolate", - "d3-quadtree", "d3-scale", - "d3-selection", "d3-shape", - "d3-transition", - "d3-zoom", ], // Keep local workspace packages out of Vite's optimized-deps // cache so Storybook sees newly added exports immediately. diff --git a/.storybook/preview.ts b/.storybook/preview.ts index f3a0bdfd286..1d7cbe00e94 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,12 +1,9 @@ import type { Preview } from "@storybook/react" -import { ChartsDecorator } from "./decorators/ChartsDecorator" - -import "../packages/charts/src/styles/charts.scss" +import "../packages/charts/src/react/styles/charts.scss" import "../packages/components/src/styles/main.scss" const preview: Preview = { - decorators: [ChartsDecorator], tags: ["autodocs"], parameters: { controls: { diff --git a/CLAUDE.md b/CLAUDE.md index 2a69b3e2961..b84d3dd33fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project: @buildcanada/charts -A configurable data visualization library for creating interactive charts. +Build Canada charts: a pure-function layout core with a single React SVG renderer, deterministic headless rendering, and a CLI. Functional and architectural specs live in `specs/` (architecture: `specs/28-architecture.md`). ## Commands @@ -12,84 +12,44 @@ A configurable data visualization library for creating interactive charts. bun install # Install dependencies bun run storybook # Run Storybook on port 6006 bun run build # Build Storybook for production +bun run build:packages # Build all workspace packages bun run serve-storybook # Serve production build on port 6006 bun test # Run all tests -bun test src/path/to/file.test.ts # Run single test file +bun test src/path/to/file.test.ts # Run single test file (from the package dir) bun run typecheck # TypeScript check +bun run charts -- # Run the charts CLI from source ``` ## Architecture -- **React 19** with **MobX 6** for state management -- Uses TC-39 stage 3 decorators for `@computed` and `@action` -- Observable props are listed in `makeObservable()` calls, not decorated with `@observable` -- **Vitest** for testing with jsdom environment +The charts package (`packages/charts`) is split into a DOM-free core and a thin React layer: -## File Structure +- `src/core/` — pure functions, no DOM or React: data layer, chart definition parsing, formatting, colour/theming, text metrics, layout. `layoutChart({ definition, dataset, size })` produces a `ChartScene`. +- `src/react/` — `SceneSVG` (the only renderer) + `Chart` + interactive chrome (tooltip, timeline, entity selector, tabs, settings, data table). +- `src/cli/` — `charts render|validate|scaffold|install-skill`; renders the exact same SVG as the browser via `renderToStaticMarkup`. +- `src/fixtures/` — committed fixture datasets, loadable by name in tests/stories/CLI. +- `src/corpus/` — golden SVG corpus + bless script (`bun run corpus:bless`). +- `src/stories/` — Storybook stories under `Charts/`. +- `samples/` — CLI-ready chart definitions, tested against included fixtures. -``` -src/ -├── components/ # Reusable UI components (TextWrap, MarkdownTextWrap, etc.) -├── config/ # ChartsProvider context and configuration -├── core-table/ # Data table handling (ChartsTable, CoreTable) -├── explorer/ # Explorer component (data explorer UI) -├── grapher/ # Main charting engine -│ ├── axis/ # Axis rendering -│ ├── barCharts/ # Bar chart implementations -│ ├── chart/ # Chart interface and type mapping -│ ├── color/ # Color scales and schemes -│ ├── controls/ # Interactive controls (EntityPicker, Dropdown) -│ ├── core/ # Core graphing engine (Grapher, GrapherState) -│ ├── lineCharts/ # Line chart implementations -│ ├── mapCharts/ # Map visualization -│ ├── scatterCharts/ # Scatter plot implementations -│ ├── stackedCharts/ # Stacked area/bar charts -│ └── ... -├── styles/ # Global SCSS styles (entry: charts.scss) -├── types/ # TypeScript type definitions -└── utils/ # Utility functions -``` - -## Key Components +## Workspace Packages -- **Grapher**: Main charting component with multiple visualization types (line, bar, scatter, map, stacked) -- **GrapherState**: MobX-powered state management for Grapher -- **Explorer**: Data explorer that wraps Grapher and adds additional controls -- **ChartsProvider**: React context for global chart configuration (branding, data API, error reporting) +- `packages/charts` — the charting library and CLI (`@buildcanada/charts`) +- `packages/colours` — colour palettes and themes (`@buildcanada/colours`) +- `packages/components` — shared UI primitives and styles (`@buildcanada/components`) ## Code Style - Double quotes for string literals - Use type definitions for function params and return values -- Avoid the `any` type (but note: `noImplicitAny` is disabled in tsconfig) +- Avoid the `any` type +- Core code must stay DOM-free and React-free; rendering happens only in `src/react/` - BEM conventions for CSS in separate .scss files -- Entry point for styles: `src/styles/charts.scss` +- Entry point for styles: `src/react/styles/charts.scss` - Tests use Vitest with `it()` and `expect()` from `vitest` -## MobX Pattern - -```typescript -class MyComponent { - someObservable = "value" - - constructor() { - makeObservable(this, { - someObservable: observable, - }) - } - - @computed get derivedValue() { - return this.someObservable.toUpperCase() - } -} -``` - -**Important**: Don't use `@computed` on getters that access `this.props` - props aren't observable in mobx-react. - ## Peer Dependencies This package expects the following to be provided by the consuming application: - `react` ^19.0.0 - `react-dom` ^19.0.0 -- `mobx` ^6.13.0 -- `mobx-react` ^7.6.0 diff --git a/README.md b/README.md index 7b04166f7db..7d6e8bc1690 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Part of the [Build Canada Design System](https://github.com/BuildCanada/bcds) monorepo. -A configurable data visualization library for creating interactive charts. +Build Canada charts: a pure-function layout core with a single React SVG renderer, deterministic headless rendering, and a CLI. Specs live in `specs/` (architecture: `specs/28-architecture.md`). ## Installation @@ -19,59 +19,55 @@ This library requires the following peer dependencies: ```json { "react": "^19.0.0", - "react-dom": "^19.0.0", - "mobx": "^6.13.0", - "mobx-react": "^7.6.0" + "react-dom": "^19.0.0" } ``` ## Quick Start ```tsx -import { - ChartsProvider, - Grapher, - GrapherState, - GRAPHER_CHART_TYPES, - DimensionProperty, - createTestDataset, - legacyToChartsTableAndDimensionsWithMandatorySlug, -} from "@buildcanada/charts" +import { buildDataset, parseCsv, parseDefinition, parseManifest } from "@buildcanada/charts/core" +import { Chart, Tooltip } from "@buildcanada/charts" import "@buildcanada/charts/styles.css" -// Define your data -const myData = [ - { year: 2020, entity: { id: 1, name: "Canada" }, value: 100 }, - { year: 2021, entity: { id: 1, name: "Canada" }, value: 120 }, -] - -const metadata = { id: 1, display: { name: "My Metric" } } -const dimensions = [{ variableId: 1, property: DimensionProperty.y }] - -const dataset = createTestDataset([{ data: myData, metadata }]) -const table = legacyToChartsTableAndDimensionsWithMandatorySlug(dataset, dimensions, {}) - -const grapherState = new GrapherState({ - title: "My Chart", - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - dimensions, +// 1. Dataset: manifest + CSV → Dataset. +const { manifest } = parseManifest(rawManifestJson) +const { rows } = parseCsv(csvText, manifest!) +const { dataset } = buildDataset(manifest!, rows) + +// 2. Definition: title + data + y is a publishable chart; +// everything else is progressive refinement with documented defaults. +const { definition, diagnostics } = parseDefinition({ + title: "Provincial budget spending", + data: "provincial-budgets", + y: ["total_spending"], + selectedEntities: ["Ontario", "Quebec", "British Columbia"], }) -grapherState.inputTable = table +if (definition === null) throw new Error(diagnostics.map((d) => d.message).join("; ")) +// 3. Render. function App() { - return ( - -
- -
-
- ) + return } ``` +## CLI + +The package ships a `charts` binary for validating and rendering chart definitions headlessly: + +```bash +npm install -g @buildcanada/charts + +charts validate my-chart.json +charts render my-chart.json --out chart.svg +``` + +See `packages/charts/README.md` for the full CLI reference. + ## Documentation -- **[Data Loading Guide](docs/DATA_LOADING_GUIDE.md)** - Complete guide on loading CSV/JSON data +- **[Charts README](packages/charts/README.md)** - Package layout, quick start, and CLI reference +- **[Specs](specs/)** - Functional and architectural specifications - **[Publishing Guide](docs/PUBLISHING.md)** - How to publish to npm ## Development @@ -84,29 +80,22 @@ bun test # Run tests bun run typecheck # TypeScript check ``` -## Components +## Chart Types -### Grapher - -The main charting component supporting multiple visualization types: -- Line charts -- Bar charts (discrete and stacked) -- Scatter plots -- World and regional maps -- Stacked area charts +- Line charts (with comparison lines and faceting) - Slope charts +- Discrete bar charts (plain and stacked) +- Stacked area and stacked bar charts +- Dumbbell charts +- Scatter plots - Marimekko charts -### Explorer - -A data explorer that wraps Grapher and adds additional controls for exploring complex datasets. - ## Architecture -- Built with **React 19** and **MobX 6** for state management -- Uses TC-39 stage 3 decorators -- TypeScript throughout -- SCSS for styling with BEM conventions +- `src/core/` — DOM-free, React-free: data layer, formatting, themes, text metrics, layout → `ChartScene` +- `src/react/` — `SceneSVG` (the only renderer) + `Chart` + interactive chrome (tooltip, timeline, entity selector, tabs, settings, data table) +- CLI — `charts render|validate` shares the exact same render path via `renderToStaticMarkup` +- TypeScript throughout, SCSS for styling with BEM conventions ## License diff --git a/bun.lock b/bun.lock index b7ee08e8fa9..29fa171a4ab 100644 --- a/bun.lock +++ b/bun.lock @@ -346,92 +346,13 @@ }, "packages/charts": { "name": "@buildcanada/charts", - "version": "0.3.9", - "dependencies": { - "@buildcanada/colours": "^0.3.3", - "@buildcanada/components": "^0.3.4", - "@fortawesome/fontawesome-svg-core": "^6.7.2", - "@fortawesome/free-brands-svg-icons": "^6.7.2", - "@fortawesome/free-solid-svg-icons": "^6.7.2", - "@fortawesome/react-fontawesome": "^0.2.2", - "@tippyjs/react": "^4.2.6", - "@turf/boolean-intersects": "^7.2.0", - "@turf/center": "^7.2.0", - "classnames": "^2.5.1", - "colorbrewer": "^1.3.0", - "d3": "^7.9.0", - "d3-transition": "^3.0.1", - "dayjs": "^1.11.11", - "fparser": "^3.1.0", - "fuzzysort": "^3.1.0", - "indefinite": "^2.5.1", - "js-cookie": "^3.0.5", - "lodash-es": "^4.17.21", - "mdast-util-find-and-replace": "^3.0.2", - "mdast-util-from-markdown": "^2.0.2", - "mousetrap": "^1.6.5", - "papaparse": "^5.5.3", - "react-aria-components": "^1.13.0", - "react-flip-toolkit": "^7.2.4", - "react-markdown": "^10.1.0", - "react-move": "^6.5.0", - "react-select": "^5.10.2", - "remeda": "^2.32.0", - "simple-statistics": "^7.8.8", - "string-pixel-width": "^1.10.0", - "striptags": "^3.2.0", - "topojson-client": "^3.1.0", - "ts-pattern": "^5.8.0", - "unist-util-visit": "^5.0.0", - "url-join": "^5.0.0", - "url-parse": "^1.5.10", - "url-slug": "^4.0.1", - "uuidv7": "^1.0.1", - "versor": "^0.2.0", - }, - "devDependencies": { - "@storybook/react-dom-shim": "^10.1.11", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.0", - "@types/colorbrewer": "^1.0.29", - "@types/d3": "^7.4.3", - "@types/geojson": "^7946.0.10", - "@types/indefinite": "^2.3.4", - "@types/js-cookie": "^3.0.6", - "@types/lodash-es": "^4", - "@types/mousetrap": "^1.6.9", - "@types/papaparse": "^5.3.16", - "@types/string-pixel-width": "^1.7.2", - "@types/topojson-client": "^3.1.1", - "@types/url-parse": "^1.4.8", - "debug": "^4.4.3", - "esbuild": "^0.27.2", - "eslint": "^9.37.0", - "eslint-plugin-react-hooks": "^5.2.0", - "happy-dom": "^20.1.0", - "jsdom": "^27.4.0", - "object-assign": "^4.1.1", - "side-channel": "^1.1.0", - "timezone-mock": "^1.3.6", - "vitest": "^4.0.15", - }, - "peerDependencies": { - "mobx": "^6.13.0", - "mobx-react": "^7.6.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - }, - }, - "packages/charts2": { - "name": "@buildcanada/charts2", - "version": "0.1.0", + "version": "1.0.0", "bin": { "charts": "./dist/cli/index.js", }, "dependencies": { - "@buildcanada/colours": "^0.3.3", - "@buildcanada/components": "^0.3.5", + "@buildcanada/colours": "^1.0.0", + "@buildcanada/components": "^1.0.0", "@resvg/resvg-js": "^2.6.2", "citty": "^0.1.6", "d3-array": "^3.2.4", @@ -464,7 +385,7 @@ }, "packages/colours": { "name": "@buildcanada/colours", - "version": "0.3.3", + "version": "1.0.0", "devDependencies": { "@types/bun": "latest", }, @@ -474,7 +395,7 @@ }, "packages/components": { "name": "@buildcanada/components", - "version": "0.3.5", + "version": "1.0.0", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", @@ -585,8 +506,6 @@ "@buildcanada/charts": ["@buildcanada/charts@workspace:packages/charts"], - "@buildcanada/charts2": ["@buildcanada/charts2@workspace:packages/charts2"], - "@buildcanada/colours": ["@buildcanada/colours@workspace:packages/colours"], "@buildcanada/components": ["@buildcanada/components@workspace:packages/components"], @@ -625,57 +544,57 @@ "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], @@ -1215,70 +1134,20 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - "@types/colorbrewer": ["@types/colorbrewer@1.6.0", "", { "dependencies": { "colorbrewer": "*" } }, "sha512-VHAEBRfLKoEEFjFjze+JagXhqwv4LJgUpGSuh83qQr4ZncbvU/ErT84hy9Xqwv/ZlTZtkzPcW55ZtPeiXwyCKw=="], - - "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], - "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], - - "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], - - "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], - - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - - "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], - - "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], - - "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], - - "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], - "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - - "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], - - "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], - "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], - "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="], - - "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], - - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], - - "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], - - "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="], - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], - - "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], - "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], - - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - - "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], - - "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], @@ -1299,28 +1168,16 @@ "@types/hogan.js": ["@types/hogan.js@3.0.5", "", {}, "sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA=="], - "@types/indefinite": ["@types/indefinite@2.3.4", "", {}, "sha512-X9sp9nbqkZT7hFNUKxicGr6gZLahs95lryW2Vvx5+krDxhWWle4K1OfJ4b8Sjd+FLul3tGhfYehtfeoFF1Ot4g=="], - - "@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="], - - "@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], - "@types/mousetrap": ["@types/mousetrap@1.6.15", "", {}, "sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="], - "@types/papaparse": ["@types/papaparse@5.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="], - "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], @@ -1333,16 +1190,8 @@ "@types/resolve": ["@types/resolve@1.20.6", "", {}, "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ=="], - "@types/string-pixel-width": ["@types/string-pixel-width@1.11.0", "", {}, "sha512-cJM2KRpVweTaxoGhnrhMG0bP9BjcYUaSkBF1bMdirrY5eKvTvCs4YrSeEem/ngr9HjKkr64dAODjCpx2RWw03Q=="], - - "@types/topojson-client": ["@types/topojson-client@3.1.5", "", { "dependencies": { "@types/geojson": "*", "@types/topojson-specification": "*" } }, "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw=="], - - "@types/topojson-specification": ["@types/topojson-specification@1.0.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@types/url-parse": ["@types/url-parse@1.4.11", "", {}, "sha512-FKvKIqRaykZtd4n47LbK/W/5fhQQ1X7cxxzG9A48h0BGN+S04NH7ervcCjM8tyR0lyGru83FAHSmw2ObgKoESg=="], - "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -1697,7 +1546,7 @@ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -1995,7 +1844,7 @@ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -2179,7 +2028,7 @@ "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -2427,8 +2276,6 @@ "time-zone": ["time-zone@1.0.0", "", {}, "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA=="], - "timezone-mock": ["timezone-mock@1.3.6", "", {}, "sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg=="], - "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], @@ -2599,28 +2446,16 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + + "@asamuzakjp/dom-selector/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@buildcanada/charts/@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.7.2", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" } }, "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA=="], - - "@buildcanada/charts/@fortawesome/free-brands-svg-icons": ["@fortawesome/free-brands-svg-icons@6.7.2", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" } }, "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q=="], - - "@buildcanada/charts/@fortawesome/free-solid-svg-icons": ["@fortawesome/free-solid-svg-icons@6.7.2", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" } }, "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA=="], - - "@buildcanada/charts/@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@0.2.6", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7", "react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg=="], - - "@buildcanada/charts/@storybook/react-dom-shim": ["@storybook/react-dom-shim@10.1.11", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "storybook": "^10.1.11" } }, "sha512-o8WPhRlZbORUWG9lAgDgJP0pi905VHJUFJr1Kp8980gHqtlemtnzjPxKy5vFwj6glNhAlK8SS8OOYzWP7hloTQ=="], - - "@buildcanada/charts/fparser": ["fparser@3.1.0", "", {}, "sha512-P9hS9RjO7l4JvWHcDUqos0BXAGzJN4WwJBCh7gwja/23TuW7jfpOKZ+jlGoYp4ZUDnbAJ+rDyKLkIJFCLzgZ+w=="], - - "@buildcanada/charts/side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - "@buildcanada/colours/@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@buildcanada/components/@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.7.2", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" } }, "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA=="], @@ -2669,8 +2504,6 @@ "@turf/line-intersect/sweepline-intersections": ["sweepline-intersections@1.5.0", "", { "dependencies": { "tinyqueue": "^2.0.0" } }, "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ=="], - "@types/papaparse/@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], - "@types/ws/@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], "@vercel/nft/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -2709,6 +2542,8 @@ "cosmiconfig/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "cssstyle/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "data-urls/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], @@ -2735,8 +2570,6 @@ "happy-dom/@types/node": ["@types/node@20.19.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g=="], - "hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -2747,6 +2580,8 @@ "is-regex/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], + "jsdom/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], "kapellmeister/d3-timer": ["d3-timer@1.0.10", "", {}, "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="], @@ -2765,6 +2600,8 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -2799,8 +2636,6 @@ "storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], - "storybook/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -2815,8 +2650,6 @@ "topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "vite-node/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "vite-node/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], @@ -2839,14 +2672,6 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@buildcanada/charts/@fortawesome/fontawesome-svg-core/@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@6.7.2", "", {}, "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="], - - "@buildcanada/charts/@fortawesome/free-brands-svg-icons/@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@6.7.2", "", {}, "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="], - - "@buildcanada/charts/@fortawesome/free-solid-svg-icons/@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@6.7.2", "", {}, "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="], - - "@buildcanada/charts/side-channel/side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], - "@buildcanada/colours/@types/bun/bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "@buildcanada/components/@fortawesome/fontawesome-svg-core/@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@6.7.2", "", {}, "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg=="], @@ -2877,8 +2702,6 @@ "@storybook/test/@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], - "@types/papaparse/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "@vercel/nft/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], @@ -2925,58 +2748,6 @@ "storybook/@vitest/spy/tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], - "storybook/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "storybook/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "storybook/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "storybook/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "storybook/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "storybook/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "storybook/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "storybook/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "storybook/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "storybook/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "storybook/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "storybook/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "storybook/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "storybook/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "storybook/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "storybook/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "storybook/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "storybook/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "storybook/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "storybook/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "storybook/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "storybook/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "storybook/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "storybook/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "storybook/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "storybook/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "supertap/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "sweepline-intersections/vitest/@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], @@ -3001,58 +2772,6 @@ "vite-node/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], diff --git a/docs/DATA_LOADING_GUIDE.md b/docs/DATA_LOADING_GUIDE.md deleted file mode 100644 index 63da3a915f9..00000000000 --- a/docs/DATA_LOADING_GUIDE.md +++ /dev/null @@ -1,529 +0,0 @@ -# Data Loading Guide - -This guide explains how to use `@buildcanada/charts` as a client library with your own CSV or JSON data. - -## Installation - -```bash -npm install @buildcanada/charts -# or -bun add @buildcanada/charts -``` - -### Peer Dependencies - -Your application must also have these dependencies: - -```bash -npm install react react-dom mobx mobx-react -``` - -## Quick Start - -```tsx -import { ChartsProvider, Grapher, GrapherState } from "@buildcanada/charts" -import "@buildcanada/charts/styles.css" - -function App() { - const grapherState = createYourChart() - - return ( - -
- -
-
- ) -} -``` - -## Data Format Requirements - -All charts require data with these mandatory columns: -- **Entity Name** (or "entity"): The name of the data series (e.g., country, category) -- **Year** (or time column): The time dimension -- **Value column(s)**: Your numeric data - -## Method 1: Using TestData Format (Recommended) - -The simplest approach uses the `createTestDataset` helper with a structured array: - -```tsx -import { - Grapher, - GrapherState, - createTestDataset, - fakeEntities, - legacyToChartsTableAndDimensionsWithMandatorySlug, - GRAPHER_CHART_TYPES, - DimensionProperty, -} from "@buildcanada/charts" -import type { TestData, TestMetadata } from "@buildcanada/charts" - -// Step 1: Define your data as TestData array -const myData: TestData = [ - { year: 2020, entity: { id: 1, name: "Canada", code: "CAN" }, value: 100 }, - { year: 2021, entity: { id: 1, name: "Canada", code: "CAN" }, value: 120 }, - { year: 2020, entity: { id: 2, name: "USA", code: "USA" }, value: 200 }, - { year: 2021, entity: { id: 2, name: "USA", code: "USA" }, value: 220 }, -] - -// Step 2: Define metadata for your indicator -const myMetadata: TestMetadata = { - id: 1, // unique identifier for this variable - display: { - name: "GDP Growth", - unit: "%", - shortUnit: "%", - numDecimalPlaces: 1, - }, -} - -// Step 3: Create the dataset and table -const variableId = myMetadata.id -const dimensions = [ - { variableId, property: DimensionProperty.y }, -] - -const dataset = createTestDataset([ - { data: myData, metadata: myMetadata }, -]) - -const table = legacyToChartsTableAndDimensionsWithMandatorySlug( - dataset, - dimensions, - {} // optional: color mapping for entities -) - -// Step 4: Create GrapherState -const grapherState = new GrapherState({ - title: "GDP Growth by Country", - dimensions, - chartTypes: [GRAPHER_CHART_TYPES.LineChart], -}) -grapherState.inputTable = table -``` - -## Method 2: Loading from JSON - -If you have JSON data from an API or file: - -```tsx -import { - Grapher, - GrapherState, - createTestDataset, - legacyToChartsTableAndDimensionsWithMandatorySlug, - GRAPHER_CHART_TYPES, - DimensionProperty, -} from "@buildcanada/charts" - -// Your JSON data -const jsonData = [ - { country: "Canada", year: 2020, gdp: 1.8 }, - { country: "Canada", year: 2021, gdp: 5.0 }, - { country: "USA", year: 2020, gdp: -3.4 }, - { country: "USA", year: 2021, gdp: 5.7 }, -] - -// Create entity mapping (id must be unique per entity) -const entityMap = new Map() -let entityId = 1 -jsonData.forEach((row) => { - if (!entityMap.has(row.country)) { - entityMap.set(row.country, { id: entityId++, name: row.country }) - } -}) - -// Transform to TestData format -const testData = jsonData.map((row) => ({ - year: row.year, - entity: entityMap.get(row.country)!, - value: row.gdp, -})) - -// Create metadata -const metadata = { - id: 1, - display: { - name: "GDP Growth Rate", - unit: "%", - numDecimalPlaces: 1, - }, -} - -// Build chart -const dimensions = [{ variableId: 1, property: DimensionProperty.y }] -const dataset = createTestDataset([{ data: testData, metadata }]) -const table = legacyToChartsTableAndDimensionsWithMandatorySlug(dataset, dimensions, {}) - -const grapherState = new GrapherState({ - title: "GDP Growth Rate", - dimensions, - chartTypes: [GRAPHER_CHART_TYPES.LineChart], -}) -grapherState.inputTable = table -``` - -## Method 3: Loading from CSV - -For CSV data, parse it first then convert to the TestData format: - -```tsx -import { parseDelimited } from "@buildcanada/charts" - -// CSV string (could be from file or fetch) -const csvString = `country,year,value -Canada,2020,100 -Canada,2021,120 -USA,2020,200 -USA,2021,220` - -// Parse CSV to rows -const rows = parseDelimited(csvString) as Array<{ - country: string - year: string - value: string -}> - -// Create entity mapping -const entityMap = new Map() -let entityId = 1 -rows.forEach((row) => { - if (!entityMap.has(row.country)) { - entityMap.set(row.country, { id: entityId++, name: row.country }) - } -}) - -// Convert to TestData format -const testData = rows.map((row) => ({ - year: parseInt(row.year, 10), - entity: entityMap.get(row.country)!, - value: parseFloat(row.value), -})) - -// Then create the chart as in Method 2... -``` - -### Loading CSV from a File (Browser) - -```tsx -async function loadCSVFile(file: File) { - const text = await file.text() - const rows = parseDelimited(text) - // Transform to TestData format... -} - -// Usage with file input - { - if (e.target.files?.[0]) { - loadCSVFile(e.target.files[0]) - } -}} /> -``` - -### Loading CSV from URL - -```tsx -async function loadCSVFromUrl(url: string) { - const response = await fetch(url) - const text = await response.text() - const rows = parseDelimited(text) - // Transform to TestData format... -} -``` - -## Chart Types - -Configure the chart type using `chartTypes`: - -```tsx -import { GRAPHER_CHART_TYPES } from "@buildcanada/charts" - -// Available chart types: -GRAPHER_CHART_TYPES.LineChart // Time series line chart -GRAPHER_CHART_TYPES.DiscreteBar // Bar chart (single time point) -GRAPHER_CHART_TYPES.StackedArea // Stacked area chart -GRAPHER_CHART_TYPES.StackedBar // Stacked bar chart -GRAPHER_CHART_TYPES.SlopeChart // Slope chart (two time points) -GRAPHER_CHART_TYPES.ScatterPlot // Scatter plot (requires x and y) -GRAPHER_CHART_TYPES.Marimekko // Marimekko chart - -// Example: Line chart -const grapherState = new GrapherState({ - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - // ... -}) - -// Example: Bar chart -const grapherState = new GrapherState({ - chartTypes: [GRAPHER_CHART_TYPES.DiscreteBar], - // ... -}) -``` - -## Map Charts - -For map visualizations, set `hasMapTab: true` and `tab: "map"`: - -```tsx -import { MapRegionName, ColorSchemeName } from "@buildcanada/charts" - -const grapherState = new GrapherState({ - dimensions, - hasMapTab: true, - tab: "map", - map: { - region: MapRegionName.World, // or MapRegionName.Canada for provincial maps - colorScale: { - baseColorScheme: ColorSchemeName.Blues, - }, - }, -}) -grapherState.inputTable = table -``` - -**Important for maps**: Entity names must match the geographic regions in the map data. For world maps, use standard country names. For Canada maps, use province/territory names. - -## Multiple Variables (Scatter Plots) - -Scatter plots require both X and Y variables: - -```tsx -// Define two variables -const xData: TestData = [/* x-axis values */] -const yData: TestData = [/* y-axis values */] - -const xMetadata = { id: 1, display: { name: "GDP per capita" } } -const yMetadata = { id: 2, display: { name: "Life expectancy" } } - -const dimensions = [ - { variableId: 1, property: DimensionProperty.x }, - { variableId: 2, property: DimensionProperty.y }, -] - -const dataset = createTestDataset([ - { data: xData, metadata: xMetadata }, - { data: yData, metadata: yMetadata }, -]) - -const table = legacyToChartsTableAndDimensionsWithMandatorySlug(dataset, dimensions, {}) - -const grapherState = new GrapherState({ - chartTypes: [GRAPHER_CHART_TYPES.ScatterPlot], - dimensions, -}) -grapherState.inputTable = table -``` - -## Pre-selecting Entities - -To show specific entities by default: - -```tsx -const grapherState = new GrapherState({ - // ... - selectedEntityNames: ["Canada", "USA", "Germany"], -}) -``` - -## Entity Colors - -Assign custom colors to entities: - -```tsx -const entityColors = { - "Canada": "#ff0000", - "USA": "#0000ff", - "Germany": "#ffcc00", -} - -const table = legacyToChartsTableAndDimensionsWithMandatorySlug( - dataset, - dimensions, - entityColors // Pass colors here -) -``` - -## Complete Example: CSV to Line Chart - -```tsx -import { - ChartsProvider, - Grapher, - GrapherState, - createTestDataset, - legacyToChartsTableAndDimensionsWithMandatorySlug, - parseDelimited, - GRAPHER_CHART_TYPES, - DimensionProperty, - Bounds, -} from "@buildcanada/charts" -import "@buildcanada/charts/styles.css" - -function CSVChart({ csvData }: { csvData: string }) { - // Parse CSV - const rows = parseDelimited(csvData) as Array<{ - entity: string - year: string - value: string - }> - - // Build entity map - const entityMap = new Map() - let entityId = 1 - rows.forEach((row) => { - if (!entityMap.has(row.entity)) { - entityMap.set(row.entity, { id: entityId++, name: row.entity }) - } - }) - - // Convert to TestData - const testData = rows.map((row) => ({ - year: parseInt(row.year, 10), - entity: entityMap.get(row.entity)!, - value: parseFloat(row.value), - })) - - // Build chart - const variableId = 1 - const dimensions = [{ variableId, property: DimensionProperty.y }] - - const dataset = createTestDataset([{ - data: testData, - metadata: { - id: variableId, - display: { name: "Value", numDecimalPlaces: 0 }, - }, - }]) - - const table = legacyToChartsTableAndDimensionsWithMandatorySlug( - dataset, - dimensions, - {} - ) - - const grapherState = new GrapherState({ - title: "My Chart", - chartTypes: [GRAPHER_CHART_TYPES.LineChart], - dimensions, - bounds: new Bounds(0, 0, 800, 600), - }) - grapherState.inputTable = table - - return ( - -
- -
-
- ) -} - -// Usage -const csv = `entity,year,value -Canada,2020,100 -Canada,2021,110 -Canada,2022,105 -USA,2020,200 -USA,2021,210 -USA,2022,215` - -function App() { - return -} -``` - -## Using fakeEntities for Common Countries - -The library provides pre-defined entity mappings for countries and regions: - -```tsx -import { fakeEntities } from "@buildcanada/charts" - -// fakeEntities contains ~200 countries and regions with pre-assigned IDs -const canadaEntity = fakeEntities["Canada"] // { id: X, name: "Canada", code: "CAN" } -const usaEntity = fakeEntities["United States"] // { id: Y, name: "United States", code: "USA" } - -// Use in your data -const testData = [ - { year: 2020, entity: fakeEntities["Canada"], value: 100 }, - { year: 2020, entity: fakeEntities["United States"], value: 200 }, -] -``` - -## ChartsProvider Configuration - -Customize the charts with global configuration: - -```tsx -import { ChartsProvider } from "@buildcanada/charts" - -const config = { - branding: { - poweredByText: "My Organization", - logo: { - type: "none", // or "buildCanada", "canadaSpends", "custom" - }, - }, - dataApi: { - baseUrl: "https://api.example.com/v1/indicators/", - }, -} - -function App() { - return ( - - {/* Your charts */} - - ) -} -``` - -## TypeScript Types - -Key types for working with data: - -```tsx -import type { - TestData, // Array of { year, entity, value } - TestMetadata, // Variable metadata with display options - EntityName, // string - EntityId, // number - EntityCode, // string -} from "@buildcanada/charts" - -// TestData structure -type TestData = Array<{ - year: number - entity: { id: EntityId; code?: EntityCode; name?: EntityName } - value: string | number -}> - -// TestMetadata structure -type TestMetadata = { - id: number - display?: { - name?: string - unit?: string - shortUnit?: string - numDecimalPlaces?: number - } -} -``` - -## Troubleshooting - -### Chart shows no data -- Ensure entity names match between your data and the chart -- Verify the year values are numbers, not strings -- Check that `dimensions` includes the correct `variableId` - -### Map shows blank regions -- Entity names must exactly match geographic region names -- For world maps, use standard country names (e.g., "United States", not "USA") -- For Canada maps, use full province names (e.g., "British Columbia", not "BC") - -### Colors not applying -- Pass the color map to `legacyToChartsTableAndDimensionsWithMandatorySlug` -- Entity names in the color map must exactly match data entity names diff --git a/package.json b/package.json index 20727fe4751..50c021a3d9e 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,11 @@ }, "scripts": { "build": "storybook build -o storybook-static --stats-json", - "build:packages": "bun run build:colours && bun run build:components && bun run build:charts && bun run build:charts2", + "build:packages": "bun run build:colours && bun run build:components && bun run build:charts", "build:colours": "cd packages/colours && bun run build", "build:components": "cd packages/components && bun run build", "build:charts": "cd packages/charts && bun run build", - "build:charts2": "cd packages/charts2 && bun run build", - "charts2": "bun packages/charts2/src/cli/index.ts", + "charts": "bun packages/charts/src/cli/index.ts", "typecheck": "bun run --filter '*' typecheck", "test": "bun run --filter '*' test", "storybook": "storybook dev -p 6006", diff --git a/packages/charts2/.gitignore b/packages/charts/.gitignore similarity index 100% rename from packages/charts2/.gitignore rename to packages/charts/.gitignore diff --git a/packages/charts2/README.md b/packages/charts/README.md similarity index 92% rename from packages/charts2/README.md rename to packages/charts/README.md index e7a43288fd0..7ed128f3bc4 100644 --- a/packages/charts2/README.md +++ b/packages/charts/README.md @@ -1,6 +1,6 @@ -# @buildcanada/charts2 +# @buildcanada/charts -Build Canada charts v2: a pure-function layout core with a single React SVG renderer, deterministic headless rendering, and a CLI. Specs: `bcds/specs/` (architecture: `specs/28-architecture.md`). +Build Canada charts: a pure-function layout core with a single React SVG renderer, deterministic headless rendering, and a CLI. Specs: `bcds/specs/` (architecture: `specs/28-architecture.md`). ## Layout @@ -10,14 +10,14 @@ Build Canada charts v2: a pure-function layout core with a single React SVG rend - `src/fixtures/` — committed fixture datasets (spec 26 §2), loadable by name in tests/stories/CLI - `samples/` — CLI-ready chart definitions, tested against included fixtures - `src/corpus/` — golden SVG corpus + bless script (spec 26 §1.3) -- `src/stories/` — Storybook stories under `Charts2/` +- `src/stories/` — Storybook stories under `Charts/` ## Quick start ```tsx -import { buildDataset, parseCsv, parseDefinition, parseManifest } from "@buildcanada/charts2/core" -import { Chart, Tooltip } from "@buildcanada/charts2" -import "@buildcanada/charts2/styles.css" +import { buildDataset, parseCsv, parseDefinition, parseManifest } from "@buildcanada/charts/core" +import { Chart, Tooltip } from "@buildcanada/charts" +import "@buildcanada/charts/styles.css" // 1. Dataset: manifest + CSV → Dataset. const { manifest } = parseManifest(rawManifestJson) @@ -57,7 +57,7 @@ Headless (no React state, no DOM): `layoutChart({ definition, dataset, size })` Install the CLI globally: ```bash -npm install -g @buildcanada/charts2 +npm install -g @buildcanada/charts ``` Then call it with `charts`: @@ -113,7 +113,7 @@ The generated `definition.json` uses `"data": "."`, so it resolves the colocated ### `charts install-skill` -Install the included `charts2-cli` agent skill, which teaches coding agents how to validate and render charts with this CLI. +Install the included `charts-cli` agent skill, which teaches coding agents how to validate and render charts with this CLI. ```bash charts install-skill diff --git a/packages/charts/build.test.ts b/packages/charts/build.test.ts deleted file mode 100644 index 94f03c47297..00000000000 --- a/packages/charts/build.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * Build Pipeline Integration Test - * - * This test verifies that the build output is correct and can be imported. - * Run after `bun run build` to verify the dist/ output. - */ - -import { describe, it, expect, beforeAll } from "vitest" -import { existsSync } from "node:fs" -import { readFile, readdir } from "node:fs/promises" -import { join, dirname } from "node:path" -import { fileURLToPath } from "node:url" - -const __dirname = dirname(fileURLToPath(import.meta.url)) -const DIST_DIR = join(__dirname, "dist") - -describe("Build Pipeline", () => { - beforeAll(() => { - if (!existsSync(DIST_DIR)) { - throw new Error( - "dist/ directory not found. Run `bun run build` first." - ) - } - }) - - describe("dist/ structure", () => { - it("should have index.js", async () => { - expect(existsSync(join(DIST_DIR, "index.js"))).toBe(true) - }) - - it("should have index.d.ts", async () => { - expect(existsSync(join(DIST_DIR, "index.d.ts"))).toBe(true) - }) - - it("should have index.d.ts.map", async () => { - expect(existsSync(join(DIST_DIR, "index.d.ts.map"))).toBe(true) - }) - - it("should have styles/charts.scss", async () => { - expect(existsSync(join(DIST_DIR, "styles", "charts.scss"))).toBe(true) - }) - - it("should have core directories", async () => { - const expectedDirs = [ - "config", - "core-table", - "explorer", - "grapher", - "types", - "utils", - "components", - ] - for (const dir of expectedDirs) { - expect(existsSync(join(DIST_DIR, dir))).toBe(true) - } - }) - }) - - describe("JavaScript output", () => { - it("should have valid ESM exports in index.js", async () => { - const content = await readFile(join(DIST_DIR, "index.js"), "utf-8") - - // Should use ESM export syntax - expect(content).toContain("export {") - - // Should have .js extensions in imports (esbuild preserves these) - expect(content).toContain(".js") - }) - - it("should not contain TypeScript-specific syntax", async () => { - const content = await readFile(join(DIST_DIR, "index.js"), "utf-8") - - // Should not have interface or type declarations (these go in .d.ts) - expect(content).not.toMatch(/^export interface\s/m) - expect(content).not.toMatch(/^export type\s+\w+\s*=/m) - }) - - it("should have separate module files", async () => { - // Verify key modules exist as separate files - expect(existsSync(join(DIST_DIR, "config", "index.js"))).toBe(true) - expect(existsSync(join(DIST_DIR, "grapher", "core", "Grapher.js"))).toBe(true) - expect(existsSync(join(DIST_DIR, "grapher", "core", "GrapherState.js"))).toBe(true) - }) - }) - - describe("Type declarations", () => { - it("should have type exports in index.d.ts", async () => { - const content = await readFile(join(DIST_DIR, "index.d.ts"), "utf-8") - - // Should export key types - expect(content).toContain("ChartsConfig") - expect(content).toContain("Grapher") - expect(content).toContain("GrapherState") - }) - }) - - describe("SCSS files", () => { - it("should copy charts.scss with updated import paths", async () => { - const content = await readFile( - join(DIST_DIR, "styles", "charts.scss"), - "utf-8" - ) - - // Should use the new package export path, not src/ - expect(content).toContain('@use "@buildcanada/components/styles/fonts"') - expect(content).not.toContain('@use "@buildcanada/components/src/') - }) - }) - - describe("Module structure", () => { - it("should export key symbols from index.js", async () => { - const content = await readFile(join(DIST_DIR, "index.js"), "utf-8") - - // Verify key exports are present in the file - expect(content).toContain("Grapher") - expect(content).toContain("GrapherState") - expect(content).toContain("ChartsProvider") - expect(content).toContain("defaultChartsConfig") - expect(content).toContain("Explorer") - }) - - it("should have proper ESM re-exports", async () => { - const content = await readFile(join(DIST_DIR, "index.js"), "utf-8") - - // Should re-export from submodules - expect(content).toMatch(/export\s*\{/) - expect(content).toMatch(/from\s*["']\.\//) - }) - - // Note: Direct import test skipped because TC39 decorators require - // bundler transformation (handled by Next.js/Turbopack at runtime) - }) -}) diff --git a/packages/charts/build.ts b/packages/charts/build.ts index 9606ca07512..acd69ec4151 100644 --- a/packages/charts/build.ts +++ b/packages/charts/build.ts @@ -1,9 +1,8 @@ -import { mkdir, cp, rm, readFile, writeFile } from "node:fs/promises" +import { mkdir, cp, rm, chmod, readFile, writeFile } from "node:fs/promises" import { spawn } from "node:child_process" import { existsSync } from "node:fs" import { join, dirname } from "node:path" import { Glob } from "bun" -import { transform } from "esbuild" const runCommand = (command: string, args: string[]): Promise => { return new Promise((resolve, reject) => { @@ -15,52 +14,34 @@ const runCommand = (command: string, args: string[]): Promise => { }) } -const copyAssetFiles = async () => { - // Copy SCSS, JSON, and existing JS files - const glob = new Glob("**/*.{scss,json,js}") +// SCSS, committed font-metrics JSON, and the bundled agent skill ship alongside +// the compiled JS. +// Brand font binaries (woff2) are intentionally NOT copied: the published +// package must not redistribute licensed fonts (see specs/28-architecture.md). +const copyAssets = async () => { const srcDir = "src" const distDir = "dist" - - for await (const file of glob.scan(srcDir)) { - const srcPath = join(srcDir, file) - const destPath = join(distDir, file) - const destDir = dirname(destPath) - - await mkdir(destDir, { recursive: true }) - await cp(srcPath, destPath) + for (const pattern of ["**/*.scss", "fonts/metrics/*.json"]) { + const glob = new Glob(pattern) + for await (const file of glob.scan(srcDir)) { + const destPath = join(distDir, file) + await mkdir(dirname(destPath), { recursive: true }) + await cp(join(srcDir, file), destPath) + } + } + if (existsSync("skills")) { + await cp("skills", join(distDir, "skills"), { recursive: true }) } } -const transpileFile = async (srcPath: string, destPath: string) => { - const content = await readFile(srcPath, "utf-8") - const isTsx = srcPath.endsWith(".tsx") - - const result = await transform(content, { - loader: isTsx ? "tsx" : "ts", - format: "esm", - target: "es2022", - jsx: "automatic", - sourcemap: "external", - sourcefile: srcPath, - // Emit native TC-39 stage 3 decorators. The codebase mixes mobx - // decorators (@computed, @action) with stage-3-style ones (@bind in - // utils/Util.ts), so the legacy emit path produces output that calls - // stage-3 decorators with legacy arguments at runtime. Stage 3 emit - // is what Storybook uses and matches the source authoring style. - tsconfigRaw: { - compilerOptions: { - experimentalDecorators: false, - useDefineForClassFields: true, - }, - }, - }) - - const destDir = dirname(destPath) - await mkdir(destDir, { recursive: true }) - await writeFile(destPath, result.code) - if (result.map) { - await writeFile(destPath + ".map", result.map) +const makeBinExecutable = async () => { + const binPath = "dist/cli/index.js" + if (!existsSync(binPath)) return + const content = await readFile(binPath, "utf8") + if (!content.startsWith("#!")) { + await writeFile(binPath, `#!/usr/bin/env node\n${content}`) } + await chmod(binPath, 0o755) } const build = async () => { @@ -70,35 +51,14 @@ const build = async () => { } await mkdir("dist", { recursive: true }) - console.log("Copying asset files (SCSS, JSON) to dist...") - await copyAssetFiles() - - console.log("Transpiling TypeScript files with esbuild...") - const glob = new Glob("**/*.{ts,tsx}") - const tasks: Promise[] = [] - - for await (const file of glob.scan("src")) { - if ( - !file.includes(".test.") && - !file.includes(".stories.") && - !file.includes(".spec.") - ) { - const srcPath = join("src", file) - const destPath = join("dist", file.replace(/\.tsx?$/, ".js")) - tasks.push(transpileFile(srcPath, destPath)) - } - } + console.log("Copying assets to dist...") + await copyAssets() - await Promise.all(tasks) - console.log(`Transpiled ${tasks.length} files`) + console.log("Compiling TypeScript...") + await runCommand("npx", ["tsc", "--project", "tsconfig.build.json"]) - console.log("Generating type declarations...") - await runCommand("npx", [ - "tsc", - "--project", - "tsconfig.build.json", - "--emitDeclarationOnly", - ]) + console.log("Making CLI bin executable...") + await makeBinExecutable() console.log("Build complete!") } diff --git a/packages/charts/eslint.config.mjs b/packages/charts/eslint.config.mjs deleted file mode 100644 index 31a832648a6..00000000000 --- a/packages/charts/eslint.config.mjs +++ /dev/null @@ -1,223 +0,0 @@ -// @ts-check - -import eslint from "@eslint/js" -import importPlugin from "eslint-plugin-import-x" -import prettier from "eslint-config-prettier" -import react from "eslint-plugin-react" -import reactHooks from "eslint-plugin-react-hooks" -import reactRefresh from "eslint-plugin-react-refresh" -import tseslint from "typescript-eslint" -import mobx from "eslint-plugin-mobx" - -export default tseslint.config( - eslint.configs.recommended, - tseslint.configs.recommended, - importPlugin.flatConfigs.recommended, - importPlugin.flatConfigs.typescript, - react.configs.flat.recommended, - react.configs.flat["jsx-runtime"], - reactHooks.configs["recommended-latest"], - mobx.flatConfigs.recommended, - { - files: [ - // TODO: Apply to all React files when we move off class components, - // since they are not supported. - "adminSiteClient/**/*.tsx", - "packages/@ourworldindata/components/src/**/*.tsx", - "site/**/*.tsx", - ], - plugins: { - "react-refresh": reactRefresh, - }, - rules: { - "react-refresh/only-export-components": [ - "error", - { allowConstantExport: true }, - ], - }, - }, - { - languageOptions: { - parserOptions: { - project: "./tsconfig.eslint.json", - }, - }, - settings: { - "import-x/resolver": { - typescript: true, - node: true, - }, - react: { - version: "detect", - }, - }, - rules: { - "@typescript-eslint/consistent-type-exports": [ - "warn", - { - fixMixedExportsWithInlineTypeSpecifier: true, - }, - ], - - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-empty-function": "warn", - "@typescript-eslint/no-empty-interface": "warn", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-for-in-array": "warn", - "@typescript-eslint/no-non-null-assertion": "off", - - "@typescript-eslint/no-unused-vars": [ - "warn", - { - varsIgnorePattern: "^_", - argsIgnorePattern: "^_", - }, - ], - - "@typescript-eslint/prefer-for-of": "warn", - "@typescript-eslint/restrict-plus-operands": "warn", - eqeqeq: "warn", - "import-x/no-extraneous-dependencies": [ - "warn", - { - includeInternal: true, - // The various d3 imports are all captured by d3 and @types/d3 already - whitelist: [ - "@types/d3-array", - "@types/d3-color", - "@types/d3-dsv", - "@types/d3-ease", - "@types/d3-force", - "@types/d3-format", - "@types/d3-geo", - "@types/d3-interpolate", - "@types/d3-quadtree", - "@types/d3-scale", - "@types/d3-selection", - "@types/d3-shape", - "@types/d3-transition", - "@types/d3-zoom", - ], - }, - ], - "import-x/no-relative-packages": "error", - "import-x/namespace": "off", - "import-x/no-named-as-default-member": "off", - "import-x/no-unresolved": "off", - - "no-console": [ - "warn", - { - allow: ["warn", "error"], - }, - ], - - "prefer-const": [ - "warn", - { - destructuring: "all", - }, - ], - - "react/display-name": "warn", - "react/jsx-key": "warn", - - "react/jsx-no-target-blank": [ - "warn", - { - allowReferrer: true, - }, - ], - - "react/no-render-return-value": "warn", - - "react/no-unescaped-entities": [ - "warn", - { - forbid: [">", "}"], - }, - ], - - "react/prop-types": "warn", - "@typescript-eslint/no-floating-promises": "error", - "no-constant-binary-expression": "error", - "mobx/missing-observer": "off", - - // we would want to have this rule enabled, but it doesn't allow for our mix of `makeObservable` arguments and decorators - "mobx/missing-make-observable": "off", - - "mobx/exhaustive-make-observable": "off" - }, - }, - { - files: [ - "adminSiteClient/**/*", - "adminSiteServer/**/*", - "baker/**/*", - "db/**/*", - "devTools/**/*", - "functions/**/*", - "explorerAdminServer/**/*", - "serverUtils/**/*", - "settings/**/*", - ], - rules: { - "no-console": "off", - }, - }, - { - files: [ - "db/**/*", - "packages/@ourworldindata/core-table/**/*", - "packages/@ourworldindata/grapher/src/axis/**/*", - "packages/@ourworldindata/grapher/src/barCharts/**/*", - "packages/@ourworldindata/grapher/src/bodyDiv/**/*", - "packages/@ourworldindata/grapher/src/captionedChart/**/*", - "packages/@ourworldindata/grapher/src/chart/**/*", - "packages/@ourworldindata/grapher/src/color/**/*", - "packages/@ourworldindata/grapher/src/controls/**/*", - "packages/@ourworldindata/grapher/src/core/**/*", - "packages/@ourworldindata/grapher/src/dataTable/**/*", - "packages/@ourworldindata/grapher/src/facetChart/**/*", - "packages/@ourworldindata/grapher/src/footer/**/*", - "packages/@ourworldindata/grapher/src/header/**/*", - "packages/@ourworldindata/grapher/src/lineCharts/**/*", - "packages/@ourworldindata/grapher/src/loadingIndicator/**/*", - "packages/@ourworldindata/grapher/src/mapCharts/**/*", - "packages/@ourworldindata/grapher/src/noDataModal/**/*", - "packages/@ourworldindata/grapher/src/scatterCharts/**/*", - "packages/@ourworldindata/grapher/src/selection/**/*", - "packages/@ourworldindata/grapher/src/stackedCharts/**/*", - "packages/@ourworldindata/grapher/src/timeline/**/*", - "packages/@ourworldindata/grapher/src/tooltip/**/*", - "packages/@ourworldindata/grapher/src/verticalColorLegend/**/*", - "packages/@ourworldindata/utils/**/*", - "packages/@ourworldindata/utils/src/persistable/**/*", - "settings/**/*", - ], - rules: { - "@typescript-eslint/explicit-function-return-type": "warn", - "@typescript-eslint/explicit-module-boundary-types": "warn", - }, - }, - prettier, - { - ignores: [ - "**/.*", - "**/*.cjs", - "**/*.js", - "**/dist/", - "**/knexfile.ts", - "archive/**/*", - "bakedSite/**/*", - "coverage/**/*", - "devtools/**/*", - "eslint.config.mjs", - "itsJustJavascript/**/*", - "localBake/**/*", - "packages/@ourworldindata/*/dist/", - "vite.*.mjs", - ], - } -) diff --git a/packages/charts/package.json b/packages/charts/package.json index c078e717237..96e29487f22 100644 --- a/packages/charts/package.json +++ b/packages/charts/package.json @@ -1,7 +1,7 @@ { "name": "@buildcanada/charts", - "version": "0.4.0", - "description": "A configurable data visualization library for creating interactive charts.", + "version": "1.0.0", + "description": "Build Canada charts: pure layout core with a single React SVG renderer, deterministic headless rendering, and a CLI.", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", @@ -11,11 +11,19 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./styles.css": "./dist/styles/charts.scss", - "./styles/*": "./dist/styles/*" + "./core": { + "types": "./dist/core/index.d.ts", + "import": "./dist/core/index.js" + }, + "./styles.css": "./dist/react/styles/charts.scss", + "./styles/*": "./dist/react/styles/*" + }, + "bin": { + "charts": "./dist/cli/index.js" }, "files": [ - "dist" + "dist", + "samples" ], "repository": { "type": "git", @@ -34,91 +42,49 @@ "prepublishOnly": "bun run build", "typecheck": "tsc --noEmit", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "corpus:bless": "bun src/corpus/bless.ts", + "extract-font-metrics": "bun run scripts/extract-font-metrics.ts" }, "keywords": [ "charts", "data-visualization", + "svg", "react", - "d3", - "grapher", - "explorer" + "build-canada" ], "author": "Build Canada", "license": "MIT", "peerDependencies": { - "mobx": "^6.13.0", - "mobx-react": "^7.6.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "dependencies": { - "@buildcanada/colours": "^0.4.0", - "@buildcanada/components": "^0.4.0", - "@fortawesome/fontawesome-svg-core": "^6.7.2", - "@fortawesome/free-brands-svg-icons": "^6.7.2", - "@fortawesome/free-solid-svg-icons": "^6.7.2", - "@fortawesome/react-fontawesome": "^0.2.2", - "@tippyjs/react": "^4.2.6", - "@turf/boolean-intersects": "^7.2.0", - "@turf/center": "^7.2.0", - "classnames": "^2.5.1", - "colorbrewer": "^1.3.0", - "d3": "^7.9.0", - "d3-transition": "^3.0.1", - "dayjs": "^1.11.11", - "fparser": "^3.1.0", - "fuzzysort": "^3.1.0", - "indefinite": "^2.5.1", - "js-cookie": "^3.0.5", - "lodash-es": "^4.17.21", - "mdast-util-find-and-replace": "^3.0.2", - "mdast-util-from-markdown": "^2.0.2", - "mousetrap": "^1.6.5", - "papaparse": "^5.5.3", - "react-aria-components": "^1.13.0", - "react-flip-toolkit": "^7.2.4", - "react-markdown": "^10.1.0", - "react-move": "^6.5.0", - "react-select": "^5.10.2", - "remeda": "^2.32.0", - "simple-statistics": "^7.8.8", - "string-pixel-width": "^1.10.0", - "striptags": "^3.2.0", - "topojson-client": "^3.1.0", - "ts-pattern": "^5.8.0", - "unist-util-visit": "^5.0.0", - "url-join": "^5.0.0", - "url-parse": "^1.5.10", - "url-slug": "^4.0.1", - "uuidv7": "^1.0.1", - "versor": "^0.2.0" + "@buildcanada/colours": "^1.0.0", + "@buildcanada/components": "^1.0.0", + "@resvg/resvg-js": "^2.6.2", + "citty": "^0.1.6", + "d3-array": "^3.2.4", + "d3-dsv": "^3.0.1", + "d3-format": "^3.1.0", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "zod": "^4.3.5" }, "devDependencies": { - "@storybook/react-dom-shim": "^10.1.11", - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", - "@types/colorbrewer": "^1.0.29", - "@types/d3": "^7.4.3", - "@types/geojson": "^7946.0.10", - "@types/indefinite": "^2.3.4", - "@types/js-cookie": "^3.0.6", - "@types/lodash-es": "^4", - "@types/mousetrap": "^1.6.9", - "@types/papaparse": "^5.3.16", - "@types/string-pixel-width": "^1.7.2", - "@types/topojson-client": "^3.1.1", - "@types/url-parse": "^1.4.8", - "debug": "^4.4.3", - "esbuild": "^0.27.2", - "eslint": "^9.37.0", - "eslint-plugin-react-hooks": "^5.2.0", + "@types/d3-array": "^3.2.2", + "@types/d3-dsv": "^3.0.7", + "@types/d3-format": "^3.0.4", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.7", + "@types/node": "^22.10.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "fontkit": "^2.0.4", "happy-dom": "^20.1.0", - "jsdom": "^27.4.0", - "object-assign": "^4.1.1", - "side-channel": "^1.1.0", - "timezone-mock": "^1.3.6", - "vitest": "^4.0.15" + "typescript": "~5.9.2", + "vitest": "^4.0.15", + "wawoff2": "^2.0.1" } } diff --git a/packages/charts2/samples/README.md b/packages/charts/samples/README.md similarity index 94% rename from packages/charts2/samples/README.md rename to packages/charts/samples/README.md index 7f5cc428bba..cd8969aef9b 100644 --- a/packages/charts2/samples/README.md +++ b/packages/charts/samples/README.md @@ -1,4 +1,4 @@ -# Charts2 samples +# Charts samples These definition files are ready to use with the CLI from the package root: diff --git a/packages/charts2/samples/discrete-bar-population.json b/packages/charts/samples/discrete-bar-population.json similarity index 100% rename from packages/charts2/samples/discrete-bar-population.json rename to packages/charts/samples/discrete-bar-population.json diff --git a/packages/charts2/samples/line-comparison-provincial-budgets.json b/packages/charts/samples/line-comparison-provincial-budgets.json similarity index 100% rename from packages/charts2/samples/line-comparison-provincial-budgets.json rename to packages/charts/samples/line-comparison-provincial-budgets.json diff --git a/packages/charts2/samples/line-faceted-provincial-budgets.json b/packages/charts/samples/line-faceted-provincial-budgets.json similarity index 100% rename from packages/charts2/samples/line-faceted-provincial-budgets.json rename to packages/charts/samples/line-faceted-provincial-budgets.json diff --git a/packages/charts2/samples/line-federal-departments.json b/packages/charts/samples/line-federal-departments.json similarity index 100% rename from packages/charts2/samples/line-federal-departments.json rename to packages/charts/samples/line-federal-departments.json diff --git a/packages/charts2/samples/line-provincial-budgets.json b/packages/charts/samples/line-provincial-budgets.json similarity index 100% rename from packages/charts2/samples/line-provincial-budgets.json rename to packages/charts/samples/line-provincial-budgets.json diff --git a/packages/charts2/samples/stacked-area-government-debt.json b/packages/charts/samples/stacked-area-government-debt.json similarity index 100% rename from packages/charts2/samples/stacked-area-government-debt.json rename to packages/charts/samples/stacked-area-government-debt.json diff --git a/packages/charts2/samples/stacked-bar-government-debt.json b/packages/charts/samples/stacked-bar-government-debt.json similarity index 100% rename from packages/charts2/samples/stacked-bar-government-debt.json rename to packages/charts/samples/stacked-bar-government-debt.json diff --git a/packages/charts2/samples/stacked-discrete-bar-provincial-composition.json b/packages/charts/samples/stacked-discrete-bar-provincial-composition.json similarity index 100% rename from packages/charts2/samples/stacked-discrete-bar-provincial-composition.json rename to packages/charts/samples/stacked-discrete-bar-provincial-composition.json diff --git a/packages/charts2/scripts/extract-font-metrics.ts b/packages/charts/scripts/extract-font-metrics.ts similarity index 100% rename from packages/charts2/scripts/extract-font-metrics.ts rename to packages/charts/scripts/extract-font-metrics.ts diff --git a/packages/charts2/skills/charts2-cli/SKILL.md b/packages/charts/skills/charts-cli/SKILL.md similarity index 93% rename from packages/charts2/skills/charts2-cli/SKILL.md rename to packages/charts/skills/charts-cli/SKILL.md index 5393f94b595..63a03f4a55e 100644 --- a/packages/charts2/skills/charts2-cli/SKILL.md +++ b/packages/charts/skills/charts-cli/SKILL.md @@ -1,18 +1,18 @@ --- -name: charts2-cli -description: Use when Codex or another coding agent needs to generate, validate, debug, or automate Build Canada charts from the command line with the @buildcanada/charts2 `charts` binary. Trigger for chart definition JSON files, fixture rendering, SVG/PNG chart export, CLI chart validation, render flag selection, shell workflows around `charts render`, or troubleshooting `charts` diagnostics. +name: charts-cli +description: Use when Codex or another coding agent needs to generate, validate, debug, or automate Build Canada charts from the command line with the @buildcanada/charts `charts` binary. Trigger for chart definition JSON files, fixture rendering, SVG/PNG chart export, CLI chart validation, render flag selection, shell workflows around `charts render`, or troubleshooting `charts` diagnostics. --- # Charts CLI -Use the `charts` binary from `@buildcanada/charts2` to validate chart inputs and render deterministic SVG/PNG chart outputs. Prefer this CLI path when the task is to produce chart files rather than embed a React component. +Use the `charts` binary from `@buildcanada/charts` to validate chart inputs and render deterministic SVG/PNG chart outputs. Prefer this CLI path when the task is to produce chart files rather than embed a React component. ## Install Install the package globally when `charts` is not already available: ```bash -npm install -g @buildcanada/charts2 +npm install -g @buildcanada/charts ``` Confirm the binary is available: diff --git a/packages/charts/skills/charts-cli/agents/openai.yaml b/packages/charts/skills/charts-cli/agents/openai.yaml new file mode 100644 index 00000000000..3629bfb3b48 --- /dev/null +++ b/packages/charts/skills/charts-cli/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Charts CLI" + short_description: "Generate charts with the charts CLI" + default_prompt: "Use $charts-cli to render a Build Canada chart from a definition JSON with the charts CLI." diff --git a/packages/charts2/src/cli/errors.ts b/packages/charts/src/cli/errors.ts similarity index 100% rename from packages/charts2/src/cli/errors.ts rename to packages/charts/src/cli/errors.ts diff --git a/packages/charts2/src/cli/index.ts b/packages/charts/src/cli/index.ts similarity index 100% rename from packages/charts2/src/cli/index.ts rename to packages/charts/src/cli/index.ts diff --git a/packages/charts2/src/cli/installSkill.test.ts b/packages/charts/src/cli/installSkill.test.ts similarity index 59% rename from packages/charts2/src/cli/installSkill.test.ts rename to packages/charts/src/cli/installSkill.test.ts index ea082962470..64a7e1d65c0 100644 --- a/packages/charts2/src/cli/installSkill.test.ts +++ b/packages/charts/src/cli/installSkill.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path" import { afterEach, describe, expect, it } from "vitest" import { CliUsageError } from "./errors.ts" -import { CHARTS2_SKILL_NAME, installSkill, resolveInstallTargets, resolveSkillSourceDir } from "./installSkill.ts" +import { CHARTS_SKILL_NAME, installSkill, resolveInstallTargets, resolveSkillSourceDir } from "./installSkill.ts" const tmpDirs: string[] = [] @@ -15,13 +15,13 @@ function makeTmpDir(): string { } function makeSourceSkill(): string { - const dir = join(makeTmpDir(), CHARTS2_SKILL_NAME) + const dir = join(makeTmpDir(), CHARTS_SKILL_NAME) mkdirSync(dir, { recursive: true }) writeFileSync( join(dir, "SKILL.md"), [ "---", - `name: ${CHARTS2_SKILL_NAME}`, + `name: ${CHARTS_SKILL_NAME}`, "description: Test skill", "---", "", @@ -39,9 +39,9 @@ afterEach(() => { }) describe("resolveSkillSourceDir", () => { - it("finds the bundled charts2 CLI skill", () => { + it("finds the bundled charts CLI skill", () => { const sourceDir = resolveSkillSourceDir() - expect(readFileSync(join(sourceDir, "SKILL.md"), "utf8")).toContain(`name: ${CHARTS2_SKILL_NAME}`) + expect(readFileSync(join(sourceDir, "SKILL.md"), "utf8")).toContain(`name: ${CHARTS_SKILL_NAME}`) }) }) @@ -52,7 +52,7 @@ describe("resolveInstallTargets", () => { { agent: "custom", skillsRoot: root, - destination: join(root, CHARTS2_SKILL_NAME), + destination: join(root, CHARTS_SKILL_NAME), }, ]) }) @@ -70,7 +70,7 @@ describe("installSkill", () => { it("copies the skill to every target", () => { const sourceDir = makeSourceSkill() const skillsRoot = join(makeTmpDir(), "skills") - const destination = join(skillsRoot, CHARTS2_SKILL_NAME) + const destination = join(skillsRoot, CHARTS_SKILL_NAME) installSkill({ sourceDir, @@ -84,7 +84,7 @@ describe("installSkill", () => { it("refuses to overwrite unless --force is set", () => { const sourceDir = makeSourceSkill() const skillsRoot = join(makeTmpDir(), "skills") - const destination = join(skillsRoot, CHARTS2_SKILL_NAME) + const destination = join(skillsRoot, CHARTS_SKILL_NAME) mkdirSync(destination, { recursive: true }) writeFileSync(join(destination, "SKILL.md"), "old") @@ -102,6 +102,39 @@ describe("installSkill", () => { force: true, }) - expect(readFileSync(join(destination, "SKILL.md"), "utf8")).toContain(`name: ${CHARTS2_SKILL_NAME}`) + expect(readFileSync(join(destination, "SKILL.md"), "utf8")).toContain(`name: ${CHARTS_SKILL_NAME}`) + }) + + it("removes a stale legacy charts2-cli install", () => { + const sourceDir = makeSourceSkill() + const skillsRoot = join(makeTmpDir(), "skills") + const legacyDir = join(skillsRoot, "charts2-cli") + mkdirSync(legacyDir, { recursive: true }) + writeFileSync(join(legacyDir, "SKILL.md"), ["---", "name: charts2-cli", "---"].join("\n")) + + installSkill({ + sourceDir, + targets: [{ agent: "custom", skillsRoot, destination: join(skillsRoot, CHARTS_SKILL_NAME) }], + force: false, + }) + + expect(existsSync(legacyDir)).toBe(false) + expect(existsSync(join(skillsRoot, CHARTS_SKILL_NAME, "SKILL.md"))).toBe(true) + }) + + it("leaves an unrelated charts2-cli directory alone", () => { + const sourceDir = makeSourceSkill() + const skillsRoot = join(makeTmpDir(), "skills") + const unrelatedDir = join(skillsRoot, "charts2-cli") + mkdirSync(unrelatedDir, { recursive: true }) + writeFileSync(join(unrelatedDir, "SKILL.md"), ["---", "name: something-else", "---"].join("\n")) + + installSkill({ + sourceDir, + targets: [{ agent: "custom", skillsRoot, destination: join(skillsRoot, CHARTS_SKILL_NAME) }], + force: false, + }) + + expect(existsSync(join(unrelatedDir, "SKILL.md"))).toBe(true) }) }) diff --git a/packages/charts2/src/cli/installSkill.ts b/packages/charts/src/cli/installSkill.ts similarity index 73% rename from packages/charts2/src/cli/installSkill.ts rename to packages/charts/src/cli/installSkill.ts index 9246d539275..cde9d612437 100644 --- a/packages/charts2/src/cli/installSkill.ts +++ b/packages/charts/src/cli/installSkill.ts @@ -1,19 +1,22 @@ /** - * charts install-skill - install the bundled charts2 CLI agent skill. + * charts install-skill - install the bundled charts CLI agent skill. * - * The skill is stored in packages/charts2/skills during development and copied + * The skill is stored in packages/charts/skills during development and copied * into dist/skills for published packages. This command supports both layouts. */ import { defineCommand } from "citty" -import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs" +import { cpSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs" import { homedir } from "node:os" import { basename, dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" import { CliUsageError } from "./errors.ts" -export const CHARTS2_SKILL_NAME = "charts2-cli" +export const CHARTS_SKILL_NAME = "charts-cli" + +/** Names this skill previously installed under; removed on install so stale copies don't linger. */ +const LEGACY_SKILL_NAMES = ["charts2-cli"] export type SkillAgent = "auto" | "all" | "codex" | "claude" @@ -64,28 +67,28 @@ function dedupeTargets(targets: SkillInstallTarget[]): SkillInstallTarget[] { export function resolveSkillSourceDir(): string { const cliDir = dirname(fileURLToPath(import.meta.url)) const candidates = [ - // Published build: dist/cli/installSkill.js -> dist/skills/charts2-cli - resolve(cliDir, "../skills", CHARTS2_SKILL_NAME), - // Source and monorepo dev: src/cli/installSkill.ts -> skills/charts2-cli - resolve(cliDir, "../../skills", CHARTS2_SKILL_NAME), + // Published build: dist/cli/installSkill.js -> dist/skills/charts-cli + resolve(cliDir, "../skills", CHARTS_SKILL_NAME), + // Source and monorepo dev: src/cli/installSkill.ts -> skills/charts-cli + resolve(cliDir, "../../skills", CHARTS_SKILL_NAME), ] for (const candidate of candidates) { if (existsSync(join(candidate, "SKILL.md"))) return candidate } - throw new CliUsageError(`Could not find bundled skill "${CHARTS2_SKILL_NAME}"`) + throw new CliUsageError(`Could not find bundled skill "${CHARTS_SKILL_NAME}"`) } export function resolveInstallTargets(args: { agent?: string; path?: string }): SkillInstallTarget[] { if (args.path !== undefined) { if (args.path === "") throw new CliUsageError("--path requires a value") const skillsRoot = resolve(expandHome(args.path)) - return [{ agent: "custom", skillsRoot, destination: join(skillsRoot, CHARTS2_SKILL_NAME) }] + return [{ agent: "custom", skillsRoot, destination: join(skillsRoot, CHARTS_SKILL_NAME) }] } const agent = parseAgent(args.agent) const makeTarget = (name: "codex" | "claude"): SkillInstallTarget => { const skillsRoot = skillsRootFor(name) - return { agent: name, skillsRoot, destination: join(skillsRoot, CHARTS2_SKILL_NAME) } + return { agent: name, skillsRoot, destination: join(skillsRoot, CHARTS_SKILL_NAME) } } if (agent === "codex" || agent === "claude") return [makeTarget(agent)] @@ -100,10 +103,22 @@ export function resolveInstallTargets(args: { agent?: string; path?: string }): return dedupeTargets(targets.length > 0 ? targets : [makeTarget("codex")]) } +function removeLegacySkills(skillsRoot: string): void { + for (const name of LEGACY_SKILL_NAMES) { + const legacyDir = join(skillsRoot, name) + const skillFile = join(legacyDir, "SKILL.md") + // Only remove directories that are actually an old install of this skill. + if (!existsSync(skillFile)) continue + if (!readFileSync(skillFile, "utf8").includes(`name: ${name}`)) continue + rmSync(legacyDir, { recursive: true, force: true }) + process.stdout.write(`removed legacy ${name} skill: ${legacyDir}\n`) + } +} + export function installSkill(args: { sourceDir: string; targets: SkillInstallTarget[]; force: boolean }): void { for (const target of args.targets) { - if (basename(target.destination) !== CHARTS2_SKILL_NAME) { - throw new CliUsageError(`Internal error: destination must end with ${CHARTS2_SKILL_NAME}`) + if (basename(target.destination) !== CHARTS_SKILL_NAME) { + throw new CliUsageError(`Internal error: destination must end with ${CHARTS_SKILL_NAME}`) } if (existsSync(target.destination)) { if (!args.force) { @@ -113,7 +128,8 @@ export function installSkill(args: { sourceDir: string; targets: SkillInstallTar } mkdirSync(target.skillsRoot, { recursive: true }) cpSync(args.sourceDir, target.destination, { recursive: true }) - process.stdout.write(`installed ${CHARTS2_SKILL_NAME} for ${target.agent}: ${target.destination}\n`) + removeLegacySkills(target.skillsRoot) + process.stdout.write(`installed ${CHARTS_SKILL_NAME} for ${target.agent}: ${target.destination}\n`) } } @@ -126,7 +142,7 @@ export function runInstallSkill(args: InstallSkillArgs): void { export const installSkillCommand = defineCommand({ meta: { name: "install-skill", - description: "Install the bundled charts2 CLI agent skill into Codex, Claude, or a custom skills directory", + description: "Install the bundled charts CLI agent skill into Codex, Claude, or a custom skills directory", }, args: { agent: { @@ -135,11 +151,11 @@ export const installSkillCommand = defineCommand({ }, path: { type: "string", - description: "Custom agent skills directory; installs /charts2-cli and ignores --agent", + description: "Custom agent skills directory; installs /charts-cli and ignores --agent", }, force: { type: "boolean", - description: "Replace an existing charts2-cli skill at the destination", + description: "Replace an existing charts-cli skill at the destination", default: false, }, }, diff --git a/packages/charts2/src/cli/loadInputs.test.ts b/packages/charts/src/cli/loadInputs.test.ts similarity index 100% rename from packages/charts2/src/cli/loadInputs.test.ts rename to packages/charts/src/cli/loadInputs.test.ts diff --git a/packages/charts2/src/cli/loadInputs.ts b/packages/charts/src/cli/loadInputs.ts similarity index 100% rename from packages/charts2/src/cli/loadInputs.ts rename to packages/charts/src/cli/loadInputs.ts diff --git a/packages/charts2/src/cli/render.test.ts b/packages/charts/src/cli/render.test.ts similarity index 99% rename from packages/charts2/src/cli/render.test.ts rename to packages/charts/src/cli/render.test.ts index 4334bd461a4..4215c1b2bef 100644 --- a/packages/charts2/src/cli/render.test.ts +++ b/packages/charts/src/cli/render.test.ts @@ -224,7 +224,7 @@ describe("rasterize", () => { it("renders a thumbnail PNG of provincial-budgets", () => { if (!existsSync(defaultFontsDir())) { console.warn( - "skipping PNG smoke test: .fonts-cache missing — run `bun run extract-font-metrics` in packages/charts2", + "skipping PNG smoke test: .fonts-cache missing — run `bun run extract-font-metrics` in packages/charts", ) return } diff --git a/packages/charts2/src/cli/render.ts b/packages/charts/src/cli/render.ts similarity index 98% rename from packages/charts2/src/cli/render.ts rename to packages/charts/src/cli/render.ts index dcc2417b28c..8ffaf6fda92 100644 --- a/packages/charts2/src/cli/render.ts +++ b/packages/charts/src/cli/render.ts @@ -271,7 +271,7 @@ export function rasterize(svg: string, options: RasterizeOptions): Buffer { throw new CliFailure( `Fonts directory not found: ${options.fontsDir}\n` + "PNG rasterization needs TTF copies of the brand fonts. Run `bun run extract-font-metrics` " + - "in packages/charts2 to regenerate the .fonts-cache, or pass --fonts pointing at " + + "in packages/charts to regenerate the .fonts-cache, or pass --fonts pointing at " + "licensed TTF copies.", ) } @@ -279,7 +279,7 @@ export function rasterize(svg: string, options: RasterizeOptions): Buffer { if (fontFiles.length === 0) { throw new CliFailure( `No .ttf files found in ${options.fontsDir}. Run \`bun run extract-font-metrics\` in ` + - "packages/charts2, or pass --fonts containing TTF copies of the brand fonts.", + "packages/charts, or pass --fonts containing TTF copies of the brand fonts.", ) } const resvg = new Resvg(svg, { diff --git a/packages/charts2/src/cli/scaffold.test.ts b/packages/charts/src/cli/scaffold.test.ts similarity index 100% rename from packages/charts2/src/cli/scaffold.test.ts rename to packages/charts/src/cli/scaffold.test.ts diff --git a/packages/charts2/src/cli/scaffold.ts b/packages/charts/src/cli/scaffold.ts similarity index 100% rename from packages/charts2/src/cli/scaffold.ts rename to packages/charts/src/cli/scaffold.ts diff --git a/packages/charts2/src/cli/validate.test.ts b/packages/charts/src/cli/validate.test.ts similarity index 100% rename from packages/charts2/src/cli/validate.test.ts rename to packages/charts/src/cli/validate.test.ts diff --git a/packages/charts2/src/cli/validate.ts b/packages/charts/src/cli/validate.ts similarity index 100% rename from packages/charts2/src/cli/validate.ts rename to packages/charts/src/cli/validate.ts diff --git a/packages/charts/src/components/BodyPortal/BodyPortal.tsx b/packages/charts/src/components/BodyPortal/BodyPortal.tsx deleted file mode 100644 index f201206125b..00000000000 --- a/packages/charts/src/components/BodyPortal/BodyPortal.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from "react" -import ReactDOM from "react-dom" - -interface BodyPortalProps { - children: React.ReactNode - containerClassName?: string -} - -/** - * Renders a component on Body instead of inside the current tree. - * See https://react.dev/reference/react-dom/createPortal - */ -export class BodyPortal extends React.Component { - private element: HTMLDivElement | null = null - - override componentDidMount(): void { - if (typeof document !== "undefined") { - this.element = document.createElement("div") - if (this.props.containerClassName) - this.element.className = this.props.containerClassName - - document.body.appendChild(this.element) - // Force a re-render now that `this.el` is available for the portal - this.forceUpdate() - } - } - - override componentWillUnmount(): void { - // Remove the element from the body if it was added - if (this.element && this.element.parentNode === document.body) { - document.body.removeChild(this.element) - } - this.element = null // Clean up the reference - } - - override render(): React.ReactPortal | null { - if (!this.element) return null - return ReactDOM.createPortal(this.props.children, this.element) - } -} diff --git a/packages/charts/src/components/Button/Button.scss b/packages/charts/src/components/Button/Button.scss deleted file mode 100644 index 64734796900..00000000000 --- a/packages/charts/src/components/Button/Button.scss +++ /dev/null @@ -1,231 +0,0 @@ -/******************************************************************************* - * Charts Button Component - * - * Uses Build Canada design system styling with square corners and brand colors. - ******************************************************************************/ - -.charts-btn { - @include body-3-medium; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 10px 24px; - border: 2px solid transparent; - border-radius: 0; - text-align: center; - cursor: pointer; - transition: all 150ms ease; - white-space: nowrap; - text-transform: uppercase; - letter-spacing: 0.05em; - font-weight: 500; - - &:focus-visible { - outline: 2px solid $auburn; - outline-offset: 2px; - } - - &.charts-btn--icon-only { - padding: 8px; - min-width: 40px; - min-height: 40px; - } - - svg { - font-size: 0.875em; - transition: transform 150ms ease; - } - - // Animated arrow on hover - &:hover:not(:disabled) svg.icon-right { - transform: translateX(4px); - } -} - -/******************************************************************************* - * Icon Position Modifiers - ******************************************************************************/ - -.charts-btn--icon-right { - margin-left: 8px; -} - -.charts-btn--icon-left { - margin-right: 8px; -} - -/******************************************************************************* - * Solid Variants - ******************************************************************************/ - -.charts-btn--solid-auburn, -.charts-btn--solid-vermillion { - background-color: $auburn; - border-color: $auburn; - color: $white; - - &:hover:not(:disabled) { - background-color: $auburn-600; - border-color: $auburn-600; - } - - &:active:not(:disabled) { - background-color: $auburn-700; - border-color: $auburn-700; - } -} - -.charts-btn--solid-charcoal { - background-color: $charcoal; - border-color: $charcoal; - color: $white; - - &:hover:not(:disabled) { - background-color: $gray-70; - border-color: $gray-70; - } - - &:active:not(:disabled) { - background-color: $gray-60; - border-color: $gray-60; - } -} - -.charts-btn--solid-cerulean, -.charts-btn--solid-blue { - background-color: $cerulean-500; - border-color: $cerulean-500; - color: $white; - - &:hover:not(:disabled) { - background-color: $cerulean-600; - border-color: $cerulean-600; - } - - &:active:not(:disabled) { - background-color: $cerulean-700; - border-color: $cerulean-700; - } -} - -.charts-btn--solid-dark-blue { - background-color: $cerulean-900; - border-color: $cerulean-900; - color: $white; - - &:hover:not(:disabled) { - background-color: $cerulean-950; - border-color: $cerulean-950; - } -} - -.charts-btn--solid-light-blue { - background-color: $cerulean-100; - border-color: $cerulean-100; - color: $cerulean-900; - - &:hover:not(:disabled) { - background-color: $cerulean-200; - border-color: $cerulean-200; - } -} - -.charts-btn--solid-linen { - background-color: $linen; - border-color: $linen; - color: $charcoal; - - &:hover:not(:disabled) { - background-color: $gray-20; - border-color: $gray-20; - } - - &:active:not(:disabled) { - background-color: $gray-30; - border-color: $gray-30; - } -} - -/******************************************************************************* - * Outline Variants - ******************************************************************************/ - -.charts-btn--outline-auburn, -.charts-btn--outline-vermillion { - background-color: transparent; - border-color: $auburn; - color: $auburn; - - &:hover:not(:disabled) { - background-color: $auburn; - color: $white; - } - - &:active:not(:disabled) { - background-color: $auburn-600; - border-color: $auburn-600; - color: $white; - } -} - -.charts-btn--outline-charcoal { - background-color: transparent; - border-color: $charcoal; - color: $charcoal; - - &:hover:not(:disabled) { - background-color: $charcoal; - color: $white; - } - - &:active:not(:disabled) { - background-color: $gray-70; - border-color: $gray-70; - color: $white; - } -} - -.charts-btn--outline-white { - background-color: transparent; - border-color: $white; - color: $white; - - &:hover:not(:disabled) { - background-color: $white; - color: $charcoal; - } - - &:active:not(:disabled) { - background-color: $gray-10; - border-color: $gray-10; - color: $charcoal; - } -} - -.charts-btn--outline-cerulean, -.charts-btn--outline-light-blue { - background-color: transparent; - border-color: $cerulean-500; - color: $cerulean-700; - - &:hover:not(:disabled) { - background-color: $cerulean-500; - color: $white; - } - - &:active:not(:disabled) { - background-color: $cerulean-600; - border-color: $cerulean-600; - color: $white; - } -} - -/******************************************************************************* - * Disabled State - ******************************************************************************/ - -.charts-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} diff --git a/packages/charts/src/components/Button/Button.stories.tsx b/packages/charts/src/components/Button/Button.stories.tsx deleted file mode 100644 index 4e566eb33c8..00000000000 --- a/packages/charts/src/components/Button/Button.stories.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react" -import { faDownload, faShare, faPlus } from "@fortawesome/free-solid-svg-icons" - -import { Button } from "./Button.js" - -const meta: Meta = { - title: "Components/Button", - component: Button, - parameters: { - docs: { - description: { - component: ` -A button component for the charts library with multiple theme variants. - -## Usage - -\`\`\`tsx -import { Button } from "@buildcanada/charts" - - -} diff --git a/packages/charts/src/components/Checkbox.scss b/packages/charts/src/components/Checkbox.scss deleted file mode 100644 index 949185be6a1..00000000000 --- a/packages/charts/src/components/Checkbox.scss +++ /dev/null @@ -1,105 +0,0 @@ -/******************************************************************************* - * Checkbox - * - * Custom checkbox styling. - * Uses Build Canada design system colors. - ******************************************************************************/ - -.checkbox { - $checkbox-size: 18px; - - $light-stroke: $gray-30; - $hover-stroke: $auburn-500; - $active-fill: $auburn-500; - - position: relative; - display: flex; - align-items: flex-start; - - label { - margin: 0; - } - - input { - position: absolute; - opacity: 0; - left: 0; - cursor: pointer; - pointer-events: none; - } - - .custom { - position: absolute; - left: 0; - top: 0; - content: " "; - width: $checkbox-size; - height: $checkbox-size; - - background: $white; - pointer-events: none; - border-radius: 0; - border: 2px solid $light-stroke; - color: $white; - - display: flex; - align-items: center; - justify-content: center; - transition: all 150ms ease; - - svg { - font-size: 10px; - } - } - - input:focus-visible + .custom { - outline: 2px solid $auburn; - outline-offset: 2px; - } - - input:active + .custom { - background: $auburn-100; - } - - input:checked + .custom { - background: $active-fill; - border-color: $active-fill; - } - - input:checked:active + .custom { - background: $auburn-600; - border-color: $auburn-600; - } - - input:disabled { - + .custom { - background: $gray-10; - border-color: $gray-20; - color: $gray-50; - } - - &:active + .custom { - background: $gray-10; - } - - ~ .label { - color: $gray-50; - cursor: not-allowed; - } - } - - .label { - @include grapher_label-2-regular; - padding-left: $checkbox-size + 10px; - cursor: pointer; - user-select: none; - color: $charcoal; - line-height: $checkbox-size; - } - - &:hover { - input:not(:checked):not(:disabled) + .custom { - border-color: $hover-stroke; - } - } -} diff --git a/packages/charts/src/components/Checkbox.stories.tsx b/packages/charts/src/components/Checkbox.stories.tsx deleted file mode 100644 index bf35ddfbe6c..00000000000 --- a/packages/charts/src/components/Checkbox.stories.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import * as React from "react" -import type { Meta, StoryObj } from "@storybook/react" -import { fn } from "storybook/test" - -import { Checkbox } from "./Checkbox.js" - -const meta: Meta = { - title: "Components/Checkbox", - component: Checkbox, - parameters: { - docs: { - description: { - component: ` -A checkbox component for the charts library. - -## Usage - -\`\`\`tsx -import { Checkbox } from "@buildcanada/charts" - - setIsChecked(e.target.checked)} -/> -\`\`\` - -## Rich Labels - -Labels can include React nodes for links or formatting: - -\`\`\`tsx -I agree to the terms} - onChange={handleChange} -/> -\`\`\` - -## Controlled Component - -This is a controlled component - manage the \`checked\` state externally. - `, - }, - }, - }, - argTypes: { - checked: { description: "Whether the checkbox is checked" }, - label: { description: "Label text or React node" }, - disabled: { description: "Whether the checkbox is disabled" }, - onChange: { description: "Callback when checkbox state changes" }, - }, - decorators: [ - (Story) => ( -
- -
- ), - ], -} - -export default meta -type Story = StoryObj - -export const Unchecked: Story = { - args: { - checked: false, - label: "Accept terms and conditions", - onChange: fn(), - }, -} - -export const Checked: Story = { - args: { - checked: true, - label: "Accept terms and conditions", - onChange: fn(), - }, -} - -export const Disabled: Story = { - args: { - checked: false, - label: "This option is disabled", - disabled: true, - onChange: fn(), - }, -} - -export const DisabledChecked: Story = { - args: { - checked: true, - label: "This option is disabled but checked", - disabled: true, - onChange: fn(), - }, -} - -export const WithRichLabel: Story = { - args: { - checked: false, - label: ( - - I agree to the terms of service - - ), - onChange: fn(), - }, -} - -export const Interactive: Story = { - render: function InteractiveCheckbox() { - const [checked, setChecked] = React.useState(false) - return ( - setChecked(e.target.checked)} - /> - ) - }, -} diff --git a/packages/charts/src/components/Checkbox.tsx b/packages/charts/src/components/Checkbox.tsx deleted file mode 100644 index 86c5ad58324..00000000000 --- a/packages/charts/src/components/Checkbox.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from "react" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faCheck } from "@fortawesome/free-solid-svg-icons" -import cx from "classnames" - -export const Checkbox = ({ - className, - checked, - onChange, - label, - disabled, - id, - "data-test": testHook, -}: { - className?: string - checked: boolean - onChange: React.ChangeEventHandler - label: React.ReactNode - disabled?: boolean - id?: string - "data-test"?: string -}) => { - return ( -
- -
- ) -} diff --git a/packages/charts/src/components/ExpandableToggle/ExpandableToggle.scss b/packages/charts/src/components/ExpandableToggle/ExpandableToggle.scss deleted file mode 100644 index 8f84b1bed0f..00000000000 --- a/packages/charts/src/components/ExpandableToggle/ExpandableToggle.scss +++ /dev/null @@ -1,123 +0,0 @@ -details.ExpandableToggle { - --title: var(--expandable-toggle-title, currentColor); - --border: var(--expandable-toggle-border, #d0dae3); - --content: var(--expandable-toggle-content, #{$blue-60}); - - --button: var(--expandable-toggle-button, #{$blue-90}); - --button-hover: var(--expandable-toggle-button-hover, #{$blue-60}); - - --title-size: var(--expandable-toggle-title-size, 1rem); - --content-size: var(--expandable-toggle-content-size, 0.875rem); - - border-top: 1px solid var(--border); - border-bottom: 1px solid var(--border); - - // Hide the default expand/collapse icons of the element - .ExpandableToggle__container { - list-style: none; - &::marker, - &::-webkit-details-marker { - display: none; - } - } - - .ExpandableToggle__button { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - padding: 16px 0; - color: var(--button); - background-color: transparent; - border: none; - cursor: pointer; - text-align: left; - - &:hover { - color: var(--button-hover); - } - - .ExpandableToggle__title { - @include h4-semibold; - margin: 0; - color: var(--title); - font-size: var(--title-size); - - &:hover { - color: var(--button-hover); - } - } - - .ExpandableToggle__icon { - font-size: 0.75em; - margin: 0 12px; - } - } - - .ExpandableToggle__content, - .ExpandableToggle__teaser { - height: 0; - overflow: hidden; - color: var(--content); - - @include body-3-medium; - font-size: var(--content-size); - - .article-block__text, - .article-block__list, - .article-block__html, - .article-block__numbered-list, - p { - @include body-3-medium; - font-size: var(--content-size); - } - - > *:first-child { - margin-top: 0; - } - - > *:last-child { - margin-bottom: 0; - } - } -} - -details.ExpandableToggle--stacked { - border-bottom: none; -} - -details.ExpandableToggle--teaser .ExpandableToggle__button, -.ExpandableToggle[open] .ExpandableToggle__button { - padding-bottom: 8px; -} - -details.ExpandableToggle--teaser .ExpandableToggle__teaser { - display: block; - height: 96px; - -webkit-mask-image: linear-gradient(180deg, #000 0%, transparent); -} - -details.ExpandableToggle[open] .ExpandableToggle__teaser { - display: none; -} - -details.ExpandableToggle[open] .ExpandableToggle__content { - padding-bottom: 16px; - height: auto; - -webkit-mask-image: none; -} - -// Show the correct icon based on the state of the details element -details.ExpandableToggle .ExpandableToggle__icon--expand { - display: block; -} -details.ExpandableToggle[open] .ExpandableToggle__icon--expand { - display: none; -} - -details.ExpandableToggle .ExpandableToggle__icon--collapse { - display: none; -} -details.ExpandableToggle[open] .ExpandableToggle__icon--collapse { - display: block; -} diff --git a/packages/charts/src/components/ExpandableToggle/ExpandableToggle.tsx b/packages/charts/src/components/ExpandableToggle/ExpandableToggle.tsx deleted file mode 100644 index fa51cdae277..00000000000 --- a/packages/charts/src/components/ExpandableToggle/ExpandableToggle.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import type { ReactNode } from "react" -import cx from "classnames" - -export const ExpandableToggle = ({ - label, - content, - alwaysVisibleDescription, - isExpandedDefault = false, - isStacked = false, - hasTeaser = false, -}: { - label: string - content?: ReactNode - alwaysVisibleDescription?: ReactNode - isExpandedDefault?: boolean - isStacked?: boolean - hasTeaser?: boolean -}) => ( -
- -
-
-

{label}

- {alwaysVisibleDescription && ( -
- {alwaysVisibleDescription} -
- )} -
- - {/* We show both the opened and closed icons, and use CSS to hide/show them based on the state */} - - -
- - {/* If there is a teaser, we need to place the content element inside the summary so it is visible even when the details element is closed. - Also, make it inert so it isn't included in the browser Cmd-F search - instead, the browser will auto-expand it when searching for content that's currently collapsed */} - {hasTeaser && ( -
- {content} -
- )} -
-
{content}
-
-) diff --git a/packages/charts/src/components/GrapherTabIcon.tsx b/packages/charts/src/components/GrapherTabIcon.tsx deleted file mode 100644 index c8c559ca51c..00000000000 --- a/packages/charts/src/components/GrapherTabIcon.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import * as React from "react" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { - faChartBar, - faChartColumn, - faChartLine, - faEarthAmericas, - faTable, -} from "@fortawesome/free-solid-svg-icons" -import { - GRAPHER_CHART_TYPES, - GRAPHER_TAB_NAMES, - GrapherChartType, - GrapherTabName, -} from "../types/index.js" - -export function GrapherTabIcon({ - tab, - isLineChartThatTurnedIntoDiscreteBar, -}: { - tab: GrapherTabName - isLineChartThatTurnedIntoDiscreteBar?: boolean -}): React.ReactElement { - switch (tab) { - case GRAPHER_TAB_NAMES.Table: - return - case GRAPHER_TAB_NAMES.WorldMap: - return ( - - ) - default: { - const chartIcon = - tab === GRAPHER_TAB_NAMES.LineChart && - isLineChartThatTurnedIntoDiscreteBar - ? chartIcons[GRAPHER_CHART_TYPES.DiscreteBar] - : chartIcons[tab] - return chartIcon - } - } -} - -const chartIcons: Record = { - // line chart - [GRAPHER_CHART_TYPES.LineChart]: ( - - ), - - // bar charts - [GRAPHER_CHART_TYPES.DiscreteBar]: ( - - ), - [GRAPHER_CHART_TYPES.StackedBar]: ( - - ), - [GRAPHER_CHART_TYPES.StackedDiscreteBar]: ( - - ), - - // scatter - [GRAPHER_CHART_TYPES.ScatterPlot]: ( - - - - - - - - - - - ), - - // marimekko - [GRAPHER_CHART_TYPES.Marimekko]: ( - - - - - - - - - - - ), - - // stacked area - [GRAPHER_CHART_TYPES.StackedArea]: ( - - - - - - - ), - - // slope chart - [GRAPHER_CHART_TYPES.SlopeChart]: ( - - - - - - - - - - ), -} diff --git a/packages/charts/src/components/GrapherTrendArrow.scss b/packages/charts/src/components/GrapherTrendArrow.scss deleted file mode 100644 index 26e012bf25e..00000000000 --- a/packages/charts/src/components/GrapherTrendArrow.scss +++ /dev/null @@ -1,16 +0,0 @@ -.GrapherTrendArrow { - $grey: #787878; - $red: #cc3b55; - $green: #2c8465; - - fill: $grey; - &.colored.up path { - fill: $green; - } - &.colored.down path { - fill: $red; - } - &.colored.right path { - fill: $grey; - } -} diff --git a/packages/charts/src/components/GrapherTrendArrow.tsx b/packages/charts/src/components/GrapherTrendArrow.tsx deleted file mode 100644 index 11c0dbf500a..00000000000 --- a/packages/charts/src/components/GrapherTrendArrow.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import cx from "classnames" -import { GrapherTrendArrowDirection } from "../types/index.js" - -const ARROW_PATHS = { - up: "m14,0H5c-.552,0-1,.448-1,1s.448,1,1,1h6.586L.29303,13.29297l1.41394,1.414L13,3.41394v6.58606c0,.552.448,1,1,1s1-.448,1-1V1c0-.552-.448-1-1-1Z", - down: "m14,4c-.552,0-1,.448-1,1v6.586L1.56049.14648.14655,1.56042l11.43958,11.43958h-6.58612c-.552,0-1,.448-1,1s.448,1,1,1h9c.552,0,1-.448,1-1V5c0-.552-.448-1-1-1Z", - right: "m19.59198,6.82422L13.22803.46021c-.39105-.39099-1.02405-.39099-1.414,0-.39105.39001-.39105,1.02405,0,1.414l4.65698,4.65704H.5v2h15.97101l-4.65698,4.65698c-.39105.39001-.39105,1.02399,0,1.414.38995.39099,1.02295.39099,1.414,0l6.36395-6.36401c.39001-.39001.39001-1.02399,0-1.414Z", -} as const - -export function GrapherTrendArrow({ - direction, - isColored = true, - className, -}: { - direction: GrapherTrendArrowDirection - isColored?: boolean - className?: string -}): React.ReactElement { - return ( - - - - ) -} diff --git a/packages/charts/src/components/Halo/Halo.tsx b/packages/charts/src/components/Halo/Halo.tsx deleted file mode 100644 index 851e836ffa8..00000000000 --- a/packages/charts/src/components/Halo/Halo.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from "react" -import { Color } from "../../types/index.js" - -interface HaloProps { - id: string - children: React.ReactElement> - show?: boolean - outlineWidth: number - outlineColor?: Color - style?: React.CSSProperties -} - -const defaultHaloStyle: React.CSSProperties = { - fill: "#fff", - stroke: "#fff", - strokeLinecap: "round", - strokeLinejoin: "round", - userSelect: "none", -} - -export function Halo(props: HaloProps): React.ReactElement { - const show = props.show ?? true - if (!show) return props.children - - const defaultStyle = { - ...defaultHaloStyle, - strokeWidth: props.outlineWidth, - fill: props.outlineColor ?? defaultHaloStyle.fill, - stroke: props.outlineColor ?? defaultHaloStyle.stroke, - } - const halo = React.cloneElement(props.children, { - id: props.id, - style: { - ...defaultStyle, - ...props.style, - }, - }) - return ( - - {halo} - {props.children} - - ) -} diff --git a/packages/charts/src/components/LabeledSwitch/LabeledSwitch.scss b/packages/charts/src/components/LabeledSwitch/LabeledSwitch.scss deleted file mode 100644 index 9963cf45adc..00000000000 --- a/packages/charts/src/components/LabeledSwitch/LabeledSwitch.scss +++ /dev/null @@ -1,122 +0,0 @@ -/******************************************************************************* - * Labeled Switch - * - * Toggle switch with label. - * Uses Build Canada design system colors. - ******************************************************************************/ - -@use "sass:color"; - -$light-fill: $gray-30; -$active-fill: $cerulean-100; -$active-switch: $cerulean-500; -$medium: 400; -$lato: $sans-serif-font-stack; - -// On/off switch with label written to the right -.labeled-switch { - // Keep in sync with TableFilterToggle.tsx - // where the width of a labeled switch is calculated - - display: flex; - align-items: center; - color: $gray-60; - font: $medium 13px/16px $lato; - letter-spacing: 0.01em; - - position: relative; - margin: 8px 0; - -webkit-user-select: none; - user-select: none; - - label { - color: $charcoal; - padding-left: 40px; - white-space: nowrap; - cursor: pointer; - - svg { - color: $gray-50; - height: 13px; - padding: 0 0.333em; - } - } - - .labeled-switch-subtitle { - // Only show subtitle in settings menu, otherwise use icon + tooltip - display: none; - } - - input { - position: absolute; - opacity: 0; - left: 0; - cursor: pointer; - } - - .outer { - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - content: " "; - width: 32px; - height: 18px; - background: $light-fill; - border-radius: 9px; - pointer-events: none; - transition: background 200ms ease; - - .inner { - position: relative; - content: " "; - width: 12px; - height: 12px; - background: $gray-60; - border-radius: 50%; - top: 3px; - left: 3px; - pointer-events: none; - transition: transform 200ms ease, background 200ms ease; - } - } - - &:hover { - .outer .inner { - background: $charcoal; - } - } - - input:focus-visible + .outer { - outline: 2px solid $auburn; - outline-offset: 2px; - } - - input:checked + .outer { - background: $active-fill; - - .inner { - background: $active-switch; - transform: translate(14px, 0); - } - } - - &:hover input:checked + .outer .inner { - background: $cerulean-600; - } - - &.labeled-switch--is-disabled { - opacity: 0.5; - cursor: not-allowed; - - label { - cursor: not-allowed; - } - - &:hover { - .inner { - background: $gray-60; - } - } - } -} diff --git a/packages/charts/src/components/LabeledSwitch/LabeledSwitch.stories.tsx b/packages/charts/src/components/LabeledSwitch/LabeledSwitch.stories.tsx deleted file mode 100644 index 8d4b06164f1..00000000000 --- a/packages/charts/src/components/LabeledSwitch/LabeledSwitch.stories.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import * as React from "react" -import type { Meta, StoryObj } from "@storybook/react" -import { fn } from "storybook/test" - -import { LabeledSwitch } from "./LabeledSwitch.js" - -const meta: Meta = { - title: "Components/LabeledSwitch", - component: LabeledSwitch, - parameters: { - docs: { - description: { - component: ` -A toggle switch component with a label and optional tooltip. - -## Usage - -\`\`\`tsx -import { LabeledSwitch } from "@buildcanada/charts" - - setIsEnabled(!isEnabled)} -/> -\`\`\` - -## With Tooltip - -\`\`\`tsx - -\`\`\` - -## Controlled Component - -This is a controlled component - you must manage the \`value\` state and handle \`onToggle\`. - `, - }, - }, - }, - argTypes: { - value: { description: "Current toggle state (on/off)" }, - label: { description: "Label text displayed next to the switch" }, - tooltip: { description: "Optional tooltip text on hover" }, - disabled: { description: "Whether the switch is disabled" }, - onToggle: { description: "Callback when switch is toggled" }, - }, - decorators: [ - (Story) => ( -
- -
- ), - ], -} - -export default meta -type Story = StoryObj - -export const Off: Story = { - args: { - value: false, - label: "Enable notifications", - onToggle: fn(), - }, -} - -export const On: Story = { - args: { - value: true, - label: "Enable notifications", - onToggle: fn(), - }, -} - -export const WithTooltip: Story = { - args: { - value: false, - label: "Show advanced options", - tooltip: "This will reveal additional configuration settings for power users.", - onToggle: fn(), - }, -} - -export const Disabled: Story = { - args: { - value: false, - label: "Premium feature", - disabled: true, - onToggle: fn(), - }, -} - -export const DisabledOn: Story = { - args: { - value: true, - label: "Always enabled", - disabled: true, - onToggle: fn(), - }, -} - -export const Interactive: Story = { - render: function InteractiveSwitch() { - const [value, setValue] = React.useState(false) - return ( - setValue(!value)} - /> - ) - }, -} diff --git a/packages/charts/src/components/LabeledSwitch/LabeledSwitch.tsx b/packages/charts/src/components/LabeledSwitch/LabeledSwitch.tsx deleted file mode 100644 index 8028f1f5e12..00000000000 --- a/packages/charts/src/components/LabeledSwitch/LabeledSwitch.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from "react" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faInfoCircle } from "@fortawesome/free-solid-svg-icons" -import cx from "classnames" -import { Tippy } from "../../utils/index.js" - -export const LabeledSwitch = ({ - className, - label, - value, - tooltip, - tracking, - disabled, - onToggle, -}: { - value?: boolean - label?: string - tooltip?: string - tracking?: string - onToggle: () => any - className?: string - disabled?: boolean -}): React.ReactElement => { - return ( -
- - {tooltip && ( -
{tooltip}
- )} -
- ) -} diff --git a/packages/charts/src/components/MarkdownTextWrap/MarkdownTextWrap.test.ts b/packages/charts/src/components/MarkdownTextWrap/MarkdownTextWrap.test.ts deleted file mode 100644 index 2dab44f7cd5..00000000000 --- a/packages/charts/src/components/MarkdownTextWrap/MarkdownTextWrap.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { expect, it, describe } from "vitest" - -import { FontFamily } from "../../utils/index.js" -import { - IRText, - MarkdownTextWrap, - getLineWidth, - lineToPlaintext, - recursiveMergeTextTokens, - IRWhitespace, - IRBold, - IRLink, - IRLineBreak, -} from "./MarkdownTextWrap.js" - -describe("MarkdownTextWrap", () => { - it("heavier fontWeight should be wider than plain IRText", () => { - const string = "abcdefghijklmnopqrstuvwxyz" - - const text = new IRText(string) - const bold = new IRText(string, { fontWeight: 700 }) - - expect(text.width).toBeLessThan(bold.width) - }) - - it("should render bold and calculate that it is wider", () => { - const plainString = "abcdefghijklmnopqrstuvwxyz" - const boldString = "**abcdefghijklmnopqrstuvwxyz**" - - const plainMarkdownTextWrap = new MarkdownTextWrap({ - text: plainString, - fontSize: 14, - maxWidth: 200, - }) - const boldMarkdownTextWrap = new MarkdownTextWrap({ - text: boldString, - fontSize: 14, - maxWidth: 200, - }) - - const plainWidth = getLineWidth(plainMarkdownTextWrap.htmlLines[0]) - const boldWidth = getLineWidth(boldMarkdownTextWrap.htmlLines[0]) - expect(plainWidth).toBeLessThan(boldWidth) - }) - - it("should accept and apply fontParams", () => { - const element = new MarkdownTextWrap({ - text: "abcdefghijklmnopqrstuvwxyz", - fontSize: 14, - maxWidth: 200, - fontFamily: FontFamily["comic sans ms"], - fontWeight: 800, - }) - - const output = element.renderHTML() - - expect(output?.props.style).toMatchObject({ - fontFamily: FontFamily["comic sans ms"], - fontWeight: 800, - fontSize: 14, - }) - }) - - it("should calculate height correctly", () => { - const element = new MarkdownTextWrap({ - text: "a\nb\nc", - fontSize: 10, - lineHeight: 1.5, - }) - - // 10 fontSize * 1.5 lineHeight * 3 lines = 45px - expect(element.height).toEqual(45) - }) - - it("should return zero height for empty string", () => { - const element = new MarkdownTextWrap({ - text: "", - fontSize: 16, - }) - - expect(element.height).toEqual(0) - }) - - it("should split on newline", () => { - const element = new MarkdownTextWrap({ - text: "_test\n**\nnewlineyarn \n**_test", - fontSize: 10, - lineHeight: 1, - }) - - expect(element.height).toEqual(40) - }) - - it("should convert to plaintext", () => { - const element = new MarkdownTextWrap({ - text: "I am some _bold_ text with a [really really really long detail on demand](#dod:test) and because I am so long you would think that I **span multiple lines** but when you transform me into plaintext I actually just stay as one line", - fontSize: 10, - lineHeight: 1, - }) - - expect(element.htmlLines.map(lineToPlaintext)).toEqual([ - "I am some bold text with a really really really long detail on demand and because I am so long you would think that I span multiple lines but when you transform me into plaintext I actually just stay as one line", - ]) - }) - - describe(recursiveMergeTextTokens, () => { - it("should merge adjacent text tokens", () => { - const tokens = [ - new IRText("one"), - new IRWhitespace(), - new IRText("two"), - ] - - const merged = recursiveMergeTextTokens(tokens) - - expect(merged).toEqual([new IRText("one two")]) - }) - - it("should merge inside complicated tokens", () => { - const tokens = [ - new IRBold([ - new IRText("one"), - new IRWhitespace(), - new IRText("two"), - ]), - new IRText("three"), - new IRText("four"), - ] - - const merged = recursiveMergeTextTokens(tokens) - - expect(merged).toEqual([ - new IRBold([new IRText("one two")]), - new IRText("threefour"), - ]) - }) - - it("should merge multi-level tokens", () => { - const tokens = [ - new IRBold([ - new IRText("one"), - new IRLink("https://example.com", [ - new IRText("two"), - new IRWhitespace(), - new IRLineBreak(), - ]), - new IRText("three"), - ]), - new IRText("four"), - new IRText("five"), - ] - - const merged = recursiveMergeTextTokens(tokens) - - expect(merged).toEqual([ - new IRBold([ - new IRText("one"), - new IRLink("https://example.com", [ - new IRText("two "), - new IRLineBreak(), - ]), - new IRText("three"), - ]), - new IRText("fourfive"), - ]) - }) - }) -}) - -describe("fromFragments", () => { - const fontSize = 14 - - it("should place fragments in-line by default", () => { - const textWrap = MarkdownTextWrap.fromFragments({ - main: { text: "Lower middle-income countries" }, - secondary: { text: "30 million" }, - textWrapProps: { - maxWidth: 500, - fontSize, - }, - }) - expect(textWrap.svgLines.length).toEqual(1) - expect(textWrap.htmlLines.length).toEqual(1) - }) - - it("should place the secondary text in a new line if requested", () => { - const textWrap = MarkdownTextWrap.fromFragments({ - main: { text: "Lower middle-income countries" }, - secondary: { text: "30 million" }, - newLine: "always", - textWrapProps: { - maxWidth: 1000, - fontSize, - }, - }) - expect(textWrap.svgLines.length).toEqual(2) - expect(textWrap.htmlLines.length).toEqual(2) - }) - - it("should place the secondary text in a new line if line breaks should be avoided", () => { - const textWrap = MarkdownTextWrap.fromFragments({ - main: { text: "Lower middle-income countries" }, - secondary: { text: "30 million" }, - newLine: "avoid-wrap", - textWrapProps: { - maxWidth: 250, - fontSize, - }, - }) - expect(textWrap.svgLines.length).toEqual(2) - expect(textWrap.htmlLines.length).toEqual(2) - }) - - it("should place the secondary text in the same line if possible", () => { - const textWrap = MarkdownTextWrap.fromFragments({ - main: { text: "Lower middle-income countries" }, - secondary: { text: "30 million" }, - newLine: "avoid-wrap", - textWrapProps: { - maxWidth: 1000, - fontSize, - }, - }) - expect(textWrap.svgLines.length).toEqual(1) - expect(textWrap.htmlLines.length).toEqual(1) - }) - - it("should use all available space when one fragment exceeds the given max width", () => { - const textWrap = MarkdownTextWrap.fromFragments({ - main: { text: "Long-word-that-can't-be-broken-up more words" }, - secondary: { text: "30 million" }, - textWrapProps: { - maxWidth: 150, - fontSize, - }, - }) - expect(textWrap.width).toBeGreaterThan(150) - }) - - it("should place very long words in a separate line", () => { - const textWrap = MarkdownTextWrap.fromFragments({ - main: { text: "30 million" }, - secondary: { text: "Long-word-that-can't-be-broken-up" }, - textWrapProps: { - maxWidth: 150, - fontSize, - }, - }) - expect(textWrap.svgLines.length).toEqual(2) - expect(textWrap.htmlLines.length).toEqual(2) - }) -}) diff --git a/packages/charts/src/components/MarkdownTextWrap/MarkdownTextWrap.tsx b/packages/charts/src/components/MarkdownTextWrap/MarkdownTextWrap.tsx deleted file mode 100644 index b28973cb1c3..00000000000 --- a/packages/charts/src/components/MarkdownTextWrap/MarkdownTextWrap.tsx +++ /dev/null @@ -1,1173 +0,0 @@ -import * as _ from "lodash-es" -import { CSSProperties } from "react" -import * as React from "react" -import { computed, makeObservable } from "mobx" -import { - excludeUndefined, - imemo, - Bounds, - FontFamily, -} from "../../utils/index.js" -import { DetailsMarker } from "../../types/index.js" -import { TextWrap } from "../TextWrap/TextWrap.js" -import { fromMarkdown } from "mdast-util-from-markdown" -import type { Content, Root } from "mdast" -import { match } from "ts-pattern" -import { urlRegex } from "../markdown/remarkPlainLinks.js" -import * as R from "remeda" - -const SUPERSCRIPT_NUMERALS = { - "0": "\u2070", - "1": "\u00b9", - "2": "\u00b2", - "3": "\u00b3", - "4": "\u2074", - "5": "\u2075", - "6": "\u2076", - "7": "\u2077", - "8": "\u2078", - "9": "\u2079", -} - -export interface IRFontParams { - fontSize?: number - fontWeight?: number - fontFamily?: FontFamily - isItalic?: boolean -} - -export interface IRBreakpoint { - tokenIndex: number - tokenStartOffset: number - breakOffset: number -} - -export interface IRToken { - width: number - getBreakpointBefore(targetWidth: number): IRBreakpoint | undefined - toHTML(key?: React.Key): React.ReactElement | undefined - toSVG(key?: React.Key): React.ReactElement | undefined - toPlaintext(): string | undefined -} - -export class IRText implements IRToken { - constructor( - public text: string, - public fontParams?: IRFontParams - ) {} - @imemo get width(): number { - return Bounds.forText(this.text, this.fontParams).width - } - @imemo get height(): number { - return this.fontParams?.fontSize || 13 - } - getBreakpointBefore(): undefined { - return undefined - } - toHTML(key?: React.Key): React.ReactElement { - return {this.text} - } - toSVG(key?: React.Key): React.ReactElement { - return {this.text} - } - toPlaintext(): string { - return this.text - } -} - -export class IRWhitespace implements IRToken { - constructor(public fontParams?: IRFontParams) {} - @imemo get width(): number { - return Bounds.forText(" ", this.fontParams).width - } - getBreakpointBefore(): IRBreakpoint { - // Have to give it some `breakOffset` because we designate locations - // to split based on it, and `0` leads to being exactly in between tokens. - return { tokenIndex: 0, tokenStartOffset: 0, breakOffset: 0.0001 } - } - toHTML(key?: React.Key): React.ReactElement { - return - } - toSVG(key?: React.Key): React.ReactElement { - return - } - toPlaintext(): string { - return " " - } -} - -export class IRLineBreak implements IRToken { - get width(): number { - return 0 - } - getBreakpointBefore(): undefined { - return undefined - } - toHTML(key?: React.Key): React.ReactElement { - return
- } - toSVG(): undefined { - // We have to deal with this special case in - // whatever procedure does text reflow. - return undefined - } - toPlaintext(): string { - return "\n" - } -} - -export abstract class IRElement implements IRToken { - constructor( - public children: IRToken[], - public fontParams?: IRFontParams - ) {} - - @imemo get width(): number { - return getLineWidth(this.children) - } - - getBreakpointBefore(targetWidth: number): IRBreakpoint | undefined { - return getBreakpointBefore(this.children, targetWidth) - } - - splitBefore(maxWidth: number): { - before: IRToken | undefined - after: IRToken | undefined - } { - const { before, after } = splitLineAtBreakpoint(this.children, maxWidth) - return { - // do not create tokens without children - before: before.length ? this.getClone(before) : undefined, - after: after.length ? this.getClone(after) : undefined, - } - } - - splitOnLineBreaks(): IRToken[][] { - const lines = splitAllOnNewline(this.children) - if (lines.length > 1) { - return lines.map((tokens) => - // Do not create a clone without children. - // There aren't any children in a line when the first or last - // token is a newline. - tokens.length ? [this.getClone(tokens)] : [] - ) - } - // Do not create copies of element - // if there are no newlines inside. - return [[this]] - } - - abstract getClone(children: IRToken[]): IRElement - abstract toHTML(key?: React.Key): React.ReactElement - abstract toSVG(key?: React.Key): React.ReactElement - - toPlaintext(): string { - return lineToPlaintext(this.children) - } -} - -export class IRBold extends IRElement { - getClone(children: IRToken[]): IRBold { - return new IRBold(children, this.fontParams) - } - toHTML(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toHTML(i))} - - ) - } - toSVG(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toSVG(i))} - - ) - } -} - -export class IRSpan extends IRElement { - getClone(children: IRToken[]): IRSpan { - return new IRSpan(children, this.fontParams) - } - toHTML(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toHTML(i))} - - ) - } - toSVG(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toSVG(i))} - - ) - } -} - -export class IRSuperscript implements IRToken { - constructor( - public text: string, - public fontParams?: IRFontParams - ) {} - @imemo get width(): number { - return Bounds.forText(this.text, { fontSize: this.height / 2 }).width - } - @imemo get height(): number { - return this.fontParams?.fontSize || 16 - } - getBreakpointBefore(): undefined { - return undefined - } - toHTML(key?: React.Key): React.ReactElement { - return {this.text} - } - toSVG(key?: React.Key): React.ReactElement { - // replace numerals with literals, for everything else let the font-feature handle it - const style = { fontFeatureSettings: '"sups"' } - const text = this.text.replace(/./g, (c) => - _.get(SUPERSCRIPT_NUMERALS, c, c) - ) - return ( - - {text} - - ) - } - toPlaintext(): string { - return this.text - } -} - -export class IRItalic extends IRElement { - getClone(children: IRToken[]): IRItalic { - return new IRItalic(children, this.fontParams) - } - toHTML(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toHTML(i))} - - ) - } - toSVG(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toSVG(i))} - - ) - } -} - -export class IRLink extends IRElement { - constructor( - public href: string, - children: IRToken[], - fontParams?: IRFontParams - ) { - super(children, fontParams) - } - getClone(children: IRToken[]): IRLink { - return new IRLink(this.href, children, this.fontParams) - } - toHTML(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toHTML(i))} - - ) - } - toSVG(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toSVG(i))} - - ) - } -} - -export class IRDetailOnDemand extends IRElement { - constructor( - public term: string, - children: IRToken[], - fontParams?: IRFontParams - ) { - super(children, fontParams) - } - getClone(children: IRToken[]): IRDetailOnDemand { - return new IRDetailOnDemand(this.term, children, this.fontParams) - } - toHTML(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toHTML(i))} - - ) - } - toSVG(key?: React.Key): React.ReactElement { - return ( - - {this.children.map((child, i) => child.toSVG(i))} - - ) - } -} - -function splitAllOnNewline(tokens: IRToken[]): IRToken[][] { - if (!tokens.length) return [] - let currentLine: IRToken[] = [] - const lines: IRToken[][] = [currentLine] - const unproccessed: IRToken[] = [...tokens] - while (unproccessed.length > 0) { - const token = unproccessed.shift()! - if (token instanceof IRElement) { - const [firstLine, ...otherLines] = token.splitOnLineBreaks() - if (firstLine) currentLine.push(...firstLine) - if (otherLines.length) { - lines.push(...otherLines) - currentLine = R.last(lines)! - } - } else if (token instanceof IRLineBreak) { - currentLine = [] - lines.push(currentLine) - } else { - currentLine.push(token) - } - } - return lines -} - -export function splitLineAtBreakpoint( - tokens: IRToken[], - breakWidth: number -): { before: IRToken[]; after: IRToken[] } { - let i = 0 - let offset = 0 - // finding the token where the split should be - // NOTE: the token may not be splittable, which is why we need an exact - // `breakWidth` provided, not the line width. - while (i < tokens.length - 1 && offset + tokens[i].width < breakWidth) { - offset += tokens[i].width - i++ - } - const token = tokens[i] - if (token instanceof IRElement) { - const { before, after } = token.splitBefore(breakWidth - offset) - return { - before: excludeUndefined([...tokens.slice(0, i), before]), - after: excludeUndefined([after, ...tokens.slice(i + 1)]), - } - } else { - return { before: tokens.slice(0, i), after: trimLeft(tokens.slice(i)) } - } -} - -function trimLeft(tokens: IRToken[]): IRToken[] { - let i = 0 - while (i < tokens.length && tokens[i] instanceof IRWhitespace) { - i++ - } - return tokens.slice(i) -} - -// Even though it says "before", it may return a breakpoint after, because -// there is no earlier breakpoint in the line. -export function getBreakpointBefore( - tokens: IRToken[], - maxWidth: number -): IRBreakpoint | undefined { - let tokenStartOffset = 0 - let prevBreakpoint: IRBreakpoint | undefined = undefined - for (let index = 0; index < tokens.length; index++) { - const token = tokens[index] - const candidate = token.getBreakpointBefore(maxWidth - tokenStartOffset) - if (candidate !== undefined) { - if ( - prevBreakpoint && - candidate.breakOffset + tokenStartOffset > maxWidth - ) { - break - } - prevBreakpoint = { - tokenStartOffset, - tokenIndex: index, - breakOffset: candidate.breakOffset + tokenStartOffset, - } - } - tokenStartOffset += token.width - } - return prevBreakpoint -} - -export function getLineWidth(tokens: IRToken[]): number { - return _.sum(tokens.map((token) => token.width)) -} - -// useful for debugging -export function lineToPlaintext(tokens: IRToken[]): string { - return tokens.map((t) => t.toPlaintext()).join("") -} - -export const isTextToken = (token: IRToken): token is IRText | IRWhitespace => - token instanceof IRText || token instanceof IRWhitespace - -/** - * Merges adjacent text tokens, because the way we render text in React is otherwise - * not very compatible with Google Translate, breaking our site in weird ways when - * translated. - * This is to be run _just before_ rendering to HTML, because it loses some - * information and is not easily reversible. - * See also # legacy issue 1785 - */ -export const recursiveMergeTextTokens = ( - tokens: IRToken[], - fontParams?: IRFontParams -): IRToken[] => { - if (tokens.length === 0) return [] - - // merge adjacent text tokens into one - const mergedTextTokens: IRToken[] = tokens.reduce((acc, token) => { - if (isTextToken(token)) { - const l = R.last(acc) - if (l && isTextToken(l)) { - // replace last value in acc with merged text token - acc.pop() - return [ - ...acc, - new IRText( - l.toPlaintext() + token.toPlaintext(), - fontParams - ), - ] - } - } - return [...acc, token] - }, [] as IRToken[]) - - // recursively enter non-text tokens, and merge their children - return mergedTextTokens.map((token) => { - if (token instanceof IRElement) { - return token.getClone( - recursiveMergeTextTokens(token.children, fontParams) - ) - } - return token - }) -} - -export function splitIntoLines( - tokens: IRToken[], - maxWidth: number -): IRToken[][] { - const processedLines: IRToken[][] = [] - const unprocessedLines: IRToken[][] = splitAllOnNewline(tokens) - while (unprocessedLines.length) { - const currentLine = unprocessedLines.shift()! - if (getLineWidth(currentLine) <= maxWidth) { - processedLines.push(currentLine) - } else { - const breakpoint = getBreakpointBefore(currentLine, maxWidth) - if (!breakpoint) { - processedLines.push(currentLine) - } else { - const { before, after } = splitLineAtBreakpoint( - currentLine, - breakpoint.breakOffset - ) - processedLines.push(before) - unprocessedLines.unshift(after) - } - } - } - return processedLines -} - -export const sumTextWrapHeights = ( - elements: MarkdownTextWrap[] | TextWrap[], - spacer: number = 0 -): number => - _.sum(elements.map((element) => element.height)) + - (elements.length - 1) * spacer - -type MarkdownTextWrapOptions = { - maxWidth?: number - fontFamily?: FontFamily - fontSize: number - fontWeight?: number - lineHeight?: number - style?: CSSProperties - detailsOrderedByReference?: string[] -} - -type MarkdownTextWrapProps = { text: string } & MarkdownTextWrapOptions - -type TextFragment = { text: string; bold?: boolean } - -export class MarkdownTextWrap extends React.Component { - constructor(props: MarkdownTextWrapProps) { - super(props) - makeObservable(this) - } - - static fromFragments({ - main, - secondary, - newLine = "continue-line", - textWrapProps, - }: { - main: TextFragment - secondary: TextFragment - newLine?: "continue-line" | "always" | "avoid-wrap" - textWrapProps: Omit - }) { - const mainMarkdownText = maybeBoldMarkdownText(main) - const secondaryMarkdownText = maybeBoldMarkdownText(secondary) - - const combinedTextContinued = [ - mainMarkdownText, - secondaryMarkdownText, - ].join(" ") - const combinedTextNewLine = [ - mainMarkdownText, - secondaryMarkdownText, - ].join("\n") - - if (newLine === "always") { - return new MarkdownTextWrap({ - text: combinedTextNewLine, - ...textWrapProps, - }) - } - - if (newLine === "continue-line") { - return new MarkdownTextWrap({ - text: combinedTextContinued, - ...textWrapProps, - }) - } - - // if newLine is set to 'avoid-wrap', we first try to fit the secondary text - // on the same line as the main text. If it doesn't fit, we place it on a new line. - - const mainTextWrap = new MarkdownTextWrap({ ...main, ...textWrapProps }) - const secondaryTextWrap = new MarkdownTextWrap({ - text: secondaryMarkdownText, - ...textWrapProps, - maxWidth: - mainTextWrap.maxWidth - - mainTextWrap.lastLineWidth - - Bounds.forText(" ", textWrapProps).width - - 10, // arbitrary wiggle room - }) - - const secondaryTextFitsOnSameLine = - secondaryTextWrap.svgLines.length === 1 - if (secondaryTextFitsOnSameLine) { - return new MarkdownTextWrap({ - text: combinedTextContinued, - ...textWrapProps, - }) - } else { - return new MarkdownTextWrap({ - text: combinedTextNewLine, - ...textWrapProps, - }) - } - } - - @computed get maxWidth(): number { - return this.props.maxWidth ?? Infinity - } - @computed get lineHeight(): number { - return this.props.lineHeight ?? 1.1 - } - @computed get fontSize(): number { - return this.props.fontSize - } - @computed get fontParams(): IRFontParams { - return { - fontFamily: this.props.fontFamily, - fontSize: this.props.fontSize, - fontWeight: this.props.fontWeight, - } - } - @computed get text(): string { - // NOTE: ❗Here we deviate from the normal markdown spec. We replace \n with \n to make sure that single \n are treated as - // actual line breaks but only if none of the other markdown line break rules apply. - // This is a bit different to how markdown usually works but we have a substantial - // amount of legacy charts that use newlines in this way and it seems that it is - // better to support this simple case than to do a data migration of many chart subtitles. - const baseText = this.props.text - // This replace is a bit funky - we want to make sure that single \n are treated as - // actual line breaks but only if none of the other markdown line break rules apply. - // These are: - // - \n\n is always a new paragraph - // - Two spaces before \n is a line break (this rule is not entirely checked as we only check for a single space) - // - A backslash before \n is a line break - // The code below normalizes all cases to \n which will lead to them surviving the markdown parsing - let text = baseText.trim() - text = text.replaceAll("\n\n", "@@LINEBREAK@@") - text = text.replaceAll("\\\n", "@@LINEBREAK@@") - text = text.replaceAll(" \n", "@@LINEBREAK@@") - text = text.replaceAll("\n", " \n") - text = text.replaceAll("@@LINEBREAK@@", " \n") - return text - } - @computed get detailsOrderedByReference(): string[] { - return this.props.detailsOrderedByReference || [] - } - - @computed get plaintext(): string { - return this.htmlLines.map(lineToPlaintext).join("\n") - } - - @computed get tokensFromMarkdown(): IRToken[] { - const tokens = convertMarkdownToIRTokens(this.text, this.fontParams) - return tokens - } - - @computed get htmlLines(): IRToken[][] { - const tokens = this.tokensFromMarkdown - const lines = splitIntoLines(tokens, this.maxWidth) - return lines.map((line) => - recursiveMergeTextTokens(line, this.fontParams) - ) - } - - @computed get svgLines(): IRToken[][] { - const tokens = this.tokensFromMarkdown - const lines = splitIntoLines(tokens, this.maxWidth) - return lines - } - - @computed get svgLinesWithDodReferenceNumbers(): IRToken[][] { - const references = this.detailsOrderedByReference - const tokens = this.tokensFromMarkdown - const tokensWithReferenceNumbers = appendReferenceNumbers( - tokens, - references - ) - return splitIntoLines(tokensWithReferenceNumbers, this.maxWidth) - } - - @computed get width(): number { - const { htmlLines } = this - const lineLengths = htmlLines.map((tokens) => - _.sumBy(tokens, (token) => token.width) - ) - return _.max(lineLengths) ?? 0 - } - - @computed get singleLineHeight(): number { - return this.fontSize * this.lineHeight - } - - @computed get lastLineWidth(): number { - return _.sumBy(R.last(this.htmlLines), (token) => token.width) ?? 0 - } - - @computed get height(): number { - const { htmlLines } = this - if (htmlLines.length === 0) return 0 - return htmlLines.length * this.singleLineHeight - } - - @computed get style(): any { - return { - ...this.fontParams, - ...this.props.style, - lineHeight: this.lineHeight, - } - } - - renderHTML() { - const { htmlLines } = this - if (htmlLines.length === 0) return null - return ( - - {htmlLines.map((line, i) => { - const plaintextLine = line - .map((token) => token.toPlaintext()) - .join("") - return ( - - ) - })} - - ) - } - - renderSVG( - x: number, - y: number, - { - textProps, - detailsMarker = "superscript", - id, - }: { - textProps?: React.SVGProps - detailsMarker?: DetailsMarker - id?: string - } = {} - ) { - const { fontSize, lineHeight } = this - const lines = - detailsMarker === "superscript" - ? this.svgLinesWithDodReferenceNumbers - : this.svgLines - if (lines.length === 0) return <> - - // Magic number set through experimentation. - // The HTML and SVG renderers need to position lines identically. - // This number was tweaked until the overlaid HTML and SVG outputs - // overlap. - const HEIGHT_CORRECTION_FACTOR = 0.74 - - const textHeight = fontSize * HEIGHT_CORRECTION_FACTOR - const containerHeight = lineHeight * fontSize - const yOffset = - y + (containerHeight - (containerHeight - textHeight) / 2) - - const getLineY = (lineIndex: number) => - yOffset + lineHeight * fontSize * lineIndex - - return ( - - - {lines.map((line, lineIndex) => ( - - {line.map((token, tokenIndex) => - token.toSVG(tokenIndex) - )} - - ))} - - {/* SVG doesn't support dotted underlines, so we draw them manually */} - {detailsMarker === "underline" && - lines.map((line, lineIndex) => { - const y = (getLineY(lineIndex) + 2).toFixed(1) - let currWidth = 0 - return line.map((token) => { - const underline = - token instanceof IRDetailOnDemand ? ( - - ) : null - currWidth += token.width - return underline - }) - })} - - ) - } - - // An alias method that allows MarkdownTextWrap to be - // instantiated via JSX for HTML rendering - // - override render(): React.ReactElement | null { - return this.renderHTML() - } -} - -// The rule doesn't support class components in the same file. -// eslint-disable-next-line react-refresh/only-export-components -function MarkdownTextWrapLine({ - line, -}: { - line: IRToken[] -}): React.ReactElement { - return ( - - {line.length ? line.map((token, i) => token.toHTML(i)) :
} -
- ) -} - -export function convertMarkdownToIRTokens( - markdown: string, - fontParams?: IRFontParams -): IRToken[] { - const ast: Root = fromMarkdown(markdown) - const children = ast.children.flatMap((item) => - convertMarkdownNodeToIRTokens(item, fontParams) - ) - // ensure that there are no leading or trailing line breaks - return R.dropLastWhile( - R.dropWhile(children, (token) => token instanceof IRLineBreak), - (token) => token instanceof IRLineBreak - ) -} - -// When using mdast types version 4 this should be typed as: -// node: RootContentMap[keyof RootContentMap] -function convertMarkdownNodeToIRTokens( - node: Content, - fontParams: IRFontParams = {} -): IRToken[] { - const converted = match(node) - .with( - { - type: "blockquote", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - .with( - { - type: "break", - }, - (_) => { - return [new IRLineBreak()] - } - ) - .with( - { - type: "code", - }, - (item) => { - return [new IRText(item.value, fontParams)] - } - ) - .with( - { - type: "emphasis", - }, - (item) => { - return [ - new IRItalic( - item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, { - ...fontParams, - isItalic: true, - }) - ) - ), - ] - } - ) - .with( - { - type: "heading", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - .with( - { - type: "html", - }, - (item) => { - return [new IRText(item.value, fontParams)] - } - ) - .with( - { - type: "image", - }, - (item) => { - return [new IRText(item.alt ?? "", fontParams)] - } - ) - .with( - { - type: "inlineCode", - }, - (item) => { - return [new IRText(item.value, fontParams)] - } - ) - .with( - { - type: "link", - }, - (item) => { - if (item.url.startsWith("#dod:")) { - const term = item.url.replace("#dod:", "") - return [ - new IRDetailOnDemand( - term, - item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ), - fontParams - ), - ] - } else - return [ - new IRLink( - item.url, - item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - ), - ] - } - ) - .with( - { - type: "list", - }, - (item) => { - if (item.ordered) - return item.children.flatMap((child, index) => [ - new IRLineBreak(), - new IRText(`${index + 1}) `, fontParams), - ...convertMarkdownNodeToIRTokens(child, fontParams), - ]) - else - return item.children.flatMap((child) => [ - new IRLineBreak(), - new IRText(`• `, fontParams), - ...convertMarkdownNodeToIRTokens(child, fontParams), - ]) - } - ) - .with( - { - type: "listItem", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - .with( - { - type: "paragraph", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - .with( - { - type: "strong", - }, - (item) => { - return [ - new IRBold( - item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, { - ...fontParams, - fontWeight: 700, - }) - ) - ), - ] - } - ) - .with( - { - type: "text", - }, - (item) => { - const splitted = item.value.split(/\s+/) - const tokens = splitted.flatMap((text, i) => { - const textNode = new IRText(text, fontParams) - const node = text.match(urlRegex) - ? new IRLink(text, [textNode], fontParams) - : textNode - if (i < splitted.length - 1) { - return [node, new IRWhitespace(fontParams)] - } else return [node] - }) - return tokens - } - ) - .with( - { - type: "thematicBreak", - }, - (_) => { - return [new IRText("---", fontParams)] - } - ) - .with( - { - type: "delete", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - // Now lets finish this with blocks for FootnoteDefinition, Definition, ImageReference, LinkReference, FootnoteReference, and Table - .with( - { - type: "footnoteDefinition", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - .with( - { - type: "definition", - }, - (item) => { - return [ - new IRText(`${item.identifier}: ${item.label}`, fontParams), - ] - } - ) - .with( - { - type: "imageReference", - }, - (item) => { - return [ - new IRText(`${item.identifier}: ${item.label}`, fontParams), - ] - } - ) - .with( - { - type: "linkReference", - }, - (item) => { - return [ - new IRText(`${item.identifier}: ${item.label}`, fontParams), - ] - } - ) - .with( - { - type: "footnoteReference", - }, - (item) => { - return [ - new IRText(`${item.identifier}: ${item.label}`, fontParams), - ] - } - ) - .with( - { - type: "table", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - .with( - { - type: "tableCell", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - // and now TableRow and Yaml - .with( - { - type: "tableRow", - }, - (item) => { - return item.children.flatMap((child) => - convertMarkdownNodeToIRTokens(child, fontParams) - ) - } - ) - .with( - { - type: "yaml", - }, - (item) => { - return [new IRText(item.value, fontParams)] - } - ) - .exhaustive() - return converted -} - -function appendReferenceNumbers( - tokens: IRToken[], - references: string[] -): IRToken[] { - function traverse(token: IRToken, callback: (token: IRToken) => any): any { - if (token instanceof IRElement) { - token.children.flatMap((child) => traverse(child, callback)) - } - return callback(token) - } - - const appendedTokens: IRToken[] = _.cloneDeep(tokens).flatMap((token) => - traverse(token, (token: IRToken) => { - if (token instanceof IRDetailOnDemand) { - const referenceIndex = - references.findIndex((term) => term === token.term) + 1 - if (referenceIndex === 0) return token - token.children.push( - new IRSuperscript(String(referenceIndex), token.fontParams) - ) - } - return token - }) - ) - - return appendedTokens -} - -function maybeBoldMarkdownText({ - text, - bold, -}: { - text: string - bold?: boolean -}): string { - return bold ? `**${text}**` : text -} - -export function toPlaintext(markdown: string): string { - return new MarkdownTextWrap({ - text: markdown, - fontSize: 10, // doesn't matter, but is a mandatory field - }).plaintext -} diff --git a/packages/charts/src/components/OverlayHeader.scss b/packages/charts/src/components/OverlayHeader.scss deleted file mode 100644 index 769d18f2e21..00000000000 --- a/packages/charts/src/components/OverlayHeader.scss +++ /dev/null @@ -1,18 +0,0 @@ -.overlay-header { - --padding: var(--modal-padding, 16px); - - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--padding) var(--padding) 16px; - - .title { - @include grapher_h5-black-caps; - color: $light-text; - } - - .close-button { - margin-left: 8px; - flex-shrink: 0; - } -} diff --git a/packages/charts/src/components/OverlayHeader.tsx b/packages/charts/src/components/OverlayHeader.tsx deleted file mode 100644 index f81fc15c409..00000000000 --- a/packages/charts/src/components/OverlayHeader.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react" -import cx from "classnames" -import { CloseButton } from "./closeButton/CloseButton.js" - -export function OverlayHeader({ - title, - onTitleClick, - onDismiss, - className, -}: { - title: string - onTitleClick?: () => void - onDismiss?: () => void - className?: string -}): React.ReactElement { - return ( -
-

- {title} -

- {onDismiss && } -
- ) -} diff --git a/packages/charts/src/components/RadioButton.scss b/packages/charts/src/components/RadioButton.scss deleted file mode 100644 index 61bf1a5235a..00000000000 --- a/packages/charts/src/components/RadioButton.scss +++ /dev/null @@ -1,89 +0,0 @@ -/******************************************************************************* - * Radio Button - * - * Custom radio button styling. - * Uses Build Canada design system colors. - ******************************************************************************/ - -@use "sass:math"; - -.radio { - $radio-size: 18px; - - $light-stroke: $gray-30; - $hover-stroke: $cerulean-500; - $active-fill: $cerulean-500; - - position: relative; - display: flex; - align-items: flex-start; - - label { - margin: 0; - cursor: pointer; - } - - input { - position: absolute; - opacity: 0; - left: 0; - pointer-events: none; - } - - .outer { - position: absolute; - left: 0; - top: 0; - width: $radio-size; - height: $radio-size; - - background: $white; - pointer-events: none; - border-radius: 50%; - border: 2px solid $light-stroke; - - display: flex; - align-items: center; - justify-content: center; - transition: all 150ms ease; - - .inner { - width: math.div($radio-size, 2); - height: math.div($radio-size, 2); - background-color: $active-fill; - border-radius: 50%; - opacity: 0; - transform: scale(0); - transition: all 150ms ease; - } - } - - input:focus-visible + .outer { - outline: 2px solid $auburn; - outline-offset: 2px; - } - - input:checked + .outer { - border-color: $hover-stroke; - - .inner { - opacity: 1; - transform: scale(1); - } - } - - .label { - @include grapher_label-2-regular; - padding-left: $radio-size + 10px; - cursor: pointer; - user-select: none; - color: $charcoal; - line-height: $radio-size; - } - - &:hover { - input:not(:checked) + .outer { - border-color: $hover-stroke; - } - } -} diff --git a/packages/charts/src/components/RadioButton.tsx b/packages/charts/src/components/RadioButton.tsx deleted file mode 100644 index 19eb9c24c52..00000000000 --- a/packages/charts/src/components/RadioButton.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import * as React from "react" -import cx from "classnames" - -export const RadioButton = ({ - className, - checked, - onChange, - label, - group, - disabled, - id, - "data-test": testHook, -}: { - className?: string - checked: boolean - onChange: React.ChangeEventHandler - label: React.ReactNode - group?: string - disabled?: boolean - id?: string - "data-test"?: string -}) => { - return ( -
-