# API keys Source: https://docs.graphy.dev/agents/api-keys Every request to the Graphy Agents API is authenticated with an API key. You create and manage keys in the **Graphy console**, then send the key as a Bearer token on each request. ## Get your API key 1. Go to the [Graphy console](https://agents.graphy.dev/console/). 2. Sign in with Google or GitHub. 3. Open the **API keys** section. 4. Click **Create key**, give it a name (for example `production`), and confirm. 5. **Copy the key right away** — it's shown only once and cannot be retrieved later. Store it in a secrets manager or an environment variable. > [!NOTE] > > Keys look like `graphy_…`. You can keep up to **5 active keys** at a time — create separate keys for development and production, and revoke any you no longer use from the same page in the console. ## Use your key Send the key in the `Authorization` header as a Bearer token on every request: ``` Authorization: Bearer graphy_xxxxxxxxxxxxxxxxxxxxxxxx ``` With the TypeScript SDK, pass it as `apiKey`: ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, // your graphy_… key baseUrl: 'https://agents.graphy.dev', }); ``` See [Authentication](/agents/rest/authentication) for the full header reference and authentication error codes. ## Keep your keys safe > [!WARNING] > > Never expose API keys in client-side code. Call the Agents API from your backend only. * Store keys in environment variables or a secrets manager. * Use separate keys for development and production. * Rotate keys periodically and revoke unused ones in the console. ## Next steps > **[Quickstart](/agents/quickstart)** > > Generate your first chart in 5 minutes > **[Authentication](/agents/rest/authentication)** > > Headers, base URL, and error codes # Overview Source: https://docs.graphy.dev/agents/overview > [!NOTE] > > AI Agents is currently in alpha. APIs may change. Graphy AI Agents create, transform, and analyze charts using natural language. Most agents take a `GraphConfig` and a prompt and return an updated `GraphConfig` ready to render. ## Architecture Agents are stateless. Each request contains the full context needed to process it—no sessions, no conversation history. ```mermaid theme={null} flowchart LR A[Your App] -->|GraphConfig + Prompt| B[AI Agent] B -->|Updated GraphConfig| A ``` This design simplifies integration: * No session management or cleanup * Requests are idempotent and cacheable * Easy to retry on failure * Works with any architecture (serverless, edge, traditional) ## Available Agents | Agent | Endpoint | SDK Method | Description | | - | - | - | - | | Chart Maker | `POST /api/v0/generate` | `generateGraph()` | Modify chart type, styling, and data transformations | | Suggestions | `POST /api/v0/suggestions` | `generateSuggestions()` | Suggest chart types and data preparations | | Mutation | `POST /api/v0/mutate` | `generateMutation()` | Transform the dataset — filter, group, derive, sort | | Annotation | `POST /api/v0/annotate` | `generateAnnotations()` | Add highlights, tooltips, and other annotations | | Narrative | `POST /api/v0/narrate` | `generateNarrative()` | Write the chart title, subtitle, and caption | | Extract | `POST /api/v0/extract` | `extractFromProse()` | Build a dataset from text, images, PDFs, or spreadsheets | ## Prerequisites * A Graphy API key — [create one in the console](/agents/api-keys) * Familiarity with `GraphConfig` ([schema reference](/sdk/reference/graph-config)) ## Integration Options **TypeScript SDK** — Full-featured client with streaming, retries, and type validation. ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: 'your-api-key', baseUrl: 'https://agents.graphy.dev', }); const result = await ai.generateGraph({ config: myGraphConfig, userPrompt: 'Change to a stacked bar chart', }); ``` **REST API** — Direct HTTP integration for any language. Returns Server-Sent Events for streaming progress. ```bash theme={null} curl -N https://agents.graphy.dev/api/v0/generate \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"config": {...}, "userPrompt": "Add a trend line"}' ``` ## Next Steps > **[Quickstart](/agents/quickstart)** > > First working call in 5 minutes > **[TypeScript SDK](/agents/sdk/installation)** > > Install and configure the SDK > **[REST API](/agents/rest/authentication)** > > Direct HTTP integration > **[Type Reference](/agents/reference/types)** > > Complete type definitions # Quickstart Source: https://docs.graphy.dev/agents/quickstart > [!NOTE] > > AI Agents is currently in alpha. APIs may change. Get your first AI-generated chart modification working in 5 minutes. ## Prerequisites * Node.js 18+ * A Graphy API key — [create one in the console](/agents/api-keys) * An existing `GraphConfig` (or use the example below) ## 1. Install the SDK ```bash npm theme={null} npm install @graphysdk/agents-sdk ``` ```bash yarn theme={null} yarn add @graphysdk/agents-sdk ``` ```bash pnpm theme={null} pnpm add @graphysdk/agents-sdk ``` ## 2. Create the Client ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); ``` ## 3. Prepare a Graph Configuration ```typescript theme={null} import type { GraphConfig } from '@graphysdk/agents-sdk'; const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'quarter', label: 'Quarter' }, { key: 'revenue', label: 'Revenue ($)' }, ], rows: [ { quarter: 'Q1', revenue: 50000 }, { quarter: 'Q2', revenue: 62000 }, { quarter: 'Q3', revenue: 58000 }, { quarter: 'Q4', revenue: 71000 }, ], }, }; ``` ## 4. Call the Agent ```typescript theme={null} const result = await ai.generateGraph({ config, userPrompt: 'Change this to a line chart and add a trend line', }); console.log(result.config); // Updated GraphConfig console.log(result.response.message); // "Changed chart type to line and added a trend line" ``` ## 5. Use the Result The Agents SDK returns an updated `GraphConfig`. Log it, persist it, or pass it to your renderer. ```typescript theme={null} console.log(JSON.stringify(result.config, null, 2)); ``` > [!NOTE] > > Chart rendering is separate from the Agents client. Use `@graphysdk/react` for the v2 React renderer when you are ready > to display the chart. ## Licence `npm install` is not a commercial licence. Free Trial is 32 days, local or internal, and does not cover a public page. See [licence and evaluation](/licence/evaluation). ## Next Steps > **[Configuration](/agents/sdk/configuration)** > > Timeouts, retries, and logging > **[Streaming](/agents/sdk/streaming)** > > Real-time progress updates > **[Error Handling](/agents/sdk/errors)** > > Retries and error types > **[REST API](/agents/rest/authentication)** > > Direct HTTP integration # Type Reference Source: https://docs.graphy.dev/agents/reference/types Complete type definitions for the Graphy AI SDK. ## GraphConfig The chart configuration object. See [Graph Config Schema](/sdk/reference/graph-config) for the complete reference. ```typescript theme={null} import type { GraphConfig } from '@graphysdk/agents-sdk'; ``` *** ## GenerateGraphParams Request parameters for the Chart Maker Agent. ```typescript theme={null} interface GenerateGraphParams { config: GraphConfig; userPrompt?: string; metadata?: Metadata; storytellingOptions?: StorytellingOptions; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The current chart configuration | | `userPrompt` | `string` | No | Natural language instruction | | `metadata` | `Metadata` | No | Request tracking information | | `storytellingOptions` | `StorytellingOptions` | No | Prune storytelling from config content client-side (SDK only). | *** ## GenerateGraphResponse Response from the Chart Maker Agent. ```typescript theme={null} interface GenerateGraphResponse { config: GraphConfig; response: { message: string; steps?: string[]; }; } ``` | Field | Type | Description | | - | - | - | | `config` | `GraphConfig` | Updated chart configuration. Narrative (title, subtitle, caption) is embedded in `config.content` as TipTap JSON documents. | | `response.message` | `string` | Explanation of changes made | | `response.steps` | `string[]` | Optional breakdown of modifications | *** ## GenerateMutationParams Request parameters for the Mutation Agent. ```typescript theme={null} interface GenerateMutationParams { config: GraphConfig; userPrompt?: string; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The current chart configuration, including the dataset to transform | | `userPrompt` | `string` | No | Natural language description of the transformation to apply | | `metadata` | `Metadata` | No | Request tracking information | *** ## GenerateMutationResponse Response from the Mutation Agent. ```typescript theme={null} interface GenerateMutationResponse { response: { message: string; steps: string[]; }; config: GraphConfig; } ``` | Field | Type | Description | | - | - | - | | `response.message` | `string` | Explanation of the transformations applied | | `response.steps` | `string[]` | Ordered breakdown of each transformation step | | `config` | `GraphConfig` | Updated chart configuration with the transformed dataset | *** ## GenerateAnnotationsParams Request parameters for the Annotation Agent. ```typescript theme={null} interface GenerateAnnotationsParams { config: GraphConfig; userPrompt?: string; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The current chart configuration to annotate | | `userPrompt` | `string` | No | Natural language description of the annotations to add or remove | | `metadata` | `Metadata` | No | Request tracking information | *** ## GenerateAnnotationsResponse Response from the Annotation Agent. ```typescript theme={null} interface GenerateAnnotationsResponse { response: { message: string; steps: string[]; }; config: GraphConfig; } ``` | Field | Type | Description | | - | - | - | | `response.message` | `string` | Explanation of the annotations applied | | `response.steps` | `string[]` | Ordered breakdown of each annotation step | | `config` | `GraphConfig` | Updated chart configuration with annotations applied | *** ## GenerateNarrativeParams Request parameters for the Narrative Agent. ```typescript theme={null} interface GenerateNarrativeParams { config: GraphConfig; userPrompt: string; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The chart configuration to narrate | | `userPrompt` | `string` | Yes | Natural language instruction for the narrative | | `metadata` | `Metadata` | No | Request tracking information | *** ## GenerateNarrativeResponse Response from the Narrative Agent. ```typescript theme={null} interface GenerateNarrativeResponse { response: { message: string; }; config: GraphConfig; } ``` | Field | Type | Description | | - | - | - | | `response.message` | `string` | Explanation of the narrative produced | | `config` | `GraphConfig` | Updated chart configuration. Title, subtitle, and caption are written into `config.content` as TipTap JSON documents. | *** ## GenerateGraphSuggestionsParams Request parameters for the Suggestions Agent. ```typescript theme={null} interface GenerateGraphSuggestionsParams { config: GraphConfig; userPrompt?: string; metadata?: Metadata; maxSuggestionCount?: 1 | 2 | 3 | 4; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The chart configuration containing the dataset | | `userPrompt` | `string` | No | Natural language description of what to visualize | | `metadata` | `Metadata` | No | Request tracking information | | `maxSuggestionCount` | `1` \| `2` \| `3` \| `4` | No | Optional cap (1–4) passed to the model for how many suggestions to aim for. Omitted uses the default (4). Responses are not truncated if longer. | *** ## GenerateGraphSuggestionsResponse Response from the Suggestions Agent. ```typescript theme={null} interface GenerateGraphSuggestionsResponse { suggestions: Suggestion[]; } ``` | Field | Type | Description | | - | - | - | | `suggestions` | `Suggestion[]` | List of chart type suggestions | *** ## Suggestion A single chart suggestion returned by the Suggestions Agent. ```typescript theme={null} interface Suggestion { dataPrepPrompt: string; chartType: AiChartType; summary: string; } ``` | Field | Type | Description | | - | - | - | | `dataPrepPrompt` | `string` | Prompt describing how to prepare the data for this chart | | `chartType` | `AiChartType` | Suggested chart type | | `summary` | `string` | Short description of what the chart would show | *** ## ExtractFromProseParams Request parameters for the Extract Agent. ```typescript theme={null} interface ExtractFromProseParams { sourceText?: string; attachments?: ExtractAttachmentInput[]; images?: ExtractImageInput[]; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `sourceText` | `string` | No | Raw text to extract data from. Up to 500,000 characters. | | `attachments` | `ExtractAttachmentInput[]` | No | Images, PDFs, and spreadsheets to extract data from. Up to 12 entries. | | `images` | `ExtractImageInput[]` | No | **Deprecated.** Legacy image array — use `attachments` instead. | | `metadata` | `Metadata` | No | Request tracking information | Provide `sourceText`, `attachments`, or both — at least one is required. `images` and `attachments` cannot be combined in a single request. *** ## ExtractAttachmentInput A discriminated union tagged by `kind`, used for the Extract Agent's `attachments` array. Construct entries with the `buildImageAttachment`, `buildPdfAttachment`, and `buildSpreadsheetAttachment` helpers. ```typescript theme={null} type ExtractAttachmentInput = | { kind: 'image'; mimeType: string; dataBase64: string } | { kind: 'document'; mimeType: string; dataBase64: string } | { kind: 'spreadsheet'; mimeType: string; dataBase64: string; filename?: string }; ``` | `kind` | Accepted formats | Decoded size limit | | - | - | - | | `'image'` | PNG, JPEG, WebP, GIF | 2 MB each, 8 max | | `'document'` | PDF | 6 MB each, 2 max | | `'spreadsheet'` | Excel `.xlsx` | 6 MB each, 2 max | A request accepts up to 12 attachments and 12 MB combined decoded size. *** ## ExtractFromProseResponse Response from the Extract Agent. ```typescript theme={null} interface ExtractFromProseResponse { response: { message: string; }; config: GraphConfig; extractMeta: ExtractMeta | null; lastAccuracyEvaluation?: ExtractAccuracyEvaluation; } ``` | Field | Type | Description | | - | - | - | | `response.message` | `string` | Summary of what was extracted | | `config` | `GraphConfig` | Chart configuration containing the extracted dataset | | `extractMeta` | `ExtractMeta \| null` | Trust metadata for the extraction, or `null` when unavailable | | `lastAccuracyEvaluation` | `ExtractAccuracyEvaluation` | The agent's final self-check of the extracted data, when performed | *** ## ExtractMeta Trust metadata attached to an extraction result. ```typescript theme={null} interface ExtractMeta { confidence: ExtractConfidence; needsUserInput: boolean; warnings: string[]; cellConfidence?: Array<{ rowIndex: number; columnKey: string; confidence: ExtractConfidence; quote?: string; }>; } type ExtractConfidence = 'high' | 'medium' | 'low'; ``` | Field | Type | Description | | - | - | - | | `confidence` | `ExtractConfidence` | Overall confidence in the extraction | | `needsUserInput` | `boolean` | Whether the result should be reviewed before use | | `warnings` | `string[]` | Notes about ambiguous, conflicting, or missing source data | | `cellConfidence` | `Array<…>` | Optional per-cell confidence, each with a supporting source quote | *** ## ExtractAccuracyEvaluation The Extract Agent's self-check of an extraction result. ```typescript theme={null} interface ExtractAccuracyEvaluation { score: number; issues: string[]; sufficient: boolean; summary: string; } ``` | Field | Type | Description | | - | - | - | | `score` | `number` | Accuracy score, from `0` to `1` | | `issues` | `string[]` | Specific accuracy problems the agent identified | | `sufficient` | `boolean` | Whether the accuracy is good enough to use as-is | | `summary` | `string` | Human-readable summary of the accuracy check | *** ## StorytellingOptions SDK-only options to prune storytelling fields from the config content after the API returns. For each option set to `true`, the SDK removes that field from `config.content` before returning. ```typescript theme={null} interface StorytellingOptions { excludeTitle?: boolean; excludeSubtitle?: boolean; excludeCaption?: boolean; } ``` | Field | Type | Description | | - | - | - | | `excludeTitle` | `boolean` | If true, the SDK removes `config.content.title`. | | `excludeSubtitle` | `boolean` | If true, the SDK removes `config.content.subtitle`. | | `excludeCaption` | `boolean` | If true, the SDK removes `config.content.caption`. | *** ## Metadata Optional tracking information for requests. ```typescript theme={null} interface Metadata { callId?: string; locale?: string; effort?: 'low' | 'medium' | 'high'; /** @deprecated Prefer `effort`. */ storytellingEffort?: 'none' | 'low' | 'medium' | 'high'; budget?: InvocationBudget; executionMode?: 'agentic' | 'fast'; } ``` | Field | Type | Description | | - | - | - | | `callId` | `string` | Optional. Identifier for tracking and debugging. | | `locale` | `string` | Locale for responses (e.g., `en-US`, `fr-FR`). | | `effort` | `'low' \| 'medium' \| 'high'` | Preferred invocation tier; stored on the invocation and forwarded to nested agents. Default **`medium`** when omitted. | | `storytellingEffort` | `'none' \| 'low' \| 'medium' \| 'high'` | **Deprecated** — prefer `effort`. Still accepted for chart narrative and to infer `effort` when `effort` is omitted. | | `budget` | `InvocationBudget` | Optional hard caps on time, tokens, and cost for the request. | | `executionMode` | `'agentic' \| 'fast'` | Execution strategy: `agentic` runs the full multi-step loop, `fast` runs a single-shot plan. | *** ## InvocationBudget Optional hard limits applied to a single request. The server merges any provided fields with its own per-agent defaults. ```typescript theme={null} interface InvocationBudget { maxWallClockMs?: number; maxTotalTokens?: number; maxCostUsd?: number; } ``` | Field | Type | Description | | - | - | - | | `maxWallClockMs` | `number` | Hard wall-clock timeout for the request, in milliseconds | | `maxTotalTokens` | `number` | Hard cap on total tokens consumed | | `maxCostUsd` | `number` | Hard cap on request cost, in USD | *** ## SSE Events ### ProgressEvent ```typescript theme={null} interface ProgressEvent { type: 'progress'; message: string; agentId?: string; executionId?: string; iteration?: number; metadata?: Record; } ``` ### CompleteEvent ```typescript theme={null} interface CompleteEvent { type: 'complete'; data: T; } ``` ### ErrorEvent ```typescript theme={null} interface ErrorEvent { type: 'error'; error: string; code?: string; retryable?: boolean; } ``` ### PreviewEvent An incremental preview of an agent's work-in-progress result. The payload is spread at the top level, and its shape depends on the agent. ```typescript theme={null} interface PreviewEvent { type: 'preview'; [key: string]: unknown; } ``` ### ReasoningEvent A reasoning message emitted by an agent as it works. ```typescript theme={null} interface ReasoningEvent { type: 'reasoning'; agentId: string; executionId: string; message: string; } ``` ### SSEEvent Union ```typescript theme={null} type SSEEvent = ProgressEvent | CompleteEvent | ErrorEvent | PreviewEvent | ReasoningEvent; ``` *** ## Error Types ### GraphyApiError Error thrown by the SDK when an API request fails. Covers HTTP errors, SSE stream errors, and network failures. ```typescript theme={null} import { GraphyApiError } from '@graphysdk/agents-sdk'; class GraphyApiError extends Error { readonly status: number | undefined; readonly code: string | undefined; readonly retryable: boolean; constructor(message: string, options?: GraphyApiErrorOptions); } ``` | Property | Type | Description | | - | - | - | | `status` | `number \| undefined` | HTTP status code, if from an HTTP response | | `code` | `string \| undefined` | Machine-readable error code from the API | | `retryable` | `boolean` | Whether the SDK considers this error retryable | ### GraphyApiErrorOptions ```typescript theme={null} interface GraphyApiErrorOptions { status?: number; code?: string; retryable?: boolean; } ``` ### isGraphyApiError Type guard for narrowing unknown errors to `GraphyApiError`: ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; if (isGraphyApiError(error)) { console.error(error.status, error.code, error.retryable); } ``` *** ## Configuration Types ### ClientConfig ```typescript theme={null} interface ClientConfig { apiKey: string; baseUrl: string; timeout?: number; retryConfig?: RetryConfig; logger?: Logger; } ``` ### RetryConfig ```typescript theme={null} interface RetryConfig { attempts: number; delay: number; backoff: number; } ``` **Defaults:** ```typescript theme={null} { attempts: 3, delay: 1000, backoff: 2 } ``` ### Logger ```typescript theme={null} interface Logger { log: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; debug: (...args: unknown[]) => void; } ``` *** ## AiChartType Supported chart types for AI operations. ```typescript theme={null} type AiChartType = | 'line' | 'areaStacked' | 'bar' | 'groupedBar' | 'stackedBar' | '100StackedBar' | 'column' | 'groupedColumn' | 'stackedColumn' | '100StackedColumn' | 'combo' | 'pie' | 'donut' | 'funnel' | 'heatmap' | 'scatter' | 'bubble' | 'waterfall' | 'table' | 'mekko'; ``` | Type | Description | | - | - | | `line` | Line chart | | `areaStacked` | Stacked area chart | | `bar` | Horizontal bar chart | | `groupedBar` | Grouped horizontal bars | | `stackedBar` | Stacked horizontal bars | | `100StackedBar` | 100% stacked horizontal bars | | `column` | Vertical column chart | | `groupedColumn` | Grouped vertical columns | | `stackedColumn` | Stacked vertical columns | | `100StackedColumn` | 100% stacked vertical columns | | `combo` | Combined line and column | | `pie` | Pie chart | | `donut` | Donut chart | | `funnel` | Funnel chart | | `heatmap` | Heatmap | | `scatter` | Scatter plot | | `bubble` | Bubble chart | | `waterfall` | Waterfall chart | | `table` | Data table | | `mekko` | Marimekko chart | *** ## Zod Schemas The SDK exports Zod schemas for runtime validation: ```typescript theme={null} import { AiChartTypeEnum, GenerateGraphResponseSchema } from '@graphysdk/agents-sdk'; // Validate a response const validated = GenerateGraphResponseSchema.parse(response); ``` # Annotation Agent Source: https://docs.graphy.dev/agents/rest/annotate POST /api/v0/annotate Add highlights, tooltips, stickers, and other annotations to a chart with natural language. The response is a Server-Sent Events (SSE) stream with progress, complete, and error events. Add highlights, tooltips, stickers, and other annotations to a chart with natural language. Send a `GraphConfig` and a prompt, receive an updated `GraphConfig` via Server-Sent Events. ## Response The response is a Server-Sent Events stream. See [SSE Format](/agents/rest/sse) for parsing details. ```text theme={null} event: progress data: {"message":"Adding annotations..."} event: complete data: {"config":{...},"response":{"message":"Highlighted the March peak","steps":["Added highlight","Added tooltip"]}} ``` **Final response data:** ```typescript theme={null} interface AnnotateResponse { config: GraphConfig; response: { message: string; steps?: string[]; }; } ``` ## HTTP Status Codes | Status | Description | | - | - | | 200 | Success (stream begins) | | 400 | Invalid request body | | 401 | Invalid or missing API key | | 429 | Rate limit exceeded | | 500 | Internal server error | Errors may also arrive as SSE events within a 200 response. See [Error Codes](/agents/rest/errors). # Authentication Source: https://docs.graphy.dev/agents/rest/authentication > [!NOTE] > > Don't have an API key yet? [Create one in the Graphy console](https://agents.graphy.dev/console/) — see [API keys](/agents/api-keys) for the full walkthrough. ## Base URL ``` https://agents.graphy.dev ``` ## Authorization Header ``` Authorization: Bearer YOUR_API_KEY ``` ## Required Headers | Header | Value | Required | | - | - | - | | `Content-Type` | `application/json` | Yes | | `Authorization` | `Bearer YOUR_API_KEY` | Yes, unless you are using [keyless access](/agents/rest/keyless) | > [!TIP] > > Want to try the API before signing up? [Keyless access](/agents/rest/keyless) issues you a token on your first call, with a smaller budget. ## Example call ```bash theme={null} curl -N https://agents.graphy.dev/api/v0/generate \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "config": { "type": "column", "data": { "columns": [ { "key": "quarter", "label": "Quarter" }, { "key": "revenue", "label": "Revenue ($)" } ], "rows": [ { "quarter": "Q1", "revenue": 50000 }, { "quarter": "Q2", "revenue": 62000 }, { "quarter": "Q3", "revenue": 58000 }, { "quarter": "Q4", "revenue": 71000 } ] } }, "userPrompt": "Change to a line chart" }' ``` *** ## API Key Security > [!WARNING] > > Never expose API keys in client-side code. Call the API from your backend. **Best practices:** * Store keys in environment variables * Use a secrets manager in production * Rotate keys periodically * Use separate keys for development and production *** ## Authentication Errors | Status | Error | Description | | - | - | - | | 401 | `AUTHENTICATION_ERROR` | Missing or invalid API key | | 403 | `AUTHORIZATION_ERROR` | Key lacks required permissions | Authentication errors are not retryable. Check that: * The `Authorization` header is present * The header format is `Bearer YOUR_API_KEY` (note the space) * The API key is valid and active # Chart Maker Agent Source: https://docs.graphy.dev/agents/rest/chart-maker POST /api/v0/generate Modify charts using natural language. Send a GraphConfig and a prompt, receive an updated GraphConfig. The response is a Server-Sent Events (SSE) stream with progress, complete, and error events. Modify charts using natural language. Send a `GraphConfig` and a prompt, receive an updated `GraphConfig` via Server-Sent Events. ## Response The response is a Server-Sent Events stream. See [SSE Format](/agents/rest/sse) for parsing details. ```text theme={null} event: progress data: {"message":"Analyzing chart structure..."} event: progress data: {"message":"Applying changes..."} event: complete data: {"config":{...},"response":{"message":"Changed to line chart and added trend line"}} ``` **Final response data:** ```typescript theme={null} interface GenerateResponse { config: GraphConfig; response: { message: string; steps?: string[]; }; } ``` ## HTTP Status Codes | Status | Description | | - | - | | 200 | Success (stream begins) | | 400 | Invalid request body | | 401 | Invalid or missing API key | | 429 | Rate limit exceeded | | 500 | Internal server error | Errors may also arrive as SSE events within a 200 response. See [Error Codes](/agents/rest/errors). # Error Codes Source: https://docs.graphy.dev/agents/rest/errors ## Error Event Format Errors arrive as SSE events: ```text theme={null} event: error data: {"error":"Invalid configuration","code":"VALIDATION_ERROR","retryable":false} ``` ```typescript theme={null} interface ErrorEvent { error: string; code?: string; retryable?: boolean; } ``` *** ## Error Codes | Code | HTTP Status | Retryable | Description | | - | - | - | - | | `VALIDATION_ERROR` | 400 | No | Invalid request body or parameters | | `AUTHENTICATION_ERROR` | 401 | No | Invalid or missing API key | | `RATE_LIMIT_ERROR` | 429 | Yes | Too many requests | | `PROCESSING_ERROR` | 500 | Yes | Internal processing failure | | `TIMEOUT_ERROR` | 504 | Yes | Request took too long | *** ## Common Errors ### VALIDATION\_ERROR Missing required fields (`config`, `userPrompt`), invalid `GraphConfig` structure, or malformed JSON. **Resolution:** Check the request body against the schema. ### AUTHENTICATION\_ERROR Missing `Authorization` header, malformed header (missing `Bearer ` prefix), or invalid API key. **Resolution:** Verify the API key and header format. ### RATE\_LIMIT\_ERROR Too many requests in a short period. **Resolution:** Implement exponential backoff and retry. ### PROCESSING\_ERROR / TIMEOUT\_ERROR Internal failure or request took too long. **Resolution:** Retry with exponential backoff. If persistent, contact support. *** ## HTTP Status Codes Errors may also arrive as HTTP status codes before streaming begins: | Status | Description | | - | - | | 400 | Bad Request — Invalid JSON or missing fields | | 401 | Unauthorized — Invalid or missing API key | | 429 | Too Many Requests — Rate limit exceeded | | 500 | Internal Server Error — Processing failure | | 504 | Gateway Timeout — Upstream timeout | *** ## Retry Strategy For retryable errors, use exponential backoff: ``` delay = base_delay * (backoff ^ attempt) ``` **Recommended defaults:** * `base_delay`: 1000ms * `backoff`: 2 * `max_attempts`: 3 | Attempt | Delay | | - | - | | 1 | 0ms (immediate) | | 2 | 1000ms | | 3 | 2000ms | **Best practices:** * Only retry if `retryable: true` * Set a maximum retry count * Treat network failures as retryable * The API is stateless, so retries are safe # Evaluation Source: https://docs.graphy.dev/agents/rest/evaluate POST /api/v0/evaluate Score how well every chart type fits a dataset. Deterministic — a rule-based scorer with no language model, so it returns a single result and uses no tokens. Only config.data is scored. The response is a Server-Sent Events (SSE) stream. Evaluation emits no progress events — the result arrives in a single complete event. Score how well every chart type fits a dataset. Evaluation is deterministic — a rule-based scorer with no language model — so it returns a single result and uses no tokens. Send a `GraphConfig`, receive a ranked list of chart types via Server-Sent Events. ## Request body Send a `GraphConfig` in `config`. Only `config.data` is scored — the chart `type`, styling, and annotations are ignored. Optionally pass a `chartFamily` hint (`comparison`, `relationship`, `distribution`, `composition`) to bias the ranking toward that family. ## Response The response is a Server-Sent Events stream. See [SSE Format](/agents/rest/sse) for parsing details. Evaluation is deterministic, so there are no `progress` events — the result arrives in a single `complete` event. ```text theme={null} event: complete data: {"ranking":[{"rank":1,"type":"bar","score":0.82,"verdict":"ok","factors":[{"kind":"pro","label":"Categorical x-axis","weight":0.3,"reason":"One categorical dimension maps cleanly to bars"}]}]} ``` **Final response data:** ```typescript theme={null} interface EvaluateResponse { ranking: ChartFitEntry[]; } interface ChartFitEntry { rank: number; type: AiChartType; score: number; verdict: 'ok' | 'disqualified'; factors: ChartFitFactor[]; } ``` See the [SDK Evaluation page](/agents/sdk/evaluate) for the full `ChartFitEntry` and `ChartFitFactor` field reference. ## HTTP Status Codes | Status | Description | | - | - | | 200 | Success (stream begins) | | 400 | Invalid request body | | 401 | Invalid or missing API key | | 429 | Rate limit exceeded | | 500 | Internal server error | Errors may also arrive as SSE events within a 200 response — for example, a dataset with no plottable columns. See [Error Codes](/agents/rest/errors). # Extract Agent Source: https://docs.graphy.dev/agents/rest/extract POST /api/v0/extract Build a chart-ready dataset from unstructured text, images, PDFs, or .xlsx spreadsheets. Provide sourceText, attachments, or both. The response is a Server-Sent Events (SSE) stream with progress, complete, and error events. Build a chart-ready dataset from unstructured text, images, PDFs, or Excel spreadsheets. Returns a `GraphConfig` with the extracted data via Server-Sent Events. ## Request body Provide `sourceText`, `attachments`, or both — at least one is required. Each attachment is `{ kind: "image" | "document" | "spreadsheet", mimeType, dataBase64 }`, where `dataBase64` is RFC 4648 standard base64 (not a `data:` URL). Limits: 8 images (2 MB each), 2 PDFs (6 MB each), 2 spreadsheets (6 MB each); 12 attachments and 12 MB combined. The legacy `images` array is deprecated — use `attachments` with `kind: "image"`. `images` and `attachments` cannot be combined. ## Response The response is a Server-Sent Events stream. See [SSE Format](/agents/rest/sse) for parsing details. ```text theme={null} event: progress data: {"message":"Extracting dataset from source content..."} event: complete data: {"config":{...},"response":{"message":"Extraction complete"},"extractMeta":{"confidence":"high","needsUserInput":false,"warnings":[]}} ``` **Final response data:** ```typescript theme={null} interface ExtractResponse { config: GraphConfig; response: { message: string; }; extractMeta: ExtractMeta | null; lastAccuracyEvaluation?: ExtractAccuracyEvaluation; } ``` ## HTTP Status Codes | Status | Description | | - | - | | 200 | Success (stream begins) | | 400 | Invalid request body / no source | | 401 | Invalid or missing API key | | 429 | Rate limit exceeded | | 500 | Internal server error | Errors may also arrive as SSE events within a 200 response. See [Error Codes](/agents/rest/errors). # Keyless access Source: https://docs.graphy.dev/agents/rest/keyless Try the API with no signup and no key. Send a request without an `Authorization` header and we mint a token for you, serve that same request, and hand the token back in a response header. > [!NOTE] > > Keyless is for trying things out. For anything you depend on, [create an API key](/agents/api-keys): the limits are higher and the budget is yours alone. ## Your first call ```bash theme={null} curl -i -N https://agents.graphy.dev/api/v0/evaluate \ -H "Content-Type: application/json" \ -d '{ "config": { "data": { "columns": [ { "key": "month", "label": "Month" }, { "key": "sales", "label": "Sales" } ], "rows": [ { "month": "Jan", "sales": 10 }, { "month": "Feb", "sales": 20 } ] } } }' ``` The `-i` matters. Your token is in the response headers: ```http theme={null} HTTP/1.1 200 OK content-type: text/event-stream x-graphy-anonymous-token: graphy_9lOx1t-kPLChHCBunWEt8TQGeG9kzZADlRT7m0Ik83g x-ratelimit-limit: 100 x-ratelimit-remaining: 99 ``` ## Keep the token Save it and send it as a normal Bearer token from then on. Every call that carries it reuses the same budget instead of minting again. ```bash theme={null} curl -N https://agents.graphy.dev/api/v0/evaluate \ -H "Content-Type: application/json" \ -H "Authorization: Bearer graphy_9lOx1t-..." \ -d '{ "config": { "data": { "columns": [], "rows": [] } } }' ``` > [!WARNING] > > If you throw the token away and call again, you get a new one. That is capped at **10 new tokens per hour** from one network, so a script that forgets its token will start failing. Store it. ## What you get | | Keyless | With an API key | | - | - | - | | Monthly budget | \$5 | \$15 | | Requests | 100/min | 100/min | | Signup | none | required | Keyless also shares a pool with everyone else trying the API, so a busy day can run it out. An API key does not share. ## Which endpoints work > [!TIP] > > `generate`, `mutate`, `pipeline`, `explore`, `extract`, `annotate`, `narrate`, `evaluate`, `chart-knowledge` `judge`, `suggestions`, `generate-workflow`, `feedback` and all attachment endpoints need an API key. ## Errors | Status | Code | What to do | | - | - | - | | 429 | `ANONYMOUS_CAPACITY_EXCEEDED` | Free access is out of budget for now. Try tomorrow or use an API key | | 429 | `QUOTA_EXCEEDED` | Your token spent its \$5 this month. Wait for the reset or use an API key | | 429 | `RATE_LIMIT_ERROR` | Too fast. Wait the number of seconds in `Retry-After` | | 403 | `ANONYMOUS_ACCESS_FORBIDDEN` | That endpoint needs an API key | | 503 | `ANONYMOUS_MINT_UNAVAILABLE` | We could not issue a token. Retry, and tell us if it persists | None of the 4xx ones are worth retrying immediately. Wait, or switch to a key. ## Browser use The token header is exposed to JavaScript, so a browser client can read it: ```js theme={null} const response = await fetch('https://agents.graphy.dev/api/v0/evaluate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ config: { data } }), }); const token = response.headers.get('X-Graphy-Anonymous-Token'); if (token) { localStorage.setItem('graphy_token', token); } ``` > [!WARNING] > > A keyless token is a bearer credential. Anything on the page can read it out of `localStorage`, and it does not expire. Treat it as throwaway, and never put a real API key in a browser. # Mutation Agent Source: https://docs.graphy.dev/agents/rest/mutate POST /api/v0/mutate Transform a chart's dataset with natural language — filter, group, aggregate, derive, sort. The response is a Server-Sent Events (SSE) stream with progress, complete, and error events. Transform a chart's dataset with natural language — filter, group, aggregate, derive, sort. Send a `GraphConfig` and a prompt, receive an updated `GraphConfig` via Server-Sent Events. ## Response The response is a Server-Sent Events stream. See [SSE Format](/agents/rest/sse) for parsing details. ```text theme={null} event: progress data: {"message":"Mutating dataset..."} event: complete data: {"config":{...},"response":{"message":"Grouped sales by region","steps":["Grouped rows by region","Sorted by total sales descending"]}} ``` **Final response data:** ```typescript theme={null} interface MutateResponse { config: GraphConfig; response: { message: string; steps?: string[]; }; } ``` ## HTTP Status Codes | Status | Description | | - | - | | 200 | Success (stream begins) | | 400 | Invalid request body | | 401 | Invalid or missing API key | | 429 | Rate limit exceeded | | 500 | Internal server error | Errors may also arrive as SSE events within a 200 response. See [Error Codes](/agents/rest/errors). # Narrative Agent Source: https://docs.graphy.dev/agents/rest/narrate POST /api/v0/narrate Generate a chart's title, subtitle, and caption. Narrative is written into config.content as TipTap JSON. Requires userPrompt. The response is a Server-Sent Events (SSE) stream with progress, complete, and error events. Generate a chart's title, subtitle, and caption with natural language. Send a `GraphConfig` and a prompt, receive an updated `GraphConfig` via Server-Sent Events. Narrative is written into `config.content` as TipTap JSON. ## Request body `userPrompt` is **required** — the narrative agent needs an instruction to write against. Which fields are produced (title only, title + caption, or all three) depends on the storytelling level derived from `metadata.effort`. ## Response The response is a Server-Sent Events stream. See [SSE Format](/agents/rest/sse) for parsing details. ```text theme={null} event: progress data: {"message":"Generating narrative..."} event: complete data: {"config":{...},"response":{"message":"Wrote a title and caption"}} ``` **Final response data:** ```typescript theme={null} interface NarrateResponse { config: GraphConfig; response: { message: string; }; } ``` ## HTTP Status Codes | Status | Description | | - | - | | 200 | Success (stream begins) | | 400 | Invalid request body | | 401 | Invalid or missing API key | | 429 | Rate limit exceeded | | 500 | Internal server error | Errors may also arrive as SSE events within a 200 response. See [Error Codes](/agents/rest/errors). # SSE Format Source: https://docs.graphy.dev/agents/rest/sse All API endpoints return Server-Sent Events (SSE). This page explains the wire format and how to parse it. ## Protocol Overview SSE is a simple text-based protocol. Events are separated by blank lines, and each event has fields prefixed with `event:` and `data:`. ```text theme={null} event: progress data: {"message":"Processing..."} event: complete data: {"config":{...},"response":{...}} ``` ## Event Format Each event consists of: 1. `event:` line — The event type (`progress`, `complete`, or `error`) 2. `data:` line — JSON payload 3. Blank line — Event delimiter The blank line after each event is required. *** ## Event Types ### progress Sent periodically during processing. ```json theme={null} { "message": "Analyzing chart structure..." } ``` | Field | Type | Description | | - | - | - | | `message` | `string` | Status message | Agent-emitted progress events also include `agentId`, `executionId`, and `iteration` identifiers. ### complete Sent once when processing succeeds. Contains the final result. ```json theme={null} { "config": { ... }, "response": { "message": "Changed to bar chart", "steps": ["Changed type", "Updated colors"] } } ``` ### error Sent when processing fails. ```json theme={null} { "error": "Invalid configuration", "code": "VALIDATION_ERROR", "retryable": false } ``` | Field | Type | Description | | - | - | - | | `error` | `string` | Error message | | `code` | `string` | Error code (see [Error Codes](/agents/rest/errors)) | | `retryable` | `boolean` | Whether the request can be retried | *** ## Python Example ```python theme={null} import requests import json response = requests.post( 'https://agents.graphy.dev/api/v0/generate', headers={ 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_KEY' }, json={'config': config, 'userPrompt': prompt}, stream=True ) event_type = None for line in response.iter_lines(): if not line: continue decoded = line.decode('utf-8') if decoded.startswith('event:'): event_type = decoded[6:].strip() elif decoded.startswith('data:'): data = json.loads(decoded[5:].strip()) if event_type == 'progress': print(f"Progress: {data['message']}") elif event_type == 'complete': print(f"Done: {data['config']}") elif event_type == 'error': print(f"Error: {data['error']}") ``` *** ## Connection Tips **Disable buffering** to receive events in real-time: ```bash theme={null} curl -N https://agents.graphy.dev/api/v0/generate ... ``` ```python theme={null} response = requests.post(..., stream=True) ``` **Set appropriate timeouts.** The initial connection should be quick, but processing may take up to 60 seconds: ```python theme={null} response = requests.post(url, ..., timeout=(5, 120)) ``` # Suggestions Agent Source: https://docs.graphy.dev/agents/rest/suggestions POST /api/v0/suggestions Generate chart type and data preparation suggestions from a dataset. Send a GraphConfig and a prompt, receive suggested visualizations. The response is a Server-Sent Events (SSE) stream with progress, complete, and error events. Generate chart type and data preparation suggestions from a dataset. Send a `GraphConfig` and a prompt, receive suggested visualizations via Server-Sent Events. ## Request body Besides `config`, `userPrompt`, and optional `metadata`, you may send **`maxSuggestionCount`** (integer **1–4**). When set, it is communicated to the model as the target maximum number of suggestions. The stream is not validated or truncated to that count if the model returns more. ## Response The response is a Server-Sent Events stream. See [SSE Format](/agents/rest/sse) for parsing details. ```text theme={null} event: progress data: {"message":"Generating suggestions..."} event: complete data: {"suggestions":[{"dataPrepPrompt":"...","chartType":"line","summary":"Monthly sales trend"}]} ``` **Final response data:** ```typescript theme={null} interface SuggestionsResponse { suggestions: Suggestion[]; } interface Suggestion { dataPrepPrompt: string; chartType: AiChartType; summary: string; } ``` ## HTTP Status Codes | Status | Description | | - | - | | 200 | Success (stream begins) | | 400 | Invalid request body | | 401 | Invalid or missing API key | | 429 | Rate limit exceeded | | 500 | Internal server error | Errors may also arrive as SSE events within a 200 response. See [Error Codes](/agents/rest/errors). # Annotation Agent Source: https://docs.graphy.dev/agents/sdk/annotate Add annotations to a chart with natural language. Send a `GraphConfig` and a prompt, receive a `GraphConfig` with highlights, tooltips, stickers, and other annotations applied. ## Methods ### generateAnnotations Processes the request and returns the final result. Internally handles streaming and collects the response. ```typescript theme={null} async generateAnnotations( params: GenerateAnnotationsParams, onProgress?: (event: ProgressEvent) => void, signal?: AbortSignal ): Promise ``` ### generateAnnotationsStream Returns an async iterator that yields events as they arrive. ```typescript theme={null} async generateAnnotationsStream( params: GenerateAnnotationsParams, signal?: AbortSignal ): Promise>> ``` ## Parameters ```typescript theme={null} interface GenerateAnnotationsParams { config: GraphConfig; userPrompt?: string; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The current chart configuration to annotate | | `userPrompt` | `string` | No | Natural language description of the annotations to add or remove | | `metadata` | `Metadata` | No | Request tracking information | ## Response ```typescript theme={null} interface GenerateAnnotationsResponse { response: { message: string; steps: string[]; }; config: GraphConfig; } ``` | Field | Type | Description | | - | - | - | | `response.message` | `string` | Explanation of the annotations applied | | `response.steps` | `string[]` | Ordered breakdown of each annotation step | | `config` | `GraphConfig` | Updated chart configuration with annotations applied | The response is validated with Zod. Invalid responses throw an error. *** ## Basic Usage ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; import type { GraphConfig } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: 'Jan', revenue: 100 }, { month: 'Feb', revenue: 140 }, { month: 'Mar', revenue: 220 }, { month: 'Apr', revenue: 130 }, ], }, }; const result = await ai.generateAnnotations({ config, userPrompt: 'Highlight the March peak and add a tooltip explaining it', }); console.log(result.config.annotations); // Annotations applied to the chart ``` *** ## With Progress Callback Use the `onProgress` callback to show real-time progress without full streaming: ```typescript theme={null} const result = await ai.generateAnnotations( { config, userPrompt: 'Add a difference arrow between the first and last month', }, (progress) => { console.log(progress.message); } ); ``` *** ## Streaming For full control over the event stream: ```typescript theme={null} import { isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk'; const stream = await ai.generateAnnotationsStream({ config, userPrompt: 'Highlight every month above 150', }); for await (const event of stream) { if (isProgressEvent(event)) { console.log(event.message); } if (isCompleteEvent(event)) { console.log('Result:', event.data.config); } if (isErrorEvent(event)) { console.error('Error:', event.error); } } ``` See [Streaming](/agents/sdk/streaming) for cancellation and React patterns. *** ## Error Handling ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { const result = await ai.generateAnnotations({ config, userPrompt: 'Highlight the peak value', }); } catch (error) { if (isGraphyApiError(error)) { console.error('API error:', error.message); } } ``` See [Error Handling](/agents/sdk/errors) for retry behavior and error types. # Chart Maker Agent Source: https://docs.graphy.dev/agents/sdk/chart-maker Modify charts using natural language. Send a `GraphConfig` and a prompt, receive an updated `GraphConfig`. ## Methods ### generateGraph Processes the request and returns the final result. Internally handles streaming and collects the response. ```typescript theme={null} async generateGraph( params: GenerateGraphParams, onProgress?: (event: ProgressEvent) => void, signal?: AbortSignal ): Promise ``` ### generateGraphStream Returns an async iterator that yields events as they arrive. ```typescript theme={null} async generateGraphStream( params: GenerateGraphParams, signal?: AbortSignal ): Promise>> ``` ## Parameters ```typescript theme={null} interface GenerateGraphParams { config: GraphConfig; userPrompt?: string; metadata?: Metadata; storytellingOptions?: StorytellingOptions; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The current chart configuration | | `userPrompt` | `string` | No | Natural language instruction | | `metadata` | `Metadata` | No | Request tracking information | | `storytellingOptions` | `StorytellingOptions` | No | Prune storytelling from config content client-side (SDK only). | ### Metadata ```typescript theme={null} interface Metadata { callId?: string; locale?: string; effort?: 'low' | 'medium' | 'high'; /** @deprecated Prefer `effort`. */ storytellingEffort?: 'none' | 'low' | 'medium' | 'high'; } ``` | Field | Type | Description | | - | - | - | | `callId` | `string` | Optional. Identifier for tracking and debugging. | | `locale` | `string` | Locale for responses (e.g., `en-US`, `fr-FR`). | | `effort` | `'low' \| 'medium' \| 'high'` | Preferred invocation tier; stored on the invocation and forwarded to nested agents. Default is **`medium`** when omitted (and nothing deprecated is used to infer it). See [Invocation effort and chart narrative](#invocation-effort-and-chart-narrative). | | `storytellingEffort` | `'none' \| 'low' \| 'medium' \| 'high'` | **Deprecated** — prefer `effort`. Still accepted; when only this field is set, the server infers `effort` for the rest of the pipeline. | ## Response ```typescript theme={null} interface GenerateGraphResponse { config: GraphConfig; response: { message: string; steps?: string[]; }; } ``` | Field | Type | Description | | - | - | - | | `config` | `GraphConfig` | Updated chart configuration. Narrative (title, subtitle, caption) is embedded in `config.content` as TipTap JSON documents; see [Invocation effort and chart narrative](#invocation-effort-and-chart-narrative). | | `response.message` | `string` | Explanation of changes made | | `response.steps` | `string[]` | Breakdown of individual modifications | The response is validated with Zod. Invalid responses throw an error. *** ## Invocation effort and chart narrative `metadata.effort` selects how heavy the invocation is: it is stored on the request, forwarded into agent invocations (including tools that run nested agents), and drives defaults when you do not send `storytellingEffort`. For **chart generation**, the chart agent still consumes a storytelling level (`none` / `low` / `medium` / `high`). The server sets that level as follows: * If you send **only** `effort`, the chart storytelling level is derived from `effort` (default `effort` is **`medium`**, which matches the narrative band you previously got from omitting metadata or using the old default). * If you send **only** `storytellingEffort` (deprecated), the chart uses that value directly and the server infers `effort` for the rest of the stack. * If you send **both**, `storytellingEffort` wins for the chart so existing explicit chart settings keep working; `effort` still applies everywhere else. There is no **`none`** value on `effort`; to drop narrative fields in the client, use **`storytellingOptions`** on `GenerateGraphParams` (see [StorytellingOptions](/agents/reference/types#storytellingoptions)). ```typescript theme={null} const result = await ai.generateGraph({ config, userPrompt: 'Show sales by region and add a clear title', metadata: { effort: 'high' }, }); // Narrative is embedded in the config content as TipTap JSON console.log(result.config.content?.title); // TipTap JSON doc with title text console.log(result.config.content?.subtitle); console.log(result.config.content?.caption); ``` *** ## Basic Usage ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; import type { GraphConfig } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'Jan', sales: 100 }, { month: 'Feb', sales: 120 }, { month: 'Mar', sales: 115 }, ], }, }; const result = await ai.generateGraph({ config, userPrompt: 'Change this to a bar chart and sort by sales descending', }); console.log(result.config); // Updated GraphConfig console.log(result.response.message); // Explanation ``` *** ## With Progress Callback Use the `onProgress` callback to show real-time progress without full streaming: ```typescript theme={null} const result = await ai.generateGraph( { config, userPrompt: 'Add a trend line and change colors to blue', }, (progress) => { console.log(progress.message); } ); ``` ### ProgressEvent ```typescript theme={null} interface ProgressEvent { type: 'progress'; message: string; agentId?: string; executionId?: string; iteration?: number; metadata?: Record; } ``` *** ## With Abort Signal Cancel a request mid-operation: ```typescript theme={null} const controller = new AbortController(); // Cancel after 5 seconds setTimeout(() => controller.abort(), 5000); try { const result = await ai.generateGraph( { config, userPrompt: 'Create a complex visualization' }, undefined, controller.signal ); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.log('Request cancelled'); } } ``` *** ## Streaming For full control over the event stream: ```typescript theme={null} import { isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk'; const stream = await ai.generateGraphStream({ config, userPrompt: 'Add annotations for peak values', }); for await (const event of stream) { if (isProgressEvent(event)) { console.log(event.message); } if (isCompleteEvent(event)) { console.log('Result:', event.data.config); } if (isErrorEvent(event)) { console.error('Error:', event.error); } } ``` See [Streaming](/agents/sdk/streaming) for cancellation and React patterns. *** ## Error Handling ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { const result = await ai.generateGraph({ config, userPrompt: 'Change to a bar chart', }); } catch (error) { if (isGraphyApiError(error)) { console.error('API error:', error.message); } else if (error instanceof Error && error.name === 'AbortError') { console.log('Request cancelled'); } else { console.error('Unexpected error:', error); } } ``` See [Error Handling](/agents/sdk/errors) for retry behavior and error types. # Configuration Source: https://docs.graphy.dev/agents/sdk/configuration ## ClientConfig ```typescript theme={null} interface ClientConfig { apiKey: string; baseUrl: string; timeout?: number; retryConfig?: RetryConfig; logger?: Logger; } ``` ## Options ### apiKey **Type:** `string` (required) Your Graphy API key (starts with `graphy_`). [Create one in the console](/agents/api-keys). ```typescript theme={null} const ai = new GraphyAiSdk({ apiKey: 'graphy_...', baseUrl: 'https://agents.graphy.dev', }); ``` ### baseUrl **Type:** `string` (required) The API base URL. Use `https://agents.graphy.dev` for production. ### timeout **Type:** `number` (optional) **Default:** `60000` (60 seconds) Request timeout in milliseconds. The timeout applies to the initial connection. Once streaming begins, the timeout resets for each chunk. ```typescript theme={null} const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', timeout: 30000, // 30 seconds }); ``` ### retryConfig **Type:** `RetryConfig` (optional) ```typescript theme={null} interface RetryConfig { attempts: number; // Max retry attempts (default: 3) delay: number; // Initial delay in ms (default: 1000) backoff: number; // Backoff multiplier (default: 2) } ``` **Defaults:** * `attempts`: 3 * `delay`: 1000ms * `backoff`: 2 Retry delay formula: `delay * (backoff ^ (attempt - 1))` With defaults: * Attempt 1: immediate * Attempt 2: 1000ms delay * Attempt 3: 2000ms delay **Retryable conditions:** * HTTP 5xx errors * HTTP 429 (rate limit) * Network failures **Non-retryable conditions:** * HTTP 4xx errors (except 429) * User abort signals ```typescript theme={null} const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', retryConfig: { attempts: 5, delay: 500, backoff: 1.5, }, }); ``` ### logger **Type:** `Logger` (optional) ```typescript theme={null} interface Logger { log: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; debug: (...args: unknown[]) => void; } ``` **Default:** Uses `console.log`, `console.warn`, `console.error`, `console.debug` Provide a custom logger to integrate with your logging infrastructure (e.g., pino, winston). ## Full Example ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', timeout: 45000, retryConfig: { attempts: 3, delay: 1000, backoff: 2, }, logger: console, }); ``` ## Health Check Use `ping()` to verify connectivity and measure latency: ```typescript theme={null} const health = await ai.ping(); if (health.ok) { console.log(`Connected. Latency: ${health.latency}ms`); } else { console.error('Connection failed'); } ``` **Response:** ```typescript theme={null} interface PingResponse { ok: boolean; latency: number; // milliseconds } ``` # Error Handling Source: https://docs.graphy.dev/agents/sdk/errors ## GraphyApiError All API errors are wrapped in `GraphyApiError`. Each error carries structured metadata you can use to decide how to handle the failure: ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { const result = await ai.generateGraph({ config, userPrompt }); } catch (error) { if (isGraphyApiError(error)) { console.error(`[${error.code}] ${error.message} (HTTP ${error.status})`); if (error.retryable) { // schedule retry } } } ``` | Property | Type | Description | | - | - | - | | `message` | `string` | Human-readable error description | | `status` | `number \| undefined` | HTTP status code, if from an HTTP response | | `code` | `string \| undefined` | Machine-readable error code from the API | | `retryable` | `boolean` | Whether the SDK considers this error retryable | Use the `isGraphyApiError()` type guard to safely narrow unknown errors. See the [Type Reference](/agents/reference/types#graphyapierror) for the full class definition. *** ## Retry Behavior The SDK automatically retries failed requests based on `retryConfig`. **Default configuration:** ```typescript theme={null} { attempts: 3, delay: 1000, backoff: 2 } ``` **Retry delay formula:** ``` delay * (backoff ^ (attempt - 1)) ``` With defaults: | Attempt | Delay | | - | - | | 1 | Immediate | | 2 | 1000ms | | 3 | 2000ms | ### Retryable Conditions | Condition | Retries | | - | - | | HTTP 5xx | Yes | | HTTP 429 (rate limit) | Yes | | Network failure | Yes | | HTTP 4xx (except 429) | No | | User abort | No | | `GraphyApiError` | No | ### Custom Retry Configuration ```typescript theme={null} const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', retryConfig: { attempts: 5, // More attempts delay: 500, // Shorter initial delay backoff: 1.5, // Gentler backoff }, }); ``` ### Disable Retries ```typescript theme={null} const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', retryConfig: { attempts: 1, // No retries delay: 0, backoff: 1, }, }); ``` *** ## Error Codes Errors from the API include a `code` field: | Code | HTTP Status | Retryable | Description | | - | - | - | - | | `VALIDATION_ERROR` | 400 | No | Invalid request body or parameters | | `AUTHENTICATION_ERROR` | 401 | No | Invalid or missing API key | | `RATE_LIMIT_ERROR` | 429 | Yes | Too many requests | | `PROCESSING_ERROR` | 500 | Yes | Internal processing failure | | `TIMEOUT_ERROR` | 504 | Yes | Request took too long | *** ## Network Errors Network failures throw a `GraphyApiError` with `retryable: true`: ```typescript theme={null} try { const result = await ai.generateGraph({ config, userPrompt }); } catch (error) { if (isGraphyApiError(error) && error.retryable) { console.error('Transient error, consider retrying'); } } ``` *** ## Abort Errors User cancellation throws a native `AbortError`: ```typescript theme={null} const controller = new AbortController(); try { const result = await ai.generateGraph({ config, userPrompt }, undefined, controller.signal); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { // User cancelled - not an error return; } throw error; } ``` *** ## Stream Errors In streaming mode, errors can arrive as events: ```typescript theme={null} import { isErrorEvent } from '@graphysdk/agents-sdk'; for await (const event of stream) { if (isErrorEvent(event)) { console.error('Error:', event.error); console.error('Code:', event.code); console.error('Retryable:', event.retryable); if (event.retryable) { // Implement retry logic } return; } } ``` Or as exceptions when the stream fails: ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { for await (const event of stream) { // ... } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { return; // User cancelled } if (isGraphyApiError(error)) { console.error('API error:', error.message); } } ``` *** ## Timeout Errors The SDK throws `AbortError` when a request exceeds the configured timeout: ```typescript theme={null} const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', timeout: 10000, // 10 seconds }); try { const result = await ai.generateGraph({ config, userPrompt }); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.error('Request timed out'); } } ``` *** ## Best Practices ### Log errors with context ```typescript theme={null} try { const result = await ai.generateGraph({ config, userPrompt }); } catch (error) { if (error instanceof Error) { logger.error('generateGraph failed', { error: error.message, userPrompt, configType: config.type, }); } throw error; } ``` ### Show user-friendly messages ```typescript theme={null} function getErrorMessage(error: unknown): string { if (isGraphyApiError(error)) { if (error.status === 401) return 'Authentication failed. Please check your API key.'; if (error.status === 429) return 'Too many requests. Please try again later.'; if (error.retryable) return 'Temporary issue. Please try again.'; return 'Failed to generate chart. Please try again.'; } if (error instanceof Error && error.name === 'AbortError') { return 'Request was cancelled.'; } return 'An unexpected error occurred.'; } ``` # Evaluation Source: https://docs.graphy.dev/agents/sdk/evaluate Score how well every chart type fits a dataset. Unlike the other agents, `evaluate` is deterministic — it runs a rule-based scorer with no language model, so it returns in a single fast response and uses no tokens. ## Method ### evaluate Scores the dataset and returns the ranked result. There is no streaming variant — the result is computed synchronously on the server. ```typescript theme={null} async evaluate( params: EvaluateParams, signal?: AbortSignal ): Promise ``` ## Parameters ```typescript theme={null} interface EvaluateParams { config: GraphConfig; chartFamily?: ChartFamily; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The chart configuration. Only `config.data` is scored. | | `chartFamily` | `ChartFamily` | No | Optional family hint — one of `comparison`, `relationship`, `distribution`, `composition`. When set, chart types in that family get a fit bonus. | | `metadata` | `Metadata` | No | Request tracking information | > [!NOTE] > > `evaluate` is deterministic — it reads only `config.data` and the optional `chartFamily`. The chart `type`, styling, and annotations on `config` are ignored. ## Response ```typescript theme={null} interface EvaluateResponse { ranking: ChartFitEntry[]; } ``` | Field | Type | Description | | - | - | - | | `ranking` | `ChartFitEntry[]` | Every supported chart type, scored and ranked by fit (best first) | The response is validated with Zod. Invalid responses throw an error. ### ChartFitEntry A single chart type, scored against the dataset. ```typescript theme={null} interface ChartFitEntry { rank: number; type: AiChartType; score: number; verdict: ChartFitVerdict; factors: ChartFitFactor[]; } type ChartFitVerdict = 'ok' | 'disqualified'; ``` | Field | Type | Description | | - | - | - | | `rank` | `number` | 1-based rank in `ranking`. Stable as scoring rules evolve — prefer it over `score`. | | `type` | `AiChartType` | The chart type being scored (see [AiChartType](/agents/reference/types#aicharttype)) | | `score` | `number` | Raw fit score. Drifts as rules change; use `rank` and `verdict` for stable checks. | | `verdict` | `ChartFitVerdict` | `ok` if the chart type is usable, `disqualified` if a rule rules it out | | `factors` | `ChartFitFactor[]` | The individual reasons that contributed to the score | ### ChartFitFactor ```typescript theme={null} interface ChartFitFactor { kind: ChartFitFactorKind; label: string; weight: number; reason: string; } type ChartFitFactorKind = | 'deal-breaker' | 'pro' | 'con' | 'family' | 'specificity' | 'continuity'; ``` | Field | Type | Description | | - | - | - | | `kind` | `ChartFitFactorKind` | The category of the factor | | `label` | `string` | Short label for the factor | | `weight` | `number` | How much the factor moved the score | | `reason` | `string` | Why the factor applies to this dataset | *** ## Basic Usage ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; import type { GraphConfig } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'product', label: 'Product' }, { key: 'revenue', label: 'Revenue' }, { key: 'units', label: 'Units Sold' }, ], rows: [ { product: 'Widget A', revenue: 4200, units: 120 }, { product: 'Widget B', revenue: 3100, units: 90 }, { product: 'Widget C', revenue: 5600, units: 60 }, ], }, }; const result = await ai.evaluate({ config }); const best = result.ranking[0]; console.log(`Best fit: ${best.type} — rank ${best.rank}, ${best.verdict}`); ``` *** ## Error Handling ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { const result = await ai.evaluate({ config }); } catch (error) { if (isGraphyApiError(error)) { console.error('API error:', error.message); } } ``` See [Error Handling](/agents/sdk/errors) for retry behavior and error types. # Extract Agent Source: https://docs.graphy.dev/agents/sdk/extract Build a chart-ready dataset from unstructured sources. Send raw text, images, PDFs, or Excel spreadsheets, receive a `GraphConfig` with the extracted data. ## Methods ### extractFromProse Processes the request and returns the final result. Internally handles streaming and collects the response. ```typescript theme={null} async extractFromProse( params: ExtractFromProseParams, onProgress?: (event: ProgressEvent) => void, signal?: AbortSignal ): Promise ``` ### extractFromProseStream Returns an async iterator that yields events as they arrive. ```typescript theme={null} async extractFromProseStream( params: ExtractFromProseParams, signal?: AbortSignal ): Promise>> ``` ## Parameters ```typescript theme={null} interface ExtractFromProseParams { sourceText?: string; attachments?: ExtractAttachmentInput[]; images?: ExtractImageInput[]; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `sourceText` | `string` | No | Raw text to extract data from. Up to 500,000 characters. | | `attachments` | `ExtractAttachmentInput[]` | No | Images, PDFs, and spreadsheets to extract data from. Up to 12 entries. | | `images` | `ExtractImageInput[]` | No | **Deprecated.** Legacy image array — use `attachments` with `kind: 'image'` instead. | | `metadata` | `Metadata` | No | Request tracking information | > [!NOTE] > > Provide `sourceText`, `attachments`, or both — at least one is required. `images` and `attachments` cannot be combined in a single request. ## Attachments `attachments` is a discriminated union tagged by `kind`. Use the builder helpers to construct entries — each accepts base64-encoded file data and returns a correctly-tagged attachment. ```typescript theme={null} import { buildImageAttachment, buildPdfAttachment, buildSpreadsheetAttachment, } from '@graphysdk/agents-sdk'; function buildImageAttachment(input: { mimeType: string; dataBase64: string }): ExtractAttachmentInput; function buildPdfAttachment(input: { dataBase64: string }): ExtractAttachmentInput; function buildSpreadsheetAttachment(input: { dataBase64: string; filename?: string }): ExtractAttachmentInput; ``` ### ExtractAttachmentInput ```typescript theme={null} type ExtractAttachmentInput = | { kind: 'image'; mimeType: string; dataBase64: string } | { kind: 'document'; mimeType: string; dataBase64: string } | { kind: 'spreadsheet'; mimeType: string; dataBase64: string; filename?: string }; ``` | `kind` | Accepted formats | Builder | | - | - | - | | `'image'` | PNG, JPEG, WebP, GIF | `buildImageAttachment` | | `'document'` | PDF | `buildPdfAttachment` | | `'spreadsheet'` | Excel `.xlsx` | `buildSpreadsheetAttachment` | `dataBase64` must be RFC 4648 standard base64. The builders set `mimeType` for you (`buildImageAttachment` takes it as input since images have several valid types). ### Size limits | Attachment | Max count | Max size each | Max size combined | | - | - | - | - | | Images | 8 | 2 MB | 7 MB | | PDFs | 2 | 6 MB | 8 MB | | Spreadsheets | 2 | 6 MB | 8 MB | Across all kinds, a request accepts up to **12 attachments** and **12 MB** combined. Limits are measured on the decoded bytes, not the base64 string. ## Response ```typescript theme={null} interface ExtractFromProseResponse { response: { message: string; }; config: GraphConfig; extractMeta: ExtractMeta | null; lastAccuracyEvaluation?: ExtractAccuracyEvaluation; } ``` | Field | Type | Description | | - | - | - | | `response.message` | `string` | Summary of what was extracted | | `config` | `GraphConfig` | Chart configuration containing the extracted dataset | | `extractMeta` | `ExtractMeta \| null` | Trust metadata for the extraction, or `null` when unavailable | | `lastAccuracyEvaluation` | `ExtractAccuracyEvaluation` | The agent's final self-check of the extracted data, when performed | The response is validated with Zod. Invalid responses throw an error. ### ExtractMeta ```typescript theme={null} interface ExtractMeta { confidence: ExtractConfidence; needsUserInput: boolean; warnings: string[]; cellConfidence?: Array<{ rowIndex: number; columnKey: string; confidence: ExtractConfidence; quote?: string; }>; } type ExtractConfidence = 'high' | 'medium' | 'low'; ``` | Field | Type | Description | | - | - | - | | `confidence` | `ExtractConfidence` | Overall confidence in the extraction | | `needsUserInput` | `boolean` | Whether the result should be reviewed before use | | `warnings` | `string[]` | Notes about ambiguous, conflicting, or missing source data | | `cellConfidence` | `Array<…>` | Optional per-cell confidence, each with a supporting source quote | ### ExtractAccuracyEvaluation ```typescript theme={null} interface ExtractAccuracyEvaluation { score: number; issues: string[]; sufficient: boolean; summary: string; } ``` | Field | Type | Description | | - | - | - | | `score` | `number` | Accuracy score, from `0` to `1` | | `issues` | `string[]` | Specific accuracy problems the agent identified | | `sufficient` | `boolean` | Whether the accuracy is good enough to use as-is | | `summary` | `string` | Human-readable summary of the accuracy check | *** ## Basic Usage Extract from plain text: ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const result = await ai.extractFromProse({ sourceText: ` In Q3, the Direct channel brought in $1.9M, Partner $0.4M, and Marketplace $1.1M. `, }); console.log(result.config.data); // Extracted dataset console.log(result.extractMeta?.confidence); // 'high' | 'medium' | 'low' ``` *** ## Extracting from Files Read a file, base64-encode it, and wrap it with the matching builder: ```typescript theme={null} import { GraphyAiSdk, buildImageAttachment, buildPdfAttachment, buildSpreadsheetAttachment, } from '@graphysdk/agents-sdk'; import { readFile } from 'node:fs/promises'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); // From a screenshot of a table const png = await readFile('./table.png'); const fromImage = await ai.extractFromProse({ attachments: [buildImageAttachment({ mimeType: 'image/png', dataBase64: png.toString('base64') })], }); // From a PDF report const pdf = await readFile('./q3-report.pdf'); const fromPdf = await ai.extractFromProse({ attachments: [buildPdfAttachment({ dataBase64: pdf.toString('base64') })], }); // From an Excel spreadsheet const xlsx = await readFile('./q3-report.xlsx'); const fromSpreadsheet = await ai.extractFromProse({ attachments: [buildSpreadsheetAttachment({ dataBase64: xlsx.toString('base64'), filename: 'q3-report.xlsx' })], }); ``` You can combine `sourceText` and multiple `attachments` in one request — the agent reconciles all sources into a single dataset. *** ## With Progress Callback Use the `onProgress` callback to show real-time progress without full streaming: ```typescript theme={null} const result = await ai.extractFromProse( { sourceText: longReport, }, (progress) => { console.log(progress.message); } ); ``` *** ## Streaming For full control over the event stream: ```typescript theme={null} import { isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk'; const stream = await ai.extractFromProseStream({ sourceText: longReport, }); for await (const event of stream) { if (isProgressEvent(event)) { console.log(event.message); } if (isCompleteEvent(event)) { console.log('Result:', event.data.config); } if (isErrorEvent(event)) { console.error('Error:', event.error); } } ``` See [Streaming](/agents/sdk/streaming) for cancellation and React patterns. *** ## Error Handling ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { const result = await ai.extractFromProse({ sourceText }); } catch (error) { if (isGraphyApiError(error)) { console.error('API error:', error.message); } } ``` See [Error Handling](/agents/sdk/errors) for retry behavior and error types. # Installation Source: https://docs.graphy.dev/agents/sdk/installation ## Package Installation ```bash npm theme={null} npm install @graphysdk/agents-sdk ``` ```bash yarn theme={null} yarn add @graphysdk/agents-sdk ``` ```bash pnpm theme={null} pnpm add @graphysdk/agents-sdk ``` No npm org token is required. The package publishes publicly. ## Import ```typescript theme={null} import { GraphyAiSdk, type GraphConfig } from '@graphysdk/agents-sdk'; ``` `GraphConfig` is the Agents wire type exported by the SDK. ## TypeScript Configuration The SDK is written in TypeScript and ships with type definitions. No additional `@types` packages are needed. Minimum TypeScript version: 4.7+ Recommended `tsconfig.json` settings: ```json theme={null} { "compilerOptions": { "moduleResolution": "bundler", "esModuleInterop": true, "strict": true } } ``` ## Environment Setup First, [create an API key in the Graphy console](/agents/api-keys). Then store it in an environment variable: ```bash theme={null} # .env GRAPHY_API_KEY=graphy_... ``` Access it in your code: ```typescript theme={null} const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); ``` > [!WARNING] > > Never commit API keys to version control. Use environment variables or a secrets manager. ## Verify Installation ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); // Check connectivity const health = await ai.ping(); console.log(health); // { ok: true, latency: 45 } ``` # Mutation Agent Source: https://docs.graphy.dev/agents/sdk/mutate Transform a chart's dataset with natural language. Send a `GraphConfig` and a prompt, receive a `GraphConfig` with the dataset reshaped — filtered, grouped, aggregated, derived, and sorted. ## Methods ### generateMutation Processes the request and returns the final result. Internally handles streaming and collects the response. ```typescript theme={null} async generateMutation( params: GenerateMutationParams, onProgress?: (event: ProgressEvent) => void, signal?: AbortSignal ): Promise ``` ### generateMutationStream Returns an async iterator that yields events as they arrive. ```typescript theme={null} async generateMutationStream( params: GenerateMutationParams, signal?: AbortSignal ): Promise>> ``` ## Parameters ```typescript theme={null} interface GenerateMutationParams { config: GraphConfig; userPrompt?: string; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The current chart configuration, including the dataset to transform | | `userPrompt` | `string` | No | Natural language description of the transformation to apply | | `metadata` | `Metadata` | No | Request tracking information | ## Response ```typescript theme={null} interface GenerateMutationResponse { response: { message: string; steps: string[]; }; config: GraphConfig; } ``` | Field | Type | Description | | - | - | - | | `response.message` | `string` | Explanation of the transformations applied | | `response.steps` | `string[]` | Ordered breakdown of each transformation step | | `config` | `GraphConfig` | Updated chart configuration with the transformed dataset | The response is validated with Zod. Invalid responses throw an error. *** ## Basic Usage ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; import type { GraphConfig } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'region', label: 'Region' }, { key: 'sales', label: 'Sales' }, ], rows: [ { region: 'North', sales: 100 }, { region: 'South', sales: 80 }, { region: 'North', sales: 120 }, { region: 'South', sales: 95 }, ], }, }; const result = await ai.generateMutation({ config, userPrompt: 'Total sales by region, sorted highest first', }); console.log(result.config); // GraphConfig with the aggregated dataset console.log(result.response.steps); // Ordered list of transformations applied ``` *** ## With Progress Callback Use the `onProgress` callback to show real-time progress without full streaming: ```typescript theme={null} const result = await ai.generateMutation( { config, userPrompt: 'Keep only the last 12 months and add a 3-month moving average', }, (progress) => { console.log(progress.message); } ); ``` *** ## Streaming For full control over the event stream: ```typescript theme={null} import { isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk'; const stream = await ai.generateMutationStream({ config, userPrompt: 'Filter to the EMEA region and rank by revenue', }); for await (const event of stream) { if (isProgressEvent(event)) { console.log(event.message); } if (isCompleteEvent(event)) { console.log('Result:', event.data.config); } if (isErrorEvent(event)) { console.error('Error:', event.error); } } ``` See [Streaming](/agents/sdk/streaming) for cancellation and React patterns. *** ## Error Handling ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { const result = await ai.generateMutation({ config, userPrompt: 'Group sales by region', }); } catch (error) { if (isGraphyApiError(error)) { console.error('API error:', error.message); } } ``` See [Error Handling](/agents/sdk/errors) for retry behavior and error types. # Narrative Agent Source: https://docs.graphy.dev/agents/sdk/narrate Generate a chart's title, subtitle, and caption with natural language. Send a `GraphConfig` and a prompt, receive a `GraphConfig` with narrative text written into `config.content`. ## Methods ### generateNarrative Processes the request and returns the final result. Internally handles streaming and collects the response. ```typescript theme={null} async generateNarrative( params: GenerateNarrativeParams, onProgress?: (event: ProgressEvent) => void, signal?: AbortSignal ): Promise ``` ### generateNarrativeStream Returns an async iterator that yields events as they arrive. ```typescript theme={null} async generateNarrativeStream( params: GenerateNarrativeParams, signal?: AbortSignal ): Promise>> ``` ## Parameters ```typescript theme={null} interface GenerateNarrativeParams { config: GraphConfig; userPrompt: string; metadata?: Metadata; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The chart configuration to narrate | | `userPrompt` | `string` | Yes | Natural language instruction for the narrative — tone, focus, or refinements | | `metadata` | `Metadata` | No | Request tracking information | ## Response ```typescript theme={null} interface GenerateNarrativeResponse { response: { message: string; }; config: GraphConfig; } ``` | Field | Type | Description | | - | - | - | | `response.message` | `string` | Explanation of the narrative produced | | `config` | `GraphConfig` | Updated chart configuration. Title, subtitle, and caption are written into `config.content` as TipTap JSON documents. | The response is validated with Zod. Invalid responses throw an error. Which fields are written — title only, title and caption, or all three — depends on the storytelling level. See [Invocation effort and chart narrative](/agents/sdk/chart-maker#invocation-effort-and-chart-narrative) for how `metadata.effort` controls this. *** ## Basic Usage ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; import type { GraphConfig } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const config: GraphConfig = { type: 'line', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'users', label: 'Active Users' }, ], rows: [ { month: 'Jan', users: 1200 }, { month: 'Feb', users: 1800 }, { month: 'Mar', users: 3100 }, ], }, }; const result = await ai.generateNarrative({ config, userPrompt: 'Write a punchy title and a one-line caption', }); // Narrative is embedded in the config content as TipTap JSON console.log(result.config.content?.title); console.log(result.config.content?.caption); ``` *** ## With Progress Callback Use the `onProgress` callback to show real-time progress without full streaming: ```typescript theme={null} const result = await ai.generateNarrative( { config, userPrompt: 'Make the title more specific and lead with the headline number', }, (progress) => { console.log(progress.message); } ); ``` *** ## Streaming For full control over the event stream: ```typescript theme={null} import { isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk'; const stream = await ai.generateNarrativeStream({ config, userPrompt: 'Write a title, subtitle, and caption', }); for await (const event of stream) { if (isProgressEvent(event)) { console.log(event.message); } if (isCompleteEvent(event)) { console.log('Result:', event.data.config); } if (isErrorEvent(event)) { console.error('Error:', event.error); } } ``` See [Streaming](/agents/sdk/streaming) for cancellation and React patterns. *** ## Error Handling ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { const result = await ai.generateNarrative({ config, userPrompt: 'Write a title for this chart', }); } catch (error) { if (isGraphyApiError(error)) { console.error('API error:', error.message); } } ``` See [Error Handling](/agents/sdk/errors) for retry behavior and error types. # Streaming Source: https://docs.graphy.dev/agents/sdk/streaming AI operations take time. Streaming shows real-time progress as the agent works. ## Event Types ```typescript theme={null} type SSEEvent = ProgressEvent | CompleteEvent | ErrorEvent | PreviewEvent | ReasoningEvent; ``` Every streaming method returns the same `SSEEvent` union, so the patterns below work for all of them. ### ProgressEvent A human-readable progress update. `message` is always set; the agent-loop fields are present on events emitted from inside an agent. ```typescript theme={null} interface ProgressEvent { type: 'progress'; message: string; agentId?: string; executionId?: string; iteration?: number; metadata?: Record; } ``` ### CompleteEvent ```typescript theme={null} interface CompleteEvent { type: 'complete'; data: T; } ``` The final event on a successful stream. `data` is the response type of the method you called — for example, a `generateGraphStream()` stream completes with a [GenerateGraphResponse](/agents/reference/types#generategraphresponse). ### ErrorEvent ```typescript theme={null} interface ErrorEvent { type: 'error'; error: string; code?: string; retryable?: boolean; } ``` ### PreviewEvent An incremental preview of the agent's work-in-progress result. The payload is spread at the top level, and its shape depends on the agent — [Mutation](/agents/sdk/mutate) previews carry `config`. ```typescript theme={null} interface PreviewEvent { type: 'preview'; [key: string]: unknown; } ``` ### ReasoningEvent A reasoning message emitted by an agent as it works — useful for surfacing the agent's thinking in a UI. ```typescript theme={null} interface ReasoningEvent { type: 'reasoning'; agentId: string; executionId: string; message: string; } ``` *** ## Async Iterator Pattern `generateGraphStream()` returns an async iterator: ```typescript theme={null} import { isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk'; const stream = await ai.generateGraphStream({ config, userPrompt: 'Add a trend line', }); for await (const event of stream) { if (isProgressEvent(event)) { console.log(event.message); } if (isCompleteEvent(event)) { return event.data.config; } if (isErrorEvent(event)) { throw new Error(event.error); } } ``` *** ## Preview Events Some agents emit `preview` events carrying partial results before the stream completes. Render them to show work-in-progress. ```typescript theme={null} import { isPreviewEvent, isCompleteEvent } from '@graphysdk/agents-sdk'; const stream = await ai.generateMutationStream({ config, userPrompt: 'Group sales by region', }); for await (const event of stream) { if (isPreviewEvent(event)) { // Payload shape depends on the agent — mutation previews carry `config` console.log('Preview:', event.config); } if (isCompleteEvent(event)) { console.log('Final:', event.data.config); } } ``` The fields on a preview event depend on the agent — see each agent's page for what it streams. Events you do not handle can be safely ignored. *** ## Cancellation Pass an `AbortSignal` to cancel mid-operation: ```typescript theme={null} import { isCompleteEvent } from '@graphysdk/agents-sdk'; const controller = new AbortController(); const stream = await ai.generateGraphStream({ config, userPrompt: 'Create a complex heatmap' }, controller.signal); // Cancel from a button click cancelButton.onclick = () => controller.abort(); try { for await (const event of stream) { if (isCompleteEvent(event)) { return event.data; } } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.log('Cancelled by user'); return; } throw error; } ``` *** ## Progress Callback Alternative If you want progress updates without the streaming API, use the `onProgress` callback with `generateGraph()`: ```typescript theme={null} const result = await ai.generateGraph({ config, userPrompt: 'Add a trend line' }, (progress) => { setStatus(progress.message); }); // result contains the final GenerateGraphResponse ``` This collects the stream internally and returns the final result. *** ## React Pattern Store the abort controller in a ref for proper cleanup: ```tsx theme={null} import { useState, useRef, useEffect } from 'react'; import { GraphyAiSdk, isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk'; import type { GraphConfig } from '@graphysdk/agents-sdk'; interface Props { initialConfig: GraphConfig; onUpdate: (config: GraphConfig) => void; } function ChartEditor({ initialConfig, onUpdate }: Props) { const [status, setStatus] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const abortRef = useRef(null); const ai = useRef( new GraphyAiSdk({ apiKey: process.env.NEXT_PUBLIC_GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }) ).current; const handleGenerate = async (prompt: string) => { // Cancel any in-flight request abortRef.current?.abort(); abortRef.current = new AbortController(); setIsLoading(true); setStatus(''); setError(null); try { const stream = await ai.generateGraphStream( { config: initialConfig, userPrompt: prompt }, abortRef.current.signal ); for await (const event of stream) { if (isProgressEvent(event)) { setStatus(event.message); } if (isCompleteEvent(event)) { onUpdate(event.data.config); } if (isErrorEvent(event)) { setError(event.error); } } } catch (err) { if (err instanceof Error && err.name !== 'AbortError') { setError(err.message); } } finally { setIsLoading(false); } }; // Cleanup on unmount useEffect(() => { return () => abortRef.current?.abort(); }, []); return (
{isLoading && (
{status}
)} {error &&
{error}
}
); } ``` **Key patterns:** 1. **Cancel previous requests** — Abort any in-flight request before starting a new one 2. **Store in ref** — The abort controller persists across renders 3. **Cleanup on unmount** — Cancel pending requests when the component unmounts 4. **Handle AbortError** — Don't treat user cancellation as an error `ProgressEvent` carries a `message`, not a numeric percentage — show the latest message as a status line alongside an indeterminate progress indicator. *** ## Type Guards The SDK exports `isProgressEvent()`, `isCompleteEvent()`, `isErrorEvent()`, `isPreviewEvent()`, and `isReasoningEvent()` for narrowing SSE events, and `isGraphyApiError()` for narrowing caught errors. All examples on this page use these type guards. See the [Type Reference](/agents/reference/types) for full signatures. *** ## Error Handling in Streams Errors can come from two sources: 1. **Error events** — The API returns an error during processing 2. **Exceptions** — Network failure, timeout, or abort ```typescript theme={null} import { isErrorEvent, isCompleteEvent, isGraphyApiError } from '@graphysdk/agents-sdk'; try { const stream = await ai.generateGraphStream({ config, userPrompt }); for await (const event of stream) { if (isErrorEvent(event)) { // API-level error if (event.retryable) { // Offer retry option } throw new Error(event.error); } if (isCompleteEvent(event)) { return event.data; } } } catch (error) { if (error instanceof Error && error.name === 'AbortError') { // User cancelled return; } // Network or other error console.error('Stream failed:', error); } ``` See [Error Handling](/agents/sdk/errors) for details on error codes and retry behavior. # Suggestions Agent Source: https://docs.graphy.dev/agents/sdk/suggestions Generate chart type and data preparation suggestions from a dataset. Send a `GraphConfig` and a prompt, receive a list of suggested visualizations. ## Methods ### generateSuggestions Processes the request and returns the final result. Internally handles streaming and collects the response. ```typescript theme={null} async generateSuggestions( params: GenerateGraphSuggestionsParams, onProgress?: (event: ProgressEvent) => void, signal?: AbortSignal ): Promise ``` ### generateSuggestionsStream Returns an async iterator that yields events as they arrive. ```typescript theme={null} async generateSuggestionsStream( params: GenerateGraphSuggestionsParams, signal?: AbortSignal ): Promise>> ``` ## Parameters ```typescript theme={null} interface GenerateGraphSuggestionsParams { config: GraphConfig; userPrompt?: string; metadata?: Metadata; maxSuggestionCount?: 1 | 2 | 3 | 4; } ``` | Field | Type | Required | Description | | - | - | - | - | | `config` | `GraphConfig` | Yes | The chart configuration containing the dataset | | `userPrompt` | `string` | No | Natural language description of what to visualize | | `metadata` | `Metadata` | No | Request tracking information | | `maxSuggestionCount` | `1` \| `2` \| `3` \| `4` | No | Optional cap (1–4) passed to the model for how many suggestions to aim for. Omitted uses the default (4). Responses are not truncated if longer. | ## Response ```typescript theme={null} interface GenerateGraphSuggestionsResponse { suggestions: Suggestion[]; } ``` | Field | Type | Description | | - | - | - | | `suggestions` | `Suggestion[]` | List of chart type suggestions | ### Suggestion ```typescript theme={null} interface Suggestion { dataPrepPrompt: string; chartType: AiChartType; summary: string; } ``` | Field | Type | Description | | - | - | - | | `dataPrepPrompt` | `string` | Prompt describing how to prepare the data for this chart | | `chartType` | `AiChartType` | Suggested chart type (see [AiChartType](/agents/reference/types#aicharttype)) | | `summary` | `string` | Short description of what the chart would show | The response is validated with Zod. Invalid responses throw an error. *** ## Basic Usage ```typescript theme={null} import { GraphyAiSdk } from '@graphysdk/agents-sdk'; import type { GraphConfig } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, { key: 'region', label: 'Region' }, ], rows: [ { month: 'Jan', sales: 100, region: 'North' }, { month: 'Feb', sales: 120, region: 'South' }, { month: 'Mar', sales: 115, region: 'North' }, ], }, }; const result = await ai.generateSuggestions({ config, userPrompt: 'Show me interesting trends', }); for (const suggestion of result.suggestions) { console.log(`${suggestion.chartType}: ${suggestion.summary}`); } ``` *** ## With Progress Callback Use the `onProgress` callback to show real-time progress: ```typescript theme={null} const result = await ai.generateSuggestions( { config, userPrompt: 'What charts would best show this data?', }, (progress) => { console.log(progress.message); } ); ``` *** ## Streaming For full control over the event stream: ```typescript theme={null} import { isProgressEvent, isCompleteEvent, isErrorEvent } from '@graphysdk/agents-sdk'; const stream = await ai.generateSuggestionsStream({ config, userPrompt: 'Suggest charts for sales analysis', }); for await (const event of stream) { if (isProgressEvent(event)) { console.log(event.message); } if (isCompleteEvent(event)) { console.log('Suggestions:', event.data.suggestions); } if (isErrorEvent(event)) { console.error('Error:', event.error); } } ``` See [Streaming](/agents/sdk/streaming) for cancellation and React patterns. *** ## Error Handling ```typescript theme={null} import { isGraphyApiError } from '@graphysdk/agents-sdk'; try { const result = await ai.generateSuggestions({ config, userPrompt: 'Suggest charts', }); } catch (error) { if (isGraphyApiError(error)) { console.error('API error:', error.message); } } ``` See [Error Handling](/agents/sdk/errors) for retry behavior and error types. # Version management Source: https://docs.graphy.dev/agents/versioning ## What the API version covers The API uses URL-path versioning (`/api/v0/`, `/api/v1/`, etc.). Specifically, the API version covers: * **Endpoints** - available routes and their HTTP methods * **SSE envelope** - event types (`progress`, `complete`, `error`) and their shapes * **Authentication** - how API keys are passed and validated * **Error codes** - error response structure and codes * **Request envelope** - top-level request fields (`userPrompt`, `metadata`) * **GraphConfig schema** - breaking changes to `GraphConfig` trigger a new API major version ## GraphConfig is the data contract `GraphConfig` is the central type that flows through the Agents system. The wire type ships in `@graphysdk/agents-sdk`: ```mermaid theme={null} flowchart LR A["@graphysdk/agents-sdk"] -->|defines wire| B["GraphConfig"] B -->|sent by| C["GraphyAiSdk"] C -->|over HTTP| D["Agents API"] D -->|returns| B B -->|optional render| E["@graphysdk/react"] ``` Since the Agents API **generates** `GraphConfig`, breaking changes to its shape require a new API major version. Keep the TypeScript SDK on a compatible major with the API path you call (today: SDK 1.x with API `v0`). Non-breaking additions to the wire `GraphConfig` (new optional fields, new chart types) ship as `@graphysdk/agents-sdk` minor releases and do not require an API version change. ## Current version > [!NOTE] > > The API is on `v0`. Breaking changes may occur. Use the TypeScript SDK for the most stable integration. | API version | Status | | - | - | | `v0` | Active, pre-stable. May change. | | `v1` | Planned. Stable, backward-compatible within major. | When `v1` ships, the same guarantees as the SDK packages apply: no breaking changes within a major version, with a deprecation cycle before removal. ## Keeping the SDK up to date Install or upgrade the Agents client alone: ```bash npm theme={null} npm install @graphysdk/agents-sdk@latest ``` ```bash yarn theme={null} yarn add @graphysdk/agents-sdk@latest ``` ```bash pnpm theme={null} pnpm add @graphysdk/agents-sdk@latest ``` Rendering is separate from the Agents client — use `@graphysdk/react` when you want to display charts. ## When does what change? | Change | What bumps | API version change? | | - | - | - | | New chart type or config option | `@graphysdk/agents-sdk` minor | No | | New optional field on `GraphConfig` | `@graphysdk/agents-sdk` minor | No | | Breaking `GraphConfig` change | `@graphysdk/agents-sdk` major | Yes (major bump) | | New SSE event type | Backwards-compatible addition | No | | New endpoint added | Backwards-compatible addition | No | | Endpoint renamed or removed | API major | Yes (major bump) | | Auth mechanism change | API major | Yes (major bump) | ## REST API consumers If you integrate directly with the REST API (without the TypeScript SDK), pin to a specific API version in your URLs and test before upgrading: ``` https://agents.graphy.dev/api/v0/generate ``` The TypeScript SDK handles API version targeting internally. When a new API version is available, updating the SDK is enough. You do not need to change URLs in your code. # Buffer Source: https://docs.graphy.dev/data-import-utils/buffer Parse a binary buffer (XLSX, XLS, or ODS) into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. ## fromBuffer ```typescript theme={null} import { fromBuffer } from '@graphysdk/data-import-utils/buffer'; const data = await fromBuffer(xlsxBuffer, 'xlsx'); const data = await fromBuffer(odsBuffer, 'ods', { sheet: 'Revenue' }); ``` ### Signature ```typescript theme={null} function fromBuffer( input: ArrayBuffer, format: 'xlsx' | 'xls' | 'ods', options?: SpreadsheetParseOptions ): Promise ``` ## Options **format** `BinaryFileFormat` *required* The binary format: `'xlsx'`, `'xls'`, or `'ods'`. **options.sheet** `string | number` (default: 0) Sheet to parse — name (string) or 0-based index (number). **options.locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. **options.maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. **options.maxRows** `number` (default: 100000) Maximum number of data rows to process. **options.maxCells** `number` (default: 5000000) Maximum total cells to process. ## Examples ### Parse an uploaded spreadsheet ```typescript theme={null} import { fromBuffer } from '@graphysdk/data-import-utils/buffer'; const file: File = input.files[0]; const buffer = await file.arrayBuffer(); const data = await fromBuffer(buffer, 'xlsx'); ``` ### Select a specific sheet ```typescript theme={null} import { fromBuffer } from '@graphysdk/data-import-utils/buffer'; const data = await fromBuffer(buffer, 'xlsx', { sheet: 'Revenue' }); ``` # CSV Source: https://docs.graphy.dev/data-import-utils/csv Parse CSV strings into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. ## fromCSV ```typescript theme={null} import { fromCSV } from '@graphysdk/data-import-utils/csv'; const data = fromCSV('Name,Revenue\nAcme,1000\nGlobex,2000'); const data = fromCSV('Name;Revenue\nAcme;1000\nGlobex;2000'); // semicolon-delimited ``` ### Signature ```typescript theme={null} function fromCSV(input: string, options?: DelimitedParseOptions): Data ``` ## Options **hasHeader** `boolean` (default: true) Whether the first row contains column headers. When `false`, columns are auto-named `Column 1`, `Column 2`, etc. **locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. For example, `'PT_PT'` treats `1.000,50` as `1000.5`. **maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. Throws an error if the input exceeds this limit. ## Examples ### Without Headers ```typescript theme={null} const data = fromCSV('Acme,1000\nGlobex,2000', { hasHeader: false }); // Columns auto-named: "Column 1", "Column 2" ``` ### European Number Format (semicolon-delimited) ```typescript theme={null} const csv = `Produto;Receita Widgets;1.234,56 Gadgets;7.890,12`; const data = fromCSV(csv, { locale: 'PT_PT' }); // Receita values parsed as 1234.56 and 7890.12 ``` > [!NOTE] > > The CSV delimiter is auto-detected (comma, semicolon, pipe, etc.). For > tab-separated data, you can also use [`fromTSV`](/data-import-utils/tsv). # Examples Source: https://docs.graphy.dev/data-import-utils/examples ## With the Agents SDK Parse a data file and generate a chart with the [Agents SDK](/agents/sdk/installation). ### From a File Path (Node.js) ```typescript theme={null} import { fromFile } from '@graphysdk/data-import-utils/file'; import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const data = await fromFile('quarterly-sales.csv'); const result = await ai.generateGraph({ config: { data }, userPrompt: 'line chart showing quarterly trends', }); ``` ### From a File Upload (Browser) ```typescript theme={null} import { fromXLSX } from '@graphysdk/data-import-utils/xlsx'; import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const input = document.querySelector('#file-input'); input.addEventListener('change', async () => { const file = input.files?.[0]; if (!file) return; const buffer = await file.arrayBuffer(); const data = await fromXLSX(buffer); const result = await ai.generateGraph({ config: { data }, userPrompt: 'chart from uploaded spreadsheet', }); }); ``` ## With the Graph Component Parse a data file and render it with the [`` component](/sdk/reference/graph). ### From a File Upload ```tsx theme={null} import { useState } from 'react'; import { fromXLSX } from '@graphysdk/data-import-utils/xlsx'; import { GraphProvider, Graph } from '@graphysdk/core'; import type { GraphConfig } from '@graphysdk/core'; function ChartFromUpload() { const [config, setConfig] = useState(null); const handleFile = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const buffer = await file.arrayBuffer(); const data = await fromXLSX(buffer); setConfig({ data, graphType: 'column', }); }; return (
{config && ( )}
); } ``` ### From an API Response ```tsx theme={null} import { useState, useEffect } from 'react'; import { fromCSV } from '@graphysdk/data-import-utils/csv'; import { GraphProvider, Graph } from '@graphysdk/core'; import type { GraphConfig } from '@graphysdk/core'; function ChartFromAPI() { const [config, setConfig] = useState(null); useEffect(() => { fetch('/api/report.csv') .then((res) => res.text()) .then((csv) => { const data = fromCSV(csv); setConfig({ data, graphType: 'line' }); }); }, []); if (!config) return

Loading...

; return ( ); } ``` # File Source: https://docs.graphy.dev/data-import-utils/file Read a file from disk and parse it into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. The format is auto-detected from the file extension. ## fromFile ```typescript theme={null} import { fromFile } from '@graphysdk/data-import-utils/file'; const data = await fromFile('sales.csv'); const data = await fromFile('report.xlsx', { sheet: 'Revenue' }); ``` ### Signature ```typescript theme={null} function fromFile( filePath: string, options?: FileParseOptions ): Promise ``` `FileParseOptions` combines spreadsheet options with the `hasHeader` flag from delimited parsing. ## Options **filePath** `string` *required* Path to the file. The extension determines the format. **options.hasHeader** `boolean` (default: true) Whether the first row contains column headers (CSV/TSV only). When `false`, columns are auto-named `Column 1`, `Column 2`, etc. **options.sheet** `string | number` (default: 0) Sheet to parse (spreadsheets only). Pass a sheet name or 0-based index. **options.locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. **options.maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. Throws an error if the input exceeds this limit. **options.maxRows** `number` (default: 100000) Maximum number of data rows to process (spreadsheets only). **options.maxCells** `number` (default: 5000000) Maximum total cells to process (spreadsheets only). ## Supported Extensions | Extension | Format | | - | - | | `.csv` | CSV | | `.tsv`, `.tab` | TSV | | `.xlsx` | XLSX | | `.xls` | XLS | | `.ods` | ODS | Unsupported extensions throw an error listing the supported formats. ## Examples ### With the AI SDK ```typescript theme={null} import { fromFile } from '@graphysdk/data-import-utils/file'; import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const data = await fromFile('quarterly-sales.csv'); const result = await ai.generateGraph({ config: { data }, userPrompt: 'line chart showing quarterly trends', }); ``` ### Error Handling ```typescript theme={null} try { const data = await fromFile('data.pdf'); } catch (error) { // Error: Unsupported file extension ".pdf". Supported: .csv, .tsv, .tab, .xlsx, .xls, .ods } ``` > [!NOTE] > > `fromFile` uses Node.js `fs.readFile` under the hood and is not available in > the browser. For browser usage, use the format-specific parsers (`fromCSV`, > `fromXLSX`, etc.) after reading the file with the File API. # Quickstart Source: https://docs.graphy.dev/data-import-utils/index `@graphysdk/data-import-utils` converts CSV, TSV, and spreadsheet files into the `{ columns, rows }` data shape used by Graphy agents and chart configs. No `@graphysdk/core` dependency or npm org token is required. ## Installation ```bash npm theme={null} npm install @graphysdk/data-import-utils ``` ```bash yarn theme={null} yarn add @graphysdk/data-import-utils ``` ```bash pnpm theme={null} pnpm add @graphysdk/data-import-utils ``` ## Parse a CSV ```typescript theme={null} import { fromCSV } from '@graphysdk/data-import-utils'; const data = fromCSV('Name,Revenue\nAcme,1000\nGlobex,2000'); // { columns: [{ key: 'c1', label: 'Name' }, ...], rows: [...] } ``` ## Parse a Spreadsheet ```typescript theme={null} import { fromXLSX } from '@graphysdk/data-import-utils/xlsx'; const buffer = await file.arrayBuffer(); const data = await fromXLSX(buffer, { sheet: 'Revenue' }); ``` ## Parse from Disk (Node.js) ```typescript theme={null} import { fromFile } from '@graphysdk/data-import-utils/file'; const data = await fromFile('sales.csv'); ``` `fromFile` auto-detects the format from the file extension. See [File](/data-import-utils/file) for details. ## Parse from URL ```typescript theme={null} import { fromURL } from '@graphysdk/data-import-utils/url'; const data = await fromURL('https://example.com/sales.csv'); ``` `fromURL` fetches a remote file and auto-detects the format from the URL path extension. See [URL](/data-import-utils/url) for details. ## Features | Feature | Details | | - | - | | Formats | CSV, TSV, XLSX, XLS, ODS | | SSRF protection | `fromURL` blocks private IPs and non-http(s) schemes | | Timeout support | `fromURL` supports configurable timeout and `AbortSignal` | | Byte-budget enforcement | All parsers enforce a configurable `maxFileSize` limit; `fromURL` aborts streaming downloads early | | Row / cell limits | Spreadsheet parsers enforce `maxRows` (default 100k) and `maxCells` (default 5M) | ## Entrypoints ### Sources | Source | Function | Entrypoint | | - | - | - | | Local file | `fromFile()` | `@graphysdk/data-import-utils/file` | | Remote URL | `fromURL()` | `@graphysdk/data-import-utils/url` | | Text (CSV/TSV) | `fromText()` | `@graphysdk/data-import-utils/text` | | Binary (XLSX/XLS/ODS) | `fromBuffer()` | `@graphysdk/data-import-utils/buffer` | ### Formats | Format | Function | Input type | Entrypoint | | - | - | - | - | | CSV | `fromCSV()` | `string` | `@graphysdk/data-import-utils` (root) or `/csv` | | TSV | `fromTSV()` | `string` | `@graphysdk/data-import-utils` (root) or `/tsv` | | XLSX | `fromXLSX()` | `ArrayBuffer` | `@graphysdk/data-import-utils/xlsx` | | XLS | `fromXLS()` | `ArrayBuffer` | `@graphysdk/data-import-utils/xls` | | ODS | `fromODS()` | `ArrayBuffer` | `@graphysdk/data-import-utils/ods` | ## Licence `npm install` is not a commercial licence. Free Trial is 32 days, local or internal, and does not cover a public page. See [licence and evaluation](/licence/evaluation). # ODS Source: https://docs.graphy.dev/data-import-utils/ods Parse ODS (OpenDocument Spreadsheet) files into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. ## fromODS ```typescript theme={null} import { fromODS } from '@graphysdk/data-import-utils/ods'; const buffer = await file.arrayBuffer(); const data = await fromODS(buffer); ``` ### Signature ```typescript theme={null} function fromODS( input: ArrayBuffer, options?: SpreadsheetParseOptions ): Promise ``` ## Options **sheet** `string | number` (default: 0) Sheet to parse. Pass a sheet name (`string`) or a 0-based index (`number`). Defaults to the first sheet. **locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. **maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. Throws an error if the input exceeds this limit. **maxRows** `number` (default: 100000) Maximum number of data rows to process. Throws an error if the sheet contains more rows. **maxCells** `number` (default: 5000000) Maximum total cells (rows x columns) to process. Throws an error if the limit is exceeded. ## Examples ### Select Sheet by Name ```typescript theme={null} const data = await fromODS(buffer, { sheet: 'Revenue' }); ``` ## Cell Value Handling | Cell type | Result | | - | - | | Number | `number` | | String | `string` | | Boolean | `1` or `0` | | Date | ISO 8601 string | | Formula | Computed result | | Rich text | Plain text | | Empty | `null` | > [!WARNING] > > `fromODS` uses the same XLSX-based parser under the hood. It works with > modern `.ods` files that are actually in XLSX format (common in LibreOffice). > Genuine native ODS format is not supported. # Text Source: https://docs.graphy.dev/data-import-utils/text Parse a text string (CSV or TSV) into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. ## fromText ```typescript theme={null} import { fromText } from '@graphysdk/data-import-utils/text'; const data = fromText(csvContent, 'csv'); const data = fromText(tsvContent, 'tsv', { hasHeader: false }); ``` ### Signature ```typescript theme={null} function fromText( input: string, format: 'csv' | 'tsv', options?: DelimitedParseOptions ): Data ``` ## Options **format** `TextFileFormat` *required* The text format: `'csv'` or `'tsv'`. **options.hasHeader** `boolean` (default: true) Whether the first row contains column headers. When `false`, columns are auto-named `Column 1`, `Column 2`, etc. **options.locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. **options.maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. ## Examples ### Parse CSV from memory ```typescript theme={null} import { fromText } from '@graphysdk/data-import-utils/text'; const csv = 'Name,Revenue\nAcme,1000\nGlobex,2000'; const data = fromText(csv, 'csv'); ``` ### Handle a browser text file upload ```typescript theme={null} import { fromText } from '@graphysdk/data-import-utils/text'; const file: File = input.files[0]; const text = await file.text(); const data = fromText(text, 'csv'); ``` # TSV Source: https://docs.graphy.dev/data-import-utils/tsv Parse TSV (tab-separated values) strings into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. ## fromTSV ```typescript theme={null} import { fromTSV } from '@graphysdk/data-import-utils/tsv'; const data = fromTSV('Name\tRevenue\nAcme\t1000\nGlobex\t2000'); ``` ### Signature ```typescript theme={null} function fromTSV(input: string, options?: DelimitedParseOptions): Data ``` ## Options **hasHeader** `boolean` (default: true) Whether the first row contains column headers. When `false`, columns are auto-named `Column 1`, `Column 2`, etc. **locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. For example, `'PT_PT'` treats `1.000,50` as `1000.5`. **maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. Throws an error if the input exceeds this limit. ## Examples ### Without Headers ```typescript theme={null} const data = fromTSV('Acme\t1000\nGlobex\t2000', { hasHeader: false }); // Columns auto-named: "Column 1", "Column 2" ``` > [!NOTE] > > Files with `.tab` extension are also treated as TSV. # URL Source: https://docs.graphy.dev/data-import-utils/url Fetch a remote file by URL and parse it into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. The format is auto-detected from the URL path extension. ## fromURL ```typescript theme={null} import { fromURL } from '@graphysdk/data-import-utils/url'; const data = await fromURL('https://example.com/sales.csv'); const data = await fromURL('https://example.com/report.xlsx', { sheet: 'Revenue' }); ``` ### Signature ```typescript theme={null} function fromURL( url: string, options?: URLParseOptions ): Promise ``` ## Options **url** `string` *required* URL to fetch. The path extension determines the format, falling back to the response `Content-Type` header. **options.headers** `Record\` Custom headers to include in the fetch request. Useful for authenticated endpoints (e.g. `{ Authorization: 'Bearer token' }`). **options.timeout** `number` (default: 30000) Fetch timeout in milliseconds. The request is aborted if it takes longer. **options.signal** `AbortSignal` Optional external abort signal for cancellation. Combined with the internal timeout signal via `AbortSignal.any`. **options.hasHeader** `boolean` (default: true) Whether the first row contains column headers (CSV/TSV only). When `false`, columns are auto-named `Column 1`, `Column 2`, etc. **options.sheet** `string | number` (default: 0) Sheet to parse (spreadsheets only). Pass a sheet name or 0-based index. **options.locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. **options.maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. The response body is streamed with a byte budget -- the download is aborted early if the limit is exceeded. **options.maxRows** `number` (default: 100000) Maximum number of data rows to process (spreadsheets only). **options.maxCells** `number` (default: 5000000) Maximum total cells to process (spreadsheets only). ## Supported Extensions | Extension | Format | | - | - | | `.csv` | CSV | | `.tsv`, `.tab` | TSV | | `.xlsx` | XLSX | | `.xls` | XLS | | `.ods` | ODS | Unsupported extensions throw an error listing the supported formats. ## Examples ### With the AI SDK ```typescript theme={null} import { fromURL } from '@graphysdk/data-import-utils/url'; import { GraphyAiSdk } from '@graphysdk/agents-sdk'; const ai = new GraphyAiSdk({ apiKey: process.env.GRAPHY_API_KEY, baseUrl: 'https://agents.graphy.dev', }); const data = await fromURL('https://data.example.com/quarterly-sales.csv'); const result = await ai.generateGraph({ config: { data }, userPrompt: 'line chart showing quarterly trends', }); ``` ### Error Handling ```typescript theme={null} try { const data = await fromURL('https://example.com/data.pdf'); } catch (error) { // Error: Unsupported file extension ".pdf". Supported: .csv, .tsv, .tab, .xlsx, .xls, .ods } ``` ### With Custom Headers ```typescript theme={null} const data = await fromURL('https://api.example.com/data.csv', { headers: { Authorization: 'Bearer my-token' }, }); ``` ### With Timeout and Cancellation ```typescript theme={null} const controller = new AbortController(); const data = await fromURL('https://example.com/large-file.xlsx', { timeout: 10_000, signal: controller.signal, }); ``` > [!WARNING] > > **SSRF Protection:** `fromURL` validates URLs before fetching. Private and > reserved IP addresses (e.g. `127.0.0.1`, `10.x.x.x`, `192.168.x.x`, > `169.254.x.x`, `localhost`, IPv6 loopback/link-local) are blocked, and only > `http:` and `https:` schemes are allowed. This prevents server-side request > forgery when URLs are provided by end users. > [!NOTE] > > `fromURL` uses the global `fetch` API and works in Node.js 18+ and all modern > browsers. For authenticated endpoints, you can either pass `headers` in the > options or fetch the data yourself and use the format-specific parsers > (`fromCSV`, `fromXLSX`, etc.) directly. # XLS Source: https://docs.graphy.dev/data-import-utils/xls Parse XLS spreadsheet files into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. ## fromXLS ```typescript theme={null} import { fromXLS } from '@graphysdk/data-import-utils/xls'; const buffer = await file.arrayBuffer(); const data = await fromXLS(buffer); ``` ### Signature ```typescript theme={null} function fromXLS( input: ArrayBuffer, options?: SpreadsheetParseOptions ): Promise ``` ## Options **sheet** `string | number` (default: 0) Sheet to parse. Pass a sheet name (`string`) or a 0-based index (`number`). Defaults to the first sheet. **locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. **maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. Throws an error if the input exceeds this limit. **maxRows** `number` (default: 100000) Maximum number of data rows to process. Throws an error if the sheet contains more rows. **maxCells** `number` (default: 5000000) Maximum total cells (rows x columns) to process. Throws an error if the limit is exceeded. ## Examples ### Select Sheet by Name ```typescript theme={null} const data = await fromXLS(buffer, { sheet: 'Revenue' }); ``` ## Cell Value Handling | Cell type | Result | | - | - | | Number | `number` | | String | `string` | | Boolean | `1` or `0` | | Date | ISO 8601 string | | Formula | Computed result | | Rich text | Plain text | | Empty | `null` | > [!WARNING] > > `fromXLS` uses the same XLSX-based parser under the hood. It works with > modern `.xls` files that are actually in XLSX format (common in Excel 2007+). > Genuine legacy binary XLS (BIFF) format is not supported. # XLSX Source: https://docs.graphy.dev/data-import-utils/xlsx Parse XLSX spreadsheet files into the `{ columns, rows }` [Data structure](/sdk/core/data-structure) used by Graphy agents and chart configs. ## fromXLSX ```typescript theme={null} import { fromXLSX } from '@graphysdk/data-import-utils/xlsx'; const buffer = await file.arrayBuffer(); const data = await fromXLSX(buffer); ``` ### Signature ```typescript theme={null} function fromXLSX( input: ArrayBuffer, options?: SpreadsheetParseOptions ): Promise ``` ## Options **sheet** `string | number` (default: 0) Sheet to parse. Pass a sheet name (`string`) or a 0-based index (`number`). Defaults to the first sheet. **locale** `VizLocale` (default: EN\_US) Locale for number parsing. Determines thousand/decimal separator conventions. **maxFileSize** `number` (default: 5) Maximum allowed input size in megabytes. Throws an error if the input exceeds this limit. **maxRows** `number` (default: 100000) Maximum number of data rows to process. Throws an error if the sheet contains more rows. **maxCells** `number` (default: 5000000) Maximum total cells (rows x columns) to process. Throws an error if the limit is exceeded. ## Examples ### Select Sheet by Name ```typescript theme={null} const data = await fromXLSX(buffer, { sheet: 'Revenue' }); ``` ### Select Sheet by Index ```typescript theme={null} const data = await fromXLSX(buffer, { sheet: 2 }); // Third sheet (0-indexed) ``` ## Cell Value Handling | Cell type | Result | | - | - | | Number | `number` | | String | `string` | | Boolean | `1` or `0` | | Date | ISO 8601 string | | Formula | Computed result | | Rich text | Plain text | | Empty | `null` | # Graphy Source: https://docs.graphy.dev/index Graphy gives you three ways to build and ship interactive charts. Pick the one that fits your project — or combine them. > **[Charting SDK](/sdk-next/quickstart)** > > Render interactive charts in React with a grammar of graphics — start with > `@graphysdk/react`. > **[Agents API](/agents/quickstart)** > > Generate and edit charts from natural language, via REST or the TypeScript > SDK. > **[Data Import](/data-import-utils/index)** > > Turn CSV, TSV, and spreadsheets into chart-ready datasets. ## New to Graphy? Start with the [Charting SDK quickstart](/sdk-next/quickstart) to render your first chart, or jump straight to the [Agents API](/agents/quickstart) to generate and edit charts with AI. > [!NOTE] > > Building with the AI agents? You'll need an API key — [create one in the > console](/agents/api-keys). The `@graphysdk/core` component is documented under [SDK (Classic)](/sdk/core/quickstart). # Licence & evaluation Source: https://docs.graphy.dev/licence/evaluation `npm install` is not a commercial licence. Every Graphy package ships three PolyForm grants. You use the one whose conditions you meet. **Free Trial is 32 consecutive calendar days of local or internal evaluation. It does not let you distribute the SDK.** A live public page needs Small Business, Noncommercial, or a commercial deal. > [!NOTE] > > This page is plain language. If it and the licence text disagree, the licence > in the package wins: > [Noncommercial](https://polyformproject.org/licenses/noncommercial/1.0.0), > [Small Business](https://polyformproject.org/licenses/small-business/1.0.0), > [Free Trial](https://polyformproject.org/licenses/free-trial/1.0.0). ## Can I put this chart on a public page? **Not under Free Trial.** Shipping the package in a public JS bundle is distributing a copy of the software. Free Trial forbids that. | You want to… | Free Trial | Who else can | | - | - | - | | Try it on your laptop | Yes, for 32 days | Any grant | | Run it on internal staging, not public | Yes, for 32 days | Small Business, Noncommercial, or commercial | | Put it on a public marketing page | No | Small Business, Noncommercial, or commercial | | Embed it in a SaaS product customers use | No | Small Business or commercial | | Republish or redistribute the SDK itself | No | Small Business or Noncommercial (keep the licence notice), or commercial | After day 32, Free Trial ends. Keep using the software only if you qualify for another grant, or contact [hello@graphy.dev](mailto:hello@graphy.dev). ## Which grant am I on? Pick one. You do not stack them. * **Noncommercial** — personal, charitable, educational, research, or government use. Not for commercial advantage. * **Small Business** — commercial use if your company has fewer than 100 people (employees and contractors) and under the revenue threshold in the Small Business licence (\$1,000,000 USD in 2019 dollars, CPI-adjusted). * **Free Trial** — everyone else, for 32 consecutive calendar days, locally or internally, with no right to distribute copies. * **Commercial** — beyond those terms. Email [hello@graphy.dev](mailto:hello@graphy.dev) or see [graphy.dev](https://graphy.dev). Questions about which grant applies: [hello@graphy.dev](mailto:hello@graphy.dev). # Commands & history Source: https://docs.graphy.dev/sdk-next/advanced/commands-and-history A **command** is a single, undoable edit to a chart's spec: "hide the y-axis", "widen the line", "move the legend to the bottom". Instead of rebuilding the spec and passing a new `spec`, you dispatch a command. The provider applies it, recompiles, and records it on an undo stack. Import command classes from `@graphysdk/react/editable` and hooks from `@graphysdk/react`. ```tsx theme={null} import { SetAxisVisibilityCommand } from '@graphysdk/react/editable'; import { useGraphCommands } from '@graphysdk/react'; function AxisToggle({ visible }: { visible: boolean }) { const { dispatch } = useGraphCommands(); return ( ); } ``` Every edit that reaches the chart goes through this one path: a toolbar button, an inline edit in `editable` mode, or a command streamed in from an agent. All of them land in the same history and all of them are undoable. ## Dispatching `useGraphCommands` returns `dispatch(command, options?)` and `commit()`. It must be called from inside a ``. A command is a plain object you construct with its params: ```tsx theme={null} dispatch( new SetStyleRuleCommand({ list: 'defaults', rule: style.geom.line({ strokeWidth: 3 }), }) ); dispatch(new SetScaleDomainCommand({ scaledAesthetic: 'y', domainMin: 0 })); dispatch(new SetLegendPositionCommand({ position: 'bottom' })); ``` Most commands that target a layer take an optional `layerId`. Omit it and the command finds the first layer of a compatible geom. That is enough for a single-layer chart; a combo chart should name the layer: ```tsx theme={null} dispatch( new SetLineCurveCommand({ layerId: 'trend', curve: 'smooth', }) ); ``` A layer's id comes from the spec. Set it when you build the layer, and every reference to it (commands, highlights, annotation anchors) can use that name: ```tsx theme={null} pipe( createSpec({ x: 'month' }), geom.bar({ id: 'bars' }), geom.line({ id: 'trend' }) ); ``` Leave `id` off and one is assigned at compile time. A layer always has a stable identity; you just don't choose the name. Four commands **require** `layerId`: `SetLineStyleCommand`, `SetPointSizeCommand` and `SetBarCornerRadiusCommand`, which write per-layer stylesheet entries and so need an address, plus `RemoveLayerCommand`, which needs to know what it is removing. Nothing is mutated. A command reads the current spec and returns a new one, so the provider swaps the compiled result in without touching the object you passed as `spec`. ### Persisting edits The provider's `onSpecChange` fires with the spec a command produced. Store that if edits need to survive a reload: ```tsx theme={null} ``` Handing that same value straight back as `spec` is a no-op: the provider recognizes the echo and does not recompile. ### Live gestures A slider or color picker that edits while it is held would otherwise leave one undo entry, and one `onSpecChange`, per frame. Pass `{ transient: true }` on those frames and call `commit()` when the gesture ends: ```tsx theme={null} import { SetStyleRuleCommand } from '@graphysdk/react/editable'; import { style, useGraphCommands } from '@graphysdk/react'; function CornerRadiusSlider({ cornerRadius }: { cornerRadius: number }) { const { dispatch, commit } = useGraphCommands(); return ( dispatch( new SetStyleRuleCommand({ list: 'overrides', rule: style.graph({ cornerRadius: event.target.valueAsNumber }), }), { transient: true } ) } onPointerUp={commit} onBlur={commit} /> ); } ``` A transient dispatch recompiles and repaints like any other, but it replaces the run's single undo entry instead of pushing a new one, and keeps the run's *oldest* revert. One undo therefore returns to before the gesture began, not to its previous frame. `onSpecChange` fires once, when `commit()` closes the run. A run covers one thing being edited: dispatches fold together only while they are the same command type against the same edit target. A different target, or a different command type on the same target, starts its own entry. So does a committed dispatch, an undo, a redo, or a new `spec`, `data` or color scheme from the host; each closes the open run first. Forgetting to `commit()` delays that gesture's `onSpecChange` until one of them arrives. It does not merge two gestures into one step. Dragging something across the canvas is a different shape: move it locally in pixels and dispatch one command on drop. Transient dispatch is for gestures whose feedback needs the spec to recompile. ## Undo & redo `useGraphHistory` gives you the controls plus the state a history UI reads. It subscribes to the stack, so a component using it re-renders whenever a step becomes available. ```tsx theme={null} import { useGraphHistory } from '@graphysdk/react'; function UndoRedoToolbar() { const { undo, redo, canUndo, canRedo, undoDescription, redoDescription } = useGraphHistory(); return ( <> ); } ``` **canUndo / canRedo** `boolean` Whether a step is available in each direction. **undoDescription / redoDescription** `string | null` Human-readable label for the next step, such as `"Set line width to 3"`, for a button tooltip or a menu item. `null` when there is nothing to step. **undoStack / redoStack** `CommandMetadata[]` The full history, for a history panel. Each entry carries `id`, `timestamp`, `description` and `author`. `undoStack` runs oldest first; `redoStack` runs in the order the steps were undone. In both, the *last* entry is next: the end of `undoStack` is what an undo reverses, the end of `redoStack` is what a redo re-applies. ### Keyboard shortcuts Shortcuts are opt-in. `useGraphHistoryShortcuts` binds them, driving the graph through a `handleRef`, so it can be called from wherever your key handling lives, including above the provider: ```tsx theme={null} import { useRef } from 'react'; import { type GraphHandle, useGraphHistoryShortcuts } from '@graphysdk/react'; function EditableChart() { const handleRef = useRef(null); useGraphHistoryShortcuts(handleRef); return ( ); } ``` It binds ⌘/Ctrl+Z, ⌘/Ctrl+Shift+Z and Ctrl+Y, and leaves alone any chord already handled by the app or typed into a text field. Pass `{ target }`, an element or a ref holding one, to scope it to a subtree instead of `window`; `null` binds nothing. Pass `{ enabled: false }` to unbind. A chord is only claimed when the chart has a step to take. With an empty history the keystroke is left uncancelled, so an undo your app owns elsewhere on the page still gets it. The same `handleRef` exposes `commands.dispatch`, `commands.commit`, `undo` and `redo`, for a toolbar or menu bar mounted outside the provider where the hooks can't reach, plus `getScene` and `subscribe` for reading the chart from there. `undo` and `redo` return whether the chart took the step; `false` is the signal to fall through to your own handling. The chart declines a step when there is nothing to step or when the older spec no longer compiles. ## When a command doesn't apply Two outcomes leave the chart exactly as it was: * **No-op**: the command found nothing to change. The value already matches, or the layer or scale it targets isn't in the spec. Nothing recompiles and nothing joins the history, so there is no empty step to undo past. * **Rejected**: the edit produced a spec that fails to compile. The last good chart stays on screen and the diagnostics go to the provider's `onError`. An edit the chart never took is not one you can undo, so the history is left untouched. Undo and redo are gated the same way. If restoring an older spec fails to compile, say because the data loaded since no longer has a column that spec referenced, the step is refused, neither stack moves, and `undo()` returns `false` so the keystroke falls through to your app. The step stays on the stack and applies once the data supports it again. The steps behind it are reachable only through it. ## History lifecycle The history belongs to the chart the provider is currently showing: * A **new `spec`** from the host is a new baseline and clears the history, once that input compiles. The stored steps restore values from a spec that no longer exists, and may target layers or scales the new one doesn't have. This is by reference, so a `spec` rebuilt on every render leaves nothing undoable; [keep it stable](/sdk-next/rendering/provider-and-renderer#keep-inputs-stable). * A **`spec` that fails to compile** keeps it. The chart behind the error panel is still the last good one, so an undo is the way back to something renderable. * **New `data`** or a **color scheme change** keeps it. The recorded steps still address the spec they were built from. The theme and the plugins are fixed at mount; switching either means remounting the provider with a new React `key`, which starts a fresh history. * History depth is capped at 100 steps; the oldest is dropped past that. ## Available commands > **All commands** > > Each takes its params as the first constructor argument and optional metadata as the second. Scale commands take `scaledAesthetic` to pick the scale by the aesthetic it drives (`'x'`, `'y'`, `'color'`, …). Axis commands take `axis`: `'x' | 'y'`. `SetAxisLabelCommand`, `SetAxisVisibilityCommand` and `SetAxisTickModeCommand` also accept `'ySecondary'`, and omitting their value there drops the override, so the secondary axis follows `y` again. On `y`, those two reach both vertical axes unless `keepSecondaryAxis: true` leaves the secondary axis as it is drawn. The two grid line-style commands accept `'both'` so a change across the whole grid is one undo entry. > > `AddLayerCommand`, `AddHighlightCommand` and `AddAnnotationCommand` separate what you construct with from what serializes: the constructor's `id` is optional and minted when omitted, while the serialized params carry it settled, so a deserialized command replays to the same spec instead of adding a second thing. > > `SequenceCommand({ commands })` takes live command instances and applies them as one undo step, in the order given, each against the spec the one before produced. It is about whatever its first command is about, and carries that command's description. A command that does nothing is skipped, and the sequence does nothing when together they leave the spec unchanged. It serializes like any other command, with its params holding the serialized list; a deserialized sequence with an empty list fails with `INVALID_SEQUENCE`. > > **Chart type** > > | Command | Params | > | - | - | > | `SetChartTypeCommand` | `{ coordType: 'cartesian' \| 'flip', geom, position }`, or `{ coordType: 'polar', geom, position, theta, innerRadius }` | > | `SetMappingCommand` | `aesthetic`, `variable: string \| null` | > > What a chart *is* (a column chart, a stacked bar, a donut) spans the coordinate system, every data layer and the scales those geoms need, so `SetChartTypeCommand` writes them together as one undo entry. `readChartType(spec)` reads it back for a picker, returning `null` for a combo chart no single option describes. Reference lines and fitted trends keep their own shape across the switch. > > `SetMappingCommand` points one aesthetic of the chart-wide mapping at a column (`null` unmaps it) and infers the scale that column needs. A layer's own `aes` is left alone. > > **Titles & content** > > | Command | Params | > | - | - | > | `SetContentTitleCommand` | `title` | > | `SetContentSubtitleCommand` | `subtitle` | > | `SetContentCaptionCommand` | `caption` | > | `SetContentSourceCommand` | `source` | > | `ToggleContentVisibilityCommand` | `part`, `visible` | > > **Axes & grid** > > | Command | Params | > | - | - | > | `SetAxisLabelCommand` | `axis`, `label` | > | `SetAxisPositionCommand` | `axis`, `position` | > | `SetAxisVisibilityCommand` | `axis`, `visible` | > | `SetAxisTicksVisibilityCommand` | `axis`, `visible` | > | `SetAxisTickModeCommand` | `axis`, `tickMode` | > | `SetGridVisibilityCommand` | `axis`, `visible: boolean \| null` (`null` defers to the compiler) | > | `SetGridLineStyleCommand` | `axis: 'x' \| 'y' \| 'both'`, `lineStyle?`, `strokeWidth?` (only those given are written; `null` defers to the stylesheet) | > > A saved `set-grid-line-width` command still loads, as `SetGridLineStyleCommand` with its `lineWidth` as `strokeWidth`. > > **Legend & headline** > > | Command | Params | > | - | - | > | `SetLegendPositionCommand` | `position` | > | `SetLegendAlignCommand` | `align` | > | `SetLegendPlacementCommand` | `position`, `align` | > | `SetLegendDisplayCommand` | `display` | > | `SetHeadlineShowCommand` | `show` | > | `SetHeadlinePositionCommand` | `position` | > | `SetHeadlineSizeCommand` | `size` | > | `SetHeadlineCompareWithCommand` | `compareWith` | > > **Appearance & formatting** > > | Command | Params | > | - | - | > | `SetHighlightDimStyleCommand` | `dimStyle: 'dim' \| 'desaturate'` | > | `SetNumberFormatDecimalsCommand` | `decimals` | > | `SetNumberFormatAbbreviationCommand` | `abbreviation` | > > Paint, including the graph's background, border, corner radius and text scale, is a stylesheet edit rather than a config one: use `SetStyleRuleCommand` with `style.graph({ … })`. See [Styling](/sdk-next/config/styling) for the vocabulary. > > **Coordinates** > > | Command | Params | > | - | - | > | `SetCoordLimitsCommand` | `xLimits`, `yLimits` | > | `SetPolarInnerRadiusCommand` | `innerRadius` | > | `SetPolarStartAngleCommand` | `startAngle` | > > **Layers** > > | Command | Params | > | - | - | > | `AddLayerCommand` | `layer`, `index?` | > | `RemoveLayerCommand` | `layerId` | > | `SetLayerPositionCommand` | `layerId?`, `position` | > | `SetLayerStatCommand` | `layerId?`, `stat` (a name such as `'count'`, an object such as `stat.count()`, or a plugin stat) | > | `SetLayerYScaleTypeCommand` | `layerId?`, `yScaleType` | > | `SetLineCurveCommand` | `layerId?`, `curve` | > | `SetLineMissingValuesCommand` | `layerId?`, `missingValues` | > | `ToggleLinePointsCommand` | `layerId?`, `showPoints`, `where?`, `removedLayer?` | > | `SetLineStyleCommand` | `layerId`, `where?`, `lineStyle?`, `strokeWidth?: number \| null`, `showFill?` | > | `SetPointSizeCommand` | `layerId`, `size: number \| null`, `where?` | > | `SetBarWidthCommand` | `layerId?`, `width?` | > | `SetBarCornerRadiusCommand` | `layerId`, `cornerRadius: BorderRadiusToken \| number \| null` | > | `SetRuleValueCommand` | `layerId?`, `value` | > | `SetRuleLabelCommand` | `layerId?`, `label` | > | `ToggleGoalLineCommand` | `showGoalLine`, `layerId?`, `value?`, `label?`, `removedLayer?` | > | `SetStatLineCommand` | `line: 'none' \| 'trend' \| 'average'`, `layerId?`, `method?`, `group?`, `label?`, `removedLayer?` | > | `ToggleDataLabelsCommand` | `layerId?`, `showDataLabels` | > | `SetDataLabelsFormatCommand` | `layerId?`, `format` | > | `ToggleCategoryLabelsCommand` | `layerId?`, `showCategoryLabels` | > | `ToggleStackTotalsCommand` | `layerId?`, `showStackTotals` | > > `AddLayerCommand` and `RemoveLayerCommand` are the two directions of every "show a trend line" or "add a layer" toggle, so `AddLayerCommand` carries a whole layer rather than a per-feature flag. The toggles that remove a layer of their own (`ToggleLinePointsCommand`, `ToggleGoalLineCommand`, `SetStatLineCommand`) carry a `removedLayer` for the same reason: it lets undo bring back the layer's own settings and identity rather than a fresh one. > > `SetLineStyleCommand`, `SetPointSizeCommand`, `SetBarWidthCommand` and `SetBarCornerRadiusCommand` are typed shortcuts over the stylesheet. Each writes a `{ layer }`-scoped entry through `SetStyleRuleCommand`, merging into whatever else that layer's entry paints. `SetPointSizeCommand` and `SetLineStyleCommand` take an optional `where`, which writes that paint on one group instead of the whole layer. Without `where`, the paint also leaves every group's rule on that layer, so each group takes the layer's value. > > `ToggleLinePointsCommand` takes a `where` too. One point layer draws the dots of every group, so a group's points hide by drawing that group at `size: 0`. Hiding the last group that draws dots removes the point layer, and showing points without a `where` takes every size 0 away. `SetPointSizeCommand` without a `where` leaves a group at size 0 hidden, and while the layer itself draws at size 0 it sizes only the groups shown on top of it. > > `SetLineStyleCommand` sets a line's or an area's dash, stroke width and fill as one undo step, writing only the fields you pass. `lineStyle` is solid, dashed, or dotted; `strokeWidth: null` hands the width back to the stylesheet; `showFill` draws the wash beneath a line, and an area ignores it. Without `where`, the fields you pass also leave every group's rule on that layer, so the whole path takes the new value. Saved commands of the removed `set-line-width`, `toggle-line-fill` and `set-observations-dash` types still load and apply as `SetLineStyleCommand`; a saved fill's `fillAlpha` is not carried. > > ```tsx theme={null} > dispatch( > new SetLineStyleCommand({ > layerId: 'trend', > lineStyle: 'dashed', > strokeWidth: 3, > }) > ); > dispatch( > new SetLineStyleCommand({ > layerId: 'trend', > where: { variable: 'region', eq: 'EU' }, > showFill: true, > }) > ); > ``` > > **Styles** > > | Command | Params | > | - | - | > | `SetStyleRuleCommand` | `list`, `rule`, `index?` | > | `SetObservationsColorCommand` | `target`, `color: string \| null` | > > `SetStyleRuleCommand` is the one command for paint (widths, dashes, opacities, colors beyond the palette). An entry is addressed **structurally**: the rule's own `select` and `when` say which entry it is, so the command writes whatever paints those elements under those conditions, whoever authored it. An entry's `id` travels inside the rule, through the builder's options bag: `style.geom.line({ strokeWidth: 3 }, { id: 'emphasis' })`. > > ```tsx theme={null} > // insert or replace > dispatch( > new SetStyleRuleCommand({ > list: 'overrides', > rule: style.geom.line({ strokeWidth: 3 }), > }) > ); > > // remove every entry at that address > dispatch( > new SetStyleRuleCommand({ > list: 'overrides', > rule: { select: { target: 'geom', kind: 'line' }, declarations: null }, > }) > ); > ``` > > Declarations at an address the list already holds replace that entry's, keeping its position in the cascade. `declarations: null` removes every entry at the address; an address the list doesn't hold is a no-op. See [Styling](/sdk-next/config/styling) for the targets and the vocabulary each one takes. > > `SetObservationsColorCommand` colors a selection of observations as one undo step, by writing the style rules that selection needs. `target` is an `observations` edit target, `{ kind: 'observations', layerId, scope, predicate }`. `color` is a hex string, or `null` to remove the color. > **Scales** > > | Command | Params | > | - | - | > | `SetScaleDomainCommand` | `scaledAesthetic`, `domainMin`, `domainMax` | > | `SetScaleRangeCommand` | `scaledAesthetic`, `range` | > | `SetScaleTransformCommand` | `scaledAesthetic`, `transform` | > | `SetScaleReverseCommand` | `scaledAesthetic`, `reverse` | > > `SetScaleRangeCommand` sets the values a discrete scale hands its groups in order, such as the colors of a color scale. `range: null` hands a color scale back to the theme's categorical colors. > > **Highlights** > > | Command | Params | > | - | - | > | `AddHighlightCommand` | `predicate`, `id?`, `scope?`, `layerId?`, `index?` | > | `RemoveHighlightCommand` | `id` | > > **Annotations** > > | Command | Params | > | - | - | > | `AddAnnotationCommand` | `kind`, `annotation`, `index?` | > | `RemoveAnnotationCommand` | `id` | > | `UpdateAnnotationCommand` | `kind`, `id`, `patch` | > | `MoveAnnotationCommand` | `id`, `translation` | > | `SetDifferenceArrowStyleCommand` | `annotationId`, `arrow?`, `label?` | > > Annotations are addressed by an `id` unique across every kind, so removing, updating and moving take the id alone. `UpdateAnnotationCommand` patches whichever fields you name; moving one and restyling it are the same command, so a drag and a color picker share a path. `MoveAnnotationCommand` is the specialized drag: a translation in panel fractions, clamped inside the panel, relative rather than absolute, and reverted by the inverse patch since clamping isn't invertible. `SetDifferenceArrowStyleCommand` writes one difference arrow's line entry (`arrow`) and label entry (`label`) in the `overrides` list as one step; `null` removes an entry and an omitted one is left alone. ## Commands over the wire Commands are registered by type, so they round-trip through JSON. `commandRegistry` turns a command into a `SerializedCommand`, `{ type, params, metadata }`, and a `SerializedCommand` back into a live instance. That is what lets an edit arrive from an agent, a collaborator, or a replayed session log and land on the same history as a click: ```tsx theme={null} import { commandRegistry, type SerializedCommand, } from '@graphysdk/react/editable'; const payload: SerializedCommand = commandRegistry.serialize(command); dispatch(commandRegistry.deserialize(payload)); ``` `deserialize` throws on a type it doesn't know, so treat an unrecognised payload as a version mismatch rather than a no-op. This round trip is also why the `Params` types are the settled ones: a replayed command reproduces the same spec rather than minting a second id. ## Related * [Provider & renderer](/sdk-next/rendering/provider-and-renderer) — `onSpecChange`, `handleRef`, `mode="editable"` * [Serializable spec](/sdk-next/concepts/serializable-spec) — the spec commands edit, and how to persist it # Statistics Source: https://docs.graphy.dev/sdk-next/advanced/statistics A **stat** summarizes a layer's data before it is drawn. Instead of plotting raw rows, the layer plots a computed result: a count, a mean, or a fitted regression line. A stat is set per layer through the geom's `stat` option. A layer that names none runs its geom's default stat, which is `identity` (draw the data as-is) for every built-in geom. ```tsx theme={null} import { stat } from '@graphysdk/react'; geom.line({ stat: stat.smooth({ method: 'linear' }) }); ``` ## Available stats | Stat | Result | | - | - | | `stat.identity()` *(default)* | The data unchanged | | `stat.count()` | The number of observations per x-value, per group | | `stat.sum()` | The total of `y` per x-value, per group | | `stat.mean()` | The mean of `y` across the whole layer, as a single observation | | `stat.summary({ interval? })` | The mean or median of `y` per x-value, per group, with an optional interval around it | | `stat.smooth({ method })` | A fitted regression curve through the points | `count`, `sum` and `summary` group by x plus the layer's grouping variables, so the groups a reader compares stay separate. `mean` reduces the layer to one observation, which is why it pairs with `geom.rule()` rather than a bar or a line. `count` computes `y` for you; mapping `y` alongside it is a `CONFLICTING_STAT_MAPPING` error. `sum`, `mean`, `summary` and `smooth` need a numeric `y`; a non-numeric one is an `INCOMPATIBLE_TYPE` error. Every stat also has a string shorthand, so `geom.bar({ stat: 'count' })` is the same as `geom.bar({ stat: stat.count() })`. The names are `'identity'`, `'count'`, `'smooth'`, `'mean'`, `'sum'` and `'summary'`. ## Trendlines with `smooth` The `smooth` stat fits a regression through a layer's points. Add it to a second `geom.line()` layer over the raw data: ```tsx theme={null} pipe( createSpec({ x: 'date', y: 'sales' }), geom.point(), // the raw points geom.line({ stat: stat.smooth({ method: 'linear' }) }), // the trend scale.x(), scale.y() ); ``` `method` selects the regression family: **method** `'linear' | 'loess' | 'exponential' | 'logarithmic' | 'quadratic' | 'power' | 'polynomial'` (default: linear) The regression method to fit. The object builder asks for it; the `'smooth'` string shorthand takes the default. **order** `number` (default: 3) Polynomial order. Only used when `method: 'polynomial'`. **bandwidth** `number` (default: 0.3) Smoothing bandwidth. Only used when `method: 'loess'`. ```tsx theme={null} stat.smooth({ method: 'polynomial', order: 4 }); stat.smooth({ method: 'loess', bandwidth: 0.5 }); ``` ## A mean reference line `stat.mean()` reduces the layer to one observation, which is what a rule needs. Pair the two to draw a horizontal line at the layer's average: ```tsx theme={null} pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), geom.rule({ stat: stat.mean() }), // a line at the mean revenue scale.x(), scale.y() ); ``` ## Estimates and intervals with `summary` The `summary` stat reduces `y` to one observation per x-value and group: an estimate, and an interval around it if you ask for one. The estimate is mapped to `y` and the interval's ends to `lower` and `upper`, which is what a geom drawing an interval reads. ```tsx theme={null} pipe( createSpec({ x: 'treatment', y: 'recoveryDays', color: 'ageBand' }), geom.bar({ stat: stat.summary() }), // the mean per treatment and age band scale.x(), scale.y() ); ``` **estimate** `'mean' | 'median'` (default: mean) The value each x-value and group reduces to. **interval** `'stderr' | 'stdev' | 'ci' | 'iqr' | 'range'` The interval around the estimate. It has no default: leave it out and the stat computes the estimate alone, so a chart that draws an interval always says which one it is. * `'stderr'`: the mean ± `mult` standard errors * `'stdev'`: the mean ± `mult` standard deviations * `'ci'`: a `level` confidence interval for the mean, from Student's t distribution * `'iqr'`: the first to the third quartile * `'range'`: the smallest value to the largest `'stderr'`, `'stdev'` and `'ci'` describe a mean, so they need `estimate: 'mean'`. `'iqr'` describes a median, so it needs `estimate: 'median'`. **level** `number` (default: 0.95) The confidence level, between `0` and `1`. Only used with `interval: 'ci'`. **mult** `number` (default: 1) How many standard errors or deviations each end sits from the mean. Only used with `interval: 'stderr'` or `'stdev'`. ```tsx theme={null} stat.summary({ interval: 'ci', level: 0.9 }); stat.summary({ interval: 'stderr', mult: 2 }); stat.summary({ estimate: 'median', interval: 'iqr' }); ``` An x-value and group with a single value, or with values that are all equal, gets no interval: there is no spread to measure. A param outside its allowed values fails with `INVALID_STAT_PARAM`. ## Setting a stat at runtime Two [commands](/sdk-next/advanced/commands-and-history) reach the stat: | Command | Params | What it does | | - | - | - | | `SetLayerStatCommand` | `layerId?`, `stat` (a name such as `'count'`, an object such as `stat.count()`, or a plugin stat) | Sets one layer's stat | | `SetStatLineCommand` | `line: 'none' \| 'trend' \| 'average'`, `layerId?`, `method?`, `group?`, `label?`, `removedLayer?` | The switch behind "show a trendline" and "show an average"; swapping one for the other is one undo entry | ## Custom stats A plugin can contribute its own stat: a `Stat` subclass registered through `plugins`, which gets a typed builder beside the built-ins. Its spec is only `{ type }`, so it reads any settings from the layer's `params`. A custom geom can also declare one as its `defaultStat`; its layers then summarize without naming it, and registering the geom registers the stat. See [a summary is a stat](/sdk-next/extending/custom-geoms#a-summary-is-a-stat) for writing one, and [Extending](/sdk-next/extending/index) for registration. ## Next * [Transforms](/sdk-next/advanced/transforms) — reshape data before it reaches a stat * [Reference lines](/sdk-next/emphasis/reference-lines) — the rules `stat.mean()` draws * [Geoms & layers](/sdk-next/concepts/geoms) — where the `stat` option lives # Transforms Source: https://docs.graphy.dev/sdk-next/advanced/transforms A **transform** reshapes the data before it is charted: pivoting columns into rows, filtering, sorting, aggregating, or adding a constant column. Transforms run before [mappings](/sdk-next/concepts/mappings) are read, so a mapping can reference a column a transform produced. ```tsx theme={null} import { transform } from '@graphysdk/react'; pipe( createSpec(), transform.reshape({ reshape: ['revenue', 'profit'], keyName: 'metric', valueName: 'amount', }), mapping({ x: 'month', y: 'amount', color: 'metric' }), geom.line(), scale.x(), scale.y(), scale.color.discrete() ); ``` ## Reshape: wide to long The most common transform. Many datasets are **wide**, with one column per group: ```tsx theme={null} { month: 'Jan', revenue: 12000, profit: 3000 } ``` A mapping wants a **long** shape: one row per group, with a column naming the group to split on: ```tsx theme={null} { month: 'Jan', metric: 'revenue', amount: 12000 } { month: 'Jan', metric: 'profit', amount: 3000 } ``` `transform.reshape` pivots wide to long so you can map the new column to `color`: **reshape** `string[]` The numeric columns to collapse into rows. Defaults to all numeric columns. **keep** `string[]` Columns to carry through unchanged. Defaults to all categorical/temporal columns. **keyName** `string` (default: key) Name of the output column holding the original column names. **valueName** `string` (default: value) Name of the output column holding the values. ## Other transforms | Transform | Purpose | | - | - | | `transform.filter({ variableName, operator, value })` | Keep only rows matching a comparison | | `transform.sort({ variableName, direction })` | Order rows by a column | | `transform.aggregate({ groupby, operations })` | Group and summarize (sum, mean, …) into new columns | | `transform.constant({ variableName, type, value })` | Add a column with the same value on every row | ```tsx theme={null} transform.filter({ variableName: 'revenue', operator: 'gt', value: 0 }); transform.sort({ variableName: 'revenue', direction: 'desc' }); transform.aggregate({ groupby: ['region'], operations: [{ op: 'sum', variableName: 'revenue', as: 'total' }], }); ``` `operator` is one of `'eq'`, `'neq'`, `'gt'`, `'gte'`, `'lt'`, `'lte'`. `neq` belongs to this comparison vocabulary only: the [highlight predicate language](/sdk-next/emphasis/highlights#predicates) has no `neq` and writes the same idea as `{ not: … }`. `direction` is `'asc'` or `'desc'`, defaulting to `'asc'`. `op` is one of `'count'`, `'sum'`, `'mean'`, `'median'`, `'mode'`, `'min'`, `'max'`. The first four always produce a numeric output column; `mode`, `min` and `max` keep the source column's type, which decides whether the result can be mapped to a continuous scale. `transform.constant`'s `type` is a `DataType`: `'numeric'`, `'categorical'` or `'temporal'`. ## Custom transforms A plugin can contribute its own transform: a `TransformStrategy` object with a `transformType` name and an `apply(data, transform)` that returns a new `Dataset`. It goes into `plugins` and gets a typed builder, `kit.transform.({ options? })`; the `options` bag arrives on the `transform` argument of `apply`. See [Extending](/sdk-next/extending/index) for registration. ## Spec-level vs layer-level Piping a transform into the spec applies it to **all** layers. To reshape the data for a single geom, such as a line overlay that needs a different shape than the bars beneath it, pass `transforms` on that geom: ```tsx theme={null} geom.line({ transforms: [ transform.aggregate({ groupby: ['month'], operations: [{ op: 'sum', variableName: 'revenue', as: 'total' }], }), ], aes: { y: 'total' }, }); ``` ## Next * [Statistics](/sdk-next/advanced/statistics) — per-layer summaries that run after transforms * [Mappings & aesthetics](/sdk-next/concepts/mappings) — reference the columns a transform produces # Coordinate systems Source: https://docs.graphy.dev/sdk-next/concepts/coordinate-systems A **coordinate system** decides the plane a geom is drawn in. The same [geom](/sdk-next/concepts/geoms) and [scales](/sdk-next/concepts/scales) give very different charts under different coords: a stacked bar is a stacked column in cartesian coordinates, a horizontal bar when the axes are flipped, and a pie when the plane is bent into a circle. Set the coordinate system by piping a `coord.*()` part into the spec. When you don't, the chart is cartesian. ## Cartesian The standard x–y plane, and the default. ```tsx theme={null} pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), scale.x(), scale.y(), coord.cartesian() // optional — this is the default ); ``` ## Flip `coord.flip()` swaps the x and y axes. Its main use is turning vertical columns into horizontal bars: keep the natural mapping (band on `x`, value on `y`) and the flip lays it on its side. ```tsx theme={null} pipe( createSpec({ x: 'country', y: 'population' }), geom.bar(), scale.x(), scale.y(), coord.flip() // horizontal bars ); ``` ## Polar `coord.polar()` bends the plane into a circle. This is how pie, donut, rose and radial charts are made. It takes: **theta** `'x' | 'y'` (default: x) Which aesthetic maps to the angle. `theta: 'y'` sweeps the value around the circle (pie, donut); `theta: 'x'` sweeps the band around it, with the value as radius (rose). A bare `coord.polar()` gives the rose form; pass `theta: 'y'` for a pie. **innerRadius** `number` (default: 0) Inner radius as a fraction `0–1`. `0` is a full pie; a value like `0.5` cuts out the center for a donut. **startAngle** `number` (default: 0) Starting angle in degrees. A pie chart is a single stacked-fill bar wrapped around `theta: 'y'`. The `'fill'` position normalizes the stack to 100%, which is what closes the circle: ```tsx theme={null} pipe( createSpec({ x: '', y: 'revenue', color: 'region' }), geom.bar({ position: 'fill' }), coord.polar({ theta: 'y' }), scale.x(), scale.y(), scale.color.discrete() ); ``` The rest of the polar family comes from the same two options: | Chart | Recipe | | - | - | | Pie | `geom.bar({ position: 'fill' })` + `coord.polar({ theta: 'y' })` | | Donut | the same, plus `innerRadius: 0.5` | | Radar | `geom.line()` or `geom.area()` + `coord.polar({ theta: 'x' })` | | Rose | `geom.bar()` + `coord.polar({ theta: 'x' })` | See [Pie & donut](/sdk-next/graph-types/pie) and [Radar & radial](/sdk-next/graph-types/radial) for worked examples. Polar coordinates also set the `padding` of every discrete [scale](/sdk-next/concepts/scales) to `0`, so wedges meet edge to edge and `scale.x.discrete({ padding: 0.2 })` has no effect inside a pie. ### Time around the circle Mapping time to the angle turns a chart into a clock or a calendar: a day of tide heights, a year of temperatures. Pair `theta: 'x'` with a datetime or continuous x-scale: ```tsx theme={null} pipe( createSpec({ x: 'recordedAt', y: 'tideHeight' }), geom.line(), coord.polar({ theta: 'x' }), scale.x.datetime(), scale.y() ); ``` Each reading gets its own slot. The turn closes one step past the last reading, so it never lands back on the first. The step is the median spacing, so twelve monthly readings close on the following January and each month keeps its true width. See [Radar & radial](/sdk-next/graph-types/radial#time-around-the-circle) for a rendered example. A numeric axis works the same way: a wind rose binned every 10 degrees gives 350 its own slot before the turn wraps back to 0. A `log` or `sqrt` x-scale keeps its data domain, since an even step there is not an even slot. When the circle stands for a fixed period, name its bounds, so a missing reading cannot shift the others. The inferred step only knows the readings it is given: ```tsx theme={null} scale.x.continuous({ domainMin: 0, domainMax: 360 }); // wind direction scale.x.datetime({ domainMin: Date.UTC(2025, 5, 1), domainMax: Date.UTC(2025, 5, 2), }); // one day, midnight to midnight ``` ## One coord per spec Only one coordinate system applies at a time. Piping a second `coord.*()` replaces the first; it does not merge: ```tsx theme={null} // theta is gone — the second call is the whole coord. pipe(spec, coord.polar({ theta: 'y' }), coord.polar({ innerRadius: 0.5 })); // Pass both params in one call instead. pipe(spec, coord.polar({ theta: 'y', innerRadius: 0.5 })); ``` ## Next * [Chart types](/sdk-next/graph-types/index) — coords combined with geoms and positions * [Pie & donut](/sdk-next/graph-types/pie) — the `theta: 'y'` polar recipes in full * [Radar & radial](/sdk-next/graph-types/radial) — the `theta: 'x'` polar family # Geoms & layers Source: https://docs.graphy.dev/sdk-next/concepts/geoms A **geom** is what a chart draws for its data: a line, a bar, a point. Each geom is a **layer** in the spec; add several and they stack into one chart. [Mappings](/sdk-next/concepts/mappings) and [scales](/sdk-next/concepts/scales) decide where things go; geoms decide what is drawn there. ## The built-in geoms | Geom | Draws | Reads | | - | - | - | | `geom.point()` | A point per observation | `x`, `y`, `color`, `size`, `alpha`, `label` | | `geom.line()` | A path connecting observations | `x`, `y`, `color`, `strokeWidth`, `lineType`, `alpha`, `label` | | `geom.area()` | A filled band under a line | `x`, `y`, `color`, `strokeWidth`, `lineType`, `alpha`, `label` | | `geom.bar()` | A rectangle (or arc, in polar) per observation | `x`, `y`, `color`, `alpha`, `label` | | `geom.rule()` | A single reference line across the panel | `x` or `y` | | `geom.tile()` | A cell filling its band on both axes; cartesian only | `x`, `y`, `color` (required), `label` | Each is a function that returns a layer. Pass options to configure the layer, and geom-specific `params` for its geometry and behavior. Paint (colors, widths, opacities) lives in the [stylesheet](/sdk-next/config/styling) instead: ```tsx theme={null} geom.line({ params: { curve: 'smooth', missingValues: 'connect' } }); geom.bar({ position: 'stack', params: { width: 0.8 } }); ``` ## Params and paint, geom by geom `params` set geometry and behavior; the `style.*` target beside them holds the geom's paint. | Geom | Params | Paint target | | - | - | - | | `point` | none | `style.geom.point({ size, symbol, stroke, strokeWidth })` | | `line` | `curve: 'linear' \| 'smooth'` (default `'linear'`), `missingValues: 'zero' \| 'gap' \| 'connect'` (default `'gap'`) | `style.geom.line({ strokeWidth, dashArray, fillAlpha })` | | `area` | the same two, but `missingValues` defaults to `'zero'` — an area can't hold a gap mid-stack | `style.geom.area({ alpha, strokeWidth, dashArray, strokeAlpha })` | | `bar` | `width` (default `0.7`, or `1` in polar) — the fraction of the band the bar fills, in `(0, 1]` | `style.geom.bar({ cornerRadius, stroke, strokeWidth })` | | `rule` | `label` (optional text), `labelPosition: 'start' \| 'end'` (default `'start'`) | `style.geom.rule({ stroke, strokeWidth, dashArray })`, and `style.geom.rule.label({ … })` for its text | | `tile` | none — a cell's geometry is the two bands it sits in | `style.geom.tile({ cornerRadius, stroke, strokeWidth })` | Every target also takes the shared `alpha` and `saturation`, plus its paint word: `fill` on a bar, area, point or tile, `stroke` on a line or rule. A dash pattern is a `dashArray` in the stylesheet (`[]` solid, `[4, 2]` dashed) and a `lineType` name (`'solid'`, `'dashed'`, `'dotted'`) when mapped from data. Two things catch people out. A geom's `cornerRadius` is a token (`'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'`) or a number of pixels. And a `defaults` entry loses to a mapped aesthetic, while an `overrides` entry beats it. [Styling](/sdk-next/config/styling) has the full cascade. > [!NOTE] > > A `params` key the geom does not declare is accepted and ignored. ## Layers Every `geom.*()` you pipe in adds a layer. Layers draw in order, so later ones sit on top. They share the spec-level [mapping](/sdk-next/concepts/mappings) unless one overrides it. This is how combo charts are built: ```tsx theme={null} pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), // the trend geom.point(), // a dot on every vertex, on top scale.x(), scale.y() ); ``` ## Common layer options Beyond `params`, every geom accepts these options: | Option | Purpose | | - | - | | `id` | A stable name for the layer. `style.*(…, { layer: id })`, highlights and annotation anchors point at it. Set one to style a single layer of a multi-layer chart | | `aes` | A [layer-local mapping](/sdk-next/concepts/mappings#where-a-mapping-lives), merged over the spec's | | `position` | How overlapping observations arrange — see below | | `stat` | A [statistical transform](/sdk-next/advanced/statistics) for this layer — `stat.count()`, `stat.sum()`, `stat.mean()`, `stat.smooth()`; the default is `stat.identity()` | | `yScaleType` | Bind the layer to the `'primary'` or `'secondary'` y-axis | | `dataLabels` | Show [value labels](/sdk-next/config/data-labels) on each observation | | `transforms` | [Transforms](/sdk-next/advanced/transforms) applied to this layer's view of the data | | `interactive` | Whether the layer answers hover. `true` for every geom except `geom.rule()` | | `attachTo` | The layer this one describes: `'previous'`, `'none'` or a layer `id`. An attached layer joins that layer's hover and its tooltip rows sit under that layer's. `'none'` for every built-in geom; see [Interactivity](/sdk-next/rendering/interactivity#how-hover-works) | ## Position modes When several observations share the same x (because `color` or `group` splits the data), a layer's `position` decides how they arrange: | Position | Effect | | - | - | | `'identity'` | Draw at the raw value; observations may overlap | | `'stack'` | Stack observations end to end | | `'fill'` | Stack, then normalize each stack to 100% | | `'dodge'` | Place observations side by side within the band | Each geom has its own default: `geom.bar()` is `'dodge'`, `geom.area()` is `'stack'`, and `geom.point()`, `geom.line()`, `geom.rule()` and `geom.tile()` are `'identity'`. So a bar chart with a `color` mapping comes out grouped and an area chart comes out stacked, with no `position` of your own. `geom.tile()` accepts only `'identity'`; any other value fails to compile. ```tsx theme={null} geom.bar({ position: 'stack' }); // stacked columns instead of grouped geom.bar({ position: 'fill' }); // 100% stacked columns geom.area({ position: 'identity' }); // overlapping bands instead of stacked ``` Dodge works the same way for every geom: each group gets a share of the band. A bar fills its share; a point or line vertex stands at its center. Dodged layers line up, so dodge the points over a grouped bar chart and each point lands on its own bar: ```tsx theme={null} geom.bar(); // grouped: 'dodge' by default geom.point({ position: 'dodge' }); // each group's point on its own bar ``` A dodged line moves with its points, so the two stay together. Dodge needs a band, so it only moves anything on a band axis. On a continuous x axis a dodged layer stays at its values. ## Next * [Scales](/sdk-next/concepts/scales) — map values to positions, colors and sizes * [Styling](/sdk-next/config/styling) — the stylesheet that paints every geom * [Statistics](/sdk-next/advanced/statistics) — summarize a layer before drawing it * [Chart types](/sdk-next/graph-types/index) — geoms assembled into recipes * [Heatmap & waffle](/sdk-next/graph-types/heatmap) — the tile geom end to end # How a chart is built Source: https://docs.graphy.dev/sdk-next/concepts/how-a-chart-is-built Every chart is a **spec**: a plain, immutable object you build by folding parts together. ## The builder pipeline You start a spec with `createSpec` and fold parts onto it with `pipe`. Each part is a small tagged object produced by a builder (`geom.*`, `scale.*`, `coord.*`, `config`, …): ```tsx theme={null} import { createSpec, pipe, geom, scale } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), scale.x(), scale.y() ); ``` `pipe(spec, ...parts)` returns a new spec with each part folded in, left to right. It never mutates. Each kind folds in its own way: | Kind | How it folds | | - | - | | `geom.*`, `scale.*`, `transform.*`, `highlight` | Appended to a list | | `mapping` | Shallow-merges over the current mapping | | `config` | Deep-merges over the current config | | `coord` | Overwrites — one coord per spec, the last one is used | | `styles` | Becomes the new stylesheet, with the previous one nested into its `extends` | Two of those deserve a second look. `coord` overwrites: piping `coord.polar({ innerRadius: 0.5 })` after `coord.polar({ theta: 'y' })` leaves the inner radius and no `theta`, so pass both params in one call. A piped `styles(...)` sits above everything piped before it, so where two stylesheets declare the same property the later one decides. Because a spec is a plain object, you can build it up conditionally, share fragments across charts, and [store or serialize it](/sdk-next/concepts/serializable-spec). ```tsx theme={null} import { createSpec, pipe, geom, scale, stat } from '@graphysdk/react'; declare const showAverage: boolean; // Build in stages — each call returns a new spec. let spec = createSpec({ x: 'month', y: 'revenue' }); spec = pipe(spec, geom.line(), scale.x(), scale.y()); if (showAverage) { spec = pipe(spec, geom.rule({ stat: stat.mean() })); } ``` ## The parts of a spec A spec brings together these parts. Each has its own page: | Part | What it decides | Builder | | - | - | - | | [Mapping](/sdk-next/concepts/mappings) | Which data columns feed which aesthetics | `mapping`, `createSpec` | | [Geoms](/sdk-next/concepts/geoms) | The shapes drawn — line, bar, point, area, rule | `geom.*` | | [Scales](/sdk-next/concepts/scales) | How data values become positions, colors and sizes | `scale.*` | | [Coordinate systems](/sdk-next/concepts/coordinate-systems) | The plane observations are drawn in | `coord.*` | | [Statistics](/sdk-next/advanced/statistics) | Per-layer summaries (count, sum, mean, smoothing) | `stat.*` | | [Transforms](/sdk-next/advanced/transforms) | Reshaping the data before it's drawn | `transform.*` | | [Styles](/sdk-next/config/styling) | How the geoms and chart chrome are painted | `styles`, `style.*` | | [Highlights](/sdk-next/emphasis/highlights) | Which observations are called out | `highlight` | | [Annotations](/sdk-next/emphasis/annotations) | Text, arrows and brackets drawn over the plot | `annotation.*` | Plus [configuration](/sdk-next/config/index) (`config`) for chart chrome — titles, axes, legend, layout. ## Compile, then render A spec is a description, not pixels. Turning it into a chart takes two stages, one per package: ```mermaid theme={null} flowchart LR S[Spec] --> C[viz-engine: compile] D[Data] --> C C --> R[Scene] R --> P[react-renderer: paint] P --> SVG[SVG chart] ``` 1. **Compile (`@graphysdk/viz-engine`).** The engine takes your spec and data and produces a `Scene`. Scales get concrete domains, statistics run, every observation is placed in a normalized `[0,1]` position space, the stylesheet is validated with its `token()` references inlined, and guides (axes, legend) are worked out. This stage is pure data in, data out, with no DOM. It is also what keeps a styled spec serializable: paint comes out as plain values. 2. **Render (`@graphysdk/react-renderer`).** The renderer takes the `Scene` and paints it: it lays out pixel rectangles, formats numbers and dates for the locale, picks the light or dark half of every color pair, and wires up hover and animation. In a React app you do not call the compiler yourself. `` does, and recompiles when the spec, data or `colorScheme` change. The scheme reaches both stages: the compiler picks the default palette's tones for it, and the renderer picks the light or dark half of every color pair as it reads: ```tsx theme={null} import { GraphProvider, GraphRenderer } from '@graphysdk/react'; ; ``` ## Why the split matters The split between the two packages shapes how you use the SDK: * **Data lives outside the spec.** A spec references columns by name; the rows are passed to `` separately. One spec can draw many datasets. * **The compiler never formats.** It emits raw values and descriptors; the renderer turns them into text for the locale. That is why `formattingLocale` is a provider prop, not part of the spec. * **Scales are resolved once, at compile time.** A position the spec places without a scale gets an inferred one then, from the column's type. Authoring — mapping, geoms, scales, stats, and the [stylesheet](/sdk-next/config/styling) that paints the geoms — lives in the engine. Presentation — layout, formatting, interactivity and animation — lives in the renderer. Both are imported from `@graphysdk/react`. ## Next * [Scales](/sdk-next/concepts/scales) — how values become positions and colors, and which scales the engine adds * [Serializable spec](/sdk-next/concepts/serializable-spec) — a spec is plain JSON you can persist and reload # Mappings & aesthetics Source: https://docs.graphy.dev/sdk-next/concepts/mappings A **mapping** binds data columns to **aesthetics**: the visual properties a chart draws with, such as horizontal position, vertical position and color. It links a row of data to what is drawn on screen. ```tsx theme={null} createSpec({ x: 'month', y: 'revenue', color: 'product' }); ``` This reads: put `month` on the x-axis, `revenue` on the y-axis, and give each `product` its own color. The strings are column [keys](/sdk-next/data-structure). ## Built-in aesthetics The built-in aesthetics: | Aesthetic | Encodes | Typical column | | - | - | - | | `x` | Horizontal position | Text, date, or number | | `y` | Vertical position | Number | | `label` | The text [data labels](/sdk-next/config/data-labels) show | Any column | | `color` | Fill / stroke color | Text (or number, for a gradient) | | `size` | Size of each observation | Number | | `alpha` | Opacity (0–1) | Number | | `strokeWidth` | Line/border thickness | Number | | `lineType` | Dash pattern (solid, dashed, dotted) | Text | | `group` | Splits observations into groups **without** a visual encoding | Text | Not every geom reads every aesthetic: `size` means something to `geom.point()` and nothing to `geom.line()`. Each [geom](/sdk-next/concepts/geoms) lists the aesthetics it reads. `lineType` takes only a text column; a numeric one fails to compile. A [custom geom](/sdk-next/extending/custom-geoms) can declare position channels of its own, such as a candlestick's `open` / `high` / `low` / `close`, and the engine scales them like the built-in ones. ### What splits data into groups Grouping follows one rule: * If `group` is mapped, it is the only thing that groups. * Otherwise, every mapped visual aesthetic that points at a text column — `color`, `size`, `alpha`, `strokeWidth`, `lineType` — groups, one group per unique combination. A lone `lineType: 'scenario'` mapping splits one line into several, with no color involved. Position aesthetics never group. A visual aesthetic that points at the same column as `x` or `y` is left out too, since position already splits the data by it. `color` and `group` differ in what else they do. `color` splits and also paints: a hue per value, with a legend. `group` only splits. Use `group` for one line per value in a single color. ## Variables and constants An aesthetic can be bound to a **variable** (a column, read per row) or a **constant** (one literal value applied to every observation): ```tsx theme={null} createSpec({ y: 'revenue', // variable — shorthand for { variable: 'revenue' } color: { value: 'red' }, // constant — every observation is red }); ``` * `'revenue'` — string shorthand for a variable mapping. * `{ variable: 'revenue' }` — the explicit variable form. * `{ value: 'red' }` — a constant. The value still flows through the aesthetic's [scale](/sdk-next/concepts/scales), so `{ value: 'Pro' }` on `color` gets whatever color the scale assigns to `Pro`. ## Where a mapping lives A mapping can sit at two levels: **Spec-level** — shared by every layer. Set it with `createSpec({ ... })`: ```tsx theme={null} pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), geom.point(), // both layers inherit x and y scale.x(), scale.y() ); ``` **Layer-level** — merged over the spec-level mapping for one geom only, via a geom's `aes` option. This is how combos give each layer a different `y`: ```tsx theme={null} pipe( createSpec({ x: 'month' }), // shared x geom.bar({ aes: { y: 'revenue' } }), // this layer's y, on the primary axis geom.line({ aes: { y: 'profit' }, yScaleType: 'secondary' }), // its own axis scale.x(), scale.y(), scale.ySecondary() ); ``` A layer mapping decides where the two overlap; anything it leaves unset comes from the spec-level mapping. Two metrics on one chart need more than a second mapping. Set `yScaleType: 'secondary'` on the layer that belongs to the right-hand axis. Without it the layer is placed against the primary domain, which draws a plausible but wrong chart. The `scale.ySecondary()` above is optional: the engine infers one as soon as a layer asks for it. Declaring it lets you give that axis its own options. > [!NOTE] > > A layer's `aes` option is named `mapping` in the serialized spec: `geom.line({ aes: { y: 'profit' } })` > becomes `{ "type": "layer", "geom": "line", "mapping": { "y": "profit" } }`. This matters when you > [hand-write the JSON](/sdk-next/concepts/serializable-spec). ## Every mapped position gets a scale A [scale](/sdk-next/concepts/scales) turns the values of a mapped `x` or `y` into positions. When the spec declares none for that position, the engine infers one from the column, as a bare `scale.x()` or `scale.y()` would. Declare a typed scale to choose its type or options yourself. ## Next * [Scales](/sdk-next/concepts/scales) — turn mapped values into positions and colors * [Geoms & layers](/sdk-next/concepts/geoms) — which aesthetics each geom reads # Scales Source: https://docs.graphy.dev/sdk-next/concepts/scales A **scale** turns data values into visual values: a number into a position, a text value into a color, a magnitude into a point size. Every [aesthetic](/sdk-next/concepts/mappings) you map needs one. ```tsx theme={null} scale.x(); // infer an x-axis scale from the data scale.y.continuous({ domainMin: 0 }); // an explicit continuous y-axis, pinned to zero scale.color.discrete({ range: ['#4C6EF5', '#F76707'] }); ``` ## Scales the engine adds Three kinds of scale appear on their own when the spec implies them: * **Position scales.** When a layer places something on `x` or `y` and the spec has no scale for it, the engine infers one, the same as a bare `scale.x()` or `scale.y()`. Placing counts the axis mapped directly, an aesthetic the geom puts on that axis (a candlestick's `open`, `high`, `low` and `close`), and a stat that computes it (`stat: 'count'` writes `y`). Declare a typed scale, such as `scale.y.continuous({ domainMin: 0 })`, when you want to fix its type or options. * **A color scale.** With no `color` scale in the spec, the engine appends `scale.color.discrete()`, so mapped groups still get the default palette. Under a geom that draws its value as color (`geom.tile()`), the appended scale is inferred from the mapped column instead, so a numeric column lands on a continuous ramp. * **A secondary y scale.** As soon as any layer sets `yScaleType: 'secondary'`, an inferred `ySecondary` scale is appended if you didn't declare one. Declaring two scales for the same aesthetic isn't an error — the last one is used. ## Inference vs explicit types Call a position scale bare to infer its type from the mapped column's [value format](/sdk-next/data-structure#value-format-detection). On `x`, `y` and `ySecondary`: * text → **discrete** (band) scale * number / percentage / currency → **continuous** scale * date → **`datetime`** scale ```tsx theme={null} scale.x(); // inferred scale.x({ nice: true }); // inferred, with options ``` > [!NOTE] > > A geom's needs come before the column's format. Under a bar layer, `scale.x()` > resolves to a band and `scale.y()` to a continuous scale; under a > [tile](/sdk-next/graph-types/heatmap) layer, both resolve to bands. A value > axis that bars or areas stand on always holds zero, declared explicitly or > not. A `domainMin` above zero gives way to it, with an > `INCOMPATIBLE_SCALE_DOMAIN` warning. Name the type to opt out of inference. The scale must still fit the column's type, or the compile fails with `INCOMPATIBLE_TYPE`. When the column's type is wrong, [declare its format](/sdk-next/data-structure#declaring-a-format). ```tsx theme={null} scale.x.continuous(); scale.x.discrete(); scale.x.datetime(); scale.x.log(); scale.x.sqrt(); ``` A geom that needs a band still overrules you: a continuous or datetime scale there is replaced by a band, with an `UNSUPPORTED_SCALE_TYPE` warning. Only its `reverse` carries over. A bar's band applies to the whole plot's shared x scale, sibling line layers included. With numeric x values, `geom.line()` plus `geom.bar()` and `scale.x.continuous()` resolves to a band scale, with `UNSUPPORTED_SCALE_TYPE`, and the line uses those bands. A line alone keeps its continuous x scale. ## Scales by aesthetic | Aesthetic | Methods | | - | - | | `x`, `y`, `ySecondary` | callable (inferred), `.continuous()`, `.discrete()`, `.datetime()`, `.log()`, `.sqrt()` | | `color` | `.continuous()`, `.discrete()` | | `size`, `alpha`, `strokeWidth` | `.continuous()`, `.discrete()`, `.identity()` | | `lineType` | `.discrete()`, `.identity()` | ### Categorical color ```tsx theme={null} scale.color.discrete(); // the theme's palette, else the built-in one scale.color.discrete({ domain: ['A', 'B'], range: ['#4C6EF5', '#F76707'] }); // fixed colors ``` Group colors follow one rule. `scale.color.discrete()` takes the theme's `palettes.categorical` list when a mounted theme has one, else the built-in palette: the multicolor set, or brick tones spread by group count when the geoms touch (stacked bars and areas, pies, tiles). `scale.color.discrete({ range })` sets explicit colors in group order and cycles when groups outnumber colors. ### Continuous color Map a numeric column to `color` and the values run through a ramp instead of a set of hues: ```tsx theme={null} scale.color.continuous(); // the theme's sequential list, else the built-in brick ramp scale.color.continuous({ domainMid: 0 }); // diverging around zero: the theme's diverging list, else blue to gray to brick scale.color.continuous({ range: ['#FFFFFF', '#0B5FFF'] }); // an explicit ramp ``` The ramp is the scale's `range` when it names one. Otherwise it is the mounted theme's `sequential` list, or its `diverging` list when `domainMid` is set and falls inside the domain. With no theme list the scale takes the built-in ramp of that class: light brick to dark brick, or blue through neutral gray to brick with the gray at the midpoint. **range** `ReadonlyArray\` (default: null) An explicit ramp of two or more stops. A `range` takes precedence over the theme's lists. **interpolate** `'rgb' | 'lab' | 'hcl' | 'hsl'` (default: lab) The space the ramp's stops are interpolated in. `'lab'` is perceptually near-uniform; `'rgb'` matches d3's raw output. **reverse** `boolean` (default: false) Flips the ramp so the first stop lands on the high end. **domainMid** `number` (default: null) Maps this data value, usually `0`, to the ramp's midpoint. The scale becomes diverging only when the value lies strictly inside the domain. Use an odd number of stops with a neutral color in the middle. Even-length diverging ramps still render, blending the two central colors and emitting an `EVEN_DIVERGING_RAMP` warning. **symmetric** `boolean` Symmetrises the domain about `domainMid` so equal magnitudes get equal color intensity. Defaults to `true` once `domainMid` is set, and has no effect without it. ### Identity scales An identity scale passes data values straight through: `{ size: 10 }` becomes 10px. Use it when the data already holds pixel sizes, opacities or stroke widths. ```tsx theme={null} scale.size.identity(); ``` `scale.size.continuous()` is not identity: it defaults to a `sqrt` transform, so a value maps to the point's area, not its radius. Doubling a value doubles the area, which is what the eye reads. ## Continuous scale options The continuous methods (and `.log()` / `.sqrt()`, which are continuous with a transform) accept: **domainMin** `number` Pin the lower bound of the domain; the maximum still follows the data. `domainMin: 0` keeps zero on the axis, from the top when the data is all negative. **domainMax** `number` Pin the upper bound of the domain; the minimum still follows the data. **nice** `boolean` (default: true) Extend the domain to round values — `[3, 97]` becomes `[0, 100]`. **reverse** `boolean` (default: false) Reverse the direction of the scale. **transform** `'linear' | 'log' | 'sqrt'` (default: linear) A mathematical transform applied to the scale. `scale.y.log()` is shorthand for `transform: 'log'`. On `size`, the default is `sqrt`. A log scale whose domain includes zero falls back to linear, with an `INCOMPATIBLE_SCALE_DOMAIN` warning. **clamp** `boolean` Pin out-of-domain values to the ends of the range instead of extrapolating past them. Defaults to `false` on `x` / `y` / `ySecondary` and `true` on every other aesthetic. **range** `[number, number]` The output range for the magnitude aesthetics. Ignored on position aesthetics, where the range is the panel. Defaults per aesthetic: `size` `[4, 20]`, `alpha` `[0.1, 1]`, `strokeWidth` `[1, 4]`. A continuous `color` scale takes a color ramp here instead — see [Continuous color](#continuous-color). ## Datetime scale options `scale.x.datetime()` and its siblings take epoch-millisecond bounds: **domainMin** `number` Lower bound, in milliseconds since the epoch — `Date.parse('2020-01-01')`. **domainMax** `number` Upper bound, in milliseconds since the epoch. **nice** `boolean` (default: false) Extend the domain to round temporal values. Off by default: rounding to whole months can stretch or clip the data in ways that read as a bug. **reverse** `boolean` (default: false) Reverse the direction of the scale. **clamp** `boolean` (default: false) Pin out-of-domain instants to the ends of the range. ## Discrete scale options **domain** `Array\` The band values, in display order. Omit to take them from the data. **range** `Array\` Explicit output values — colors, sizes, or dash patterns — aligned to `domain`. **padding** `number` (default: 0.1) The gap between bands as a fraction of the band step, `0–1`. Inner padding is `padding`; outer padding is `padding / 2`. Under [`coord.polar`](/sdk-next/concepts/coordinate-systems) every discrete scale's padding is forced to `0`, so pie wedges meet with no gap. `geom.tile()` defaults its position scales to `0` for the same reason; an explicit `padding` still applies. **reverse** `boolean` (default: false) Reverse the band order — the first domain entry maps to the end of the range. Defaults to `true` on the cross axis of a geom banded on both axes, so a [heatmap](/sdk-next/graph-types/heatmap) reads first value at the top, like a matrix. ## Next * [Coordinate systems](/sdk-next/concepts/coordinate-systems) — the plane scales place values into * [Chart types](/sdk-next/graph-types/index) — scales at work in each recipe # Serializable spec Source: https://docs.graphy.dev/sdk-next/concepts/serializable-spec The builders — `createSpec`, `pipe`, `geom.*`, `scale.*`, `config` — are a convenience. What they produce is a plain, immutable object of objects, arrays, strings, numbers and booleans. A spec holds no functions, class instances or DOM references, so it round-trips through `JSON.stringify` / `JSON.parse` unchanged. You can store a spec in a database, send it over the wire, generate it from another language, or write one by hand. ## The builders only assemble an object `pipe(...)` and the `geom.*` / `scale.*` / `config` helpers assemble and merge a plain object. This call: ```tsx theme={null} import { createSpec, pipe, geom, scale, config } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue', color: 'product' }), geom.line(), scale.x(), scale.y.continuous({ domainMin: 0 }), scale.color.discrete(), config({ legend: { position: 'bottom' } }) ); ``` produces exactly this object — and `JSON.stringify(spec)` gives you exactly this JSON (fields left `undefined` drop out): ```json theme={null} { "mapping": { "x": "month", "y": "revenue", "color": "product" }, "layers": [{ "type": "layer", "geom": "line" }], "scales": [ { "type": "scale", "scaledAesthetic": "x", "scaleType": "inferred" }, { "type": "scale", "scaledAesthetic": "y", "scaleType": "continuous", "domainMin": 0, "domainMax": null, "domainMid": null }, { "type": "scale", "scaledAesthetic": "color", "scaleType": "discrete" } ], "transforms": [], "highlights": [], "config": { "legend": { "position": "bottom" } } } ``` You could paste that JSON into your source and pass it to `GraphProvider` as `spec`, with no builder in sight. What the builders add is types: autocomplete for aesthetics, options and params, and a type check on the shape. Defaults are filled in at compile time either way. ## Every top-level key `createSpec` seeds six keys — `mapping`, `layers`, `scales`, `transforms`, `highlights` and `config` — so they are present even when empty. The other three appear only once something is piped in: | Key | Holds | | - | - | | `mapping` | The spec-level [aesthetic mapping](/sdk-next/concepts/mappings) | | `layers` | One entry per [geom](/sdk-next/concepts/geoms) | | `scales` | One entry per declared [scale](/sdk-next/concepts/scales) | | `transforms` | Spec-level [transforms](/sdk-next/advanced/transforms) | | `highlights` | [Highlights](/sdk-next/emphasis/highlights) | | `config` | [Chart chrome](/sdk-next/config/index) | | `coords` | The [coordinate system](/sdk-next/concepts/coordinate-systems) — a single object, not a list | | `styles` | The [stylesheet](/sdk-next/config/styling) | | `annotations` | [Annotations](/sdk-next/emphasis/annotations), grouped by kind | ## A styled spec is still JSON The stylesheet is the largest thing a spec carries, and it round-trips like the rest. A `token('brand')` reference is a plain object the compile stage inlines. A light/dark color pair is a plain object the renderer picks from at read time. Neither needs a function: ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style, token, } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), scale.x(), scale.y(), styles({ tokens: { brand: { light: '#0B5FFF', dark: '#6AA1FF' } }, defaults: [style.geom({ fill: token('brand'), stroke: token('brand') })], }) ); ``` ```json theme={null} { "styles": { "type": "styles", "tokens": { "brand": { "light": "#0B5FFF", "dark": "#6AA1FF" } }, "defaults": [ { "select": { "target": "geom" }, "declarations": { "fill": { "token": "brand" }, "stroke": { "token": "brand" } } } ] } } ``` ## Writing the JSON by hand Two things to know before you do. A layer's aesthetic override is called `aes` on the builder and `mapping` in the object: `geom.line({ aes: { y: 'profit' } })` serializes as `{ "type": "layer", "geom": "line", "mapping": { "y": "profit" } }`. A spec that uses a [plugin](/sdk-next/extending/index) geom, stat or transform still serializes: a custom layer is just `{ "geom": "candlestick", "params": { … } }`. The plugin itself is code. It reaches the chart through the `plugins` array on `GraphProvider`, not through the spec. Store the spec; ship the plugins with your app. ## Next * [How a chart is built](/sdk-next/concepts/how-a-chart-is-built) — the compile → render pipeline that consumes a spec * [Data structure](/sdk-next/data-structure) — the table a spec references by column name * [Extending](/sdk-next/extending/index) — custom geoms, stats and transforms, and the `plugins` they ship in # Axes Source: https://docs.graphy.dev/sdk-next/config/axes The `axes` section controls how each position [scale](/sdk-next/concepts/scales) is drawn: its label, side, grid lines and ticks. It is keyed by axis: `x`, `y`, and `ySecondary` for [combo charts](/sdk-next/graph-types/combo). ```tsx theme={null} config({ axes: { x: { label: 'Month' }, y: { label: 'Revenue (£)', position: 'left' }, }, }); ``` ## Per-axis options **label** `string | null` (default: null) Axis title. With `null` the axis shows the column's `label` from the [data](/sdk-next/data-structure) when one is declared, otherwise no title. **visible** `boolean` (default: true) Whether the axis is drawn at all. **position** `'left' | 'right' | 'top' | 'bottom'` Which side the axis sits on. Defaults to `'bottom'` for `x` and `'right'` for `y`. A side that does not match the axis orientation is ignored: on a flipped graph `x` runs vertically, so `'bottom'` gives way to the side the coord assigns. **grid** `object` Grid lines for this axis — see below. **ticks** `object` Ticks for this axis — see below. ## Grid lines Each axis owns its grid. Config decides whether the lines are drawn: ```tsx theme={null} config({ axes: { y: { grid: { visible: true } }, x: { grid: { visible: false } }, }, }); ``` **grid.visible** `boolean | null` (default: null) `true` or `false` forces visibility. `null` lets the geom decide: bars hide the band-axis grid, tiles hide both. With nothing to decide from, the `x` and `y` grids show and a secondary y grid stays hidden. How the lines look (color, width, dash pattern) comes from the stylesheet's `gridLine` target. The built-in stylesheet draws `y` grid lines dashed and gives `x` grid lines `strokeWidth: 0`, so vertical grid lines appear only once you give them a width, even where config allows them: ```tsx theme={null} styles({ defaults: [ style.gridLine.y({ stroke: '#E9E9E9', strokeWidth: 1, dashArray: [] }), style.gridLine.x({ strokeWidth: 1 }), ], }); ``` See [Styling](/sdk-next/config/styling) for the full target list. ## Ticks **ticks.visible** `boolean` Whether tick marks, the short lines that step out of the axis, are drawn. Defaults to `true` for `x` and `false` for `y`. Tick labels render either way. Their paint lives on the stylesheet's `tickLabel` target; the marks' paint on `tickLine`. The built-in `tickLine` has `strokeWidth: 0` and `length: 0`, so tick marks stay invisible until the stylesheet sizes them. `ticks.visible` then decides whether they are drawn and whether their `length` reserves space: ```tsx theme={null} styles({ defaults: [style.tickLine({ strokeWidth: 1, length: 4 })] }); ``` **ticks.mode** `'auto' | 'edges'` (default: auto) `'auto'` places ticks across the axis. `'edges'` keeps only the two domain endpoints of a continuous or datetime axis. Discrete axes keep their full domain either way. > [!NOTE] > > The axis controls labels, ticks and grid. The range comes from its > [scale](/sdk-next/concepts/scales): use > `scale.y.continuous({ domainMin: 0, nice: true })` to pin bounds. ## The second y axis `ySecondary` is a sparse override of the primary y axis, for [combo charts](/sdk-next/graph-types/combo) that pair two value scales. Every field you leave unset is inherited at compile time: | Field | Inherited from | | - | - | | `position` | the side opposite `y` | | `visible`, `grid`, `ticks` | `y` | | `label` | nothing; a secondary axis starts unlabelled | ```tsx theme={null} config({ axes: { y: { label: 'Revenue (£)' }, ySecondary: { label: 'Orders' }, }, }); ``` A field you set stays set: once you give `ySecondary.position`, it keeps that side however `y` moves. ## Panel border and baseline The panel is the plot area. Its border is painted from the stylesheet, one entry per edge. That is how you draw a single baseline under the bars, or a full frame around the plot: ```tsx theme={null} styles({ defaults: [ style.panelBorder({ strokeWidth: 0 }), style.panelBorder.bottom({ strokeWidth: 1, dashArray: [], stroke: '#1D1D1B', }), ], }); ``` Each edge builder (`.top`, `.right`, `.bottom`, `.left`) takes `stroke`, `strokeWidth` and `dashArray`. An edge with `strokeWidth: 0` is hidden and reserves no space, so the bare `style.panelBorder({ strokeWidth: 0 })` entry above clears all four before the baseline is put back. A tick that lands on a drawn edge draws no grid line of its own; the edge stands in for it. Corner rounding applies to the whole panel and lives on the bare builder: ```tsx theme={null} styles({ defaults: [style.panelBorder({ cornerRadius: 12 })] }); ``` The built-in default is a dashed border with `cornerRadius: 6`. See [Styling](/sdk-next/config/styling) for how entries resolve. ## Related * [Scales](/sdk-next/concepts/scales) — the axis range and type * [Styling](/sdk-next/config/styling) — grid, tick, panel-border and frame paint * [Layout › Panel overflow](/sdk-next/config/layout#panel-overflow) — how the panel makes room for labels that spill past its edge # Content Source: https://docs.graphy.dev/sdk-next/config/content The `content` section sets the text around the chart: title, subtitle, caption, your data-source attribution, and the "Made with Graphy" mark. Each text slot has a visibility flag, so a value can be kept while hidden. In `@graphysdk/react` the mark is on by default. ```tsx theme={null} config({ content: { title: 'Monthly revenue', showTitle: true, subtitle: 'By product, 2026', showSubtitle: true, source: { label: 'Internal data', url: 'https://example.com' }, showSource: true, }, }); ``` ## Slots **title** `string | RichTextContent` The chart's headline text. Pair with `showTitle`. **subtitle** `string | RichTextContent` A secondary line under the title. Pair with `showSubtitle`. **caption** `string | RichTextContent` A note shown beneath the chart. Pair with `showCaption`. **source** `\{ label?: string; url?: string \}` Your data-source attribution, shown under the caption (for example "Internal data"). Not the Graphy provenance badge. Pair with `showSource`. The visibility defaults differ by slot. `showTitle` and `showSubtitle` are `true`, so a title or subtitle shows as soon as you set the text. `showCaption` and `showSource` are `false`, so a caption or source also needs its flag turned on. ```tsx theme={null} config({ content: { caption: 'Figures exclude refunds.', showCaption: true, }, }); ``` ## Provenance badge ("Made with Graphy") A translucent capsule with the Graphy glyph and the text "Made with Graphy". It does not inherit the stylesheet's typography. It links to `graphy.app` with UTM tags. The full pill becomes a circular mini badge when the chart is narrower than 200 px, and disappears below 120 × 80. **brandMark** `\{ enabled?: boolean; placement?: 'footer' | 'header'; variant?: 'full' | 'mini' \}` Badge settings. Defaults: `{ placement: 'footer', variant: 'full' }`. `enabled` defaults to `true` in `@graphysdk/react` and to `false` in `@graphysdk/react-renderer`. `variant: 'mini'` draws the circular badge at every size. **isBrandMarkVisible** `boolean` Alias for `brandMark.enabled`. Config resolution keeps the two in sync; when both are set, `brandMark.enabled` decides. ```tsx theme={null} // Turn the badge off config({ content: { brandMark: { enabled: false }, }, }); // Header top-right, circular mini config({ content: { brandMark: { placement: 'header', variant: 'mini' }, }, }); // With your own source: source on the left, badge on the right config({ content: { source: { label: 'Internal data', url: 'https://example.com' }, showSource: true, }, }); ``` > [!NOTE] > > `source` is your data attribution. `brandMark` is Graphy's own provenance > badge. They can appear together, either alone, or neither. ## Rich text Any text slot accepts a plain string or a rich-text document in TipTap's JSON shape, which mixes colors, weights and headings within one title: ```tsx theme={null} config({ content: { showTitle: true, title: { type: 'doc', content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Revenue climbed ' }, { type: 'text', text: '24%', marks: [{ type: 'textStyle', attrs: { color: '#D4594C' } }], }, { type: 'text', text: ' this quarter.' }, ], }, ], }, }, }); ``` Recognized marks: `textStyle` (with `color`, `font` as a font id, and `fontSize`), `bold`, `italic`, `underline`, `strike`, `code` and `link`. On nodes, the renderer reads `heading.level` (1–3) and `paragraph.textAlign`. Unrecognised keys are ignored. > [!NOTE] > > `fontSize` is a number of pixels and follows `style.graph({textScale})` like > every other `fontSize`. A string such as `'14px'` is ignored. Rendering rich text needs no extra packages. Editing it in place needs the [TipTap peer packages](/sdk-next/quickstart#1-install-the-package). ## Related * [Configuration overview](/sdk-next/config/index) — how `config` composes * [Headline numbers](/sdk-next/config/headline-numbers) — summary metrics in the header * [Slots](/sdk-next/extending/slots) — replacing the header and footer regions * [Layout](/sdk-next/config/layout) — `style.header` and `style.footer` margin around those rows # Data labels Source: https://docs.graphy.dev/sdk-next/config/data-labels Data labels print each observation's value on the observation itself. Unlike the other topics in this section they are not a `config` field. They are a per-layer option on a [geom](/sdk-next/concepts/geoms), because a label belongs to one observation. ```tsx theme={null} geom.bar({ dataLabels: { showDataLabels: true }, }); ``` ## Options **showDataLabels** `boolean` (default: false) Turns labels on for this layer. `geom.tile()` is the exception: a [heatmap](/sdk-next/graph-types/heatmap) is read cell by cell, so its labels start on and you turn them off instead. **format** `'absolute' | 'percentage'` (default: absolute) Show the raw value, or its share of the total. **position** `'auto' | 'inside' | 'outside'` (default: auto) Where the label sits relative to the observation. `'auto'` lets the engine fit, flip, rotate or drop labels as space allows; only `'auto'` does any of that. `'inside'` and `'outside'` render exactly as asked and are never dropped. On stacked and filled cartesian bar segments `'outside'` becomes `'inside'`, since every segment edge borders a neighbor. Pie wedges keep `'outside'`. **justify** `'start' | 'center' | 'end' | 'panel-start' | 'panel-end'` (default: end ('center' for stacked/filled bars)) Anchor along the observation's value axis. `'end'` is the value tip whatever the orientation or sign. `'panel-start'` and `'panel-end'` anchor to the panel instead of the observation's box, so labels sit flush at the chart edge however long the geom is. Read only when `position` is explicit. **align** `'start' | 'center' | 'end'` (default: center) Anchor across the observation's other axis: the bandwidth for bars, the angle for wedges, x for points, lines and areas. Read only when `position` is explicit. **offset** `number` Gap in pixels between the observation's edge and the label. Defaults to 4 for bars, wedges and points, 8 for lines and areas. Stack totals ignore it and sit 6px off the stack end. **showStackTotals** `boolean` (default: false) On stacked bars, print the stack total at the end of each stack. **showCategoryLabels** `boolean` (default: false) On polar bars (pie and donut) prepends the category to the value ("North · 35%"). On cartesian bars draws a second label per bar with the category name, placed by the `category*` fields below and independent of `showDataLabels`. Other geoms ignore it. ## Category labels On cartesian bars the category label has placement fields of its own, so it can sit at the base of the bar while the value label sits at the tip: ```tsx theme={null} geom.bar({ dataLabels: { showDataLabels: true, showCategoryLabels: true, categoryPosition: 'inside', categoryJustify: 'start', }, }); ``` **categoryPosition** `'inside' | 'outside'` (default: inside) Where the category label sits relative to its bar. There is no `'auto'`: category labels render exactly as asked. On stacked and filled segments `'outside'` becomes `'inside'`. **categoryJustify** `'start' | 'center' | 'end' | 'panel-start' | 'panel-end'` (default: start) Anchor along the bar's value axis. Takes panel anchors like `justify`. **categoryAlign** `'start' | 'center' | 'end'` (default: center) Anchor across the bar's bandwidth. **categoryOffset** `number` (default: 4) Gap in pixels between the anchored edge and the category label box. ## Styling This page places labels; the stylesheet paints them. Type, box padding, background, border and corner radius come from the `dataLabel` target and its roles: `.observation` and `.category`, each with an `.inside` and `.outside` variant, plus `.aggregate` for stack totals: ```tsx theme={null} styles({ defaults: [ style.dataLabel({ fontSize: 11 }), style.dataLabel.observation.outside({ fill: '#FFFFFF', paddingInline: 4 }), ], }); ``` A label's final position picks the variant. Over the geom it takes inside styling (white text, no background). Off the geom, by placement, by offset, or because it did not fit, it takes outside styling. Area labels always use outside styling, because a translucent fill cannot back white text. See [Styling](/sdk-next/config/styling). ## Examples Percentage labels on a pie's wedges, with the category name: ```tsx theme={null} geom.bar({ position: 'stack', dataLabels: { showDataLabels: true, format: 'percentage', showCategoryLabels: true, }, }); ``` Totals at the top of each stacked column: ```tsx theme={null} geom.bar({ position: 'stack', dataLabels: { showDataLabels: true, showStackTotals: true }, }); ``` ## Related * [Geoms & layers](/sdk-next/concepts/geoms) — where layer options live * [Number format](/sdk-next/config/number-format) — how label values are formatted * [Custom geoms](/sdk-next/extending/custom-geoms#data-labels) — how a geom of your own places its labels # Headline numbers Source: https://docs.graphy.dev/sdk-next/config/headline-numbers Headline numbers show a summary metric (a total, an average or the latest value) beside the chart, with an optional trend indicator. They are set through the `headline` section. ```tsx theme={null} config({ headline: { show: 'total', compareWith: 'previous' }, }); ``` On a cartesian graph the headline is a strip with one card per color group (a single card when nothing is mapped to `color`), and it takes the place of the pill legend. The `y` variable must be numeric, and `'total'` on `position: 'fill'` bars draws nothing, since every stack sums to 100%. On a pie or donut the headline is one grand total. Only `show: 'total'` draws anything there, and only on a `position: 'fill'` layer, which is how a [pie](/sdk-next/graph-types/pie) is built. ## Options **show** `'total' | 'average' | 'current' | 'conversion' | 'none'` (default: none) Which aggregate to display: * `'total'` — the sum of the group's values. * `'average'` — the arithmetic mean. * `'current'` — the last value (useful for time series). * `'conversion'` — accepted by the type but produces no headline. * `'none'` — no headline. **compareWith** `'previous' | 'first' | 'none'` (default: none) Reference point for the trend indicator. The trend appears only with `show: 'current'` on a datetime x axis, and shows the change from the reference to the latest value: * `'previous'` — the preceding observation. * `'first'` — the first observation in the group. * `'none'` — the default; a `current` headline still trends, against the first observation. **size** `'auto' | 'small' | 'medium' | 'large'` (default: auto) Visual size. `'auto'` picks the largest of the three whose strip fits the container width. **position** `'above' | 'center'` (default: above) Where the headline sits. `'above'` places it in the header region. `'center'` places it in the hole of a [donut](/sdk-next/graph-types/pie). A pie without an inner radius has no hole, and a cartesian graph has no center, so there `'center'` behaves as `'above'`. ## In a donut's center Pair `position: 'center'` with a donut to put the total in the hole: ```tsx theme={null} pipe( createSpec({ x: '', y: 'revenue', color: 'region' }), geom.bar({ position: 'fill' }), coord.polar({ theta: 'y', innerRadius: 0.6 }), scale.x(), scale.y(), scale.color.discrete(), config({ headline: { show: 'total', position: 'center' } }) ); ``` ## Related * [Pie & donut](/sdk-next/graph-types/pie) — the donut's hole, home of a centerd headline * [Number format](/sdk-next/config/number-format) — how the headline value is formatted * [Styling](/sdk-next/config/styling) — `style.headline` and `style.headlineItem` paint the strip and its cards # Overview Source: https://docs.graphy.dev/sdk-next/config/index Titles, axes, legend, headline numbers and number formatting are configuration. You add them by piping a `config(...)` part into the spec. Space around the title, legend, axes and footer is `margin` on the stylesheet; see [Layout](/sdk-next/config/layout). ```tsx theme={null} import { createSpec, pipe, geom, scale, config } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), scale.x(), scale.y(), config({ content: { title: 'Monthly revenue', showTitle: true }, axes: { y: { label: 'Revenue (£)' } }, legend: { position: 'bottom' }, }) ); ``` ## How config composes `config` is a spec part like any other and follows the same [pipe rules](/sdk-next/concepts/how-a-chart-is-built#the-builder-pipeline), with one difference: config deep-merges. Piping several `config(...)` parts combines them instead of replacing, so a shared base can carry per-chart overrides: ```tsx theme={null} const base = config({ numberFormat: { decimals: 0 }, }); const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), scale.x(), scale.y(), base, config({ content: { title: 'Q1', showTitle: true } }) // merged onto base ); ``` Anything you leave unset keeps its default. ## Sections | Section | Controls | Page | | - | - | - | | `content` | Title, subtitle, caption, source, provenance mark | [Content](/sdk-next/config/content) | | `axes` | Labels, position, grid visibility, ticks | [Axes](/sdk-next/config/axes) | | `legend` | Position, alignment and display mode | [Legend](/sdk-next/config/legend) | | `headline` | Summary numbers above or inside the chart | [Headline numbers](/sdk-next/config/headline-numbers) | | `numberFormat` | Decimals, abbreviation, separators | [Number format](/sdk-next/config/number-format) | | `panel` | How the plot area accommodates overflowing content | [Layout](/sdk-next/config/layout#panel-overflow) | | `tooltip` | Which hovered observations the tooltip lists | [Interactivity](/sdk-next/rendering/interactivity#tooltips) | Data labels are a [per-layer geom option](/sdk-next/config/data-labels) rather than a `config` section, because a label belongs to one observation. > [!NOTE] > > Paint is a separate surface. The graph background, the panel border, grid and tick strokes, > label typography and the space around title, legend, axes and footer all come from a stylesheet > you pipe in as `styles({...})`. See [Styling](/sdk-next/config/styling) and > [Layout](/sdk-next/config/layout). `style.graph({ textScale })` multiplies every text size. It changes measured text, and so the size of every region, which is why it is documented with [Layout](/sdk-next/config/layout#text-scale). ## Locale **parsingLocale** `'en-GB' | 'en-US' | 'ar' | 'pt-PT'` (default: en-US) The locale used to read source values, and the fallback for formatting display output: axis labels, tick labels, tooltips and headline numbers. Pass `formattingLocale` to `GraphProvider` to override display only; it resolves as `formattingLocale ?? parsingLocale`. ```tsx theme={null} config({ parsingLocale: 'en-GB' }); ``` # Layout Source: https://docs.graphy.dev/sdk-next/config/layout The engine stacks the title, legend, axes, plot and footer. `gap` is the space between items inside a chrome target (pills in a legend, cards in a headline). `margin` is the air around a chrome target that occupies a layout slot. `padding` is the frame. Legend position lives on `config.legend`. Two neighboring keys also change how much room the chart needs, so they are covered here too: `panel.overflow` and `style.graph({ textScale })`. ```tsx theme={null} style.graph({ padding: 32 }); style.header({ margin: { bottom: 10 } }); ``` ## Padding **style.graph.padding** `number | \{ top?: number; right?: number; bottom?: number; left?: number \}` (default: 24) Outer padding in pixels, authored on `style.graph`. A number applies to all four sides; an object (`{ top?, right?, bottom?, left? }`) sets sides one by one. Omitted edges keep the built-in 24. A side left out of the object keeps the 24px default, so this trims the left edge and opens up the top while the other two stay put: ```tsx theme={null} style.graph({ padding: { top: 40, left: 0 } }); ``` An explicit `0` removes a side's padding. Padding is in pixels and does not follow `textScale`. ## Margin Air around a chrome target that occupies a layout slot lives on that target's `margin`. Values are pixels and do not follow `textScale`. A number sets every side. An object sets single edges; omitted sides stay absent, so they do not overwrite earlier entries. `0` closes a side. ```tsx theme={null} style.header({ margin: { bottom: 10 } }); style.legend({ gap: 8, margin: 10 }); style.axisLabel.bottom({ margin: { top: 4, bottom: 16 } }); ``` When two boxes meet, the larger of the two facing margins is used, not their sum. A box the chart does not draw contributes none. A margin that faces the frame adds to `style.graph({ padding })`. `style.legend`, `style.axisLabel` and `style.tickLabel` take `.top` / `.right` / `.bottom` / `.left` as well as `.x` / `.y`. A bare call covers every instance; a later match decides. | Target | What `margin` spaces | | - | - | | `header` | The title + subtitle row | | `footer` | Caption, source and provenance | | `legend` | A pill legend (`gap` is still items inside) | | `headline` | The headline strip | | `axisLabel` | An axis title. `.left` / `.right` do not feed layout | | `tickLabel` | The tick band. `offset` stays; ticks have no built-in margin | The built-in entries: ```tsx theme={null} style.header({ margin: { bottom: 10 } }), style.headline({ margin: { bottom: 4 } }), style.legend.top({ margin: { bottom: 8 } }), style.legend.bottom({ margin: { top: 16, bottom: 10 } }), style.legend.left({ margin: { right: 10 } }), style.legend.right({ margin: { left: 10 } }), style.axisLabel.top({ margin: { bottom: 8 } }), style.axisLabel.bottom({ margin: { top: 4, bottom: 16 } }), style.tickLabel({ offset: 10 }), style.tickLabel.left({ offset: 14 }), style.tickLabel.right({ offset: 14 }), style.footer({ margin: { top: 16 } }), ``` A tight preset that wants less space below the title: ```tsx theme={null} style.header({ margin: { bottom: 4 } }); style.tickLabel({ offset: 6 }); ``` `style.header({ margin: 4 })` would also add 4px above the title, on top of the padding. `legend.margin` does nothing when no legend is drawn. Direct labels sit on the plot and read `style.directLabel`. ## Panel overflow The panel is the plot area. Data labels and difference arrows can reach past its edge. `panel.overflow` says what the engine does about it, per content type and per direction: ```tsx theme={null} config({ panel: { overflow: { dataLabels: { x: 'inside', y: 'outside' }, differenceArrows: { x: 'outside', y: 'outside' }, }, }, }); ``` **overflow\.dataLabels** `\{ x: PanelOverflowStrategy; y: PanelOverflowStrategy \}` (default: \{ x: 'inside', y: 'inside' }) Strategy for [data labels](/sdk-next/config/data-labels) that reach past the panel edge. **overflow\.differenceArrows** `\{ x: PanelOverflowStrategy; y: PanelOverflowStrategy \}` (default: \{ x: 'outside', y: 'outside' }) Strategy for difference-arrow annotations. Each direction takes one of three strategies: * `'outside'` — the element lands outside the panel and the panel shrinks to make room. * `'inside'` — the element stays inside the panel and the content within it shrinks instead. * `'none'` — nothing moves; the element may overlap whatever is beside it. `x` covers the left and right edges, `y` the top and bottom. Only cartesian graphs make room; a polar graph ignores `panel.overflow`. ## Text scale **style.graph.textScale** `number` (default: 1) A multiplier on every text size in the chart. `1.2` makes titles, axis labels, tick labels and data labels 20% larger. Authored on `style.graph`. ```tsx theme={null} styles({ defaults: [style.graph({ textScale: 1.2 })] }); ``` It changes measured text, and so the size of every region the engine arranges. `gap`, `offset`, `margin`, padding and stroke widths do not scale. > [!NOTE] > > Renderer contract: a [custom renderer](/sdk-next/extending/slots) must apply > `textScale` both when it measures text and when it renders it. The engine > assumes the measured sizes already include the multiplier, so applying it to > only one of the two breaks the layout. Slot `measure` callbacks receive it on > `SlotMeasureContext`. ## Related * [Styling](/sdk-next/config/styling) — the graph background, frame border, corner radius, `textScale`, outer `padding` and chrome `margin` * [Legend](/sdk-next/config/legend) — legend position stays on `config.legend` # Legend Source: https://docs.graphy.dev/sdk-next/config/legend The `legend` section controls where the legend sits and how it presents groups. A legend appears when a chart maps a visual aesthetic such as `color` or `size`. Under `position: 'auto'`, a legend with a single item is dropped on bar, line, area and polar graphs. ```tsx theme={null} config({ legend: { position: 'bottom', display: 'pill' }, }); ``` ## Options **position** `'auto' | 'top' | 'bottom' | 'left' | 'right' | 'none'` (default: auto) Where the legend sits. `'none'` hides it. `'auto'` resolves to `'right'` for a line, area or unflipped stacked bar graph with three or more items, and to `'top'` everywhere else, combo graphs included. **display** `'pill' | 'direct' | 'auto'` (default: auto) How groups are labeled: * `'pill'` — a boxed legend with a swatch and label per group. * `'direct'` — labels drawn next to each group's endpoint, with no legend box. Reads well on line charts. * `'auto'` — `'direct'` where the graph can anchor direct labels, `'pill'` otherwise. Direct labels need the legend on the right and a single layer on an unflipped cartesian graph: a line, an area, or bars with `position: 'stack'` or `'fill'`. Anywhere else `'direct'` falls back to `'pill'`. **align** `'auto' | 'start' | 'center' | 'end'` (default: auto) Placement of the items along the legend's flow: * A horizontal (`top`/`bottom`) legend runs along the row: `start` is left and `end` is right, on right-to-left pages too. `center` centers on the panel, in line with the axis title. * A vertical (`left`/`right`) legend runs down the column: `start` is top, `end` is bottom. * `'auto'` resolves to `start` for horizontal legends and `center` for vertical ones. Across the flow, a vertical legend always sits against the plot border. ```tsx theme={null} config({ legend: { position: 'bottom', align: 'end' }, }); ``` ## Direct labels Pin the legend to the right and ask for direct labels: ```tsx theme={null} config({ legend: { position: 'right', display: 'direct' }, }); ``` The labels sit on the plot. `style.directLabel` paints them, not the legend targets. ## Continuous color A `color` scale that runs a ramp rather than a set of hues gets a legend of its own, one pill per domain stop: the low and high ends, plus the midpoint of a diverging ramp, each in the color the ramp gives that value. It never merges with another aesthetic's legend and counts as one item for `'auto'` placement. `position`, `align` and `'none'` steer it like any other legend. ## Headline numbers and the legend A per-group [headline](/sdk-next/config/headline-numbers) names the same groups as a pill legend, so it takes the legend's place. Direct labels stay. ## Hiding the legend ```tsx theme={null} config({ legend: { position: 'none' } }); ``` ## Related * [Mappings & aesthetics](/sdk-next/concepts/mappings) — `color` and `group` create groups * [Scales](/sdk-next/concepts/scales#continuous-color) — the color scale that assigns groups their swatches, and the ramps behind a color bar * [Layout](/sdk-next/config/layout) — `style.legend` margin around a pill legend. Position stays on `config.legend`. # Number format Source: https://docs.graphy.dev/sdk-next/config/number-format The `numberFormat` section controls how numeric values are displayed in tooltips, data labels, legends and headlines. Axis ticks abbreviate on their own. The [renderer's locale](/sdk-next/quickstart#5-color-scheme-and-locale) sets the separators. What a value is (a currency, a percentage, a date) is the column's `valueFormat` in the [data](/sdk-next/data-structure#value-format-detection), not a config key. ```tsx theme={null} config({ numberFormat: { decimals: 0, abbreviation: 'auto' }, }); ``` ## Options **decimals** `number | 'auto'` (default: auto) Decimal places. A number fixes them (`2` gives `1234.56`); `'auto'` varies with magnitude. **abbreviation** `'auto' | 'k' | 'm' | 'b' | 'none'` (default: auto) How large numbers are shortened: * `'none'` — `1,234,567` * `'auto'` — `1.23m`, chosen by magnitude * `'k'` / `'m'` / `'b'` — force thousands / millions / billions **thousandsSeparator** `string` Declared on the type, but the formatters do not read it: the thousands separator always follows the locale. **decimalSeparator** `string` Declared on the type, but the formatters do not read it: the decimal separator always follows the locale. ## Example ```tsx theme={null} config({ numberFormat: { decimals: 1, abbreviation: 'none', }, }); ``` ## Related * [Configuration overview](/sdk-next/config/index) * [Configuration overview › Locale](/sdk-next/config/index#locale) — `parsingLocale`, the fallback for display formatting * [Quickstart › locale](/sdk-next/quickstart#5-color-scheme-and-locale) — the renderer's `formattingLocale` * [Interactivity › tooltips](/sdk-next/rendering/interactivity#tooltips) — per-row `format` on a layer's tooltip # Styling Source: https://docs.graphy.dev/sdk-next/config/styling Everything the chart draws (geoms, grid lines, tick marks, the panel border, the graph background, axis, tick and data-label type) is painted from a stylesheet on the spec. You add one by piping a `styles(...)` part in, the same way you pipe `config(...)`. ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style, token, } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'sales' }), geom.bar(), scale.x(), scale.y(), styles({ tokens: { brand: { light: '#0B5FFF', dark: '#6AA1FF' } }, defaults: [style.geom({ fill: token('brand'), stroke: token('brand') })], overrides: [ style.geom( { fill: '#E5484D', stroke: '#E5484D' }, { where: { variable: 'sales', lt: 0 } } ), ], }) ); ``` Because it lives on the spec, a stylesheet is serializable and travels with the chart, like mappings, scales and config. > [!NOTE] > > Theme tokens (the built-in light/dark chrome, selected by `colorScheme`) are a > separate surface covering the HTML chrome around the plot. See > [Theming](/sdk-next/rendering/theming). ## The four keys **defaults** `StyleRule[]` Applied where no mapped aesthetic decided the value: the look when nothing else speaks. **overrides** `StyleRule[]` Applied over whatever a mapping decided. **tokens** `Record\` Named colors that entries reference with `token('name')`. **extends** `Stylesheet[]` Composes other stylesheets underneath this one. Tokens merge name by name, lists concatenate, later entries decide. This is how a house style ships as a reusable preset. Piping several `styles(...)` parts stacks them in order, each above everything piped before it. Within a single list, order is specificity: the last matching entry that declares a property decides. ## How a value is resolved Each property resolves through three tiers, in order: | Tier | Source | | - | - | | **override** | an `overrides` entry whose conditions match | | **data** | the encoding: a mapped aesthetic (`color`, `size`, `alpha`, …) through its scale | | **default** | a `defaults` entry, with the engine's built-in stylesheet behind it | So `defaults` never fight your mappings, and `overrides` always do. To recolour a group that is mapped to `color` you need an `overrides` entry; a `defaults` entry loses to the scale. Entries scoped to a state (`{ state: 'hovered' | 'dimmed' }`) sit above the whole stateless cascade. ## Targets Call `style.(declarations, options?)`. Geom targets take conditions (`where`, `state`, `layer`). Annotation targets take `{ annotation: id }`. Chrome targets take declarations only. Every entry may also carry a `coord`. | Target | Partitions | Declarations | | - | - | - | | `style.geom` | `.bar` `.line` `.area` `.point` `.rule` `.tile`, plus `{ layer }` | `fill`, `overpaint`, `stroke`, `alpha`, `fillAlpha`, `strokeAlpha`, `cornerRadius`, `saturation`, `blur`, `brightness`, `contrast`, `shadow`, `blendMode` (a bar, area, point or tile fills; a line or rule strokes) | | `style.geom.bar` | | ↑ plus `strokeWidth`, `maxWidth` (pixels; cartesian bars only) | | `style.geom.line` | | ↑ plus `strokeWidth`, `dashArray`, `lineCap`, `lineJoin` | | `style.geom.area` | | ↑ plus `strokeWidth`, `dashArray`, `lineCap`, `lineJoin` | | `style.geom.point` | | ↑ plus `size`, `symbol`, `strokeWidth` | | `style.geom.rule` | `.label` | `stroke`, `strokeAlpha`, `strokeWidth`, `dashArray`, `lineCap`, `lineJoin`, `shadow`, `blendMode` | | `style.geom.rule.label` | | type: the `axisLabel` words without `margin` | | `style.geom.tile` | | ↑ plus `strokeWidth` | | `style.gridLine` | `.x` `.y` | `stroke`, `strokeWidth`, `dashArray` | | `style.tickLine` | `.x` `.y` | `stroke`, `strokeWidth`, `dashArray`, `length` | | `style.axisLabel` | `.x` `.y`; `.top` `.right` `.bottom` `.left` | `fontFamily`, `fontSize`, `fontWeight`, `fontStyle`, `letterSpacing`, `textTransform`, `textDecoration`, `textOutlineColor`, `textOutlineWidth`, `textShadow`, `lineHeight`, `textColor`, `margin` | | `style.tickLabel` | `.x` `.y`; `.top` `.right` `.bottom` `.left` | ↑ plus `offset`, `margin` | | `style.dataLabel` | `.observation` / `.category`, each `.inside` `.outside`; `.aggregate` | ↑ plus `paddingInline`, `paddingBlock`, `fill`, `stroke`, `strokeWidth`, `cornerRadius`, `shadow`, `alpha` | | `style.panelBorder` | `.top` `.right` `.bottom` `.left` | `stroke`, `strokeWidth`, `dashArray`; `cornerRadius` on the bare builder | | `style.graph` | | `fill`, `overpaint`, `stroke`, `strokeWidth`, `cornerRadius`, `shadow`, `alpha`, `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `textColor`, `textScale`, `padding`, `blendMode` | | `style.heading` | `.h1` `.h2` | `fontFamily`, `fontSize`, `fontWeight`, `fontStyle`, `letterSpacing`, `textTransform`, `textDecoration`, `textOutlineColor`, `textOutlineWidth`, `textShadow`, `lineHeight`, `textColor` | | `style.caption` | | ↑ | | `style.source` | `.link` | ↑ | | `style.header` | | `margin` only | | `style.footer` | | `margin` only | | `style.hoverGuide` | | `stroke` (the crosshair or spoke), `fill` (the band or wedge) | | `style.editOutline` | `.hover` `.selected` | `stroke`, `strokeAlpha`, `strokeWidth`, `dashArray` (the ink), `haloColor`, `haloAlpha` (the band under it, the graph background unless set), `offset`, `cornerRadius`; `.hover` before a click, `.selected` after | | `style.headline` | | `gap`, `margin` | | `style.headlineItem` | `.number` / `.number.center`, `.caption`, `.label`, `.swatch`, `.trend` plus `.up` `.down` `.flat` | `gap` on the bare entry, between a card's rows; type on text parts; `size` on swatch; `.trend` colors the reference text, `.up` `.down` `.flat` the percentage | | `style.legend` | `.popover`; `.top` `.right` `.bottom` `.left` | `gap`, `margin` on a bare or edge entry; `focusStroke` on a bare entry, the overflow button's focus outline; `.popover`: `fill`, `stroke`, `strokeWidth`, `cornerRadius`, `paddingInline`, `paddingBlock`, `shadow`, `alpha` | | `style.legendItem` | `.swatch` | type plus `gap` (swatch to label), `paddingInline`, `paddingBlock`, `fill`, `stroke`, `strokeWidth`, `cornerRadius`, `shadow`, `alpha`; `size` and `strokeWidth` on the swatch | | `style.directLabel` | | type plus `strokeWidth`, `dashArray` | | `style.tooltip` | `.heading` `.label` `.value` `.primaryRow` | box: `fill`, `stroke`, `strokeWidth`, `cornerRadius`, `paddingInline`, `paddingBlock`, `gap`, `shadow`, `alpha`; `gap` is the padding above and below each row, default 4; type on the text parts; `fill` on the primary row | | `style.annotation` | `.shape` `.arrow` `.differenceArrow` `.text` `.image` `.pinnedNumber` `.comment`, plus `{ annotation: id }` | `fill`, `stroke`, `alpha`, `fillAlpha`; each kind reads only the words in its own vocabulary | | `style.annotation.shape` | | `fill`, `fillAlpha`, `alpha`, `stroke`, `strokeWidth` | | `style.annotation.arrow` | `.outline` | `stroke`, `strokeWidth`, `dashArray`; `.outline`: `stroke`, `strokeWidth`, `shadow` | | `style.annotation.differenceArrow` | `.label` | `stroke`, `strokeWidth` | | `style.annotation.differenceArrow.label` | | the `dataLabel` words: type, paddings, `fill`, `stroke`, `strokeWidth`, `cornerRadius`, `shadow`, `alpha` | | `style.annotation.text` | | the `dataLabel` words plus `fillAlpha` | | `style.annotation.image` | | `alpha`, `cornerRadius`, `shadow` | | `style.annotation.pinnedNumber` / `.comment` | `.label` | `fill`, `size`, `stroke`, `strokeWidth`, `shadow` | | their `.label` part | | the `dataLabel` words | Notes on the vocabulary: * A bar's `cornerRadius` is a token, `'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'`, defaulting to `'sm'`, or a number of pixels: `style.geom.bar({ cornerRadius: 3 })`. A box rounds with `cornerRadius` in pixels, one number or one per corner: `dataLabel`, the tooltip, a legend pill, a text or image annotation and a callout label take `style.tooltip({ cornerRadius: { topLeft: 0, topRight: 8, bottomRight: 8, bottomLeft: 8 } })`; `panelBorder`, `style.graph` and the edit outline take one number. * A box is painted with `fill`, and its border with `stroke` and `strokeWidth`: the graph frame, the tooltip, a data label, a legend pill, a text annotation and a callout label all take the same words. * `style.graph({ fontFamily })` is the chart's base family: every text region without a family of its own follows it, in paint and in measurement. `fontSize`, `fontWeight`, `lineHeight` and `textColor` on `graph` are the same type words; the content targets declare their own. * `style.heading`, `style.heading.h1` and `style.heading.h2` are the title and the subtitle. A bare heading entry addresses both levels. Color, family and size on rich-text runs paint over this base type. `style.caption` and `style.source` are the footer type; `style.source.link` is the URL. * `style.graph({ textScale })` multiplies every text size at resolve time. `1` is the built-in; a preset can set a house scale through `extends`. `gap`, `offset`, `margin`, padding and stroke widths do not scale. * `style.graph({ padding })` is the outer frame, in pixels, and does not follow `textScale`. A number applies to every side; an object sets sides, and omitted edges keep the builtin 24. * `margin` is air around a chrome target that occupies a layout slot (`header`, `footer`, `legend`, `headline`, `axisLabel`, `tickLabel`). A number sets every side; omitted object sides stay absent, so they do not overwrite earlier entries. `0` closes a side. `gap` is the space between items inside a chrome target. See [Layout](/sdk-next/config/layout). The built-in tick `offset` is 10, left and right 14. * Every geom takes a `shadow` (`'none'` or `{ offsetX, offsetY, blur, color }`) and a `blendMode` (`'normal' | 'multiply' | 'screen' | 'overlay' | 'darken' | 'lighten'`); so does `style.graph` for the frame. A line, an area and the polar kinds read both once per layer, from the first observation. The built-in `hovered` state lifts the geom under the pointer with a soft shadow, bars with a larger one. `style.geom.point({ shadow: 'none' }, { state: 'hovered' })` drops it. * Every text target takes the same type words: `fontFamily`, `fontSize`, `fontWeight`, `fontStyle` (`'normal' | 'italic' | 'oblique'`), `letterSpacing` (pixels, negative allowed), `textTransform` (`'none' | 'uppercase' | 'lowercase' | 'capitalize'`), `textDecoration` (`'none' | 'underline' | 'line-through'`), `textOutlineColor` and `textOutlineWidth` (the width visible outside the glyphs), `textShadow` (the geom `shadow` value), `lineHeight` and `textColor`. Layout measures the string with its case and letter spacing applied, so a label reserves the space its glyphs take. Tick labels carry a built-in outline in the graph background, so a label stays legible where it crosses a grid line; `style.tickLabel({ textOutlineWidth: 0 })` removes it. * A line, an area and a rule take `lineCap` (`'butt' | 'round' | 'square'`) and `lineJoin` (`'miter' | 'round' | 'bevel'`). None has a built-in: a path keeps its rounded caps until an entry says otherwise. * Every geom takes `blur` (pixels) and `brightness`, `contrast` and `saturation` (multiples, `1` unchanged). A filter declared without a state draws on the geom itself. A filter declared with `{ state: 'dimmed' }` draws on the whole layer over the geoms already painted: `style.geom({ blur: 1, saturation: 0.3 }, { state: 'dimmed' })` blurs and grays a dimmed layer. * `fill` is a paint, not only a color. Beside a color, a token or a light-dark pair it takes: * a gradient: `{ gradient: 'linear', angle?, stops: [{ offset, color }, …] }` or `{ gradient: 'radial', stops }`, with at least two stops; * a pattern: `{ pattern: 'diagonal' | 'dots' | 'crosshatch' | 'lines', color, background?, size? }`, repeating every `size` screen pixels. `lines` draws one level line across the top third of each tile; * an image: `{ image, fit?, alpha?, size?, fallback }`, a data URI only. `fit: 'tile'`, the default, repeats it every `size` screen pixels wide at its own proportions; `fit: 'stretch'` fills the shape with one copy. `fallback` always paints behind the image; `alpha` affects only the image. Every color inside takes tokens. `stroke` stays a color. Bars, tiles, polar bars, polar areas, the graph's frame and HTML boxes draw the paint. The swatches of bars, tiles and polar bars draw it too when it covers the swatch's whole color group: declared for the layer, or by a `where` that reads only the color variable. A `where` on any other variable picks out observations, so it stays off the swatch. Everywhere else only one color can be drawn (a contrast pick, a hover marker, a line's wash, a cartesian area, a point, any other swatch), so it is picked from the paint: a gradient's first stop, a pattern's color, an image's fallback. * Every geom and `style.graph` take an `overpaint`: a paint drawn over what the target already drew. It takes what `fill` takes, a list of those with the first on top, or `'none'`: ```ts theme={null} const scanlines = { pattern: 'lines', color: 'rgba(0, 0, 0, 0.3)', size: 3 }; const grain = { image: grainDataUri, alpha: 0.2, fallback: 'transparent' }; styles({ defaults: [ style.graph({ overpaint: scanlines }), // over the whole graph at full strength, its text included style.geom.bar({ overpaint: scanlines }), // over each bar, whatever color it is mapped to style.geom.point({ overpaint: grain }), // grain over each point, its color showing through ], }); ``` Use `fallback: 'transparent'` to keep the chart's colors visible beneath an image overpaint. An opaque fallback covers them even when the image's `alpha` is low. A geom's overpaint is drawn over the shapes the geom paints it on: everything a built-in geom draws, and the main shapes of a geom from a package, such as a boxplot's boxes and not its whiskers, median or outliers. It is as strong as the paint under it: over a bar whose `fillAlpha` is `0.3` it is drawn at `0.3` too. It is drawn over the shapes as one, so it cannot vary by observation: an overpaint under `where` is ignored and the compiler warns about it. `{ state: 'hovered' }` sets the overpaint of the hover repaint. A marker drawn on the layer for a hover elsewhere keeps the layer's resting overpaint. The graph's overpaint does not reach the tooltip, which is a box of its own. A geom's overpaint is painted through a mask of the shapes it covers, so the browser draws those shapes twice. On a layer with many thousands of shapes, the graph overpaint costs less than a geom overpaint. Geom overpaints support SVG renderers only. An `overpaint` declared with `{ state: 'dimmed' }` also reports `INVALID_STYLE_RULE` and is ignored; other properties in the entry still apply, and the resting overpaint fades with the layer and takes its dim filter. The overpaint is part of the shapes it covers, so it casts the layer's shadow with them. * Every box takes a `shadow` and an `alpha` beside its `fill`: a data label, the tooltip, a legend pill and the legend popover, a text or image annotation, a callout or difference-arrow label, and `style.graph` for the frame. `style.tooltip({ shadow: { offsetX: 0, offsetY: 4, blur: 12, color: 'rgba(0, 0, 0, 0.2)' }, alpha: 0.95 })` lifts and fades the popover; the frame's `alpha` thins its background alone, so the plot keeps its opacity. * A point takes a `symbol` (`'circle' | 'square' | 'diamond' | 'triangle' | 'cross' | 'star' | 'wye'`), built in `circle`. Every symbol covers the area of the circle its `size` names, so `style.geom.point({ symbol: 'diamond' })` keeps the layer's weight; the hover marker, the highlight dot and the swatch draw the same symbol. * Hide a panel-border edge with `strokeWidth: 0`. The built-in `gridLine.x` and `tickLine` entries use the same `strokeWidth: 0`, so vertical grid lines and tick marks appear only once you give them a width (and tick marks a `length`). * A shape annotation's border wears its `fill` unless `stroke` is declared; `strokeWidth: 0` draws none, and `fill: 'transparent'` leaves a stroke-only outline. `{ annotation: id }` addresses one annotation. * An arrow annotation's sticker look is its own part, `style.annotation.arrow.outline({ stroke, strokeWidth, shadow })`: an outline around the line and a drop shadow, both off by default. * A difference arrow is two parts: the line (`strokeWidth` also sizes the route around the observations) and its label, `style.annotation.differenceArrow.label`. A color no entry names falls to the group color both ends share; the label's box border takes the arrow's color unless `stroke` is declared. * A text annotation's `fill` is its box; with none declared there is no box. The content's own marks paint over the base type. An image's `cornerRadius` clips its corners, in pixels. * A pinned number and a comment are a marker dot plus the label bubble beside it (`.label`). A marker whose `fill` no entry names borrows its observation's color; the swatch inside a pinned-number bubble always keeps it. * Stack totals (`.aggregate`) always sit outside the geom, so they take no `.inside` / `.outside`. * `style.headlineItem({ gap })` is the space between a card's rows; everything else on a card is a part. `.number.center` is the donut-hole figure; without it the hole uses `.number`. `gap` on `style.headline`, `style.headlineItem`, `style.legend` and `style.legendItem` is pixels and does not follow `textScale`. A bare `style.legend` entry takes `gap`, `margin` and `focusStroke`; the overflow popover box is `style.legend.popover` (`fill`, `stroke`, `strokeWidth`, `cornerRadius`, `paddingInline`, `paddingBlock`, `shadow`, `alpha`). * Swatch opacity and bar corner radius follow the layer geom, including size-legend bubbles; `style.legendItem.swatch` and `style.headlineItem.swatch` only size the square box. ## Colors Any color-valued property takes one of three forms: ```tsx theme={null} style.geom.bar({ fill: '#E5484D' }); // literal style.geom.bar({ fill: { light: '#E5484D', dark: '#FF6369' } }); // one per scheme style.geom.bar({ fill: token('alert') }); // token reference ``` `{ light, dark }` and `token(...)` resolve against the provider's [`colorScheme`](/sdk-next/rendering/theming). Prefer them to literals, which look the same in both schemes. ## Conditions Geom entries take a `where` predicate (the same language as [highlights](/sdk-next/emphasis/highlights)), a `state`, and a `layer`: ```tsx theme={null} styles({ overrides: [ style.geom( { fill: token('alert'), stroke: token('alert') }, { where: { variable: 'sales', lt: 0 } } ), style.geom.line({ strokeWidth: 4 }, { layer: 'total' }), style.geom({ alpha: 0.15 }, { state: 'dimmed' }), ], }); ``` `layer` scopes an entry to one authored layer `id`, which is how you style a single layer of a [combo chart](/sdk-next/graph-types/combo) without touching the others. Every entry, chrome included, takes a `coord`, which keeps it to graphs drawn in that coordinate system. This stylesheet rules off the left edge of a bar graph and leaves a pie without one: ```tsx theme={null} styles({ defaults: [ style.panelBorder.left({ strokeWidth: 2, dashArray: [] }), style.panelBorder.left({ strokeWidth: 0 }, { coord: 'polar' }), ], }); ``` `coord` is `'cartesian'` or `'polar'`; a flipped graph is cartesian. ## Re-skinning through tokens The engine's built-in stylesheet sits behind every chart, and its defaults are written in terms of tokens. Redefining a built-in token name restyles the default it backs, with no entries at all: ```tsx theme={null} styles({ tokens: { geom: '#1D1D1B', gridLine: '#E9E9E9', graphBackground: { light: '#FFFFFF', dark: '#141414' }, }, }); ``` | Token | Backs | | - | - | | `geom` | every geom's fill when nothing is mapped to `color` | | `geomBorder` | bar outlines | | `ruleLine` | reference, goal and average lines | | `hoverAffordance` | point outlines, and the hovered outline on bars, points and tiles | | `hoverGuideLine` | the hover guide's crosshair line | | `hoverGuideFill` | the hover guide's band fill | | `legendFocus` | the legend overflow button's focus outline | | `gridLine` | grid lines and the panel border | | `graphBackground` | the graph background, and the outline around tick labels | | `graphBorder` | the graph frame | | `differenceLabelBackground` | the box behind a difference-arrow label | | `textPrimary` | axis labels, data labels, direct labels, caption, tooltip heading and labels, annotation text, headline numbers, the edit outline's ink | | `textSecondary` | tick labels, legend items, source line, tooltip values, headline captions | | `tooltipBackground` | the tooltip box, and a callout's label and marker outline | | `tooltipBorder` | the tooltip box border, and a callout label's border | | `tooltipPrimaryRow` | the highlighted tooltip row | | `annotationShape` | shape annotations | | `annotationArrow` | arrow and difference-arrow annotations | | `trendPositive` | headline trend up | | `trendNegative` | headline trend down | | `trendNeutral` | headline trend flat | The built-in defaults these produce: bars `cornerRadius: 'sm'` with a 1px border; lines and areas `strokeWidth: 2`, `dashArray: []`; areas `fillAlpha: 0.3`; points `size: 8` with a 1px outline; tiles `cornerRadius: 8` and no border; rules dashed at 1px; the y grid lines dashed, the x grid lines and tick lines `strokeWidth: 0`; the panel border dashed with `cornerRadius: 6`; the graph frame with `cornerRadius: 8` and `padding: 24`; tick labels offset 10px; the `dimmed` state `alpha: 0.4`. ## Presets with `extends` `extends` composes stylesheets, which is how a house style becomes reusable: ```tsx theme={null} const houseStyle = { tokens: { geom: '#1D1D1B', gridLine: '#C9C6BE' }, defaults: [ style.gridLine({ dashArray: [] }), style.panelBorder({ strokeWidth: 0 }), style.tickLabel({ fontSize: 12 }), ], }; const spec = pipe( createSpec({ x: 'month', y: 'sales' }), geom.bar(), scale.x(), scale.y(), styles({ extends: [houseStyle], defaults: [style.geom.bar({ cornerRadius: 'full' })], }) ); ``` ## Two token namespaces `textPrimary` and `textSecondary` name both a stylesheet token and a theme token. They are different values in different namespaces: the stylesheet tokens above drive the plot, the theme tokens of the same name drive the chrome around it. ## When an entry is invalid An entry the engine cannot use is reported as an `INVALID_STYLE_RULE` warning and skipped; the chart still renders. Compile a spec headlessly to see warnings before wiring it into React. ## Custom geoms A [custom geom renderer](/sdk-next/extending/geom-renderers) reads paint through the value accessors on its render input (`getColor`, `getAlpha`, `getSize`, …), which expose the data tier. To resolve the full cascade, read `styleReaders` on the render input (or `useGeomStyleReader(layer)`): ```tsx theme={null} styleReaders.get('color', observation); ``` > [!WARNING] > > The render input already carries the graph's color scheme. A geom that paints more than one part declares each one, and the kit adds a builder for it: `kit.style.geom.lollipop.stem({ stroke: token('brand') })`. See [what the geom paints](/sdk-next/extending/custom-geoms#what-the-geom-paints). # Data structure Source: https://docs.graphy.dev/sdk-next/data-structure Your data and your spec are separate. Data is a plain table you pass to `GraphProvider` as the `data` prop. The spec names that table's columns by key through its [mapping](/sdk-next/quickstart#2-render-your-first-chart). So one spec can draw many datasets, and one dataset can be drawn many ways. ## Basic structure Data is a table with declared columns and rows: ```tsx theme={null} const data = { columns: [ { key: 'category', label: 'Category' }, { key: 'value', label: 'Value' }, ], rows: [ { category: 'A', value: 100 }, { category: 'B', value: 200 }, { category: 'C', value: 150 }, ], }; ``` The `columns` array defines the shape; `rows` holds the values. ## Referencing columns from a spec A spec never holds data. It names the columns it needs in its mapping. The strings you pass to `mapping` (or the `createSpec` shorthand) are column `key`s: ```tsx theme={null} const spec = pipe( // 'category' and 'value' are column keys createSpec({ x: 'category', y: 'value' }), geom.bar(), scale.x(), scale.y() ); ``` The binding is by key, so a column's `label` is display text only. Legends and tooltips show it, and fall back to the key when there is none. An axis title comes from `config({ axes: { y: { label: '…' } } })`; without one, the axis shows the column's `label`, or no title when the column has none. > [!NOTE] > > Row keys must exactly match a column `key`. Values under keys with no matching > column definition are ignored. ## Value format detection Every column gets one **value format**, which drives how a scale interprets it and how the renderer formats it. Inference runs per column, in two passes: 1. **The year pass.** The engine looks at the first 5 non-empty rows. The first undeclared column with at least two non-empty cells there, all whole numbers from 1800 to 2199, becomes a temporal `year` column. This keeps `2021, 2022, 2023` off a numeric axis. If any column declares `valueFormat: { type: 'year' }`, the pass is skipped. 2. **The first non-empty cell.** Every other undeclared column takes its format from its first non-empty cell alone, tried in this order: number → date → weekly date range → percentage → currency → text. A `Date` object is a date. Every cell in the column is then parsed under that one format. A cell that does not fit becomes `null`, with no warning. So row order matters: ```tsx theme={null} // value → decimal (the first cell is 12); 'n/a' parses to null rows: [{ value: '12' }, { value: 'n/a' }, { value: '15' }]; // value → text (the first cell is 'n/a'); all three stay strings, and the // column lands on a discrete scale rows: [{ value: 'n/a' }, { value: '12' }, { value: '15' }]; ``` If a column looks empty on the chart, check its first non-empty cell first. ### Declaring a format The same cell can mean two things: `2010` is a headcount or a year, `'1'` a category or a number. When you know, set `valueFormat` on the column. A declared column skips inference and is parsed and formatted under that format: ```tsx theme={null} columns: [ { key: 'headcount', valueFormat: { type: 'integer' } }, // 2010, 2050, 2100 stay counts, not years { key: 'cluster', valueFormat: { type: 'text' } }, // '1', '2', '3' stay categories { key: 'price', valueFormat: { type: 'currency', iso: 'usd' } }, // bare numbers format as dollars ], ``` Any format below is accepted. A temporal one can carry the `dateFormat` its cells use, such as `{ type: 'date', dateFormat: 'dd-MM-yyyy' }`. A cell that does not fit becomes `null`, as under inference. Declare a numeric format for every count column whose values could pass for years. Declaring the `year` column as `integer` keeps that column numeric, but the year pass still checks its undeclared siblings: an undeclared `population` column holding `1950, 2000` would become dates. Declare both to keep both numeric: ```tsx theme={null} const data = { columns: [ { key: 'year', valueFormat: { type: 'integer' } }, { key: 'population', valueFormat: { type: 'integer' } }, ], rows: [ { year: 2020, population: 1950 }, { year: 2021, population: 2000 }, ], }; ``` Only a declaration with `type: 'year'` stops the year pass for the whole table; an `integer` declaration does not. ### Numbers Numbers and numeric strings are read as numbers. Thousands separators, decimals and magnitude suffixes are supported: ```tsx theme={null} { category: 'A', value: 100 } { category: 'B', value: '1,250.50' } { category: 'C', value: '2.5m' } // k, m, b, t suffixes supported ``` ### Dates Date-like strings are parsed in many formats: ISO dates, named months and locale-specific orderings: ```tsx theme={null} { date: '2024-01-15', value: 100 } // YYYY-MM-DD { date: 'January 2024', value: 200 } // month name + year { date: '15/01/2024', value: 150 } // DD/MM/YYYY (en-GB, the default locale) { date: '01/15/2024', value: 150 } // MM/DD/YYYY (en-US) { date: '2024-01-15T12:30:00Z', value: 300 } // ISO datetime { date: 'Q1 2024', value: 400 } // quarter ``` Short month names (`Jan`, `Feb`) work as well as full names. `data._metadata.parsingLocale` decides whether an ambiguous date like `01/02/2024` is read day-first (`en-GB`, `pt-PT`, `ar`) or month-first (`en-US`). The first non-empty cell fixes one date format for the whole column, so a column that mixes `2024-01-15` with `15/01/2024` loses the cells that do not match. Weekly date ranges are also detected: `1 Feb – 7 Feb` or `1 Feb 2024 – 7 Feb 2024`, each a 7-day span. #### Dates without a year A format with no year — `month` (`Jan`), `day_month` (`14 Jan`) or a weekly range without one — gets a synthetic year. The engine walks the column in row order, one sequence per [group](/sdk-next/concepts/mappings#what-splits-data-into-groups). The sequence starts in the current year. Each time a date falls earlier in the year than the one before it, the year advances. So `Jan … Dec, Jan, Feb` reads as 14 consecutive months instead of folding back on itself, and reordering the rows moves the axis. ### Percentages Values with a `%` suffix are percentages, stored as fractions: `'64.5%'` becomes `0.645`. The renderer multiplies by 100 when it formats, so the axis reads as you wrote it. ```tsx theme={null} { category: 'Desktop', share: '64.5%' } { category: 'Mobile', share: '28.3%' } ``` A bare number in a percentage column is not scaled, so a `12` among `'64.5%'` values renders as 1200%. Write `'12%'`. ### Currencies Currency strings are parsed by their symbol. Supported symbols: `$`, `€`, `£`, `¥`, `₹`, `₱`, `₩`, `₪`, `₫`, `₽`, `฿`, `₦`, `₺`, `zł`, `kr`, `Fr`, `R`, `P`, `R$`, `Rp`, `RM`, `د.إ`, `﷼`, `Ch$`, `NT$`, `HK$`, `S$`, `A$`, `C$`, `NZ$`, `MX$`. ```tsx theme={null} { product: 'A', price: '$1,250' } { product: 'B', price: '£2,450.50' } { product: 'C', price: '€3,000' } ``` Symbols can appear as prefix or suffix, and negative values are supported (`-$100`, `$-100`). ### Text Anything else is text. ## How formats reach scales The format feeds the scale you declare for each aesthetic. A bare position scale (`scale.x()`) picks its type from the column's format. For `x`, `y` and `ySecondary`: * A **text** column → a discrete (band) scale. * A **numeric**, **percentage** or **currency** column → a continuous scale. * A **date** column → a `datetime` scale. The visual aesthetics read the same formats differently. On `color`, a text column infers a discrete scale; a numeric or date column infers a continuous one. On `size`, `alpha` and `strokeWidth`, a date column infers continuous: timestamps become plain numbers, since a ramp needs no date-aware ticks. On `lineType`, every format infers discrete. An aesthetic bound to a constant (`{ value: 5 }`), left unmapped, or pointing at a column that is not in the data falls back to continuous. An explicit method — `scale.x.continuous()`, `scale.x.discrete()`, `scale.x.datetime()` — overrides the inference. It must still fit the column's format: a continuous scale on a date column fails to compile with `INCOMPATIBLE_TYPE`. See the [line chart guide](/sdk-next/graph-types/line) for worked examples. ## Empty and missing values A cell is empty when it is `null`, `undefined`, an empty or whitespace-only string, or `'-'`. Empty cells become `null` in the parsed column. Two rules follow: * A row whose every cell is empty is dropped. * A column with no non-empty cell at all is dropped, so nothing can map to it. Each geom handles the gaps its own way. For lines, the `missingValues` param breaks the path, bridges it, or treats the gap as zero. ```tsx theme={null} rows: [ { month: 'Jan', revenue: 12000 }, { month: 'Feb', revenue: null }, // missing { month: 'Mar', revenue: 18000 }, ]; ``` ## Schema **data.columns** `Column[]` *required* The column definitions. **columns\[].key** `string` *required* A unique, stable name for the column. Row objects use it as their key, and a spec's mapping references it. **columns\[].label** `string` Display text for legends, tooltips and, when the axis config sets no label, the axis title. Legends and tooltips fall back to the `key`. **columns\[].valueFormat** `ExplicitValueFormat` How to read the column's cells when you know better than inference, for example `{ type: 'integer' }`, `{ type: 'text' }` or `{ type: 'currency', iso: 'usd' }`. A declared column skips inference; see [Declaring a format](#declaring-a-format). **data.rows** `Row[]` *required* The rows. Each is an object keyed by column key. A value is a `string`, `number`, `Date` or `null`. **data.\_metadata** `object` Host state carried with the table. The engine reads `parsingLocale`; the other keys are set by the Graphy editor and are not part of the SDK. **properties** **parsingLocale** `'en-GB' | 'en-US' | 'ar' | 'pt-PT'` Locale used to infer column formats and parse cells (dates, numbers). Defaults to `'en-GB'`. * `'en-GB'`, `'pt-PT'`, `'ar'`: DD/MM/YYYY date format * `'en-US'`: MM/DD/YYYY date format > [!NOTE] > > There are two `parsingLocale` settings with different jobs. > `data._metadata.parsingLocale` (default `'en-GB'`) decides how data cells are > parsed. `config({parsingLocale})` (default `'en-US'`) decides how values > written in the spec, such as highlight predicates and annotation anchors, are > read. It is also the display locale unless `formattingLocale` on > `GraphProvider` overrides it. The config one does not change how a data cell > is read. # Annotations Source: https://docs.graphy.dev/sdk-next/emphasis/annotations Annotations draw callouts over the plot: a shaded region, a labeled arrow, a note pinned to a spike. They carry information the data does not contain, and the `annotation` builder pipes them into the spec like any other feature. Annotations resolve again on every compile. Panel-anchored ones re-flow when the chart resizes; data-anchored ones move with the observation they are pinned to. ## Anchoring An anchor positions every annotation. It describes a relationship to the graph, not a fixed pixel. Anchors come in two shapes: a point for the kinds that sit somewhere, and a region for the kinds that fill an area. ### Point anchors `PointAnchorSpec` is a union of five `anchorType`s: | `anchorType` | Fields | Resolves to | | - | - | - | | `'panel'` | `x`, `y`, `offset?` | A fraction of the plot rectangle, `[0, 1]` from the top-left. Doesn't snap to data, so it holds its place as the data changes | | `'observation'` | `layerId?`, `anchorValue`, `groupValue?`, `crossValue?`, `align?`, `offset?` | A point on one observation's box. The annotation follows that observation wherever it lands | | `'axis'` | `x`, `y`, `align?`, `offset?` | A pair of axis *values*, mapped through the position scales. Dropped when either fails to map — a value outside a discrete domain, a missing scale, or polar coords | | `'selection'` | `predicate`, `align`, `offset?` | A point on the box of everything a predicate matches — the [same predicate language highlights use](/sdk-next/emphasis/highlights#predicates). Dropped when nothing matches | | `'annotation'` | `ref`, `align?`, `offset?` | A point on another annotation's box. Dropped on a missing `ref` or a reference cycle | ```tsx theme={null} // floats at the top-center of the plot { anchorType: 'panel', x: 0.5, y: 0 } // pins to the 'Jun' observation of the 'North' group { anchorType: 'observation', anchorValue: 'Jun', groupValue: 'North' } // sits at the data coordinates (Jun, 3000), wherever the scales put them { anchorType: 'axis', x: 'Jun', y: 3000 } // sits above whatever the predicate matches { anchorType: 'selection', predicate: { variable: 'sales', gte: 2000 }, align: 'top' } ``` #### Addressing a cell or a point An observation anchor normally narrows by group: `anchorValue` names the position on the main axis and `groupValue` picks the group. On a [heatmap](/sdk-next/graph-types/heatmap) or a [scatter](/sdk-next/graph-types/scatter) one main-axis value names several observations, a whole column of cells or every point at that x, so `crossValue` names the value on the other axis that picks one out: ```tsx theme={null} // the Q3 × Beta cell of a tile layer { anchorType: 'observation', anchorValue: 'Q3', crossValue: 'Beta' } ``` Only a layer whose geom places observations in two dimensions (tiles and points) reads `crossValue`; every other layer ignores it. A tile layer has no groups, so it ignores `groupValue`. A cell anchor resolves to the center of its tile. ### Region anchors `RegionAnchorSpec` is a union of three: | `anchorType` | Fields | Resolves to | | - | - | - | | `'panel'` | `x`, `y`, `width`, `height` | A rectangle in panel fractions, top-left origin | | `'selection'` | `predicate`, `padding?` | The tight bounding box of everything the predicate matches, grown by `padding`. Dropped when nothing matches | | `'annotation'` | `ref` | A copy of another annotation's box. Dropped on a missing `ref`, a cycle, or a zero-area target | `padding` is a number (both axes, in panel fractions) or an `AnchorOffset`. Omit it and a selection region gets `{ x: 8, y: 8, unit: 'px' }`, so a box around bars does not touch them. ```tsx theme={null} annotation.shape({ region: { anchorType: 'selection', predicate: { variable: 'quarter', oneOf: ['Q3', 'Q4'] }, }, zOrder: 'background', }); ``` ### `align` and `offset` `align` names which point of a box an anchor resolves to: ``` 'center' | 'top' | 'right' | 'bottom' | 'left' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' ``` Omit it on a data anchor and you get the geom's natural point, such as the midpoint of a bar's top edge. `offset` is a nudge applied once the target has resolved: `{ x?, y?, unit?: 'panel' | 'px' }`, with `unit` defaulting to `'panel'`. Pixel offsets are resolved at render time, against the measured box. ### Naming the layer On a combo chart, more than one layer can hold the same address: bars and a trend line both have a value at `'Jun'`. Give the layer you mean an id and pass it as `layerId`: ```tsx theme={null} pipe( createSpec({ x: 'month' }), geom.bar({ id: 'bars', aes: { y: 'sales' } }), geom.line({ aes: { y: 'trend' } }), annotation.pinnedNumber({ at: { layerId: 'bars', anchorValue: 'Jun' } }) ); ``` Omit `layerId` and the anchor resolves against the first layer that matches, skipping layers marked `interactive: false`. A named layer is searched whatever it is. An id no layer carries drops that annotation with an `UNKNOWN_LAYER_ID` warning rather than moving it somewhere else. The kinds differ in what they take: `pinnedNumber` and `comment` take a bare `ObservationAnchorSpec`, the fields above with no `anchorType` key, while `text`, `sticker` and `arrow` take a full `PointAnchorSpec`, where `anchorType` is required. ### Ids and references An annotation's `id` is unique across every kind, and an `annotation` anchor's `ref` points at it. The first annotation to declare an id keeps it; a second one declaring the same id gets a generated one plus an `ANNOTATION_DUPLICATE_ID` warning. Only ids you write yourself can be referenced. A `ref` naming nothing raises `ANNOTATION_REF_NOT_FOUND` and drops the annotation that referred to it. ### One attachment per observation An observation carries at most one of `sticker`, `pinnedNumber`, `comment` or `image`. Adding a second displaces the first: a comment replaces the pinned number on its bar rather than joining it. `AddAnnotationCommand` reverts by putting the displaced annotation back, so one undo restores both halves of a replacement. An image has no observation region anchor of its own, so it never displaces anything. ## The annotation kinds Each method on `annotation` adds one callout. Multiple calls of the same kind accumulate. | Method | What it draws | | - | - | | `annotation.differenceArrow()` | A labeled delta between **two observations** — reads the measured gap and labels it as an absolute, relative, or proportional change | | `annotation.shape()` | A shaded rectangle over a region — the classic "forecast" or "recession" band | | `annotation.arrow()` | A free-standing arrow between two point anchors, each floating or pinned | | `annotation.text()` | A free rich-text label | | `annotation.image()` | An image placed in a region | | `annotation.sticker()` | A built-in emoji-like sticker pinned to a point | | `annotation.pinnedNumber()` | A dot on one observation; its value shows in a mini view, full tooltip on hover | | `annotation.comment()` | A dot on one observation carrying rich-text content | ### Difference arrow A difference arrow spans two observations and labels the change between them. You choose what the label measures: ```tsx theme={null} pipe( annotation.differenceArrow({ id: 'growth', start: { anchorValue: 'Jan', groupValue: 'North' }, end: { anchorValue: 'Jun', groupValue: 'North' }, label: 'relative-difference', // 'absolute-difference' | 'relative-difference' | 'proportion' labelCrossPosition: 0.5, // where the label sits along the arrow, as a fraction of its length }), styles({ overrides: [ style.annotation.differenceArrow( { strokeWidth: 3 }, { annotation: 'growth' } ), style.annotation.differenceArrow.label( { fontSize: 16, paddingInline: 8 }, { annotation: 'growth' } ), ], }) ); ``` This is distinct from `annotation.arrow()`, a plain arrow between two positions you specify, which reads no data. A difference arrow's paint comes from the [stylesheet](/sdk-next/config/styling). `style.annotation.differenceArrow({ stroke, strokeWidth })` styles every arrow's line; `style.annotation.differenceArrow.label({ fontSize, fontWeight, textColor, fill, stroke, strokeWidth, cornerRadius, paddingInline, paddingBlock, ... })` styles its label; and `{ annotation: id }` in the options narrows an entry to one arrow. With nothing declared the line is 2px and the label an 11.5px boxed number, and the route around the observations scales with `strokeWidth`. An arrow with no declared `stroke` takes the color its two ends share, when both sit on one layer and one group with the same color; otherwise it is a dark stroke (light under a dark scheme). The label's box border takes the arrow's color unless the label declares its own `stroke`. > [!NOTE] > > A difference only means something when both ends measure the same thing. > Endpoints on different bases, such as one on the primary y axis and one on the > secondary, or a revenue value against a margin value in the same reshaped > column, raise an `INCOMPARABLE_ARROW_ENDPOINTS` warning. The arrow still > draws, so you can see what it claims. ### Shaded region ```tsx theme={null} pipe( annotation.shape({ id: 'forecast', region: { anchorType: 'panel', x: 0, y: 0.7, width: 1, height: 0.3 }, zOrder: 'background', // beneath the geoms; 'foreground' draws on top }), styles({ overrides: [ style.annotation.shape( { fill: '#e15759', alpha: 0.12 }, { annotation: 'forecast' } ), ], }) ); ``` `zOrder` decides whether a shape or image sits behind the data (`'background'`) or in front of it (`'foreground'`, the default). A wash of color under the geoms needs `'background'` written out, as above. | Field | Type | Default | | - | - | - | | `kind` | `'rectangle'` | `'rectangle'` | | `zOrder` | `'background' \| 'foreground'` | `'foreground'` | A shape's paint comes from the [stylesheet](/sdk-next/config/styling): `style.annotation.shape({ fill, fillAlpha, alpha, stroke, strokeWidth })` styles every shape, and `{ annotation: id }` in the options narrows an entry to one. With nothing declared a shape is a quarter-strength blue wash inside a 1px border of the same blue at full strength. The border takes `fill` unless `stroke` says otherwise, `strokeWidth: 0` removes it, and `fill: 'transparent'` leaves only the border. ### Text A text annotation takes a [rich-text node](/sdk-next/config/content) and a point anchor. `width` is a fraction of the plot width; height follows the content. ```tsx theme={null} annotation.text({ content: { type: 'doc', content: [ { type: 'paragraph', content: [{ type: 'text', text: 'Product launch' }], }, ], }, at: { anchorType: 'panel', x: 0.4, y: 0.1 }, width: 0.25, }); ``` `at` places the point of the text's own box that `align` names, `'center'` by default, so the sample above centers the block on `(0.4, 0.1)`. Pass `align: 'top-left'` to hang it from that corner instead. | Field | Type | Default | | - | - | - | | `align` | one of the nine `AnchorAlign` values | `'center'` | A text annotation's box and base type come from the [stylesheet](/sdk-next/config/styling): `style.annotation.text({ fill, fillAlpha, alpha, stroke, strokeWidth, cornerRadius, paddingInline, paddingBlock, fontFamily, fontSize, fontWeight, lineHeight, textColor })` styles every text annotation, and `{ annotation: id }` in the options narrows an entry to one. With nothing declared the words sit on no box at all, in 15px medium type. Declare a `fill` and the box appears, with a border a shade darker than the fill unless `stroke` says otherwise; `fillAlpha` fades the fill and `alpha` the whole box. The content's own formatting (a color, a font, a size) paints over the base type, so a size in the content is a multiple of the stylesheet's `fontSize`. ### Arrow `annotation.arrow()` draws a plain arrow between two point anchors. Either end can float in panel fractions or pin to data: ```tsx theme={null} pipe( annotation.arrow({ id: 'to-june', start: { anchorType: 'panel', x: 0.3, y: 0.2 }, end: { anchorType: 'observation', anchorValue: 'Jun', align: 'top' }, endArrowheadStyle: 'line-arrow', // 'none' | 'line-arrow' }), styles({ overrides: [ style.annotation.arrow( { stroke: '#e15759', strokeWidth: 8, dashArray: [1.5, 3] }, { annotation: 'to-june' } ), ], }) ); ``` | Field | Type | Default | | - | - | - | | `startArrowheadStyle` | `'none' \| 'line-arrow'` | `'none'` | | `endArrowheadStyle` | `'none' \| 'line-arrow'` | `'line-arrow'` | An arrow's paint comes from the [stylesheet](/sdk-next/config/styling): `style.annotation.arrow({ stroke, strokeWidth, dashArray })` styles every arrow, and `{ annotation: id }` in the options narrows an entry to one. With nothing declared an arrow is a solid 4px dark line (light under a dark scheme). `strokeWidth` is the width on a wide panel, scaled down on narrow ones, and the arrowheads scale with it. The sticker look is the arrow's outline part, `style.annotation.arrow.outline({ stroke, strokeWidth, shadow })`: an outline around the line plus a drop shadow. The outline picks a color that reads against the arrow unless `stroke` is declared. An endpoint on a data anchor with a directional `align` and no `offset` of its own is backed off 4px from the target's edge, so an arrow pointing at a bar stops just short of touching it. Supply an explicit `offset` to place it yourself. ### Image Place an image in a panel region. `fit` decides how it scales inside the box: `'fill'` stretches, `'contain'` letterboxes, `'cover'` crops to fill. ```tsx theme={null} annotation.image({ src: 'https://example.com/logo.png', // URL or data URI region: { anchorType: 'panel', x: 0.05, y: 0.08, width: 0.24, height: 0.5 }, fit: 'contain', // 'fill' | 'contain' | 'cover' }); ``` Like a shape, an image takes an optional `zOrder` (`'background'` or `'foreground'`, default `'foreground'`). `fit` defaults to `'contain'`. An image's paint comes from the [stylesheet](/sdk-next/config/styling): `style.annotation.image({ alpha, cornerRadius, shadow })` styles every image, and `{ annotation: id }` in the options narrows an entry to one. With nothing declared an image is opaque with square corners and no shadow; `cornerRadius` rounds the corners, in pixels. ### Sticker A sticker pins a built-in emoji-like image to one observation. Give it a `sticker` id from the catalog and an observation anchor, and it travels with that observation through re-sorts and filters: ```tsx theme={null} annotation.sticker({ sticker: 'rocket', at: { anchorType: 'observation', anchorValue: 'Q4' }, }); ``` The catalog holds five ids: `'rocket'`, `'thumbs-up'`, `'thumbs-down'`, `'clapping-hands'` and `'grinning-face'`. Any other id renders nothing. ### Pinned number and comment Both pin a marker dot to one observation and place a small bubble beside it. A pinned number shows the observation's formatted value behind a swatch; a comment shows its text on one line, cut with an ellipsis when long. Hovering a bubble opens the full tooltip. Both take a bare observation anchor: ```tsx theme={null} annotation.pinnedNumber({ at: { anchorValue: 'Q4' } }); annotation.comment({ at: { anchorValue: 'Q2' }, content: doc }); ``` Their paint comes from the [stylesheet](/sdk-next/config/styling), in two parts per kind. The marker is `style.annotation.pinnedNumber({ fill, size, stroke, strokeWidth, shadow })`, or `style.annotation.comment(...)`, and the bubble is their `.label(...)` part, which takes the `dataLabel` properties plus `shadow`. `{ annotation: id }` in the options narrows an entry to one. A marker with no declared `fill` takes the observation's color, and the swatch inside a pinned-number bubble always keeps that color. ## Editing annotations at runtime Four [commands](/sdk-next/advanced/commands-and-history) cover editing: | Command | Params | | - | - | | `AddAnnotationCommand` | `kind`, `annotation`, `index?` | | `RemoveAnnotationCommand` | `id` | | `UpdateAnnotationCommand` | `kind`, `id`, `patch` | | `MoveAnnotationCommand` | `id`, `translation` | `MoveAnnotationCommand` is the drag path: a translation in panel fractions, clamped inside the panel on apply. It is relative, so applying it twice moves twice. It reverts through the inverse patch rather than the opposite translation, because clamping cannot be inverted. `AddAnnotationCommand` takes a fully resolved annotation, so callers spread the kind's defaults: `{ ...SHAPE_DEFAULTS, region }`. Only `id` is optional, and it is settled in the constructor so a serialized command replays to the same spec. ## Related * [Reference lines](/sdk-next/emphasis/reference-lines) — for marking a constant value rather than a region * [Highlights](/sdk-next/emphasis/highlights) — the predicate language `selection` anchors take * [Content](/sdk-next/config/content) — the rich-text node shape used by text and comment annotations * [Mappings & aesthetics](/sdk-next/concepts/mappings) — the groups and x-values an observation anchor names # Highlights Source: https://docs.graphy.dev/sdk-next/emphasis/highlights A highlight emphasizes the observations that match a predicate by dimming the ones that don't. You describe what to match, not which pixels to paint, so the highlight follows the data through re-sorts, filters and updates. ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, highlight, } from '@graphysdk/react'; const data = { columns: [{ key: 'quarter' }, { key: 'region' }, { key: 'sales' }], rows: [ { quarter: 'Q1', region: 'EU', sales: 1200 }, { quarter: 'Q1', region: 'US', sales: 1500 }, { quarter: 'Q2', region: 'EU', sales: 1800 }, { quarter: 'Q2', region: 'US', sales: 1600 }, { quarter: 'Q3', region: 'EU', sales: 2400 }, { quarter: 'Q3', region: 'US', sales: 2200 }, { quarter: 'Q4', region: 'EU', sales: 2800 }, { quarter: 'Q4', region: 'US', sales: 3100 }, ], }; export function RegionalSales() { const spec = useMemo( () => pipe( createSpec({ x: 'quarter', y: 'sales', color: 'region' }), geom.bar({ position: 'dodge' }), scale.x(), scale.y(), scale.color.discrete(), highlight({ variable: 'region', eq: 'EU' }) // EU bars stay vivid, the rest fade ), [] ); return ( ); } ``` ## Predicates The first argument to `highlight()` is a predicate: a test against your post-transform columns. A field predicate names a `variable` and a comparison: | Predicate | Matches | | - | - | | `{ variable: 'region', eq: 'EU' }` | exact value | | `{ variable: 'quarter', oneOf: ['Q3', 'Q4'] }` | any of a set | | `{ variable: 'sales', gt: 2000 }` | `gt` / `gte` / `lt` / `lte` — ordered comparison | | `{ variable: 'sales', range: [1500, 2500] }` | inclusive numeric range | > [!NOTE] > > Ordered operators (`gt`, `gte`, `lt`, `lte`, `range`) need a numeric or date > column. Using one on a text column is an `INVALID_PREDICATE_OPERATOR` warning > that drops the whole highlight; the chart still renders without it. Naming a > column the data doesn't have (`UNKNOWN_VARIABLE`) does the same, and one bad > leaf inside an `and` or `or` drops the highlight it belongs to. Both arrive as > [diagnostics](/sdk-next/concepts/how-a-chart-is-built). There is no `neq` here; write `{ not: { variable: 'region', eq: 'EU' } }` instead. The `neq` operator on [`transform.filter`](/sdk-next/advanced/transforms) belongs to a separate comparison vocabulary. Combine predicates with `and`, `or` and `not`: ```tsx theme={null} highlight({ and: [ { variable: 'region', eq: 'EU' }, { variable: 'quarter', oneOf: ['Q3', 'Q4'] }, ], }); // EU, but only in the second half ``` ## Scope By default a highlight matches single observations. The `scope` option widens each match to a larger unit: | Scope | Effect | | - | - | | `'data-point'` *(default)* | Only the rows that satisfy the predicate; the rest of their group stays dimmed | | `'series'` | Any matching row expands to its whole group — highlight an entire line, area, or set of bars | | `'x-value'` | Any matching row expands to every observation sharing its x — highlight a vertical slice across groups | ```tsx theme={null} highlight({ variable: 'region', eq: 'EU' }, { scope: 'series' }); ``` ## Scoping to one layer In a multi-layer chart (bars plus a trend line, say), bind the highlight to one layer: give the layer an `id` and name it with `layerId`. Observations in other layers are never tested, so a Q4 highlight on the bars leaves the trend line alone: ```tsx theme={null} pipe( createSpec({ x: 'month' }), geom.bar({ id: 'bars', aes: { y: 'sales' } }), geom.line({ aes: { y: 'trend' } }), // ...scales highlight( { variable: 'month', oneOf: ['Oct', 'Nov', 'Dec'] }, { layerId: 'bars' } ) ); ``` Omit `layerId` to test the predicate against every layer. A layer you never name gets an id at resolve time, so scoping is the one case where naming it yourself matters. ## Combining highlights Multiple `highlight()` calls accumulate, and the engine unions their matches: an observation matched by any highlight is emphasized. Two separate highlights behave like an `or`: ```tsx theme={null} pipe( // ... highlight({ variable: 'sales', gte: 2000 }), highlight({ variable: 'region', eq: 'US' }) // rows ≥ 2000 OR any US row ); ``` The union is exactly an `or`, so this equals a single highlight with an `or` predicate: the same observations match and the result is the same: ```tsx theme={null} pipe( // ... highlight({ or: [ { variable: 'sales', gte: 2000 }, { variable: 'region', eq: 'US' }, ], }) ); ``` Use one `or` predicate when the two conditions are one idea. Use two highlights when they are independent and you may toggle them separately. ## Styling the de-emphasis Every non-matched observation carries the `dimmed` [style state](/sdk-next/config/styling), and one stylesheet entry decides what that state looks like. The built-in stylesheet dims to `alpha: 0.4`. Put your own entry in `overrides`, where it decides the cascade: ```tsx theme={null} styles({ overrides: [style.geom({ saturation: 0, alpha: 0.6 }, { state: 'dimmed' })], }); ``` `alpha` lowers the opacity of non-matched observations; `saturation: 0` drains their color to gray. `SetHighlightDimStyleCommand` makes the same choice as a command, for a picker in an editing UI. It writes exactly that entry: ```tsx theme={null} import { SetHighlightDimStyleCommand, readHighlightDimStyle, } from '@graphysdk/viz-engine'; dispatch(new SetHighlightDimStyleCommand({ dimStyle: 'desaturate' })); // 'dim' | 'desaturate' ``` `'dim'` writes the built-in `alpha: 0.4` out in full; `'desaturate'` writes `{ saturation: 0, alpha: 0.6 }`. `readHighlightDimStyle(spec)` reads the current choice back, to select the right item in the picker. ## Highlight commands `highlight()` has a one-for-one command mirror: | Command | Params | | - | - | | `AddHighlightCommand` | `predicate`, `id?`, `scope?`, `layerId?`, `index?` | | `RemoveHighlightCommand` | `id` | The id is settled when the command is constructed, so `RemoveHighlightCommand` can address a highlight that was added without one. ## Related * [Styling](/sdk-next/config/styling) — the stylesheet the dimmed entries live in, and the `where` condition that takes this same predicate language * [Mappings & aesthetics](/sdk-next/concepts/mappings) — the variables a predicate can name * [Transforms](/sdk-next/advanced/transforms) — predicates run against post-transform columns # Annotations & emphasis Source: https://docs.graphy.dev/sdk-next/emphasis/index Graphy has three tools for directing attention. All three are piped into the [spec](/sdk-next/concepts/how-a-chart-is-built) next to your geoms and scales. ## Three ways to draw attention | Tool | Builder | What it does | | - | - | - | | **Highlights** | `highlight()` | De-emphasize everything that *doesn't* match a predicate, so the matched observations stand out | | **Reference lines** | `geom.rule()` | Draw a constant line — a goal, threshold, or baseline — that the scale grows to fit | | **Annotations** | `annotation.*` | Layer callouts over the plot: difference arrows, shaded regions, text, arrows, images, stickers, pinned numbers and comments | Each is a spec feature. It composes left to right in `pipe` like any geom or scale, and it resolves again on every compile: annotations follow the data they are pinned to, and rules extend the axis domain. ```tsx theme={null} pipe( createSpec({ x: 'month', y: 'revenue', color: 'region' }), geom.line(), scale.x.discrete(), scale.y(), scale.color.discrete(), highlight({ variable: 'region', eq: 'EU' }), // emphasis geom.rule({ aes: { y: { value: 2500 } }, params: { label: 'Target' } }), // reference line annotation.shape({ id: 'forecast', region: { anchorType: 'panel', x: 0.75, y: 0, width: 0.25, height: 1 }, zOrder: 'background', }), // callout styles({ overrides: [ style.annotation.shape( { fill: '#e15759', alpha: 0.1 }, { annotation: 'forecast' } ), ], }) ); ``` > **[Highlights](/sdk-next/emphasis/highlights)** > > Predicate-driven emphasis: pick observations, groups, or a whole x-slice. > **[Reference lines](/sdk-next/emphasis/reference-lines)** > > Goal lines, thresholds, and baselines with `geom.rule()`. > **[Annotations](/sdk-next/emphasis/annotations)** > > Difference arrows, shaded regions, text, arrows, images, stickers and pinned > dots. ## Highlight vs. annotation vs. reference line They overlap in purpose but differ in mechanism: * Use a highlight when the emphasis is about the data: "show me the EU group", "dim everything below target". The predicate selects real observations, so it survives data changes and re-sorts. * Use a reference line to mark a constant value the data is read against: a sales goal, a capacity ceiling, a zero baseline. * Use an annotation to add something the data does not contain: a label explaining a spike, an arrow between two points, a shaded "forecast" band. # Reference lines Source: https://docs.graphy.dev/sdk-next/emphasis/reference-lines A reference line marks a constant value the rest of the chart is read against: a sales target, a capacity ceiling, a zero baseline. It is a geom like any other, `geom.rule()`, added to the spec with its own value and style. ## A horizontal goal line Bind the rule to a constant `y` with `aes: { y: { value } }`. The `{ value }` form is a [constant mapping](/sdk-next/concepts/mappings#variables-and-constants): it pins the aesthetic to a literal instead of a column. ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, } from '@graphysdk/react'; const data = { columns: [{ key: 'month' }, { key: 'revenue' }], rows: [ { month: 'Jan', revenue: 1200 }, { month: 'Feb', revenue: 1800 }, { month: 'Mar', revenue: 2400 }, { month: 'Apr', revenue: 1600 }, { month: 'May', revenue: 3200 }, { month: 'Jun', revenue: 2800 }, ], }; export function RevenueWithTarget() { const spec = useMemo( () => pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), geom.rule({ aes: { y: { value: 2500 } }, params: { label: 'Target' }, }), scale.x(), scale.y() ), [] ); return ( ); } ``` The rule value takes part in the y-scale's auto-domain. Set a target above the tallest bar and the axis grows to fit it, so the line is never clipped off the top. A rule is not interactive by default: it answers no hover and shows no tooltip. ## The label A rule's `params` carry its label and nothing else: **label** `string` Optional inline text drawn alongside the line. Omit to draw a bare line. **labelPosition** `'start' | 'end'` (default: start) Anchors the label at the start or end of the line. Under [`coord.flip()`](/sdk-next/concepts/coordinate-systems) a rule runs vertically, and `'end'` puts the label at the top of the line rather than at its foot. `ToggleGoalLineCommand` and `SetStatLineCommand` pick that end for you when they build a line under a flipped coord. ## Painting the line Stroke and label type come from the [stylesheet](/sdk-next/config/styling), like every other geom. `style.geom.rule()` takes `stroke`, `strokeWidth`, `dashArray`, `strokeAlpha`, `lineCap`, `lineJoin`, `shadow` and `blendMode`; `style.geom.rule.label()` takes `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `textColor` and the other text properties. Both take a `{ layer }` option, so one rule can be painted without touching the others. Leave `textColor` undeclared and the label's text color is picked to read against the pill. ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style, token, } from '@graphysdk/react'; pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), geom.rule({ id: 'floor', aes: { y: { value: 1000 } }, params: { label: 'Floor' }, }), scale.x(), scale.y(), styles({ overrides: [ style.geom.rule( { stroke: token('ruleLine'), strokeWidth: 2, dashArray: [] }, { layer: 'floor' } ), style.geom.rule.label( { fontSize: 11, fontWeight: 600, textColor: '#FFFFFF' }, { layer: 'floor' } ), ], }) ); ``` The built-in stylesheet paints every rule dashed, 1px wide, in the `ruleLine` token, so a bare `geom.rule()` draws a dashed line. Redefine `ruleLine` through `styles({ tokens: { ruleLine } })` to recolour every rule, goal line and average line at once. ## Vertical lines Mark a constant along the x-axis with `aes: { x: { value } }` instead: ```tsx theme={null} geom.rule({ aes: { x: { value: 40 } }, params: { label: 'Threshold' } }); ``` > [!NOTE] > > A vertical rule needs a numeric x-axis: it marks a constant x-value, which > only has meaning when x is continuous. Band and date x-axes support horizontal > rules only. A rule reads one value from exactly one axis, so supplying both > `x` and `y` is an `INVALID_RULE_MAPPING` error, as is supplying neither. ## Multiple rules Rules compose like any other layer and paint in spec order: a rule added after `geom.bar()` draws in front of the bars. Add as many as you need: ```tsx theme={null} pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), geom.rule({ id: 'floor', aes: { y: { value: 1000 } }, params: { label: 'Floor' }, }), geom.rule({ aes: { y: { value: 2500 } }, params: { label: 'Target' } }), geom.rule({ id: 'ceiling', aes: { y: { value: 3500 } }, params: { label: 'Ceiling' }, }), scale.x(), scale.y(), styles({ overrides: [ style.geom.rule({ dashArray: [0, 2] }, { layer: 'floor' }), style.geom.rule({ dashArray: [0, 2] }, { layer: 'ceiling' }), ], }) ); ``` Name the rules you want painted apart: a `{ layer }` entry addresses the layer's `id`. ## Flipped and secondary axes A rule tracks the data space, not the screen. Under [`coord.flip()`](/sdk-next/concepts/coordinate-systems) a `{ y: { value } }` rule still marks a constant y; the renderer paints it as a vertical line because the y-axis now runs horizontally. To pin a rule to a [secondary axis](/sdk-next/graph-types/combo), add `yScaleType: 'secondary'`: ```tsx theme={null} geom.rule({ aes: { y: { value: 7 } }, yScaleType: 'secondary', params: { label: 'SLA' }, }); ``` ## An average line A rule's value can also be computed. Give it [`stat.mean()`](/sdk-next/advanced/statistics) instead of a constant and the line sits at the layer's average: ```tsx theme={null} import { createSpec, pipe, geom, scale, stat } from '@graphysdk/react'; pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), geom.rule({ stat: stat.mean() }), scale.x(), scale.y() ); ``` ## Editing a rule at runtime Four [commands](/sdk-next/advanced/commands-and-history) cover reference lines in an editing UI: | Command | Params | What it does | | - | - | - | | `ToggleGoalLineCommand` | `showGoalLine`, `layerId?`, `value?`, `label?`, `removedLayer?` | Adds or removes the goal line. `value` and `label` are read on the way on; `removedLayer` is what makes undo bring the old one back intact | | `SetRuleValueCommand` | `layerId?`, `value` | Moves an existing rule along its axis | | `SetRuleLabelCommand` | `layerId?`, `label` | Sets the inline text; `null` removes it | | `SetStatLineCommand` | `line: 'none' \| 'trend' \| 'average'`, `layerId?`, `method?`, `group?`, `label?`, `removedLayer?` | The single switch behind "show a trendline" and "show an average" — swapping one for the other is one undo entry | ```tsx theme={null} import { ToggleGoalLineCommand } from '@graphysdk/react/editable'; dispatch( new ToggleGoalLineCommand({ showGoalLine: true, value: 2500, label: 'Target', }) ); ``` ## Related * [Styling](/sdk-next/config/styling) — the stylesheet entries that paint a rule and its label * [Statistics](/sdk-next/advanced/statistics) — `stat.mean()` and the fitted trends a rule can carry * [Mappings & aesthetics](/sdk-next/concepts/mappings#variables-and-constants) — the `{ value }` constant form * [Scales](/sdk-next/concepts/scales) — how the rule value extends the auto-domain * [Coordinate systems](/sdk-next/concepts/coordinate-systems) — rules under `coord.flip()` # Custom geoms Source: https://docs.graphy.dev/sdk-next/extending/custom-geoms A custom geom is a new kind of shape a chart can draw. It has two halves: a **compile half**, a `Geom` definition that declares the geom's aesthetics and places its observations in data space, and a **render half** that paints them. This page covers the compile half; the [geom renderers](/sdk-next/extending/geom-renderers) page covers the render half. The guiding rule: **the compiler owns every position, the renderer invents none.** Your geom declares which position roles it needs, writes their values in data units, and the compiler scales them onto the axes. An observation then stays anchored through any domain change (zoom, a log scale, a flipped coordinate) because the same scales resolved its coordinates as every other geom's. ## Anatomy of a geom Extend the `Geom` class, parameterised by its params type. Here is a lollipop: a dot on a stem dropped to the baseline. ```tsx theme={null} import { defineGeomStyleTarget, Geom, POSITION_VARIABLES, token, } from '@graphysdk/react'; import type { GeomCompileResult, GeomCompilerInput } from '@graphysdk/react'; class LollipopGeom extends Geom<{ stemWidth: number }> { readonly type = 'lollipop' as const; override readonly defaultParams = { stemWidth: 2 }; override readonly positionRoles = [ { axis: 'x', role: 'point' }, // sources the spec-level `x` mapping { axis: 'y', role: 'min' }, // the baseline { axis: 'y', role: 'max', aes: 'y' }, // the dot height ] as const; override readonly aesthetics = [{ kind: 'visual', name: 'color' }] as const; override readonly supportedCoordTypes = ['cartesian'] as const; override readonly spatialKind = 'spanned-buckets'; // The dot is the observation; // the stem is a part a stylesheet addresses on its own. override readonly styleTarget = defineGeomStyleTarget({ observation: { vocabulary: { size: 'pixels' }, rest: { size: 8 } }, parts: { stem: { vocabulary: { stroke: 'color', alpha: 'unitInterval' }, aesthetics: { stroke: 'color' }, rest: { stroke: token('geomBorder') }, hovered: { stroke: token('hoverAffordance') }, }, }, }); compile({ data }: GeomCompilerInput): GeomCompileResult { // The baseline is a position, not a render constant: // write it in data units so the y-scale maps it. const withBaseline = data.hasVariable(POSITION_VARIABLES.yMin) ? data : data.addConstantVariable(POSITION_VARIABLES.yMin, 'numeric', 0); return { data: withBaseline, mapping: {} }; } } ``` ## What the definition declares **type** `string` *required* The geom's name, written `as const`. It becomes the typed builder method (`kit.geom.lollipop`) and the renderer registry key. **defaultParams** `object` *required* Default values for the geom's params, `{}` when it has none. The type parameter on `Geom` types both these and the `params` the builder accepts. The layer's params are merged over them; override `resolveParams` for an invariant the merge can't express. **positionRoles** `readonly PositionRole[]` (default: \[]) The positions the geom occupies, each `{ axis, role }`. A `'point'` role is a single coordinate on its axis and sources that axis aesthetic implicitly: `createSpec({ x })` satisfies `{ axis: 'x', role: 'point' }`, with no `aes` on the role. `'min'` / `'max'` form an interval (like a bar or area); `'scalar'` is a magnitude. A `min` / `max` / `scalar` role may name a custom positional `aes`; the lollipop's `max` role is exposed as the `y` aesthetic. Write the array `as const` so the typed builder can constrain `aes` to exactly these aesthetics. **aesthetics** `readonly GeomAesthetic[]` The visual and data aesthetics the geom reads beyond position; here, `color`. Each is `{ kind, name, required? }`, with `kind` one of `'visual'` (trained through a visual scale, like `color` or `size`) or `'data'` (a raw input a layout geom hands straight to its algorithm, unscaled). Declaring a name registers it so the mapping is recognized; `required: true` also enforces its presence. **styleTarget** `GeomStyleTarget` The paint a [stylesheet](/sdk-next/config/styling) can address on this geom: `observation` for the layer's own shapes, and one entry of `parts` per other piece the geom paints. Each names the properties it paints and its built-in look per state. Geometry stays in `params`; a style target is what a chart restyles without changing the shape. See [what the geom paints](#what-the-geom-paints). **supportedCoordTypes** `readonly CoordType[]` (default: \['cartesian', 'flip']) Which coordinate systems the geom compiles under, drawn from the spec-level union `'cartesian' | 'polar' | 'flip'`. The lollipop narrows the default to cartesian alone. This union is the authoring one; the render half keys its contracts on `'cartesian' | 'polar'`, where a flipped chart is a cartesian variant. See [the render contract](/sdk-next/extending/geom-renderers#the-contract). **spatialKind** `'buckets' | 'filled-buckets' | 'spanned-buckets' | 'rects' | 'cells' | 'points' | 'noop' | 'render-hit-test'` How the compiler builds the hover hit-test index from the geom's scaled positions. * `'buckets'`: groups by x and joins each group's observations along the layer, so the cursor snaps to the nearest x (lines). * `'filled-buckets'`: the same, and the band between each observation's cross-axis bounds counts as painted (areas), so a cursor inside the fill is on the observation rather than snapped to its edge. * `'spanned-buckets'`: groups by x, but the stroke runs across each x rather than joining one to the next, so a press anywhere along it takes that x (a dumbbell's connector, this lollipop's stem). The stroke spans the x's observations and any interval they declare, so a single observation still has length. * `'rects'`: rectangular bounds (bars). * `'cells'`: self-contained boxes read by containment, one observation each, with no band or stroke to snap along (tiles, heatmaps). Cartesian only; a bandless polar layer declares `'rects'` and hit-tests as an enclosure (pie, donut) instead. * `'points'`: proximity to the shape itself (scatter). * `'noop'`: opts out of hit-testing (rules). * `'render-hit-test'`: for a [layout geom](/sdk-next/extending/geom-renderers#hit-testing-layout-geoms) whose geometry the compiler can't see. **identityKey** `IdentityKey` (default: 'x-group') What makes "the same observation" across recompiles, which morphs and hover resolve against. `'x-group'` derives it from the layer's x + group columns (the standard cartesian case), `'x-y'` from both position columns (a tile grid, where x repeats down a column with no group to tell rows apart), `'index'` from position in the dataset, and `{ variable: 'nodeId' }` names a column the geom owns. A geom whose observations aren't x-and-group keyed must say so: a `'render-hit-test'` geom left on the default raises `RENDER_HIT_TEST_IDENTITY` and its hover lookup is built empty. **scaleConstraints** `ScaleConstraints` Domain constraints the geom imposes on the inferred scales: `requireMainAxisBands` forces a band on the main axis (what a bar needs), `requireCrossAxisBands` one on the cross axis (a tile), `bandPadding` the band's padding, `includeZero` anchors the value scale at zero, and `inferColorScale` asks for a color scale when the layer maps one. Unset imposes none. **categorySpanAxes** `readonly AxisKey[]` (default: \[]) Lets categories have unequal widths. Declare `['x'] as const`, map `x` to the category, and write each observation's interval into `xMin`/`xMax`. Then: * `x` is required. * The x scale is continuous and trains on the intervals. It isn't niced unless the spec sets `nice`. * Each observation sits at the center of its interval. * The axis ticks each category at its center. * Layers of the geom can't use `position: 'dodge'`, which would overwrite the intervals. It raises `UNSUPPORTED_POSITION`. Only `x` is supported. Declaring `y` raises `UNSUPPORTED_CATEGORY_SPAN_AXIS`. **defaultPosition** `PositionAdjustment` (default: 'identity') How overlapping observations of this geom arrange when the layer names no position: `'stack'`, `'dodge'`, `'fill'`, or `'identity'` to leave them where they fall. Under `'dodge'`, `x` is the center of the observation's share of the band and `xMin`/`xMax` its edges, so a geom drawn at `x` dodges with no extra work. **defaultStat** `Stat | null` (default: null) The stat a layer of this geom runs when it names none; `null` draws the data as-is (`identity`). Registering the geom registers the stat with it, so the kit also gains `kit.stat.()`. A layer can name another stat, or `identity` to map already-summarized columns itself. See [a summary is a stat](#a-summary-is-a-stat). **tooltip** `readonly TooltipField[]` (default: \[]) The rows the hover tooltip reads off an observation. Each field is `{ aes, key?, heading? }`, reading one aesthetic, or `{ range: [from, to], key?, heading? }`, printing two as a range. `key` is the display label, and `heading: true` prints the field as the tooltip's heading. The default is empty, so a geom that declares nothing shows no tooltip body. **highlightStrategy** `HighlightStrategy | null` (default: null) How the geom composes [highlight](/sdk-next/emphasis/highlights) matches above its base render: `'overlay-anchor'` surfaces matched observations as a marker at the renderer's `getOverlayAnchor`, `'observation-rerender'` repaints the matched subset through the renderer's `renderHighlight` (or `render`) above the dimmed layer, and `null` opts out. A geom names the strategy its renderer serves: an `'overlay-anchor'` geom whose renderer has no `getOverlayAnchor` dims with nothing raised above it. ## The compile step `compile(input)` receives `{ data, mapping, params, coordType }`: the layer's post-stat `Dataset`, its effective mapping (which carries any custom positional aesthetics the geom declared), the resolved params and the spec's coord type. It returns a `GeomCompileResult`, `{ data, mapping }`. This is where the geom writes any positions it derives rather than reads from the mapping. The lollipop's baseline isn't in the data, so `compile` adds it as a constant column in data units; the compiler then scales `yMin` and `yMax` through the shared y-scale like any other position. A geom with `'data'`-kind aesthetics reads `mapping` here: those columns never pass through a scale, so the algorithm has to fetch them itself. Do the minimum here: derive positions, and leave scaling, stacking, and axis placement to the pipeline. A geom that computes screen pixels in `compile` has crossed the [compile/render seam](/sdk-next/concepts/how-a-chart-is-built). A geom whose shapes summarize many rows doesn't summarize here either: the summary is a [stat](#a-summary-is-a-stat), so an author can replace it. ## A summary is a stat Some geoms draw a summary rather than the rows: a boxplot draws quartiles, a range bar the lowest and highest value per x-value. In the grammar that summary is a **stat**, and the geom names it as its `defaultStat`. A layer that names no stat runs it; a layer whose data is already summarized names `identity` and maps the summary columns itself. Here is a span, a bar from the lowest to the highest `y` of each `x`, whose stat computes the range: ```tsx theme={null} import { Geom, getGroupVariables, Stat, UserInputError, } from '@graphysdk/react'; import type { GeomCompileResult, GeomCompilerInput, StatCompileResult, StatCompilerInput, } from '@graphysdk/react'; class SpanStat extends Stat { readonly type = 'span' as const; // The aesthetics this stat maps, so a layer running it needn't map them. readonly computedVariables = new Set(['low', 'high']); protected computeStat({ data, mapping, }: StatCompilerInput): StatCompileResult { const { x, y } = mapping; if (typeof x !== 'string' || typeof y !== 'string') { throw new UserInputError({ code: 'MISSING_STAT_VARIABLE', message: "'span' needs x and y mapped to columns", }); } // Split by x and by the variables the layer groups on, so each color gets its own span. const summary = data .groupBy(...new Set([x, ...getGroupVariables(data, mapping)])) .rollup({ spanLow: { variableName: y, aggregation: 'min' }, spanHigh: { variableName: y, aggregation: 'max' }, }); return { data: summary, mapping: { low: 'spanLow', high: 'spanHigh' } }; } } class SpanGeom extends Geom { readonly type = 'span' as const; override readonly defaultParams = {}; override readonly defaultStat = new SpanStat(); override readonly positionRoles = [ { axis: 'x', role: 'point' }, { axis: 'y', role: 'min', aes: 'low' }, { axis: 'y', role: 'max', aes: 'high' }, ] as const; override readonly aesthetics = [{ kind: 'visual', name: 'color' }] as const; compile({ data }: GeomCompilerInput): GeomCompileResult { return { data, mapping: {} }; } } ``` The layer maps the raw values, and the stat turns them into the span: ```tsx theme={null} kit.pipe( kit.createSpec({ x: 'month', y: 'sales' }), kit.geom.span({ aes: { color: 'region' } }), kit.scale.x(), kit.scale.y() ); // Already summarized: skip the stat and map the columns. kit.geom.span({ stat: kit.stat.identity(), aes: { low: 'minSales', high: 'maxSales' }, }); ``` What a stat gets and owes: * **`computedVariables`** names the aesthetics the stat maps, including a geom's custom positional ones. Layer validation takes it at its word and doesn't ask the author to map them, so the stat must deliver each one the geom requires, mapped to a column it writes. A stat that doesn't fails with `MISSING_STAT_OUTPUT`, naming the stat. * **The stat runs before grouping.** Split the rows by `x` and by `getGroupVariables(data, mapping)`, the same variables the layer groups on, as the built-in `count` does. A group column the output keeps stays mapped, so `color: 'region'` still colors each span. * **Consumed aesthetics are dropped.** The rolled-up rows no longer hold `sales`, so after the stat `y` is no longer mapped. An aesthetic stays mapped only if the stat maps it or its column is still in the output. * **`params` are the layer's params.** One param list serves the geom and its stat, so a stat reads its settings where the author sets the geom's: `kit.geom.span({ params: { … } })`. A geom and its default stat must not use one param name for two meanings. * **The stat runs on an empty layer too.** When every row is filtered out, `computeStat` still runs, so the aesthetics it maps stay mapped. An empty frame's column types are placeholders, so check a column's type only when there are rows. * **Report a bad input** with `UserInputError` and a user-input code, as the built-in stats do. ## Anchoring annotations [Annotations](/sdk-next/emphasis/annotations) attach to an observation, and the geom is what knows where that observation sits. Implement `resolveAnchorPosition` to answer, in normalized panel `[0, 1]` space: ```tsx theme={null} import { getX, getYMax } from '@graphysdk/react'; import type { AnchorContext, AnchorPosition, Observation, } from '@graphysdk/react'; const resolveLollipopAnchor = ( observation: Observation, { coordSystem }: AnchorContext ): AnchorPosition | null => { if (coordSystem.type === 'polar') return null; const x = getX(observation); const y = getYMax(observation); // the dot — the lollipop's one anchor point if (x === null || y === null) return null; return { x, y }; }; ``` Assign it on the definition: `override readonly resolveAnchorPosition = resolveLollipopAnchor;`. The second argument is an `AnchorContext`, not a bare coord system: `{ coordSystem, position, purpose, align? }`. `position` is the layer's resolved position adjuster, the sole authority on whether the value columns hold cumulative stack bounds. `purpose` is `'pin'` (something sits *on* the observation, like a comment, so it moves off an edge shared with a neighboring segment) or `'value'` (something reads the value off the axis, like a difference arrow, so it stays on the outer edge). `align` names a point of the geom's box when the anchor asks for one. A geom that anchors every purpose to the same point, like the lollipop's dot, can ignore all three. The annotation stage skips geoms that don't implement the hook, so their observations can't be annotated. On a `'render-hit-test'` geom the omission also raises `MISSING_ANCHOR_CAPABILITY`. ## Data labels A geom places its own [data labels](/sdk-next/config/data-labels). Declare the coords it places under on `dataLabelCoordTypes` and implement `getDataLabelPlacements`, which returns each label's box center in panel pixels: ```tsx theme={null} import { buildPanelAnchorBox, Geom, resolveAnchoredLabelCenter, } from '@graphysdk/react'; import type { PlacedDataLabel, PlacementResult, PluginPlacementContext, } from '@graphysdk/react'; class LollipopGeom extends Geom<{ stemWidth: number }> { override readonly dataLabelCoordTypes = ['cartesian'] as const; override readonly getDataLabelPlacements = ( context: PluginPlacementContext ): PlacementResult => { const labels: PlacedDataLabel[] = []; if (!context.layer.dataLabels.showDataLabels) return { labels, droppedCount: 0 }; const { justify, align, offset } = context.layer.dataLabels; for (const observation of context.layer.data) { const text = context.formatLabel(observation); const { width, height } = context.measureDataLabel( 'observation', 'outside', text ); const anchored = resolveAnchoredLabelCenter({ box: dotBox(observation, context.panelRect), // the dot, in panel pixels panel: buildPanelAnchorBox(context.panelRect), orientation: { valueAxis: 'y', valueDirectionSign: -1 }, // the value grows upward placement: 'outside', justify, align, offset, labelWidth: width, labelHeight: height, }); labels.push({ layerId: context.layer.id, x: anchored.x, y: anchored.y, text, width, height, rotated: false, role: 'observation', position: anchored.position, }); } return { labels, droppedCount: 0 }; }; } ``` Simplified: `dotBox` stands in for converting the dot's scaled position to a box in panel pixels. A `SceneLayer`'s `data` is a `Dataset`, which you iterate with `for...of` or spread into an array; it has no `map`. The context carries the formatters (`formatLabel`, `formatAbsolute`, `formatCategory`), so a label prints what the axis and the tooltip print without the geom formatting anything itself. `measureDataLabel` returns the themed box the placement must fit, and `styleReaders` answers the geom's own paint, such as a dot's `size`. `resolveAnchoredLabelCenter` resolves the author's `position`, `justify`, `align` and `offset` against a geom's box, so every geom means the same thing by them; `'auto'` is the geom's own heuristic to resolve before calling it. Each placed label carries its center, its size, its `role` and the `position` it resolved to. The hook runs only when the layer asks for a label of some kind, and reads which of `showDataLabels`, `showStackTotals` and `showCategoryLabels` it honors. The last two are bar vocabulary that most geoms ignore. Declaring the coords without implementing the hook raises `MISSING_DATA_LABEL_PLACEMENT`: the flag would be accepted and nothing would paint. ## What the geom paints A `styleTarget` declares the paint a [stylesheet](/sdk-next/config/styling) can address on the geom: `observation` for the layer's own shapes, and one entry of `parts` per other piece the geom paints, such as the lollipop's stem beside its dot. `observation` is the layer's own paint. Like a built-in geom it reads the encoding, and it takes every geom entry that names no part. It paints the shared geom vocabulary (`fill`, `stroke`, `alpha`, `fillAlpha`, `strokeAlpha` and `saturation`) plus whatever it declares, so the lollipop above adds `size` and keeps the rest. `sharedPaint: false` declines the shared words, for a geom that speaks fewer of them. `aesthetics` names which aesthetic answers each property from the encoding; unset, `color` answers both `fill` and `stroke` and `alpha` answers `alpha`. A geom whose outline keeps a color of its own declares `{ fill: 'color', alpha: 'alpha' }`, and a line-like geom `{ stroke: 'color', strokeAlpha: 'alpha' }`. Each entry of `parts` declares everything it paints and takes only the entries that name it. A part does not read the encoding unless it declares `aesthetics`. A part that joins observations, such as a dumbbell's connector, has no row to read from. A part painted once per observation, such as the lollipop's stem, is painted for that observation, so `aesthetics: { stroke: 'color' }` gives the stem its dot's color from the scale. `rest`, `dimmed` and `hovered` are the built-in look in each state, checked against the vocabulary they belong to. `rest` is what paints when nothing is happening to the geom. The shared geom defaults, the house color and the dimmed alpha included, reach only the observations. The kit types the geom's own style builder from the observation vocabulary, and adds one per part: ```tsx theme={null} const kit = createGraphyKit({ plugins: [lollipop] }); styles({ tokens: { brand: { light: '#0B5FFF', dark: '#6AA1FF' } }, overrides: [ kit.style.geom.lollipop({ size: 12 }), kit.style.geom.lollipop.stem({ stroke: token('brand') }), ], }); ``` The render half reads all of it through `styleReaders`; see [a geom with several parts](/sdk-next/extending/geom-renderers#a-geom-with-several-parts). ## Further declarations The base class defaults everything else, and a geom overrides only what differs: | Declaration | Default | What it decides | | - | - | - | | `defaultInteractive` | `true` | Whether layers of this geom take part in hover hit-testing | | `defaultAttachTo` | `'none'` | The layer a layer of this geom attaches to when the spec sets no `attachTo`. `'previous'` for a geom drawn over another (an error bar over its bar) | | `supportedPositions` | all | The position adjustments a layer of this geom may name; another is rejected | | `derivedVariables` | `[]` | Names the geom computes in its own output that an author may map an aesthetic to | | `composite` | `false` | `true` when the geom draws one geometry per group (a path) rather than one shape per row | | `grid` | `{}` | Per-coord grid and panel-border visibility the geom requests from the axes guide | | `legend` | `{}` | How the geom relates to the color legend: suppression, placement, direct labels | | `dataLabels` / `dataLabelCoordTypes` | `{}` / `[]` | Per-coord data-label defaults, and the coords this geom places labels under | | `summaries` | `{}` | Aggregates the geom opts into: grand total, stack totals, per-group headline | | `validateMapping` | optional | A bespoke mapping requirement no position role expresses | | `resolveBandFraction` | optional | The fraction of its band the geom occupies, so an axis anchor aligns to the shape's edge | | `resolveValueSource` | optional | The aesthetic an observation's value lives on, where that is not its `y` | | `resolveDataLabelDefaults` | optional | Data-label defaults that depend on the layer's position adjuster | | `getDataLabelPlacements` | optional | Places this layer's data labels; see [data labels](#data-labels) | | `resolvePercentageValueStrategy` | optional | What a `format: 'percentage'` label divides by; unimplemented, it prints the value | | `resolveParams` | merge | Resolves a layer's params over `defaultParams`; override for an invariant the merge can't express | ## Registering and using it The compile half pairs with a render half through [`defineGeomRenderer`](/sdk-next/extending/geom-renderers), and the paired result goes into the `plugins` array. From there the typed builder method appears, constrained to exactly the aesthetics and params the definition declared: ```tsx theme={null} const lollipop = defineGeomRenderer(new LollipopGeom(), { coord: 'cartesian', render: ({ layer }) => , renderHover: ({ primary }) => ( ), renderHoverCompanions: () => null, }); const kit = createGraphyKit({ plugins: [lollipop] }); const spec = kit.pipe( kit.createSpec({ x: 'category', y: 'revenue' }), kit.geom.lollipop({ aes: { color: 'category' }, params: { stemWidth: 3 } }), kit.scale.color.discrete() ); ``` > [!NOTE] > > A custom geom's positions get inferred scales like any other geom's, including > the aesthetics its position roles place on an axis. Declare `scale.x()` or > `scale.y()` only to fix a type or options. See > [Scales](/sdk-next/concepts/scales#scales-the-engine-adds). ## Related * [Geom renderers](/sdk-next/extending/geom-renderers) — the render half, `defineGeomRenderer`, hit-testing, hover * [How a chart is built](/sdk-next/concepts/how-a-chart-is-built) — where `compile` sits in the pipeline * [Scales](/sdk-next/concepts/scales) — how declared positions get trained and mapped * [Diagnostics](/sdk-next/extending/index#diagnostics) — what a misdeclared definition reports # Geom renderers Source: https://docs.graphy.dev/sdk-next/extending/geom-renderers A geom renderer owns the React paint for one `(geom, coord)` composition: the **render half** of a geom, opposite its [compile half](/sdk-next/extending/custom-geoms). Renderers paint observations and respond to hover. They don't own layout, guides, or data-label placement. The registry is keyed on the `(geom, coord)` pair, so `bar × cartesian` (a column) and `bar × polar` (a pie wedge) are independent renderers that share no code. Adding one never touches the other. ## `defineGeomRenderer` is dual-target The same function binds a renderer two ways, depending on what you pass first: ### A whole custom geom Pass a `Geom` **instance** to pair both halves. The result carries the compile definition on `.definition`, so dropping it into `plugins` registers the geom *and* derives its typed builder method: ```tsx theme={null} import { createGraphyKit, defineGeomRenderer } from '@graphysdk/react'; const lollipop = defineGeomRenderer(new LollipopGeom(), { coord: 'cartesian', render: ({ layer }) => , renderHover: ({ layer, primary }) => ( ), renderHoverCompanions: () => null, }); const kit = createGraphyKit({ plugins: [lollipop] }); // kit.geom.lollipop(...) is now typed from the definition ``` ### Render-only overrides Pass a built-in geom **name** to rebind only the paint half. The built-in compile half keeps running (positions, stacking, scales, and axes are unchanged), so only the look differs. This is how you restyle bars without reinventing bar geometry: ```tsx theme={null} import { defineGeomRenderer, GraphProvider, GraphRenderer, } from '@graphysdk/react'; const sketchyBar = defineGeomRenderer('bar', { coord: 'cartesian', guideMode: 'band', render: ({ layer, coordSystem }) => { if (coordSystem.type !== 'cartesian') return null; return ; }, renderHover: ({ primary, coordSystem }) => coordSystem.type === 'cartesian' ? ( ) : null, renderHoverCompanions: () => null, }); // no kit needed — the built-in geom.bar() authors the spec, this only repaints it ; ``` The name is constrained to the built-in geom union, so overriding an unknown name is a compile-time error. To restyle a *custom* geom, rebind the definition you already hold rather than its name. ## The contract Every renderer implements a `GeomRenderContract`: **coord** `'cartesian' | 'polar'` *required* The coordinate system this contract paints under, and the discriminant of the `coordSystem` the render receives. A geom binds one contract per coord. > [!NOTE] > > A flipped chart resolves the `'cartesian'` contract: flip is an > axis-assignment variant, a `CartesianCoordSystem` with `mainAxis: 'y'`, not a > geometric paradigm of its own. Branch on `coordSystem.mainAxis` inside the > cartesian render rather than writing a third contract. The spec-level > `'cartesian' | 'polar' | 'flip'` union is a different one; it is what a > definition's > [`supportedCoordTypes`](/sdk-next/extending/custom-geoms#what-the-definition-declares) > answers with. **render** `(input) =\> ReactNode` *required* Paints the observations. Receives `{ layer, coordSystem, styleReaders, colorScheme, animateTransitions, formattingLocale, intro, panelRect }`; see [Reading the panel rect](#reading-the-panel-rect) and [Entrances and transitions](#entrances-and-transitions). The `{ fn, options: { live: true } }` form paints above the chart's pointer handling instead; see [Live geoms](#live-and-draggable-geoms). **renderHover** `(input) =\> ReactNode` *required* Paints the hover state for this layer's observations. Receives the hovered `primary` hit (a `HoverHit`, exported from `@graphysdk/react`) plus its `group` and `related` hits, the `layer`, `coordSystem`, `styleReaders` and `panelRect`. Read paint with the `'hovered'` state. **renderHoverCompanions** `(input) =\> ReactNode` *required* Cross-layer hints, such as dots dropped on a line at the hovered x. Receives the `primary` and `related` hits with the `layer`, `coordSystem` and `styleReaders`. Return `null` to opt out. **guideMode** `'crosshair' | 'band' | null` The positional guide drawn when this is the hovered layer: `'band'` shades the hovered band (bars), `'crosshair'` draws a rule at the hovered value (lines, areas). Omit or pass `null` to draw none (scatter points). **swatchShape** `'square' | 'line' | 'circle' | 'area' | 'slice' | 'interval'` The swatch shape shown for this geom in the legend and tooltip. `'interval'` is a stem capped at both ends, upright unless the chart is flipped. **renderHighlight** `(input) =\> ReactNode` Optional repaint of the matched subset for the [highlight](/sdk-next/emphasis/highlights) overlay. Falls back to `render` when omitted. Override it when the plain grouped render would misrepresent a matched subset. Its input is the render input plus `sourceLayer`, the full layer the subset was filtered from, for context the subset can't see, like a stack's silhouette. **getOverlayAnchor** `(input) =\> OverlayAnchor | null` Where a matched observation's highlight marker sits, in normalized `[0,1]` coord space. Receives `{ layer, coordSystem, observation }` and returns `{ x, y }`, or `null` for an observation it can't place. A geom whose `highlightStrategy` is `'overlay-anchor'` has to supply it, or its layer dims with nothing raised above it. > [!NOTE] > > `renderHighlight` and `getOverlayAnchor` serve a geom that declares a > [`highlightStrategy`](/sdk-next/extending/custom-geoms#what-the-definition-declares): > `'observation-rerender'` calls `renderHighlight` (or `render`) with the > matched subset, `'overlay-anchor'` calls `getOverlayAnchor`. The default is > `null`, so a custom geom that declares none composes no highlights, while a > render-only override of a bar or a line inherits the built-in geom's strategy. **getEditOutlineShapes** `(input) =\> EditOutlineShape[]` Editing only: the shapes the editor outlines when this layer's observations are hovered or selected in point and edit. Unlike the `render…` methods it returns data, not JSX, always in panel pixels: `{ kind: 'region', pathData }` for the area the observations cover (one path where there are many, with rounded corners traced in), `{ kind: 'stroke', pathData, width }` for a line at the width it is drawn, and `{ kind: 'dots', centers, size }` for circles. The editor draws the outline around them in its own look. Receives `{ layer, sourceLayer, coordSystem, panelRect, styleReaders, animateTransitions }`; copy `animateTransitions` onto a region or stroke whose paint springs, so the outline morphs with it. Omit it and the editor outlines each observation's bounding box. A render-only override that draws the built-in geometry in its own look can return the built-in's outline: `getEditOutlineShapes: (input) => getBuiltInEditOutlineShapes('line', 'cartesian', input)`. **hitTest** `(input) =\> RenderHitTester` A per-cursor spatial query for a [layout geom](#hit-testing-layout-geoms). Only consulted when the compile half declares `spatialKind: 'render-hit-test'`. Registering a renderer whose `(geom, coord)` pair never appears, or whose compile half has no render half at all, surfaces as a [plugin diagnostic](/sdk-next/extending/index#diagnostics) rather than a throw. `MISSING_GEOM_RENDERER`, `DUPLICATE_REGISTERED_TYPE`, and the three `render-hit-test` codes all originate here. ## Reading the panel rect `render`, `hitTest`, `renderHover`, and `renderHighlight` all receive `panelRect`: the data rectangle in SVG-local pixels, already inset from axes and chrome. Shapes paint in local `0…width` / `0…height`. `x`/`y` already positioned the geom-layers SVG, so adding them double-offsets. `GraphRenderer`'s `onResize` reports the whole graph container instead; don't scrape `[data-testid="plot"]`. Width and height are the aspect a layout geom needs for shapes that must stay circular or uniformly spaced: ```tsx theme={null} render: ({ panelRect }) => { const radius = Math.min(panelRect.width, panelRect.height) / 4; return ( ); }; ``` A live render paints in the same `0…width` / `0…height`. To place a mark under the pointer, convert the pointer with `live.readPanelPoint(event)` rather than subtracting a client rect, which is off by the graph's scale. ## Reading scaled positions The renderer reads coordinates off each `Observation` with the value readers; it never re-derives a position. The compiler already scaled them. The reader hands you the resolved value, and helpers convert it to the paint frame: ```tsx theme={null} import { getX, getYMin, getYMax, toPercent, toViewBoxX, toViewBoxY, } from '@graphysdk/react'; const x = getX(observation); // scaled x const yTop = getYMax(observation); // scaled dot height const yBase = getYMin(observation); // scaled baseline ; ``` A reader returns `null` for a missing or unscaled value. Guard for it and skip the observation rather than painting `NaN`. For a custom scalar aesthetic (a candlestick's `open`/`close`), `getScaledAesthetic(observation, 'open')` reads the scaled position the mapper wrote into a derived column. For a render-only override of a built-in geom, higher-level readers hand you the geom's normalized `[0,1]` bounds directly. A tile reads `getTileRectBounds(observation)`. A bar reads `createBarBoundsReader(layer, mainAxis, styleReaders, panelRect)`, built once per render, which narrows each bar to its `maxWidth` as the built-in bar paints it. `getBarRectBounds(mainAxis, observation)` returns the full band and ignores the cap. ## Paint frames `UnitSpaceSvg` stretches the `[0, 1]` square onto the panel (`preserveAspectRatio="none"`) so paint and hit-test share one frame. Paths belong there. A `` in that frame becomes an ellipse on a non-square panel. Circles and glyphs go in `UnitBoxSvg`: a nested SVG over a unit-space box with no viewBox. Children live in an unscaled local space, where `50%` is the box center and a pixel radius stays a pixel. ```tsx theme={null} import { UnitBoxSvg, UnitSpaceSvg } from '@graphysdk/react'; ``` A point circle can also sit as a sibling of `UnitSpaceSvg` with percent positions and a pixel radius, the same way the built-in point geom paints. ## Reading paint Paint resolves through the [stylesheet](/sdk-next/config/styling)'s three-tier cascade, override → data → default, and the first answer is used. The value readers `getColor`, `getAlpha`, `getSize`, `getStrokeWidth` and `getLineType` are the data tier alone: they hand you what the encoding decided and nothing else. Reading one and falling back to a constant therefore skips the two tiers around it: a chart's `style.geom({ fill }, { where })` override, and any default a stylesheet gives an unmapped layer. The geom paints its own hard-coded value where the chart's stylesheet says otherwise. Read the whole cascade from `styleReaders` on the render input. Inside a React renderer under the provider, `useGeomStyleReader(layer)` is the same object. A geom with no `styleTarget` reads the shared geom vocabulary, whose `fill` and `stroke` have no built-in value. The `color` aesthetic still feeds both, so a mapped color arrives, but `get('fill')` answers nothing on an unmapped layer. Declare a `styleTarget` with a `rest` fill, `token('geom')`, so it always answers. A `fill` is a `ResolvedPaint`: a color string, or a gradient or pattern object. `toPaintColor` reduces it to the one color an SVG attribute takes. ```tsx theme={null} import { toPaintColor } from '@graphysdk/react'; import type { GeomRendererInput, Observation } from '@graphysdk/react'; const LollipopMarks = ({ layer, styleReaders }: GeomRendererInput) => ( <> {[...layer.data].map((observation: Observation, index: number) => ( ))} ); ``` A line or an area edge that repaints its stroke passes its `dashArray` and `strokeWidth` reads through `resolvePathStroke`, with the cap the stroke would otherwise paint: its `lineCap` read to follow the stylesheet, or a cap of its own. It returns `strokeWidth`, `strokeDasharray` and `strokeLinecap`, named as SVG props so they spread onto a `` as is, with the dashes scaled by the width. A dotted line is a run of zero-length dashes, which a butt cap leaves invisible, so a dotted stroke comes back round-capped under any cap, and at least 1.5px wide. ```tsx theme={null} import { resolvePathStroke } from '@graphysdk/react'; // A cap of its own; pass styleReaders.get('lineCap', observation) to follow the stylesheet. const stroke = resolvePathStroke( styleReaders.get('dashArray', observation), styleReaders.get('strokeWidth', observation), 'butt' ); ; ``` A line that repaints itself also draws the area under it when `fillAlpha` reads above 0, at that opacity: the chart menu's Fill under line writes `fillAlpha`. `createLineFillAreaGenerator(layer.params)` from `@graphysdk/react` builds that area's path in the unit square, down to the bottom of the panel, following the layer's curve and missing values the way `createLinePathGenerator` does for the line. Key elements by data identity rather than index where you can, so a data change moves an element instead of remounting it. `createStableKeyGenerator(layer.data, layer.mapping, layer.id)` from `@graphysdk/react` returns a key function. > [!WARNING] > > The render input already carries the graph's color scheme as `colorScheme`; > don't read it from the DOM. The [Styling](/sdk-next/config/styling) page covers the cascade itself: tiers, selectors, tokens and states. ### A geom with several parts A geom that paints more than one thing declares [a `styleTarget`](/sdk-next/extending/custom-geoms#what-the-geom-paints) naming each one, such as a lollipop's stem beside its dot. `styleReaders` answers for the observations, as above; `styleReaders.part(name)` answers for a declared part. Pass the observation you paint it for, so an entry's `where` can test it: ```tsx theme={null} const stem = styleReaders.part('stem'); ; ``` A part reads the encoding only through the `aesthetics` it declares. Otherwise its paint comes from the stylesheet and the geom's declared defaults alone, and a part the geom didn't declare reads back `undefined`. Pass a state third to read one: `stem.get('stroke', observation, 'hovered')` is the look [`renderHover`](#hover) paints. ## Entrances and transitions Two members of the render input drive motion. Both are offered rather than imposed; a renderer that ignores them appears at once. **intro** `LayerIntroPlan | null` The engine's plan for how this layer enters on first mount, discriminated on `type`: `{ type: 'grow', durationSeconds, baseline, growAxis, delayByKey }`, `{ type: 'wipe', durationSeconds, axis }` or `{ type: 'fade', durationSeconds }`. Every timing is already in seconds with the chart's duration scale applied; apply the numbers verbatim. **animateTransitions** `boolean` Whether an ongoing *data change* should tween rather than cut. Separate from the one-shot entrance. Which plan a layer gets follows from its compile half's `spatialKind`: | `spatialKind` | Coord | Plan | | - | - | - | | `'rects'` | cartesian | `grow` along `growAxis: 'x' \| 'y'` from a `baseline` | | `'rects'` | polar | `grow` along `growAxis: 'angle' \| 'radius'` | | `'buckets'`, `'filled-buckets'` | cartesian | `wipe` traveling along `axis: 'x' \| 'y'` | | `'spanned-buckets'`, `'cells'` | any | `fade` | | `'points'` | any | `grow` along `growAxis: 'size'` | | `'buckets'`, `'filled-buckets'` | polar | `null` | | `'noop'`, `'render-hit-test'` | any | `null` | A wipe travels along a continuous path, so it is reserved for the shapes that draw one across the layer. A spanned bucket strokes within its own bucket and joins nothing to the next, so it fades instead, as a tile grid does. An empty layer also resolves to `null`. So a layout or live geom never receives a plan and owns its own entrance, while a custom geom declaring any of the shapes above **does** get a plan and has to consume it, or it pops in while every built-in layer animates. `@graphysdk/react` ships the same primitives the built-in geoms animate through, so a plugin never takes a `motion` dependency of its own: ```tsx theme={null} import { GeomCircle, GeomLine, GeomRect, IntroFade, IntroWipeClip, } from '@graphysdk/react'; // The entrance: wrap the layer's whole paint in the wrapper its plan calls for. const paint = ( ); if (intro?.type === 'fade') return {paint}; if (intro?.type === 'wipe') return {paint}; return paint; ``` `GeomCircle`, `GeomLine` and `GeomRect` spring their geometry to each new value when `animateTransitions`, and snap when it is off. That is the data-change half, independent of the entrance. All three take positions in whichever space the geom paints in, percentages or unit coords, as long as a value and its successor share units. Each shape takes `effects`, the shadow, blend mode and filters a stylesheet gives the geom, read with `readGeomEffects(styleReaders, observation, state)`. The renderer lays a geom's overpaint, the paint a stylesheet lays over its shapes, over everything each render hook draws. A geom with detail that should stay clear of it, such as a boxplot's whiskers and median, sets `overpaintPlacement: 'manual'` and wraps its main shapes in `GeomOverpaint` itself. The overpaint then covers what the wrapper holds and nothing else, so draw the strokes that sit under the main shapes before it and anything drawn over them after it: ```tsx theme={null} <> {whiskers} {boxes} {medians} ``` Pass `state: 'hovered'` in the hover repaint, and no state at rest. A manual geom that draws no `GeomOverpaint` takes no overpaint. Wrap a nested view box from outside rather than the shapes inside it, since the pattern is laid out in panel pixels. A shape springs only while it keeps its element, so key each one on something that survives the change. That holds for the layer too: a generated layer id churns across recompiles and remounts every shape under it, so a chart whose shapes should tween pins its geom: `geom.dumbbell({ id: 'pay-gap' })`. A `grow` plan is per shape rather than per layer, so a geom declaring `'rects'` or `'points'` still animates its own entrance from the plan's `baseline`, `growAxis` and `delayByKey`. A theme can replace these shapes through `Theme.shapes`. A theme shape whose drawing depends on pixels, such as hatching lines a set distance apart, reads the panel's pixel size with `useGeomPanelSize()`. It then converts the shape's percentages to pixels with `readPositionPixels(position, extent)`, or with `readRectPixels(rect, panel)` for a rect. The resulting pixels are relative to the panel, not to the graph container. `useGeomPanelSize()` returns `null` outside a geom layer and inside a `UnitSpaceSvg`. Inside a `UnitBoxSvg`, it returns the size of that box. Where it returns a size, a numeric position is already in pixels and the helpers return it unchanged. They return `null` only for a string that holds no percentage, and the theme shape should then draw the default shape instead. A geom that draws shared shapes in unit coordinates puts them inside a `UnitSpaceSvg`, never a hand-written ``. The panel context only knows the space a renderer component declares, so shapes under a bare view box still read as pixels and a theme shape misreads their coordinates. ## Hover Hover has two render entry points, both in the contract: * **`renderHover`** repaints *this layer's* hovered observation, usually the same one drawn bolder or outlined, reading paint with the `'hovered'` state. Because positions come from the compiled observation, the highlight lands exactly over the base shape. * **`renderHoverCompanions`** paints hints on *other* layers at the hovered x. Return `null` when the observation is its own highlight. ```tsx theme={null} renderHover: ({ primary }) => , renderHoverCompanions: () => null, ``` ## Hit-testing layout geoms Most geoms are hit-tested from their scaled positions; the compiler builds the index from `spatialKind`. A **layout geom** computes its geometry with an algorithm render-side (Sankey ribbons, Treemap tiles, Voronoi cells), so the compiler can't see the shapes to index them. Such a geom declares `spatialKind: 'render-hit-test'` on its compile half and supplies a `hitTest` **factory** on the contract. The factory runs once per data change or panel resize and returns a per-cursor tester. The cursor arrives in panel-local `[0,1]` (top-left origin), and the tester returns `{ key }`, the identity key of the observation under it, or `null`. It receives the same `panelRect` as `render`: ```tsx theme={null} const treemap = defineGeomRenderer(new TreemapGeom(), { coord: 'cartesian', render: ({ layer }) => , renderHover: ({ primary }) => ( ), renderHoverCompanions: () => null, hitTest: ({ layer }) => { const tiles = buildTiles(layer.data); return ({ x, y }) => findTileKey(tiles, x, y); // → { key } or null }, }); ``` The renderer registers the tester on your behalf, so central hover, tooltips, and guides work the same as for a built-in geom. (`useGeomHitTest` is the underlying hook, but the contract's `hitTest` is the path you write against.) The key the tester returns is the observation's `identityKey` value, so the compile half has to name an identity its own geometry owns: `{ variable: 'markId' }` or `'index'`. Left on the default `'x-group'`, the lookup is built empty and every hit resolves to nothing (`RENDER_HIT_TEST_IDENTITY`). ## Live and draggable geoms A geom that runs its own simulation or drag interaction, such as a force-directed graph, must own its pointer events. For that, `render` takes the live form: `{ fn, options: { live: true } }`. It paints above the layer that handles the chart's hover, so its marks get the pointer first, and it paints in the panel's units like every other geom, so it scales with the graph. Its function receives `input.live`: * `pushHover(key, cursor)` feeds the central hover store; `pushHover(null)` clears it. * `readPanelPoint(event)` converts a pointer's client coordinates to panel units, so a dragged mark follows the pointer on a graph drawn at any size. Each `pushHover` resolves against the same `render-hit-test` index, so the compile half declares `spatialKind: 'render-hit-test'` and an identity of its own here too: ```tsx theme={null} class ForceGeom extends Geom { readonly type = 'force' as const; override readonly defaultParams = { chargeStrength: 450 }; override readonly supportedCoordTypes = ['cartesian'] as const; // The geom pushes hover by key, and the key comes from this index. override readonly spatialKind = 'render-hit-test'; override readonly identityKey = { variable: 'markId' } as const; compile({ data }: GeomCompilerInput): GeomCompileResult { return { data, mapping: {} }; } } const force = defineGeomRenderer(new ForceGeom(), { coord: 'cartesian', render: { fn: ({ layer, panelRect, live }) => ( ), options: { live: true }, }, renderHover: () => null, renderHoverCompanions: () => null, }); ``` Without that `spatialKind`, the geom still paints but every `pushHover` resolves against no index (no hover, no tooltip) and the chart reports `LIVE_RENDER_REQUIRES_RENDER_HIT_TEST`. A live render and a `hitTest` factory are alternatives, not a pair: the live render pushes hover from its own handlers, the factory answers the central pull. Declaring both is `CONFLICTING_RENDER_HIT_TEST`, and only the live render is used. Declaring neither on a `'render-hit-test'` layer is `MISSING_RENDER_HIT_TEST`; the layer paints with no hover at all. `useGeomHover(layerId)` is the lower-level push hook the live form wraps. Reach for it only when the live form doesn't cover your case. ## Related * [Custom geoms](/sdk-next/extending/custom-geoms) — the compile half a renderer pairs with * [Styling](/sdk-next/config/styling) — the cascade a renderer reads paint from * [Interactivity](/sdk-next/rendering/interactivity) — the hover model renderers plug into * [Slots](/sdk-next/extending/slots) — replace a *region's* render without writing a geom * [Diagnostics](/sdk-next/extending/index#diagnostics) — what a misdeclared renderer reports # Extending Source: https://docs.graphy.dev/sdk-next/extending/index The engine ships with a fixed set of built-in geoms, stats, and transforms, but the grammar is open. You can register your own and they become first-class: a typed builder method, compiled through the same pipeline, painted through the same renderer registry. This is the advanced surface; most charts never need it. There are two ways in. **Plugins** extend the grammar itself: new geoms, stats, and transforms that compile and render like the built-ins. **Slots** are lighter: they replace how one *region* of the chart renders (a header, legend, or tooltip) without touching the grammar. Most of this section is about plugins; slots are a self-contained escape hatch on the renderer. > [!NOTE] > > Before writing a plugin, check whether a [render-only > override](/sdk-next/extending/geom-renderers#render-only-overrides) or an > existing geom composed differently gets you there. A new geom is the right > tool for a new shape, not a restyle of an existing one. ## One array, both halves Everything you add is registered through a single `plugins` array. Passing it once seeds two things: what you can *write* (the typed builder methods) and what can *compile and render*. The authoring surface and the runtime cannot drift apart. ```tsx theme={null} import { createGraphyKit, defineGeomRenderer } from '@graphysdk/react'; const lollipop = defineGeomRenderer(new LollipopGeom(), { coord: 'cartesian', render: ({ layer }) => , renderHover: ({ primary }) => ( ), renderHoverCompanions: () => null, }); const kit = createGraphyKit({ plugins: [lollipop] }); // kit.geom.lollipop(...) now exists and is typed from the definition ``` `LollipopGeom` is the [compile half](/sdk-next/extending/custom-geoms); the object literal is the [render contract](/sdk-next/extending/geom-renderers). A stat or a transform has no render half, so it goes into the same array on its own. ## Two entry points **createGraphyKit** `@graphysdk/react` The ergonomic one. `createGraphyKit({ plugins, theme? })` returns the typed builder (`geom`, `stat`, `transform`, `style`, `styles`, `token`, `scale`, `coord`, `mapping`, `highlight`, `annotation`, `config`, `createSpec`, `pipe` and the `plugins` array itself) **and** a `GraphProvider` already bound to the same plugins and theme, so it takes neither prop. Use this in a React app. **createSpecBuilder + \** `viz-engine` The primitives `createGraphyKit` wraps. Use `createSpecBuilder({ plugins })` for the headless authoring surface and pass the *same array* to ``. Choose this for framework-agnostic or advanced wiring. Both take the same `plugins` array. The kit calls `createSpecBuilder` and pre-binds a provider, nothing more. Either way the set is fixed at mount: to change the registered plugins, remount the provider with a new React `key`. ## What you can add | Extension | Adds | Halves | | - | - | - | | **Custom geom** | A new kind of shape | Compile half (`Geom`) + render half (`defineGeomRenderer`) | | **Render-only override** | A restyle of a built-in geom's paint | Render half only; geometry unchanged | | **Custom stat** | A per-layer data reshaping before mapping | Compile half only | | **Custom transform** | A dataset reshaping | Compile half only | A geom has two halves because it both *places* observations (compile) and *paints* them (render). Stats and transforms only touch data, so they are compile-only. A stat is a `Stat` subclass, a transform a `TransformStrategy` object; each goes into `plugins` on its own and gets a typed builder method (`kit.stat.()`, `kit.transform.()`) beside the built-in [statistics](/sdk-next/advanced/statistics) and [transforms](/sdk-next/advanced/transforms). > **[Slots](/sdk-next/extending/slots)** > > Replace a region's render. No plugin, no geom. > **[Custom geoms](/sdk-next/extending/custom-geoms)** > > Define a new shape with `Geom`: position roles, aesthetics, and the compile > step. > **[Geom renderers](/sdk-next/extending/geom-renderers)** > > `defineGeomRenderer`, render-only overrides, hit-testing, and hover. > **[Statistics](/sdk-next/advanced/statistics)** > > The layer-scoped summaries a `stat` computes, and the vocabulary a custom > `Stat` joins. > **[Transforms](/sdk-next/advanced/transforms)** > > The dataset reshapings a `transform` applies, and the vocabulary a custom > `TransformStrategy` joins. ## Diagnostics A plugin mistake surfaces as a `VizDiagnostic` rather than a throw, so the chart keeps rendering around it. All but one are warnings, and several are the only signal that an otherwise silent plugin is broken. Read them before reaching for a debugger. | Code | Severity | Fires when | | - | - | - | | `MISSING_GEOM_RENDERER` | error | A compile definition is in `plugins` with no render half registered for its `type` | | `MISSING_GEOM_RENDERER` | warning | A compiled layer's `(geom, coord)` pair resolves to no renderer under the chart's coord system; the layer paints nothing | | `DUPLICATE_REGISTERED_TYPE` | warning | Two definitions claim one `(kind, key)`, or two render-only overrides claim one `(geom, coord)`; the last in the array is used | | `MISSING_RENDER_HIT_TEST` | warning | A `'render-hit-test'` layer whose renderer supplies neither a `hitTest` factory nor a live render; the layer has no hover | | `RENDER_HIT_TEST_IDENTITY` | warning | A `'render-hit-test'` geom left on the default `identityKey: 'x-group'`, or keyed on a `{ variable }` its `compile()` never emits | | `CONFLICTING_RENDER_HIT_TEST` | warning | A renderer declares both a `hitTest` factory and a live render; they are alternatives, and only the live render is used | | `LIVE_RENDER_REQUIRES_RENDER_HIT_TEST` | warning | A renderer's render is live but its layer's `spatialKind` is not `'render-hit-test'`, so every `pushHover` resolves against no index | | `MISSING_ANCHOR_CAPABILITY` | warning | A `'render-hit-test'` geom implements no `resolveAnchorPosition`, so annotations anchored to its observations are dropped | | `MISSING_DATA_LABEL_PLACEMENT` | warning | A geom declares `dataLabelCoordTypes` but implements no `getDataLabelPlacements`, so its layers take the label flags and paint none | Some are computed once, when the provider reads the `plugins` array; the rest are recomputed per compile, since they depend on what a layer compiled to. ## Related * [How a chart is built](/sdk-next/concepts/how-a-chart-is-built) — the pipeline your plugin joins * [Rendering](/sdk-next/rendering/index) — the renderer that hosts slots and geom renderers # Slots Source: https://docs.graphy.dev/sdk-next/extending/slots Slots replace how a single region of the chart renders. The [spec](/sdk-next/concepts/how-a-chart-is-built) still owns *whether* that region exists and *what* data it receives, and your override gets the same render-ready props the default would. Pass them through the `slots` prop. ```tsx theme={null} ``` ## Regions | Slot | Region | | - | - | | `Header` | Title / subtitle area above the plot | | `Footer` | Caption, source, and provenance-mark area below the plot | | `Tooltip` | The hover tooltip | | `Grid` | The plot grid lines | | `Swatch` | The color swatch used in legend and tooltip | | `EditorSurface` | The chart's editor layer, mounted over the frame in `mode="editable"` | | `Legend` | The color legend | | `Headline` | Headline numbers | | `AxisTicks` | An axis's tick labels | | `AxisLabel` | An axis's title | `EditorSurface` defaults to a no-op, so a read-only embed bundles no editing code; `@graphysdk/react/editable` is what fills it. A chart that only paints leaves it alone. > [!NOTE] > > Custom `Footer` / `Header` slots receive a `brandMark` visual (`'full' | > 'mini' | 'hidden'`), resolved from `config.content.brandMark` and the frame > size. That prop is not the visibility toggle; the toggle is > `brandMark.enabled`. `source` remains your data-source attribution. The > default badge paints inside the matching region when placement matches. ## Three kinds of slot How a region earns its space decides what you hand the slot. The type reflects it: the first two kinds are bare components, and layout-coupled slots are a `SlotOverride` object. ### Box-filling — `Tooltip`, `Grid`, `Swatch`, `EditorSurface` These paint inside a box the layout already sized, or float free of it. Provide a component and nothing else: ```tsx theme={null} import type { TooltipSlotProps } from '@graphysdk/react'; function MyTooltip({ content }: TooltipSlotProps) { return (
{content.heading ? {content.heading} : null} {content.rows.map((row) => (

{row.label}: {row.value}

))}
); } ; ``` Rows follow legend order. The hovered row is the one with `primary` set, not the first. The tooltip's positioning stays built in: you own the body, not the anchor. ### DOM-measured — `Header`, `Footer` These reserve their own space, and the layout measures the rendered DOM to find out how much. Both prop types carry a `ref`. Forward it to your outer element, or the region reserves zero height and the plot paints over it. ```tsx theme={null} import type { HeaderSlotProps } from '@graphysdk/react'; function MyHeader({ ref, title, showTitle }: HeaderSlotProps) { return (
{showTitle && typeof title === 'string' ?

{title}

: null}
); } ``` ### Layout-coupled — `Legend`, `Headline`, `AxisTicks`, `AxisLabel` These reserve an edge band the layout sizes up front, so they are a `SlotOverride`: a `render` component **and** a `measure` that reports the region's size. Paint and reserved space must agree, so the layout asks `measure` how much room to leave: ```tsx theme={null} axis.label === null ? 0 : ctx.measureText(axis.label, { family: 'Inter', size: 12 * ctx.textScale, }).height + 8, }, }} /> ``` `Legend`, `AxisTicks` and `AxisLabel` take a `(region, ctx) => number`: the band's thickness in pixels, a height on the top and bottom edges and a width on the left and right. `Headline` takes a `HeadlineMeasurer` instead: an object with `measureHeadline(headline, size, inDonutHole?)` returning a `{ width, height }` box and `measureHeadlineItemWidths(headline, size)` returning each strip item's width. ```tsx theme={null} import type { HeadlineMeasurer } from '@graphysdk/viz-engine'; const headlineMeasure: HeadlineMeasurer = { measureHeadline: () => ({ width: 240, height: 64 }), measureHeadlineItemWidths: (headline) => headline.items.map(() => 120), }; ; ``` The second argument to the function-shaped measures is a `SlotMeasureContext`: `measureText(text, { family, size })`, the same Canvas-backed measurer the built-in regions use, taking a final pixel size, and the active `textScale` to multiply an em size by. Sizing from those keeps your reserved band matched to what paints. > [!WARNING] > > Give a layout-coupled slot's `measure` a stable reference (define it outside > render or memoize it). The layout reads overrides through a ref, so an inline > `measure` still takes effect on the next paint, but that paint doesn't > retrigger layout, so paint and reserved space drift apart. Two of the measures are skipped where the band is empty: `AxisTicks`'s reserves `0` for an axis with no tick band, and `Legend`'s reserves `0` for an empty legend. Your override isn't consulted in those cases. `AxisLabel`'s has no such gate. ### Pressable guides in `point-and-edit` A press on a tick label, an axis title, a legend pill, a direct label or a grid line selects its axis, legend or grid lines through attributes on the node that paints it. A custom `Grid`, `AxisTicks`, `AxisLabel` or `Legend` joins by spreading the same attributes, only where `useGuidesTakePress()` is true: ```tsx theme={null} import { stampAxisEditTarget, useGuidesTakePress, type AxisTicksSlotProps, } from '@graphysdk/react'; function MyAxisTicks({ formattedAxes, tickRects }: AxisTicksSlotProps) { const takesPress = useGuidesTakePress(); // ...for each axis and each tick at `tickIndex`: return ( {tick.formattedLabel} ); } ``` * **Tick labels** spread `stampAxisEditTarget(scaleAestheticKey, tickIndex)`, where `tickIndex` counts the axis's `ticks` in `formattedAxes`, so a double click on a band axis's tick takes that band. * **Axis titles** spread `stampAxisEditTarget(scaleAestheticKey)`, with no tick. * **Legend pills and direct labels** spread `stampLegendEditTarget({ legendIndex, itemIndex })`, counting `formattedLegends` and that legend's items, so a double click takes the item's group. * **Grid lines** spread `stampGridEditTarget(scaleAestheticKey)` on a group holding the stroke and a hit area of your own, such as a transparent stroke a few pixels wide with `pointer-events="stroke"`. The capture rect over the panel takes the event, so the editor reads the element stack under it and finds the stamp there. The attributes go on the element holding the paint and whatever area should take the press, and that element must take pointer events. A node without them takes no press, and a click there selects the chart. A grid line your slot hides or moves takes its hit area with it: nothing else answers for it. A grid line in a custom `Grid`, with its own hit stroke: ```tsx theme={null} import { stampGridEditTarget, useGuidesTakePress } from '@graphysdk/react'; function MyGridLine({ scaleAestheticKey, ...lineProps }: { scaleAestheticKey: 'x' | 'y' | 'ySecondary'; } & React.SVGProps) { const takesPress = useGuidesTakePress(); if (!takesPress) { return ; } return ( ); } ``` ## Slot props Every props type is exported from `@graphysdk/react`, along with `DefaultHeader`, `DefaultFooter`, `DefaultTooltip`, `DefaultGrid` and `DefaultSwatch` to wrap or delegate to. | Slot | Props type | Members | | - | - | - | | `Header` | `HeaderSlotProps` | `ref?`, `headerRect`, `mode?`, `title`, `showTitle`, `subtitle`, `showSubtitle`, `headingStyle`, `subtitleStyle`, `brandMark` | | `Footer` | `FooterSlotProps` | `ref?`, `footerRect`, `mode?`, `caption`, `showCaption`, `source`, `showSource`, `captionStyle`, `sourceStyle`, `brandMark` | | `Tooltip` | `TooltipSlotProps` | `content` — `{ heading, rows, comment }` | | `Grid` | `GridSlotProps` | `axes`, `panelBorderSizes`, `panelFrameRect`, `panelRect` | | `Swatch` | `SwatchSlotProps` | `shape`, `color`, `paint?`, `surface`, `label?`, `lineType?`, `width?`, `height?`, `strokeWidth?`, `alpha?`, `cornerRadius?`, `symbol?`, `mainAxis?` | | `EditorSurface` | `EditorSurfaceSlotProps` | `frameElement`, `panelRect`, `formattedAxes`, `animateTransitions` | | `Legend` | `LegendSlotProps` | `formattedLegends`, `rects` | | `Headline` | `HeadlineSlotProps` | `headline`, `rect`, `resolvedSize`, `visibleItemCount`, `inDonutHole?` | | `AxisTicks` | `AxisTicksSlotProps` | `formattedAxes`, `tickRects` | | `AxisLabel` | `AxisLabelSlotProps` | `formattedAxes`, `labelRects` | The rects a slot receives are SVG-local for the in-panel regions (`Grid`, `AxisTicks`, `AxisLabel`) and layout rects for the rest. A `Swatch` gets `color` as a plain string and, when the entry paints a gradient or pattern, the full `paint` beside it. `mainAxis` is the chart's category axis, `'y'` under `coord.flip()`, so an `interval` swatch can lie along the other one. > [!NOTE] > > A slot's `measure` lands on the layout measurer under its own name: > `AxisTicks.measure` becomes `measureAxis`, `AxisLabel.measure` becomes > `measureAxisLabel`, and `Headline.measure`'s two methods keep theirs. That is > the `SlotMeasureOverrides` shape in the layout types. ## Related * [Geom renderers](/sdk-next/extending/geom-renderers) — replace a whole geom's paint, not just a region * [Interactivity](/sdk-next/rendering/interactivity) — the `Tooltip` slot in context * [Theming](/sdk-next/rendering/theming) — restyle regions with tokens before reaching for a slot # Area Source: https://docs.graphy.dev/sdk-next/graph-types/area An area chart is a line with the region beneath it filled. Use `geom.area()` to emphasize magnitude across an ordered or continuous x-axis, and its stacked form for part-to-whole trends. ## Basic example ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, } from '@graphysdk/react'; const data = { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: '1 Jan', revenue: 1200 }, { month: '2 Jan', revenue: 1800 }, { month: '3 Jan', revenue: 2400 }, { month: '4 Jan', revenue: 1600 }, { month: '5 Jan', revenue: 3200 }, { month: '6 Jan', revenue: 2800 }, ], }; export function RevenueChart() { const spec = useMemo( () => pipe( createSpec({ x: 'month', y: 'revenue' }), geom.area(), scale.x(), scale.y() ), [] ); return ( ); } ``` ## Stacked area Map a group column to `color` and stack the bands for a part-to-whole trend. The data is long: one row per `month` × `region`: ```tsx theme={null} const data = { columns: [ { key: 'month', label: 'Month' }, { key: 'region', label: 'Region' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: 'Jan', region: 'North', revenue: 300 }, { month: 'Jan', region: 'South', revenue: 200 }, { month: 'Feb', region: 'North', revenue: 400 }, { month: 'Feb', region: 'South', revenue: 350 }, // ... ], }; const spec = pipe( createSpec({ x: 'month', y: 'revenue', color: 'region' }), geom.area({ position: 'stack' }), scale.x(), scale.y(), scale.color.discrete() ); ``` `'stack'` is the area geom's default, so the `position` above is written out for clarity. Use `position: 'fill'` for a 100% stacked area, where each band shows its share of the total. Use `position: 'identity'` to let the bands overlap at their raw values, the shape a [filled radar](/sdk-next/graph-types/radial) needs. ## Points on the area Add a `geom.point()` layer to mark every vertex. It shares the spec-level mapping, so both layers plot the same observations. Pipe it after the area so the dots sit on top, and set `interactive: false` on the point layer: ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.area(), geom.point({ id: 'vertices', interactive: false }), scale.x(), scale.y(), styles({ defaults: [style.geom.point({ size: 6 }, { layer: 'vertices' })] }) ); ``` `interactive: false` keeps the points out of hit-testing, so the area owns hover: it picks the nearest point along the x-axis and drives the tooltip. Interactive points would fire hover only when the cursor lands on a dot. Marker size is paint, so it comes from the [stylesheet](/sdk-next/config/styling): `style.geom.point({ size })`, default `8`. Give the layer an `id` and scope the entry with `{ layer }` so the size applies to these dots and nothing else. Over a stacked area, give the point layer `position: 'stack'` too. A point layer defaults to `'identity'`, so without it the dots plot at raw values and float off the stacked surface: ```tsx theme={null} const spec = pipe( createSpec({ x: 'month', y: 'revenue', color: 'region' }), geom.area({ position: 'stack' }), geom.point({ position: 'stack', interactive: false }), scale.x(), scale.y(), scale.color.discrete() ); ``` ## Area params `geom.area()` takes the same `curve` param as the line, and handles nulls its own way: **params.curve** `'linear' | 'smooth'` (default: linear) Curve family. `'smooth'` smooths the outline through every point. **params.missingValues** `'zero' | 'connect'` (default: zero) How the band handles `null` values: treat them as zero (`'zero'`), or drop the null rows so the band spans the gap (`'connect'`). `'gap'` is accepted and treated as `'zero'`, since an area cannot draw a gap mid-stack. ## Painting the band Fill opacity, and the outline's width, dash and opacity, come from the [stylesheet](/sdk-next/config/styling), through the `style.geom.area` target: ```tsx theme={null} styles({ defaults: [style.geom.area({ fillAlpha: 1, strokeWidth: 3 })] }); ``` The built-in fill is `fillAlpha: 0.3`, which is why an area reads lighter than the palette color in its legend swatch. Raise it to `1` for solid stacked bands. The outline draws at `strokeWidth: 2`, `dashArray: []` and `strokeAlpha: 1`. `style.geom.area` takes `fill`, `stroke`, `alpha`, `fillAlpha`, `strokeAlpha`, `saturation`, `strokeWidth` and `dashArray`. ## Related * [Styling](/sdk-next/config/styling) — the stylesheet, its cascade and every target * [Line](/sdk-next/graph-types/line) — an unfilled outline, or a fading gradient fill via its `fillAlpha` style property * [Geoms & layers](/sdk-next/concepts/geoms) — stacking and position modes # Bar & column Source: https://docs.graphy.dev/sdk-next/graph-types/bar `geom.bar()` draws a rectangle per observation. In the default cartesian plane the rectangles are vertical columns; add [`coord.flip()`](/sdk-next/concepts/coordinate-systems) to turn them into horizontal bars. Columns suit comparing categories, or a value over discrete time periods. Bars suit rankings, and categories with labels too long to fit under a vertical axis. ## Basic example Value on `y`, category on `x`. The default cartesian plane draws vertical columns: ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, } from '@graphysdk/react'; const data = { columns: [ { key: 'category', label: 'Product' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { category: 'Product A', revenue: 1200 }, { category: 'Product B', revenue: 1800 }, { category: 'Product C', revenue: 2400 }, { category: 'Product D', revenue: 1600 }, { category: 'Product E', revenue: 3200 }, { category: 'Product F', revenue: 2800 }, ], }; export function ProductRevenueChart() { const spec = useMemo( () => pipe( createSpec({ x: 'category', y: 'revenue' }), geom.bar(), scale.x(), scale.y() ), [] ); return ( ); } ``` ## Stacked and grouped Map a group column to `color`, then choose how the bars in each band are arranged with `position`. The data is long: one row per `quarter` × `region`: ```tsx theme={null} const data = { columns: [ { key: 'quarter', label: 'Quarter' }, { key: 'region', label: 'Region' }, { key: 'sales', label: 'Sales' }, ], rows: [ { quarter: 'Q1', region: 'North', sales: 350 }, { quarter: 'Q1', region: 'South', sales: 200 }, { quarter: 'Q1', region: 'West', sales: 500 }, // ... ], }; const spec = pipe( createSpec({ x: 'quarter', y: 'sales', color: 'region' }), geom.bar({ position: 'stack' }), // 'dodge' for grouped, 'fill' for 100% stacked scale.x(), scale.y(), scale.color.discrete() ); ``` * `'stack'` — segments stacked into one column * `'dodge'` — segments side by side within the band * `'fill'` — stacked and normalized to 100% A bar layer defaults to `'dodge'`, so a multi-group bar chart with no `position` renders grouped. See [position modes](/sdk-next/concepts/geoms#position-modes) for the full list. ## Negative values `'identity'` draws each bar at its raw value, so a negative `y` hangs below the zero baseline instead of joining a stack: ```tsx theme={null} const spec = pipe( createSpec({ x: 'month', y: 'pnl' }), // pnl rows may be negative geom.bar({ position: 'identity' }), scale.x(), scale.y() ); ``` ## Counting rows Map only `x` and set the `count` [stat](/sdk-next/advanced/statistics). Each bar's height is the number of rows in its category, with no value column needed: ```tsx theme={null} const spec = pipe( createSpec({ x: 'category' }), geom.bar({ stat: 'count' }), scale.x(), scale.y() ); ``` ## Horizontal bars Keep the mapping as it is, category on `x` and value on `y`, and add [`coord.flip()`](/sdk-next/concepts/coordinate-systems) to swap the axes. Stacking, grouping and styling carry over unchanged: ```tsx theme={null} import { createSpec, pipe, geom, scale, coord } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'country', y: 'users' }), geom.bar(), scale.x(), scale.y(), coord.flip() // horizontal bars ); ``` ## Ranking To order bars by value, sort the data with a [transform](/sdk-next/advanced/transforms). This is most useful with horizontal bars: ```tsx theme={null} import { transform } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'country', y: 'users' }), transform.sort({ variableName: 'users', direction: 'desc' }), geom.bar(), scale.x(), scale.y(), coord.flip() ); ``` ## Bar geometry `geom.bar()` takes one param, `width`. It is geometry, not paint: **params.width** `number` (default: 0.7) Bar width as a fraction of its band, in `(0, 1]`. Under `coord.polar()` it defaults to `1`, so wedges touch. ```tsx theme={null} geom.bar({ params: { width: 0.6 } }); ``` A value outside `(0, 1]` draws with a substitute width and reports an `INVALID_GEOM_PARAM` warning. ## Painting the bars Fill, opacity, corner rounding and the border come from the [stylesheet](/sdk-next/config/styling), through the `style.geom.bar` target: ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'category', y: 'revenue' }), geom.bar({ params: { width: 0.6 } }), scale.x(), scale.y(), styles({ defaults: [ style.geom.bar({ cornerRadius: 'md', stroke: '#ffffff', strokeWidth: 2, }), ], }) ); ``` `style.geom.bar` declares `fill`, `alpha`, `saturation`, `cornerRadius`, `stroke`, `strokeWidth` and `maxWidth`. ### Corner rounding `cornerRadius` is a named token, not a pixel value, so each coordinate system renders it its own way: a rectangle corner in cartesian, an arc corner in polar: | Token | Effect | | - | - | | `'none'` | Square corners | | `'xs'` `'sm'` `'md'` `'lg'` `'xl'` | Increasing rounding; `'sm'` is the built-in default | | `'full'` | Rounds to half the bar's cross-axis thickness — a pill | ```tsx theme={null} styles({ defaults: [style.geom.bar({ cornerRadius: 'full' })] }); ``` A stacked column rounds only the outer corners of the whole column, so the segments inside it stay flush against each other. ### Capping the width `maxWidth` caps a bar's thickness in pixels without moving its center. It applies to vertical and flipped cartesian bars; polar bars ignore it. Without a cap, the bar keeps the width set by `geom.bar({ width })`. A stack narrows to the smallest cap among its segments, so its edges stay aligned. Data labels and annotations place against the narrowed bar. ```tsx theme={null} styles({ defaults: [style.geom.bar({ cornerRadius: 'full', maxWidth: 32 })] }); ``` ### Borders A bar draws a 1px translucent border by default, from the `geomBorder` token. A border in the chart's background color is the usual way to separate stacked segments or pie wedges. `strokeWidth: 0` removes the border: ```tsx theme={null} styles({ defaults: [style.geom.bar({ stroke: '#ffffff', strokeWidth: 2 })], }); ``` Scope an entry with `{ layer }` to paint one bar layer of a [combo](/sdk-next/graph-types/combo) differently, or with `{ where }` to paint by value, such as negative bars in a warning color. ## Related * [Styling](/sdk-next/config/styling) — the stylesheet, its cascade and every target * [Coordinate systems](/sdk-next/concepts/coordinate-systems) — how `coord.flip()` works * [Geoms & layers](/sdk-next/concepts/geoms) — position modes and layering * [Scales](/sdk-next/concepts/scales) — control the value axis range # Boxplot Source: https://docs.graphy.dev/sdk-next/graph-types/boxplot A boxplot compares how values spread across categories. Each box runs from the first to the third quartile with a line at the median, whiskers reach out to the furthest values close to the box, and any value beyond them is drawn on its own as an outlier. > [!WARNING] > > The boxplot ships as its own package, `@graphysdk/geom-boxplot`, on a `0.x` > version line. While it is on `0.x`, a minor release may change its aesthetics, > params, style parts or look; pin a caret range (`^0.1.0`) to take only > patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-boxplot ``` Its only peer dependencies are `@graphysdk/react` and React. Register the plugin once with `createGraphyKit`, which adds `kit.geom.boxplot()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { boxplot } from '@graphysdk/geom-boxplot'; const kit = createGraphyKit({ plugins: [boxplot] }); ``` ## One box per category Pass the raw values, one row per value, and map the category to `x` and the value to `y`. The geom's `boxplot` stat computes the summary: ```tsx theme={null} const data = { columns: [{ key: 'service' }, { key: 'region' }, { key: 'ms' }], rows: [ { service: 'Search', region: 'EU', ms: 128 }, { service: 'Search', region: 'US', ms: 141 }, { service: 'Checkout', region: 'EU', ms: 204 }, // … ], }; const spec = kit.pipe( kit.createSpec({ x: 'service', y: 'ms' }), kit.geom.boxplot(), kit.scale.x.discrete(), kit.scale.y.continuous() ); export function ResponseTimes() { return ( ); } ``` The quartiles interpolate between the middle values, as R, NumPy and d3 compute them. Each whisker reaches the furthest value within 1.5 interquartile ranges of the box (Tukey's rule), and every value past it is an outlier. The value axis fits every value, outliers included, instead of holding zero. The category axis is always discrete. ## Whisker reach `extent` sets how far a whisker may reach, in interquartile ranges. `'min-max'` runs the whiskers to the smallest and largest values, so no value is drawn as an outlier: ```tsx theme={null} kit.geom.boxplot({ params: { extent: 3 } }); kit.geom.boxplot({ params: { extent: 'min-max' } }); ``` The whiskers and outliers are computed on the raw values, before a log scale transforms them. ## Summarized data If your data already holds the summary, one row per box, set `stat` to `identity` and map your own columns: `lowerWhisker`, `q1`, `median`, `q3` and `upperWhisker` are required. ```tsx theme={null} const data = { columns: [ 'service', 'low', 'lower', 'middle', 'upper', 'high', 'requests', 'slow', ].map((key) => ({ key })), rows: [ { service: 'Search', low: 120, lower: 126, middle: 131, upper: 135, high: 138, requests: 10, slow: null, }, { service: 'Checkout', low: 80, lower: 130, middle: 190, upper: 250, high: 310, requests: 7, slow: null, }, { service: 'Search', low: null, lower: null, middle: null, upper: null, high: null, requests: null, slow: 260, }, ], }; const spec = kit.pipe( kit.createSpec({ x: 'service' }), kit.geom.boxplot({ stat: kit.stat.identity(), aes: { lowerWhisker: 'low', q1: 'lower', median: 'middle', q3: 'upper', upperWhisker: 'high', count: 'requests', outlier: 'slow', }, }), kit.scale.x.discrete(), kit.scale.y.continuous() ); ``` The rest is optional. A row that holds only an `outlier` value is drawn as an outlier on its category's box. `mean`, `notchLower` and `notchUpper` are drawn when you map them, and `count` appears in the tooltip and sets the widths under `varwidth`. Grouping by `color` works as it does for raw values. The default is `stat: kit.stat.boxplot()`; naming it does the same as leaving `stat` out. Under it, mapping a summary aesthetic is an error, since the stat computes them all from `y`. ## Outliers, notches and means Each option is a param, and each is off by default except the outliers: ```tsx theme={null} kit.geom.boxplot({ params: { outliers: false } }); // hide them, and the room they held on the value axis kit.geom.boxplot({ params: { notch: true } }); // notch each box at its median's rough 95% interval kit.geom.boxplot({ params: { showMean: true } }); // mark each box's mean ``` Two boxes whose notches do not overlap have medians that differ, roughly at the 95% level. A notch wider than its box folds over the box's end, which says the median is uncertain. ## Box width `width` sets how much of its band a category's boxes span, as a fraction from just above `0` up to `1`. It is `0.6` by default, and grouped boxes share it. A width above `1` is clamped to the full band, and one at or below `0` falls back to the default; the compiler warns in both cases: ```tsx theme={null} kit.geom.boxplot({ params: { width: 0.8 } }); ``` `varwidth: true` scales each box's width by the square root of how many values it summarizes, relative to the largest box, so a box drawn from few values reads as thinner evidence. ## Groups Map a group to `color` and each category draws one box per group, side by side across its band, colored from the scale: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'service', y: 'ms', color: 'region' }), kit.geom.boxplot(), kit.scale.x.discrete(), kit.scale.y.continuous() ); ``` `position: 'identity'` draws a category's boxes over one another instead. A boxplot does not stack. ## Horizontal Add `kit.coord.flip()`. The categories move to the vertical axis and the boxes lie across. ## Styling The boxes are the observations, and each other piece has a builder of its own. Every piece takes its group's color from the scale unless an entry names one: | Builder | Properties | Default | | - | - | - | | `kit.style.geom.boxplot()` | `fill`, `stroke`, `strokeWidth`, their alphas and more | Fill at 0.3 alpha, 1.5px border | | `kit.style.geom.boxplot.median()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | 2px | | `kit.style.geom.boxplot.whisker()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | 1.5px | | `kit.style.geom.boxplot.cap()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | 1.5px, across half the box | | `kit.style.geom.boxplot.outlier()` | `fill`, `fillAlpha`, `stroke`, `alpha`, `size`, `strokeWidth` | A hollow 6px dot | | `kit.style.geom.boxplot.mean()` | `fill`, `fillAlpha`, `stroke`, `alpha`, `size`, `strokeWidth` | A solid 7px dot with a border | To drop the caps and keep the whiskers, set `kit.style.geom.boxplot.cap({ strokeWidth: 0 })`. ```tsx theme={null} const spec = kit.pipe( // … styles({ defaults: [ kit.style.geom.boxplot({ fillAlpha: 0.85, stroke: 'transparent' }), kit.style.geom.boxplot.median({ stroke: '#3a3833', strokeWidth: 2.5 }), kit.style.geom.boxplot.whisker({ stroke: '#898373', dashArray: [3, 3] }), kit.style.geom.boxplot.outlier({ fillAlpha: 1, size: 5 }), ], }) ); ``` ## The summary variables A box's summary values are variables you can name in a `highlight`, an entry's `where` or an aesthetic's mapping, in the units of `y`. The package exports their names as `BOXPLOT_VARIABLES`: | Variable | Value | | - | - | | `boxplotQ1` | The first quartile | | `boxplotMedian` | The median | | `boxplotQ3` | The third quartile | | `boxplotLowerWhisker` | The lowest value the lower whisker reaches | | `boxplotUpperWhisker` | The highest value the upper whisker reaches | | `boxplotCount` | How many values the box summarizes | | `boxplotMean` | The mean, outliers included | | `boxplotNotchLower` | The notch's lower end | | `boxplotNotchUpper` | The notch's upper end | | `boxplotOutlier` | An outlier's value, on the outlier alone | | `boxplotRelativeWidth` | The box's width as a share of the widest, under `varwidth` | A box keeps any other variable its rows all agree on, such as the category and the group, and leaves the rest empty. `y` is one of them, so a box has a `y` only when all its values are equal. An outlier keeps its own row, `y` included, so a highlight on `y` reaches the outliers it matches. ## Hover and highlight Hovering a box emphasizes it and its outliers, and the tooltip lists its upper whisker, third quartile, median, first quartile, lower whisker and count, then its mean when `showMean` is on. A press anywhere from one whisker's end to the other takes the box. In a grouped boxplot the box under the cursor answers alone. Hovering an outlier emphasizes it alone, and the tooltip shows its value. A highlight raises each matched box with its outliers. Match on the category, the group or a summary variable: ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'boxplotQ3', gt: 250 }) ); ``` ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the boxplot renders, hovers, selects and takes annotations on each box's median, but it is authored in code. * **Other stats.** A layer `stat` that computes `y` (`count`, `mean`, `sum`) is rejected: a box needs a summary, from the `boxplot` stat or your own columns. * **A continuous category axis or polar coordinates.** `x` is always read as categories, and the boxplot draws in cartesian coordinates, vertical or under `coord.flip()`. * **Data labels.** `dataLabels` prints nothing on a box yet. * **Raw points, jittered or as a violin.** Draw them as a separate point layer. # Candlestick Source: https://docs.graphy.dev/sdk-next/graph-types/candlestick A candlestick chart shows how a price moved in each trading session. Each candle has a body from the open to the close and a wick from the low to the high, colored by whether the session rose or fell. > [!WARNING] > > The candlestick ships as its own package, `@graphysdk/geom-candlestick`, on a > `0.x` version line. While it is on `0.x`, a minor release may change its > aesthetics, style parts or look; pin a caret range (`^0.1.0`) to take only > patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-candlestick ``` Its only peer dependencies are `@graphysdk/react` and React. Register the plugin once with `createGraphyKit`, which adds `kit.geom.candlestick()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { candlestick } from '@graphysdk/geom-candlestick'; const kit = createGraphyKit({ plugins: [candlestick] }); ``` ## One candle per session Map the session to `x`, and each price to its aesthetic: ```tsx theme={null} const data = { columns: [ { key: 'date', label: 'Date' }, { key: 'open', label: 'Open' }, { key: 'high', label: 'High' }, { key: 'low', label: 'Low' }, { key: 'close', label: 'Close' }, ], rows: [ { date: 'Apr 01', open: 100, high: 103, low: 99, close: 102 }, { date: 'Apr 02', open: 102, high: 104, low: 101, close: 101 }, // … ], }; const spec = kit.pipe( kit.createSpec({ x: 'date' }), kit.geom.candlestick({ aes: { open: 'open', high: 'high', low: 'low', close: 'close' }, }), kit.scale.x.discrete(), kit.scale.y.continuous() ); export function Prices() { return ( ); } ``` All four prices are required. The sessions sit one per band at equal spacing, so a weekend or holiday takes no room. The session axis is always discrete, even for dates. The price axis fits the prices rather than holding zero: A session rises when its close is at or above its open, so a doji (open equal to close) counts as rising and draws as a flat body. The close is the candle's `y`: a highlight or a headline that reads `y` reads the close. A `y` in the root mapping does not reach the candlestick layer, and the compiler warns that it is ignored. ## Horizontal Add `kit.coord.flip()`. The sessions move to the vertical axis and the wicks lie across. ## Body width `width` sets how much of its band each body spans, as a fraction from just above `0` up to `1`, like a bar's. It defaults to `0.6`; at `1` neighboring bodies touch. The wick stays on the band center. ```tsx theme={null} kit.geom.candlestick({ aes: { open: 'open', high: 'high', low: 'low', close: 'close' }, params: { width: 0.8 }, }); ``` A width above `1` is clamped to the full band, and one at or below `0` falls back to the default; the compiler warns in both cases. ## Styling A candle's color says which way its session went, so it comes from a part per direction. The bodies' other properties and the wicks have builders of their own: | Builder | Properties | Default | | - | - | - | | `kit.style.geom.candlestick.rising()` | `fill`, `stroke` | The positive trend color | | `kit.style.geom.candlestick.falling()` | `fill`, `stroke` | The negative trend color | | `kit.style.geom.candlestick()` | `alpha`, `fillAlpha`, `strokeWidth` | 1px border | | `kit.style.geom.candlestick.wick()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | 1px, in its direction's `stroke` | ```tsx theme={null} const spec = kit.pipe( // … styles({ defaults: [ kit.style.geom.candlestick.rising({ fill: 'transparent', stroke: '#1f7a6d', }), kit.style.geom.candlestick.falling({ fill: '#3a3833', stroke: '#3a3833', }), kit.style.geom.candlestick.wick({ stroke: '#898373' }), ], }) ); ``` A wick takes its direction's `stroke` unless a wick entry names one. A chart-wide `style.geom({ fill })` does not reach the candles, so it cannot flatten them to one color. To color the candles from the data instead, see [Mapped color](#mapped-color). Each session carries a `candlestickDirection` variable, `'rising'` or `'falling'`, which replaces a data column of the same name. Name it in an entry's `where` to style one direction's bodies or wicks: ```tsx theme={null} kit.style.geom.candlestick( { strokeWidth: 2 }, { where: { variable: 'candlestickDirection', eq: 'falling' } } ); ``` ## Mapped color `color` is opt-in. Map it to `candlestickDirection` to color the directions through a color scale, with a legend: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'date' }), kit.geom.candlestick({ aes: { open: 'open', high: 'high', low: 'low', close: 'close', color: 'candlestickDirection', }, }), kit.scale.x.discrete(), kit.scale.y.continuous(), kit.scale.color.discrete({ domain: ['rising', 'falling'], range: ['#1f8a70', '#c8453b'], }) ); ``` Map it to a column you compute to color by your own rule. A third value paints a doji (open equal to close) in a color of its own: ```tsx theme={null} const rows = sessions.map((row) => ({ ...row, move: row.close > row.open ? 'up' : row.close < row.open ? 'down' : 'flat', })); kit.scale.color.discrete({ domain: ['up', 'down', 'flat'], range: ['#1f8a70', '#c8453b', '#8a8a8a'], }); ``` Hollow candles color each session by its close against the previous close, and hollow out the bodies that closed above their open: ```tsx theme={null} const rows = sessions.map((row, index) => ({ ...row, versusPrevious: row.close >= (sessions[index - 1]?.close ?? row.open) ? 'higher' : 'lower', })); // aes: { …, color: 'versusPrevious' } styles({ overrides: [kit.style.geom.candlestick.rising({ fill: 'transparent' })], }); ``` Or map it to a data column to color the candles by its groups. Color then does not show the direction, so give it a second cue, such as hollow rising bodies: ```tsx theme={null} styles({ overrides: [kit.style.geom.candlestick.rising({ fill: 'transparent' })], }); ``` A mapped color replaces the direction parts' built-in colors and outranks their `defaults` entries. An `overrides` entry outranks the mapped color, which is why the hollow bodies above are an override. A wick follows its body's color unless a wick entry names one. ## Hover and highlight Hovering a session emphasizes its candle, and the tooltip lists its open, high, low and close, each labeled by its column's `label` (or its key when it has none). A press anywhere on the candle, body or wick, takes its session. A highlight raises each matched candle whole, wick and body together. Match on a price, or on the direction: ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'candlestickDirection', eq: 'falling' }) ); ``` ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the candlestick renders, hovers, selects and takes annotations on each session's close, but it is authored in code. * **Data labels.** `dataLabels` prints nothing on a candle yet. * **Volume and OHLC bars.** Draw volume as a separate bar chart; OHLC ticks in place of a body are not drawn yet. * **Aggregation.** Duplicate rows for one session all draw; aggregate them first with `transform.aggregate()`. # Combo Source: https://docs.graphy.dev/sdk-next/graph-types/combo A combo chart layers two geoms with different y-scales, typically bars for one metric and a line for another on a second axis. It shows two measures whose units or magnitudes do not share a scale. ## Dual-axis example Give each geom its own `y` through a [layer-local mapping](/sdk-next/concepts/mappings#where-a-mapping-lives), and bind the line to the secondary axis with `yScaleType: 'secondary'`. Each layer also needs something to map to `color`; otherwise both take the built-in geom color and neither gets a legend entry. A per-layer `transform.constant` adds a categorical column holding the layer's name. Mapping it to `color` gives the layer its own palette color and legend item: ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, transform, } from '@graphysdk/react'; const data = { columns: [ { key: 'quarter', label: 'Quarter' }, { key: 'revenue', label: 'Revenue' }, { key: 'growth', label: 'Growth' }, ], rows: [ { quarter: 'Q1', revenue: 12000, growth: 5 }, { quarter: 'Q2', revenue: 15000, growth: 25 }, { quarter: 'Q3', revenue: 14000, growth: -7 }, { quarter: 'Q4', revenue: 18500, growth: 32 }, { quarter: 'Q5', revenue: 21000, growth: 14 }, { quarter: 'Q6', revenue: 19500, growth: -7 }, ], }; export function RevenueAndGrowth() { const spec = useMemo( () => pipe( createSpec({ x: 'quarter' }), // shared x geom.bar({ id: 'revenue', aes: { y: 'revenue', color: 'revenueSeries' }, transforms: [ transform.constant({ variableName: 'revenueSeries', type: 'categorical', value: 'Revenue', }), ], }), // primary axis geom.line({ id: 'growth', aes: { y: 'growth', color: 'growthSeries' }, yScaleType: 'secondary', transforms: [ transform.constant({ variableName: 'growthSeries', type: 'categorical', value: 'Growth', }), ], }), // secondary axis scale.x(), scale.y(), scale.ySecondary(), scale.color.discrete() ), [] ); return ( ); } ``` Each scale trains only on the layers bound to it, so the two metrics keep independent ranges. A layer's `transforms` run after any spec-level transforms and apply to that layer alone. Use them whenever one geom needs the data in a different shape from its siblings. `scale.ySecondary()` is optional: the compiler infers one as soon as a layer declares `yScaleType: 'secondary'`. Declare it to control the domain, as with any other scale. ## Configuring the second axis The secondary axis is keyed `ySecondary` under [`axes`](/sdk-next/config/axes). It is a sparse override: every field you leave out is inherited. `position` is the side opposite `y`; `visible`, `grid` and `ticks` come from `y`: ```tsx theme={null} config({ axes: { y: { label: 'Revenue' }, ySecondary: { label: 'Growth (%)' }, }, }); ``` ## Styling one layer Give a layer an `id` and a stylesheet entry can scope to it with `{ layer }`. This is what makes an overlay line read as an overlay rather than another group: ```tsx theme={null} styles({ overrides: [ style.geom.line({ strokeWidth: 4, dashArray: [4, 2] }, { layer: 'growth' }), style.geom.bar({ cornerRadius: 'md' }, { layer: 'revenue' }), ], }); ``` `overrides` outranks a mapped aesthetic; `defaults` does not. None of these properties is mapped, so `defaults` would work here too, but an entry that recolours a layer whose `color` is mapped has to be an override. See [Styling](/sdk-next/config/styling) for the full cascade. ## When to share the axis instead A second axis is right when the two layers measure different things. When they measure the same thing (an average, a target, a running total over the bars it summarizes), omit `yScaleType` and let the line sit on the primary scale. A dual axis there quietly detaches the line from the bars it annotates: ```tsx theme={null} const spec = pipe( createSpec({ x: 'quarter' }), geom.bar({ aes: { y: 'sales', color: 'region' }, position: 'dodge' }), geom.line({ aes: { y: 'average' } }), // same scale as the bars scale.x(), scale.y(), scale.color.discrete() ); ``` For a flat reference line at a constant value, use `geom.rule()`. It is non-interactive by default and draws a 1px dashed line, painted through `style.geom.rule({ stroke, strokeWidth, dashArray })`. `label` prints inline text at the line's start, or at its end with `labelPosition: 'end'`: ```tsx theme={null} geom.rule({ aes: { y: 'target' }, params: { label: 'Target', labelPosition: 'end' }, }); ``` ## Related * [Styling](/sdk-next/config/styling) — per-layer scoping with `{ layer }` * [Axes](/sdk-next/config/axes) — the `ySecondary` axis * [Mappings & aesthetics](/sdk-next/concepts/mappings) — layer-local mappings * [Transforms](/sdk-next/advanced/transforms) — reshaping data per layer * [Scales](/sdk-next/concepts/scales) — the secondary y-scale * [Geoms & layers](/sdk-next/concepts/geoms) — how layers stack # Dumbbell Source: https://docs.graphy.dev/sdk-next/graph-types/dumbbell A dumbbell compares values within each category: a dot per value on the value axis, joined by a connector from the smallest to the largest. It shows the gap between groups, such as pay by sex across roles or life expectancy across two years. > [!WARNING] > > The dumbbell ships as its own package, `@graphysdk/geom-dumbbell`, on a `0.x` > version line. While it is on `0.x`, a minor release may change its aesthetics, > style parts or look; pin a caret range (`^0.1.0`) to take only patches. Tell > us what works and what doesn't on [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-dumbbell ``` Its only peer dependencies are `@graphysdk/react` and React. Register the plugin once with `createGraphyKit`, which adds `kit.geom.dumbbell()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ plugins: [dumbbell] }); ``` ## Long data: one row per dot Map the category to `x`, the value to `y` and the group to `color`. The geom groups the dots of each category itself, so the legend, palette, tooltip order and highlights all come from the color scale: ```tsx theme={null} const data = { columns: [{ key: 'role' }, { key: 'sex' }, { key: 'salary' }], rows: [ { role: 'Product', sex: 'Women', salary: 125 }, { role: 'Product', sex: 'Men', salary: 140 }, { role: 'Eng', sex: 'Women', salary: 118 }, { role: 'Eng', sex: 'Men', salary: 132 }, // … ], }; const spec = kit.pipe( kit.createSpec({ x: 'role', y: 'salary', color: 'sex' }), kit.geom.dumbbell(), kit.scale.x.discrete(), kit.scale.y.continuous(), kit.scale.color.discrete() ); export function PayGap() { return ( ); } ``` A dumbbell encodes position, not length, so the value axis fits the data instead of holding zero as a bar axis does, and the gaps read at full width. A category with one dot draws the dot and no connector. With three or more dots, the connector spans the smallest to the largest and the others sit on it. Leave `color` unmapped and every dot takes the default geom color, with no legend. The layer's `position` is always `'identity'`: the geom groups a category's dots itself, and `'dodge'` or `'stack'` is rejected with `UNSUPPORTED_POSITION`. ## Wide data When each group is its own column, reshape it to long in the spec: ```tsx theme={null} const data = { columns: [{ key: 'region' }, { key: '1970' }, { key: '2020' }], rows: [ { region: 'Europe', 1970: 71, 2020: 81 }, { region: 'Africa', 1970: 45, 2020: 61 }, // … ], }; const spec = kit.pipe( kit.createSpec({ x: 'region', y: 'years', color: 'year' }), kit.transform.reshape({ keep: ['region'], reshape: ['1970', '2020'], keyName: 'year', valueName: 'years', }), kit.geom.dumbbell(), kit.scale.x.discrete(), kit.scale.y.continuous(), kit.scale.color.discrete(), kit.coord.flip() ); ``` ## Horizontal Add `kit.coord.flip()`, as for a horizontal bar. The categories move to the vertical axis and the connectors lie across. ## Styling The dots are the layer's observations and are styled through the geom's own builder. The connector is a part with a builder of its own: | Builder | Properties | Default | | - | - | - | | `kit.style.geom.dumbbell()` | `fill`, `stroke`, `alpha`, `fillAlpha`, `strokeAlpha`, `saturation`, `size`, `strokeWidth` | Fill from the scale, 10px, white border | | `kit.style.geom.dumbbell.connector()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | A neutral gray, 2px, solid | ```tsx theme={null} const spec = kit.pipe( // … styles({ defaults: [ kit.style.geom.dumbbell({ size: 14, strokeWidth: 2 }), kit.style.geom.dumbbell.connector({ strokeWidth: 6, stroke: '#d9d4c7' }), ], }) ); ``` An entry that names no part, such as `style.geom({ fill: 'navy' })`, styles the dots like any other geom's observations and never the connector. The connector takes no color from the scale: it joins dots of different groups, so a scale color would be arbitrary. A connector entry's `where` is tested against the category's first dot, so write it against what every dot of the category shares, such as its `x`. ## Hover and highlight Hovering a category emphasizes its whole dumbbell. The tooltip lists every dot in legend order, with the dot nearest the cursor marked in place. A highlight matches dots. A connector is raised only when every dot it joins is raised: highlighting one group raises that group's dots and no connector; highlighting one category raises its whole dumbbell. ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'role', eq: 'Eng' }) ); ``` ## Data labels `dataLabels.showDataLabels` prints every dot's value beside it. Under the default `position: 'auto'` each label sits on the far side of its dot from the connector, so the gap the chart is read from stays clear. An explicit `position` puts every label on the side `justify` names, for a chart that wants them in a row. ```tsx theme={null} const spec = kit.pipe( // … kit.geom.dumbbell({ dataLabels: { showDataLabels: true } }) ); ``` The label prints the dot's `y`, formatted as the value axis formats it; map `label` to print another column instead. `showStackTotals` and `showCategoryLabels` are bar settings and a dumbbell places neither. ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the dumbbell renders, hovers, selects and takes annotations on its dots, but it is authored in code. * **Aggregation.** Duplicate rows for one category and group all draw; aggregate them first with `transform.aggregate()`. # Error bar Source: https://docs.graphy.dev/sdk-next/graph-types/error-bar An error bar shows the uncertainty around an estimate: a line from the lower end of an interval to its upper end, with a cap across each end. It is drawn over the bars or points that show the estimates, one error bar per category and group. > [!WARNING] > > The error bar ships as its own package, `@graphysdk/geom-error-bar`, on a `0.x` > version line. While it is on `0.x`, a minor release may change its aesthetics, > params, style or look; pin a caret range (`^0.1.0`) to take only patches. Tell > us what works and what doesn't on [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-error-bar ``` It peer-depends on `@graphysdk/react` and nothing else. Register the plugin once with `createGraphyKit`, which adds `kit.geom.errorbar()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { errorbar } from '@graphysdk/geom-error-bar'; const kit = createGraphyKit({ plugins: [errorbar] }); ``` ## Over a bar chart Pass one row per estimate. Map the category to `x`, the estimate to `y`, and the interval's two ends to `lower` and `upper`. Add the error bar as a second layer after the bars, so it draws on top of them: ```tsx theme={null} const data = { columns: [ { key: 'treatment' }, { key: 'age' }, { key: 'days', label: 'Mean days' }, { key: 'ciLow' }, { key: 'ciHigh' }, { key: 'se' }, ], rows: [ { treatment: 'Placebo', age: 'Under 50', days: 12, ciLow: 10.5, ciHigh: 13.5, se: 0.8, }, { treatment: 'Placebo', age: '50 and over', days: 15, ciLow: 12, ciHigh: 18, se: 1.5, }, // … ], }; const spec = kit.pipe( kit.createSpec({ x: 'treatment', y: 'days', color: 'age' }), kit.geom.bar(), kit.geom.errorbar({ aes: { lower: 'ciLow', upper: 'ciHigh' } }), kit.scale.x.discrete(), kit.scale.y.continuous() ); export function Recovery() { return ( ); } ``` A category holding several groups dodges its error bars across the band, the same way a grouped bar chart dodges its bars, so each error bar stands on the centre of its own group's bar. The error bars line up with bars of the default width; a bar with its own `width` param does not move its error bar with it. The value axis fits the intervals. On its own, an error bar does not hold zero on the axis, because an interval shows where an estimate may lie, not a length. Under bars, the bars hold it. ## An interval, or an error Map the interval in one of two ways: | Mapping | The interval | | - | - | | `lower` and `upper` | From one end to the other, as written. Either order works. | | `error` | From `y - error` to `y + error`, such as a standard error. | Map one or the other. Mapping `error` together with either end is an error, and so is mapping only one end. A row with a missing end, a missing estimate or a negative error draws no error bar. ```tsx theme={null} kit.geom.errorbar({ aes: { error: 'se' } }); ``` ## Over points Draw the error bar first and a point over it for a dot-and-whisker chart. Here it is flipped: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'region', y: 'score' }), kit.geom.errorbar({ aes: { lower: 'low', upper: 'high' }, params: { width: 0.3 }, }), kit.geom.point(), kit.scale.x.discrete(), kit.scale.y.continuous(), kit.coord.flip() ); ``` Points are not dodged, so a dot-and-whisker chart takes one group per category. ## Horizontal Add `kit.coord.flip()`. The categories move to the vertical axis and the intervals run across. ## Caps `width` is the geom's one param: how much of its share of the band each cap spans, from `0` to `1`. It is `0.5` by default. `0` draws a bare line with no caps. A value outside `[0, 1]` warns with `INVALID_GEOM_PARAM` and falls back to `0.5`. ```tsx theme={null} kit.geom.errorbar({ aes: { lower: 'ciLow', upper: 'ciHigh' }, params: { width: 0 }, }); ``` ## Styling The line and its caps are one observation, styled with `kit.style.geom.errorbar()`: | Properties | Default | | - | - | | `stroke`, `alpha`, `strokeWidth`, `dashArray` | The primary text colour, 1.5px | ```tsx theme={null} const spec = kit.pipe( // … styles({ defaults: [ kit.style.geom.bar({ fillAlpha: 0.35 }), kit.style.geom.errorbar({ stroke: '#1f1e1b', strokeWidth: 2.5 }), ], }) ); ``` Error bars are drawn in the primary text colour whether or not `color` is mapped. A chart usually maps `color` for all its layers, so an error bar in its bar's colour would disappear where it crosses the bar. `color` still groups and dodges the error bars. A border set with `style.geom.bar()` stays off the error bars, and so does a chart-wide `style.geom()` fill, overlay or shadow. A chart-wide `stroke`, `alpha`, `strokeWidth` or `dashArray` does reach them, so they dim with the rest of the chart. To colour a group's error bars, pick them out with `where`: ```tsx theme={null} kit.style.geom.errorbar( { stroke: '#b84737' }, { where: { variable: 'age', eq: 'Under 50' } } ); ``` ## The interval variables The geom writes the two ends of each interval as variables you can name in a `highlight` or an entry's `where`. It writes them whichever way you mapped the interval, in `y`'s units and value format: | Variable | Value | | - | - | | `errorbarLower` | The lower end of the interval | | `errorbarUpper` | The upper end of the interval | ## Hover and highlight An error bar attaches to the layer before it, the bars or points it stands on. It never takes the hover from them: hovering a bar emphasises it and its own error bar, and the tooltip gives each group's interval on an `Interval` row under the row of its own bar, which names the group: ``` Placebo ● Under 50 12 I Interval 10.5 – 13.5 ● 50 and over 15 I Interval 12 – 18 ``` Drawn alone, as a forest plot, the error bar answers the hover itself, and in a grouped chart the one under the cursor answers, not its whole category. To attach it to another layer, pass that layer's `id` as `attachTo`; `attachTo: false` lets it take the hover on its own. With `config({ tooltip: { mode: 'observation' } })`, hovering a bar lists only that bar and its own interval. With no `color` mapped, the row is named `Interval`. Rename it through the layer's `tooltip`, as `{ Interval: { title: '95% CI' } }`. A highlight raises each matched error bar. The bars have no `errorbarUpper` column, so a highlight on it reaches only the error bars. A highlight on a column the bars share raises both layers: ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'age', eq: 'Under 50' }) ); ``` ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the error bar renders, hovers, selects and takes annotations on its upper end, but it is authored in code. * **A continuous category axis.** The categories are always discrete, so error bars over a line on a time axis are not drawn yet. * **Computing the interval from raw rows.** Pass the estimate and its interval; the geom does not compute a mean or a confidence interval from raw values. * **Data labels.** `dataLabels` prints nothing on an error bar yet. # Funnel Source: https://docs.graphy.dev/sdk-next/graph-types/funnel A funnel shows how many of a starting count reach each stage of a process. Each stage is a bar centerd on a common line, as long as its count, and a band joins each bar to the next so the narrowing reads as one shape. > [!WARNING] > > The funnel ships as its own package, `@graphysdk/geom-funnel`, on a `0.x` > version line. While it is on `0.x`, a minor release may change its aesthetics, > style parts or look; pin a caret range (`^0.1.0`) to take only patches. Tell > us what works and what doesn't on [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-funnel ``` Its only peer dependencies are `@graphysdk/react` and React. Register the plugin once with `createGraphyKit`, which adds `kit.geom.funnel()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { funnel } from '@graphysdk/geom-funnel'; const kit = createGraphyKit({ plugins: [funnel] }); ``` ## One bar per stage Pass one row per stage, in the order the funnel runs. Map the stage to `x` and its count to `y`: ```tsx theme={null} const data = { columns: [{ key: 'stage' }, { key: 'visitors' }], rows: [ { stage: 'Visited', visitors: 48200 }, { stage: 'Viewed product', visitors: 31400 }, { stage: 'Added to cart', visitors: 12900 }, { stage: 'Started checkout', visitors: 7600 }, { stage: 'Paid', visitors: 5100 }, ], }; const spec = kit.pipe( kit.createSpec({ x: 'stage', y: 'visitors' }), kit.geom.funnel({ dataLabels: { showDataLabels: true } }), kit.scale.x.discrete(), kit.scale.y.continuous(), kit.coord.flip(), kit.config({ axes: { y: { visible: false } } }) ); export function Checkout() { return ( ); } ``` `coord.flip()` gives the classic funnel, with the first stage at the top. Each bar runs from minus half its count to plus half, so the value axis is centerd on zero and its labels measure from the center line, which is not a figure anyone reads a funnel for. Hide it with `config`, as above, and print the counts as data labels. The geom hides the grid lines itself. A stage with no count, or a negative one, draws no bar, and the stages either side of it are joined across the gap. ## Upright Leave out `kit.coord.flip()`. The stages run left to right and the bars stand across the center line. ## Conversion The geom's default `funnel` stat writes each stage's shares, as fractions formatted as percentages, to two variables you can name like any column: | Variable | Value | | - | - | | `funnelPercentOfFirst` | The stage's count over the first stage's | | `funnelPercentOfPrevious` | The stage's count over the stage before it; 100% for the first | Map `label` to one to print it in the bars: ```tsx theme={null} kit.geom.funnel({ aes: { label: 'funnelPercentOfFirst' }, dataLabels: { showDataLabels: true }, }); ``` A label sits in the middle of its bar, and moves past the bar's end when the bar is too short to hold it. An explicit `dataLabels.position` is honored as written. The shares run in data order. If your data already carries conversion rates, turn the stat off and map them: ```tsx theme={null} kit.geom.funnel({ stat: kit.stat.identity(), aes: { percentOfFirst: 'ofFirst', percentOfPrevious: 'ofPrevious' }, }); ``` ## Bar width `params.width` is the fraction of its band a bar spans, from just above `0` to `1`, `0.7` by default. The rest of the band is the gap the connector crosses. A width above `1` is clamped to the full band, and one at or below `0` falls back to the default; the compiler warns in both cases. ```tsx theme={null} kit.geom.funnel({ params: { width: 0.5 } }); ``` ## Color per stage Map `color` to the stage for a color each. Each band takes the color of the stage it leaves: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'stage', y: 'deals', color: 'stage' }), kit.geom.funnel({ dataLabels: { showDataLabels: true } }), // … kit.config({ legend: { position: 'none' } }) ); ``` ## Styling The bars are the observations, and the bands between them have a builder of their own: | Builder | Properties | Default | | - | - | - | | `kit.style.geom.funnel()` | The shared geom properties, and `strokeWidth` | The geom color, no border | | `kit.style.geom.funnel.connector()` | `fill`, `fillAlpha`, `stroke`, `strokeWidth` | The geom color at 25% | A chart-wide `style.geom()` reaches the bars too. ```tsx theme={null} const spec = kit.pipe( // … styles({ defaults: [ kit.style.geom.funnel({ fill: '#3a3833' }), kit.style.geom.funnel.connector({ fill: '#d8d3c4', fillAlpha: 1, stroke: '#898373', strokeWidth: 1, }), ], }) ); ``` A connector is styled against the stage it leaves, so an entry's `where` can pick connectors out by that stage's values. ## Hover and highlight Hovering anywhere in a stage's band emphasizes its bar, and the tooltip lists its count and both shares. A highlight raises each matched bar. The connectors stay with the dimmed layer: ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'funnelPercentOfPrevious', lt: 0.5 }) ); ``` ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the funnel renders, hovers, selects and takes annotations on the end of each bar, but it is authored in code. * **Several funnels side by side.** Map `group` to split the data into funnels, each with its own shares, but they overlap on the same bands. A stage that appears twice in one funnel is an error. * **Hiding the value axis from the geom.** It takes the `config` line above. # Heatmap Source: https://docs.graphy.dev/sdk-next/graph-types/heatmap `geom.tile()` draws a rectangle that fills its cell on both axes. A bar encodes its value as a length; a tile encodes it as color. The grid comes from a band on `x` and a band on `y`, and the value maps to `color` through a ramp. Use it for a matrix read at a glance: cohort retention, revenue by product and region, activity by day and hour. ## Basic example Three columns: one per axis, one for the value. Both position scales are bands, and the value's scale is a continuous color ramp: ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, } from '@graphysdk/react'; const data = { columns: [ { key: 'cohort', label: 'Cohort' }, { key: 'week', label: 'Week' }, { key: 'retention', label: 'Retention' }, ], rows: [ { cohort: 'Cohort 1', week: 'Week 1', retention: '86%' }, { cohort: 'Cohort 1', week: 'Week 2', retention: '70%' }, { cohort: 'Cohort 1', week: 'Week 3', retention: '66%' }, { cohort: 'Cohort 2', week: 'Week 1', retention: '82%' }, { cohort: 'Cohort 2', week: 'Week 2', retention: '71%' }, { cohort: 'Cohort 2', week: 'Week 3', retention: '65%' }, // ...one row per cell ], }; export function CohortRetention() { const spec = useMemo( () => pipe( createSpec({ x: 'cohort', y: 'week', color: 'retention' }), geom.tile(), scale.x(), scale.y(), scale.color.continuous() ), [] ); return ( ); } ``` `geom.tile()` takes no `params`. The two bands a tile sits in decide its geometry; corner rounding and the hover outline live on [`style.geom.tile`](/sdk-next/config/styling). See [What the geom decides](#what-the-geom-decides) for the defaults it brings. ## Data shape A heatmap wants long data: one row per cell, with both coordinates and the value. Matrix data usually arrives wide, with one data column per column of the grid. Pipe [`transform.reshape`](/sdk-next/advanced/transforms) in front of the mapping and map the columns it produces: ```tsx theme={null} const data = { columns: [ { key: 'product' }, { key: 'North' }, { key: 'South' }, { key: 'East' }, { key: 'West' }, ], rows: [ { product: 'Coffee', North: 18, South: 12, East: 15, West: 9 }, { product: 'Tea', North: 7, South: 11, East: 6, West: 14 }, { product: 'Pastry', North: 22, South: 19, East: 12, West: 10 }, { product: 'Sandwich', North: 14, South: 8, East: 17, West: 13 }, ], }; const spec = pipe( createSpec(), transform.reshape({ keep: ['product'], reshape: ['North', 'South', 'East', 'West'], keyName: 'region', valueName: 'revenue', }), mapping({ x: 'region', y: 'product', color: 'revenue' }), geom.tile(), scale.x(), scale.y(), scale.color.continuous() ); ``` ## Color is the encoding `color` is required on a tile layer, and its scale is what makes the grid readable. With no `color` scale in the spec, the engine infers one from the column: a numeric column gets the brand sequential ramp, and a categorical one gets the ordinal palette. The second is the waffle case, where a cell names a category rather than measuring one. Declare the scale to pick the ramp yourself: ```tsx theme={null} scale.color.continuous(); // the theme's sequential list, else the brand brick ramp scale.color.continuous({ range: ['#FFFFFF', '#0B5FFF'] }); // an explicit ramp scale.color.continuous({ domainMid: 0 }); // the theme's diverging list, else blue to gray to brick ``` For data that crosses zero, pin the neutral stop with `domainMid`. The colors come from the theme's diverging list, or the built-in blue to gray to brick ramp. Without the pin, the neutral color sits at the data's midpoint, which is rarely zero, and the chart reads as though the break-even point moved: ```tsx theme={null} const spec = pipe( createSpec({ x: 'month', y: 'asset', color: 'return' }), geom.tile(), scale.x(), scale.y(), scale.color.continuous({ domainMid: 0 }) ); ``` Setting `domainMid` also turns on `symmetric`, so a +8% and a −8% cell get equal color intensity. [Continuous color](/sdk-next/concepts/scales#continuous-color) covers the full option set: explicit ramps, interpolation spaces and the theme's lists. ## Gaps stay gaps Only the rows you supply paint a cell. A missing combination leaves a hole rather than a cell at the ramp's low end, so "absent" and "zero" stay distinct. A hole answers no hover either, because the hit-test index drops the same cells the renderer skips: ```tsx theme={null} const data = { columns: [{ key: 'team' }, { key: 'quarter' }, { key: 'shipped' }], rows: [ { team: 'Alpha', quarter: 'Q1', shipped: 5 }, { team: 'Alpha', quarter: 'Q2', shipped: 8 }, { team: 'Alpha', quarter: 'Q4', shipped: 11 }, // no Q3 — that cell stays empty { team: 'Beta', quarter: 'Q2', shipped: 3 }, { team: 'Beta', quarter: 'Q3', shipped: 6 }, ], }; ``` To show every combination, emit a row for it. The band domains come from the values in the data, so a value that appears nowhere gets no band. Pass `domain` on the scale to force the full set of bands: `scale.x.discrete({ domain: ['Q1', 'Q2', 'Q3', 'Q4'] })`. ## Value labels [Data labels](/sdk-next/config/data-labels) are on by default on a tile: a heatmap is read cell by cell, so the number belongs in the cell. Each label centers in its tile and is dropped when the cell is too small to hold it. The text color flips between dark and light to suit the fill under it, so labels stay legible at both ends of the ramp. Turn them off for a chart meant to be read as a texture: ```tsx theme={null} geom.tile({ dataLabels: { showDataLabels: false } }); ``` ## What the geom decides A tile carries defaults that a bar or a line wouldn't want, so a heatmap looks right before you configure anything: | Default | Why | | - | - | | Both axes are band scales | A cell is addressed by a band on each axis. Declaring a continuous scale on either raises an `UNSUPPORTED_SCALE_TYPE` warning and the band is used | | Band padding is `0` | Cells tile the plane; the small inset between painted tiles is a render-side separation, not a scale gap | | The `y` band reads top-down | First band at the top, the matrix convention. `scale.y.discrete({ reverse: false })` flips it back | | Grid lines are hidden on both axes | The cells already partition the panel | | The legend is never suppressed | The legend's pills, one per domain stop, are the only place the value scale is written down, even with a single entry | | Data labels are on | See above | | Position is `identity` | There is no value axis to stack or dodge along | ## Hover, highlights and annotations Hover hit-tests the cell the cursor is inside: the full band, including the inset around the painted tile, so the whole grid is live. The hovered cell gains an outline. The tooltip heading is the cell's `x` value, and its one row is the value the color encodes. [Highlights](/sdk-next/emphasis/highlights) work as they do everywhere: a predicate over the post-transform columns. Matched cells stay vivid while the rest step back: ```tsx theme={null} pipe( createSpec({ x: 'quarter', y: 'region', color: 'sales' }), geom.tile(), scale.x(), scale.y(), scale.color.continuous(), highlight({ variable: 'sales', gte: 2400 }) ); ``` [Annotations](/sdk-next/emphasis/annotations) pinned to an observation address a cell with two values. On most geoms, `anchorValue` plus the group is enough; on a grid, `anchorValue` names a whole column, so `crossValue` names the band that picks one cell out of it: ```tsx theme={null} annotation.pinnedNumber({ at: { anchorValue: 'Cohort 5', crossValue: 'Week 1' }, }); ``` An observation anchor resolves to the center of its cell. A pinned number prints the value the ramp encodes, which on a tile is the `color` value, not a length. A sticker centers on the cell too, so turn the cell labels off where the two would overlap. The kinds that take a [panel anchor](/sdk-next/emphasis/annotations#point-anchors) (text, arrows, shapes) float over the grid in fractions of the plot rect, free of any cell. Give the text a filled box: a ramp runs light to dark under it, so bare text is unreadable at one end or the other. The box comes from the [stylesheet](/sdk-next/config/styling), scoped to the annotation by its `id`: ```tsx theme={null} annotation.text({ id: 'caption', content: caption, at: { anchorType: 'panel', x: 0.24, y: 0.2 }, width: 0.42, }); styles({ defaults: [ style.annotation.text({ fill: '#ffffff' }, { annotation: 'caption' }), ], }); ``` An [annotation anchor](/sdk-next/emphasis/annotations#point-anchors) joins the two: the arrow in the example runs from the caption's box to a cell, so neither end is a hand-tuned fraction. The caption's box is measured in the browser, so the tail moves when the text wraps differently or the plot resizes. ```tsx theme={null} annotation.arrow({ start: { anchorType: 'annotation', ref: 'caption', align: 'bottom' }, end: { anchorType: 'observation', anchorValue: 'Cohort 5', crossValue: 'Week 5', }, endArrowheadStyle: 'line-arrow', }); ``` ## Limits * Cartesian only. `coord.flip()` and `coord.polar()` reject a tile layer with an `UNSUPPORTED_COORD` error. A heatmap has no orientation to flip: swap the two mappings instead. * `identity` position only. `'stack'`, `'dodge'` and `'fill'` raise `UNSUPPORTED_POSITION`; none of them means anything without a value axis. ## Related * [Scales](/sdk-next/concepts/scales#continuous-color) — color ramps, theme lists and diverging midpoints * [Transforms](/sdk-next/advanced/transforms) — reshaping wide matrix data to long * [Data labels](/sdk-next/config/data-labels) — the in-cell value labels * [Highlights](/sdk-next/emphasis/highlights) — emphasizing the cells a predicate matches * [Annotations](/sdk-next/emphasis/annotations) — pinning a callout to one cell # Overview Source: https://docs.graphy.dev/sdk-next/graph-types/index A chart type is a recipe: a geom, a coordinate system, a position mode and the scales you declare. A column chart and a pie chart use the same `geom.bar()`; only the coordinate system differs. ## The building blocks Every spec combines five kinds of ingredient: | Ingredient | Builder | What it does | | - | - | - | | **Mapping** | `mapping({ ... })` / `createSpec({ ... })` | Binds data columns to aesthetics (`x`, `y`, `color`, `size`, …) | | **Geom** | `geom.*()` | The shape drawn for each observation | | **Scale** | `scale.*()` | Turns data values into positions, colors and sizes | | **Coord** | `coord.*()` | The coordinate system the geom is drawn in | | **Styles** | `styles({ ... })` with `style.*` | Paint: color, opacity, borders, corner radius, stroke width, marker size | A geom's `params` are geometry: the bar's band width, the line's curve, how a path handles nulls. Paint lives in the stylesheet: see [Styling](/sdk-next/config/styling). ## Geoms Six geoms are built in, and every recipe below uses one or more of them. Six more ship as geom packages (`@graphysdk/geom-`) registered through `createGraphyKit`; their pages are listed at the end. Your own plugin can add more: see [custom geoms](/sdk-next/extending/custom-geoms). | Geom | Draws | Typical use | | - | - | - | | `geom.point()` | A point per observation | Scatter, bubble, dot plots | | `geom.line()` | A path connecting observations | Line charts, trends | | `geom.area()` | A filled band under a line | Area charts | | `geom.bar()` | A rectangle (or arc, in polar) per observation | Columns, bars, pie, donut | | `geom.rule()` | A single reference line across the panel | Goal lines, thresholds | | `geom.tile()` | A cell filling its band on both axes | Heatmaps, matrices, waffles | ## Position modes When a layer maps `color` or `group`, several observations share one x value. The layer's `position` decides how they are arranged: | Position | Effect | | - | - | | `'identity'` | Draw at the raw value; observations may overlap | | `'stack'` | Stack observations on top of each other | | `'fill'` | Stack, then normalize each stack to 100% | | `'dodge'` | Place observations side by side within the band | ```tsx theme={null} geom.bar({ position: 'stack' }); // stacked columns geom.bar({ position: 'dodge' }); // grouped columns ``` Each geom has its own default, so a multi-group layer looks right before you set anything: | Geom | Default position | | - | - | | `geom.bar()` | `'dodge'` | | `geom.area()` | `'stack'` | | `geom.point()`, `geom.line()`, `geom.rule()`, `geom.tile()` | `'identity'` | ## Coordinate systems | Coord | Effect | | - | - | | `coord.cartesian()` | Standard x–y plane (the default) | | `coord.flip()` | Swap the axes — turns columns into horizontal bars | | `coord.polar({ theta, startAngle, innerRadius })` | Bend the plane into a circle — turns bars into pie/donut wedges | `theta` names the aesthetic that sweeps the angle and defaults to `'x'`. `startAngle` is in degrees and defaults to `0`. `innerRadius` is a fraction of the outer radius, from `0` to `1`, and defaults to `0`. ## Common recipes Each is a `pipe(createSpec(...), ...)` chain. The scales are left out here: the engine infers them. | Chart | Recipe | | - | - | | **Line** | `geom.line()` | | **Smooth line** | `geom.line({ params: { curve: 'smooth' } })` | | **Area** | `geom.area()` | | **Stacked area** | `geom.area({ position: 'stack' })`, `color` mapped | | **Column** (vertical) | `geom.bar()` + `scale.x.discrete()` | | **Bar** (horizontal) | `geom.bar()` + `coord.flip()` | | **Stacked column** | `geom.bar({ position: 'stack' })`, `color` mapped | | **100% stacked column** | `geom.bar({ position: 'fill' })`, `color` mapped | | **Grouped column** | `geom.bar({ position: 'dodge' })`, `color` mapped | | **Pie** | `geom.bar({ position: 'fill' })` + `coord.polar({ theta: 'y' })` | | **Donut** | `geom.bar({ position: 'fill' })` + `coord.polar({ theta: 'y', innerRadius: 0.5 })` | | **Radar** | `geom.line()` + `coord.polar({ theta: 'x' })` + `scale.x.discrete()` + `scale.y({ domainMin: 0 })` | | **Rose / coxcomb** | `geom.bar()` + `coord.polar({ theta: 'x' })` + `scale.x.discrete()` | | **Radial bar** | `geom.bar({ position: 'stack' })` + `coord.polar({ theta: 'y', innerRadius: 0.15 })` | | **Scatter** | `geom.point()` + `scale.x.continuous()` + `scale.y.continuous()` | | **Bubble** | `geom.point()` + `size` mapped + `scale.size.continuous()` | | **Heatmap** | `geom.tile()` + a band on `x` and `y` + `scale.color.continuous()` | | **Waffle** | `geom.tile()` over a 10×10 grid + `scale.color.discrete()` | | **Combo** | `geom.bar()` + `geom.line()` layered, the line on `ySecondary` | ## Layers A geom is a layer. Add several to one spec and they draw over each other. They share the spec-level mapping unless a layer overrides it. This is how combos and decorated lines are built: ```tsx theme={null} const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), // the trend geom.point(), // a dot on every vertex scale.x(), scale.y() ); ``` Layers draw in the order you pipe them, so later geoms sit on top. Give a layer an `id` and a stylesheet entry can target it with `{ layer: 'trend' }`, which is how one layer is styled apart from the rest. Every recipe also animates in when it mounts, tuned through the renderer's [`animation` prop](/sdk-next/rendering/interactivity#animation). ## Guides for each type Each page in this section covers one chart type end to end: data shape, spec and the options that matter most. * [Line](/sdk-next/graph-types/line) — trends over a continuous or ordered x-axis * [Bar & column](/sdk-next/graph-types/bar) — vertical columns for comparing categories, horizontal bars for ranking * [Area](/sdk-next/graph-types/area) — filled trends, stacked or plain * [Pie & donut](/sdk-next/graph-types/pie) — parts of a whole * [Radar & radial](/sdk-next/graph-types/radial) — categories compared around a circle * [Scatter & bubble](/sdk-next/graph-types/scatter) — correlation and three-variable plots * [Heatmap & waffle](/sdk-next/graph-types/heatmap) — a value per cell of a band × band grid * [Dumbbell](/sdk-next/graph-types/dumbbell) — two or more values per band, joined by a connector * [Lollipop](/sdk-next/graph-types/lollipop) — a head at the value on a stem from zero * [Candlestick](/sdk-next/graph-types/candlestick) — open, high, low and close per session * [Boxplot](/sdk-next/graph-types/boxplot) — the spread of values per band * [Waterfall](/sdk-next/graph-types/waterfall) — a running total, step by step * [Funnel](/sdk-next/graph-types/funnel) — how many reach each stage * [Combo](/sdk-next/graph-types/combo) — two metrics on dual axes > [!NOTE] > > Most other charts are a small change to a recipe above: swap the geom, add > `coord.flip()`, or change the `position`. # Line Source: https://docs.graphy.dev/sdk-next/graph-types/line A line chart connects observations with a path. It shows trends and change over an ordered or continuous x-axis. In the Graphy SDK it is a `geom.line()` layer over `x` and `y` position scales. ## Basic example ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, } from '@graphysdk/react'; const data = { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: 'Jan', revenue: 1200 }, { month: 'Feb', revenue: 1800 }, { month: 'Mar', revenue: 2400 }, { month: 'Apr', revenue: 1600 }, { month: 'May', revenue: 3200 }, { month: 'Jun', revenue: 2800 }, ], }; export function RevenueChart() { const spec = useMemo( () => pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), scale.x(), scale.y() ), [] ); return ( ); } ``` `scale.x()` infers a datetime scale here, because Graphy recognizes the abbreviated month names (`Jan`, `Feb`, …) as dates. `scale.y()` infers a continuous scale from the numeric `revenue`. To treat the months as evenly spaced bands instead, use `scale.x.discrete()`. ## Multiple groups Map a categorical column to `color` to split the data into one line per group, and add a color scale: ```tsx theme={null} const data = { columns: [ { key: 'month', label: 'Month' }, { key: 'region', label: 'Region' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'Jan', region: 'North', sales: 600 }, { month: 'Jan', region: 'South', sales: 900 }, { month: 'Jan', region: 'East', sales: 1400 }, { month: 'Jan', region: 'West', sales: 1800 }, { month: 'Jan', region: 'Central', sales: 2400 }, // ... ], }; const spec = pipe( createSpec({ x: 'month', y: 'sales', color: 'region' }), geom.line(), scale.x(), scale.y(), scale.color.discrete() ); ``` `scale.color.discrete()` draws group colors from Graphy's default palette. To pin specific colors, use `scale.color.discrete({ domain: ['North', 'South'], range: ['#4C6EF5', '#F76707'] })`. `strokeWidth`, `lineType` and `alpha` can be mapped too, each with its own scale: `scale.strokeWidth.continuous()`, `scale.alpha.continuous()`, and `scale.lineType.discrete()` for a dash pattern per group. `lineType` is discrete only. Mapping it to a numeric variable is an error, since there is nothing between two dash patterns to interpolate. To single one layer out instead, give it an `id` and scope a [style](/sdk-next/config/styling) entry to it with `{ layer }`: ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'sales' }), geom.line({ id: 'trend' }), scale.x(), scale.y(), styles({ defaults: [style.geom.line({ strokeWidth: 4 }, { layer: 'trend' })], }) ); ``` ## Smoothing The `curve` param sets how the path bends. `'linear'`, the default, draws straight segments; `'smooth'` draws a curve through every point: ```tsx theme={null} const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line({ params: { curve: 'smooth' } }), scale.x(), scale.y() ); ``` ## Area fill A line draws no fill of its own. Declare a `fillAlpha` style default to add a gradient beneath it, fading from the line's color to transparent at the baseline: ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), scale.x(), scale.y(), styles({ defaults: [style.geom.line({ fillAlpha: 0.15 })] }) ); ``` For a solid filled band rather than a fading gradient, use [`geom.area()`](/sdk-next/graph-types/area) instead. ## Missing values Use `null` for a missing cell. `missingValues` controls how the path treats the gap: ```tsx theme={null} const data = { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: 'Jan', revenue: 1200 }, { month: 'Feb', revenue: 1800 }, { month: 'Mar', revenue: null }, { month: 'Apr', revenue: null }, { month: 'May', revenue: 3200 }, { month: 'Jun', revenue: 2800 }, ], }; const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line({ params: { missingValues: 'connect' } }), scale.x(), scale.y() ); ``` ## Points on the line Add a `geom.point()` layer to mark every vertex. It shares the spec-level mapping, so both layers plot the same observations. Pipe it after the line so the dots sit on top, and set `interactive: false` on the point layer: ```tsx theme={null} const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), geom.point({ interactive: false }), scale.x(), scale.y() ); ``` `interactive: false` keeps the points out of hit-testing, so the line owns hover: it picks the nearest point along the x-axis and drives the tooltip. Interactive points would fire hover only when the cursor lands on a dot. ## Line params reference **params.curve** `'linear' | 'smooth'` (default: linear) Curve family. `'smooth'` smooths the line through every point. **params.missingValues** `'gap' | 'connect' | 'zero'` (default: gap) How the path handles `null` values: break at the gap (`'gap'`), span it (`'connect'`), or treat as zero (`'zero'`). Paint (stroke width, dash, opacity and the fill wash) comes from the stylesheet: `style.geom.line({ stroke, fill, alpha, saturation, strokeWidth, dashArray, fillAlpha })`. A line draws at `strokeWidth: 2` and `dashArray: []`, with no fill until you declare a `fillAlpha`. ## Related * [Styling](/sdk-next/config/styling) — stroke width, dash, the fill wash and per-layer scoping * [Chart types overview](/sdk-next/graph-types/index) — the full recipe list * [Axes](/sdk-next/config/axes) — labels, ticks, grid and baseline * [Statistics](/sdk-next/advanced/statistics) — add a `smooth` trendline * [Data structure](/sdk-next/data-structure) — how columns feed the mapping # Lollipop Source: https://docs.graphy.dev/sdk-next/graph-types/lollipop A lollipop compares one value per category, like a bar, as a head at the value and a stem dropped to zero. It draws less per category than a bar, so it holds up when there are many categories of similar size, such as a ranking across countries. > [!WARNING] > > The lollipop ships as its own package, `@graphysdk/geom-lollipop`, on a `0.x` > version line. While it is on `0.x`, a minor release may change its aesthetics, > style parts or look; pin a caret range (`^0.1.0`) to take only patches. Tell > us what works and what doesn't on [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-lollipop ``` Its only peer dependencies are `@graphysdk/react` and React. Register the plugin once with `createGraphyKit`, which adds `kit.geom.lollipop()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { lollipop } from '@graphysdk/geom-lollipop'; const kit = createGraphyKit({ plugins: [lollipop] }); ``` ## One value per category Map the category to `x` and the value to `y`: ```tsx theme={null} const data = { columns: [{ key: 'country' }, { key: 'share' }], rows: [ { country: 'Norway', share: 89 }, { country: 'Sweden', share: 85 }, // … ], }; const spec = kit.pipe( kit.createSpec({ x: 'country', y: 'share' }), kit.geom.lollipop(), kit.scale.x.discrete(), kit.scale.y.continuous() ); export function Reading() { return ( ); } ``` A stem encodes length, as a bar does, so the value axis always holds zero. A negative value hangs its stem below the baseline: ## Grouped lollipops Map a group to `color`, with one row per lollipop. Each category's lollipops spread across its band, as a grouped bar's do, and every stem takes its head's color from the scale: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'channel' }), kit.geom.lollipop(), kit.scale.x.discrete(), kit.scale.y.continuous(), kit.scale.color.discrete() ); ``` `'dodge'` is the default. Pass `position: 'identity'` to stand a category's lollipops on one line instead. `'stack'` is rejected with `UNSUPPORTED_POSITION`, since a stacked stem would start on the head below it. When each group is its own column, reshape it to long with `kit.transform.reshape()`, as for a [dumbbell](/sdk-next/graph-types/dumbbell#wide-data). ## Horizontal Add `kit.coord.flip()`, as for a horizontal bar. The categories move to the vertical axis and the stems lie across. ## Styling The heads are the layer's observations and are styled through the geom's own builder. The stem is a part with a builder of its own: | Builder | Properties | Default | | - | - | - | | `kit.style.geom.lollipop()` | `fill`, `stroke`, `alpha`, `fillAlpha`, `strokeAlpha`, `saturation`, `size`, `strokeWidth` | Fill from the scale, 10px, white border | | `kit.style.geom.lollipop.stem()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | Stroke from the scale, 2px, solid | ```tsx theme={null} const spec = kit.pipe( // … styles({ defaults: [ kit.style.geom.lollipop({ size: 14, strokeWidth: 2 }), kit.style.geom.lollipop.stem({ strokeWidth: 1, stroke: '#b5b0a3', dashArray: [2, 3], }), ], }) ); ``` An entry that names no part, such as `style.geom({ fill: 'navy' })`, styles the heads like any other geom's observations and never the stems. A stem entry's `stroke` replaces the scale color, so one entry gives every stem a neutral color under colored heads. A stem entry's `where` is tested against the lollipop it holds up. ## Hover and highlight Hovering a category emphasizes all its lollipops. The tooltip lists every group in legend order, with the head nearest the cursor marked in place. A press anywhere along a stem takes its category. A highlight raises each matched lollipop whole, stem and head together: ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'channel', eq: 'Online' }) ); ``` ## Data labels `dataLabels.showDataLabels` prints every head's value beside it. Under the default `position: 'auto'` each label continues the stem past its head: above a rising lollipop, below a hanging one. An explicit `position` puts every label on the side `justify` names. ```tsx theme={null} const spec = kit.pipe( // … kit.geom.lollipop({ dataLabels: { showDataLabels: true } }) ); ``` The label prints the head's `y`, formatted as the value axis formats it; map `label` to print another column instead. `showStackTotals` and `showCategoryLabels` are bar settings and a lollipop places neither. ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the lollipop renders, hovers, selects and takes annotations on its heads, but it is authored in code. * **Polar lollipops.** A radial lollipop is not drawn yet; `coord.polar()` is rejected with `UNSUPPORTED_COORD`. * **Aggregation.** Duplicate rows for one category and group all draw; aggregate them first with `transform.aggregate()`. # Mekko Source: https://docs.graphy.dev/sdk-next/graph-types/mekko A mekko, or Marimekko chart, is a 100% stacked column chart whose columns are as wide as their totals. Each cell's area is then its share of the whole: how big each market is reads across, and each company's share of it reads up. > [!WARNING] > > The mekko ships as its own package, `@graphysdk/geom-mekko`, on a `0.x` > version line. While it is on `0.x`, a minor release may change its aesthetics, > style or look; pin a caret range (`^0.1.0`) to take only patches. Tell us what > works and what doesn't on [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-mekko ``` It peer-depends on `@graphysdk/react` and nothing else. Register the plugin once with `createGraphyKit`, which adds `kit.geom.mekko()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { mekko } from '@graphysdk/geom-mekko'; const kit = createGraphyKit({ plugins: [mekko] }); ``` ## A mekko chart Pass one row per cell. Map the column to `x`, the cell's amount to `y` and its group to `color`: ```tsx theme={null} const data = { columns: [ { key: 'market', label: 'Market' }, { key: 'company', label: 'Company' }, { key: 'revenue', label: 'Revenue', valueFormat: { type: 'currency', iso: 'usd' }, }, ], rows: [ { market: 'Retail', company: 'Acme', revenue: 420 }, { market: 'Retail', company: 'Globex', revenue: 260 }, { market: 'Energy', company: 'Acme', revenue: 90 }, // … ], }; const spec = kit.pipe( kit.createSpec({ x: 'market', y: 'revenue', color: 'company' }), kit.geom.mekko(), kit.scale.x.continuous(), kit.scale.y.continuous() ); export function Markets() { return ( ); } ``` Each column is as wide as its total's share of the grand total, and the columns run in the order their first row comes. Each cell is as tall as its amount's share of its column, and the cells stack in the order their group first appears in the data, so a group holds the same place in every column. Rows that repeat a column and group are summed into one cell. A missing, zero, negative or non-finite amount draws no cell and adds nothing to its column. ## Column order Pass `sort` to run the columns in another order: `'descending'` runs them largest first, `'ascending'` smallest first, and a list names them in order, any column it leaves out following in data order. An unknown `sort` is reported as a warning, and the columns keep data order. ```tsx theme={null} kit.geom.mekko({ params: { sort: 'descending' } }); kit.geom.mekko({ params: { sort: ['Energy', 'Retail'] } }); ``` ## The share axis Both axes run from 0 to 100%. The axis along the columns is continuous, so declare `scale.x.continuous()`, not `scale.x.discrete()`: a discrete axis gives every category the same width. It is ticked with each column's name at the column's center, and draws no grid line through the columns. The other axis reads each cell's share of its column, in percentages. It has no title by default, since the share is a variable the geom writes; give it one in the config: ```tsx theme={null} config({ axes: { y: { label: 'Share of market' } } }); ``` ## Horizontal Add `kit.coord.flip()`. The columns run down the chart as rows, the first at the top, and the shares run across. ## Styling Every cell is one observation, styled with `kit.style.geom.mekko()`: | Properties | Default | | - | - | | `fill`, `fillAlpha`, `alpha`, `stroke`, `strokeWidth`, and the rest of the shared geom vocabulary | The `color` scale's colour, a 1px border in the background's colour | The cells abut, so the border is what parts them. A chart-wide `style.geom()` reaches the cells as it reaches a bar. A cell's `fill` takes a pattern (`diagonal`, `crosshatch` or `dots`) or a gradient as well as a colour. A pattern replaces the colour the scale gives, so set it as an override, and pick a group out with `where`. Hatching every group but one lets the groups read apart without colour alone: ```tsx theme={null} const spec = kit.pipe( // … kit.scale.color.discrete({ domain: ['Acme', 'Globex', 'Initech', 'Umbrella'], range: ['#0b2545', '#1d4e89', '#4f86c6', '#7fa7d9'], }), styles({ defaults: [kit.style.geom.mekko({ strokeWidth: 4 })], overrides: [ kit.style.geom.mekko( { fill: { pattern: 'diagonal', color: '#1d4e89', background: '#eef3fa', }, }, { where: { variable: 'company', eq: 'Globex' } } ), // … a `crosshatch` for Initech, `dots` for Umbrella ], }) ); ``` The legend's swatches keep the scale's colours; they do not take a pattern set by an override. ## The share variables The geom writes two shares as variables you can name in a `label`, a `highlight` or an entry's `where`. Both are fractions, in the percentage format: | Variable | Value | | - | - | | `mekkoShareOfColumn` | The cell's share of its column: its height | | `mekkoColumnShare` | The column's share of the total: its width | ## Hover and highlight Hovering a cell emphasises it. The tooltip is headed by its column, and lists each group's share of that column, with the hovered one marked: ``` Retail ■ Acme 42% ■ Globex 26% ■ Initech 18% ■ Umbrella 14% ``` A highlight raises each matched cell, whether it matches a column of the data or a share the geom wrote: ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'mekkoShareOfColumn', gt: 1 / 3 }) ); ``` ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the mekko renders, hovers, selects and takes annotations on a cell's center, but it is authored in code. * **Amounts in the tooltip.** It gives each group's share of the column, not its amount or the column's share of the total. * **Column labels that would overlap.** The share axis prints every column's name, however narrow its column; a very narrow column's name can run into its neighbour's. * **Data labels.** `dataLabels` prints nothing on a cell yet. * **Warnings for dropped amounts.** A zero, negative or non-finite amount is left out without a warning. * **A discrete share axis.** Declaring `scale.x.discrete()` draws the cells in the wrong places, with no warning. * **Polar coordinates.** A mekko draws under `coord.cartesian()` and `coord.flip()` only. # Pie & donut Source: https://docs.graphy.dev/sdk-next/graph-types/pie A pie chart is one 100% stacked bar bent around a circle: `geom.bar({ position: 'fill' })` under `coord.polar({ theta: 'y' })`. Each slice's angle is proportional to its value. Add an `innerRadius` and it becomes a donut, a pie with the center cut out. Both work best for a few parts of a whole. ## Pie Map the slice category to `color`, the value to `y`, and leave `x` empty so all slices share one band: ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, coord, } from '@graphysdk/react'; const data = { columns: [ { key: 'department', label: 'Department' }, { key: 'spend', label: 'Spend' }, ], rows: [ { department: 'Engineering', spend: 420 }, { department: 'Marketing', spend: 180 }, { department: 'Sales', spend: 150 }, { department: 'Operations', spend: 95 }, { department: 'HR', spend: 80 }, { department: 'Legal', spend: 75 }, ], }; export function BudgetBreakdown() { const spec = useMemo( () => pipe( createSpec({ x: '', y: 'spend', color: 'department' }), geom.bar({ position: 'fill' }), coord.polar({ theta: 'y' }), scale.x(), scale.y(), scale.color.discrete() ), [] ); return ( ); } ``` ## Donut A donut is the same recipe with an `innerRadius` on the polar coord. The hole leaves room for a [headline number](/sdk-next/config/headline-numbers), and a thin band can read more cleanly than a full pie: ```tsx theme={null} const data = { columns: [ { key: 'browser', label: 'Browser' }, { key: 'share', label: 'Share' }, ], rows: [ { browser: 'Chrome', share: 65 }, { browser: 'Safari', share: 18 }, { browser: 'Firefox', share: 7 }, { browser: 'Edge', share: 5 }, { browser: 'Other', share: 5 }, ], }; const spec = pipe( createSpec({ x: '', y: 'share', color: 'browser' }), geom.bar({ position: 'fill' }), coord.polar({ theta: 'y', innerRadius: 0.55 }), scale.x(), scale.y(), scale.color.discrete() ); ``` `innerRadius` is a fraction of the outer radius; `0.55` cuts the hole just past halfway. Larger values give a thinner band. The hole is the natural home for a [headline number](/sdk-next/config/headline-numbers): ```tsx theme={null} config({ headline: { show: 'total', position: 'center' } }); ``` `position: 'center'` needs a non-zero `innerRadius`; a full pie has no hole to sit in. ## Where the first slice starts `coord.polar` takes a `startAngle` in degrees, `0` by default. It rotates the whole pie, which is how you put the largest slice at a chosen clock position: ```tsx theme={null} coord.polar({ theta: 'y', innerRadius: 0.55, startAngle: 90 }); ``` ## Slice labels Turn on [data labels](/sdk-next/config/data-labels) to print each slice's value or share. On polar bars, `showCategoryLabels` prefixes the category: ```tsx theme={null} geom.bar({ position: 'fill', dataLabels: { showDataLabels: true, format: 'percentage', // the resolved default under polar; 'absolute' for raw values showCategoryLabels: true, // "Engineering · 42%" }, }); ``` ## Separated wedges A border in the chart's background color leaves a crisp gap between wedges. Borders are paint, so they come from the [stylesheet](/sdk-next/config/styling): ```tsx theme={null} import { createSpec, pipe, geom, scale, coord, styles, style, } from '@graphysdk/react'; const spec = pipe( createSpec({ x: '', y: 'spend', color: 'department' }), geom.bar({ position: 'fill' }), coord.polar({ theta: 'y' }), scale.x(), scale.y(), scale.color.discrete(), styles({ defaults: [style.geom.bar({ stroke: '#ffffff', strokeWidth: 2 })], }) ); ``` The border is the only way to do this. Every slice is a segment of one stacked bar, so the segments meet edge to edge and there is no band gap to widen. ## Rounded wedges Wedges take the bar geom's `cornerRadius`, a named token the polar renderer sizes against each wedge's radial span. The default is `'sm'`, so wedge corners are already slightly rounded: ```tsx theme={null} styles({ defaults: [style.geom.bar({ cornerRadius: 'none' })] }); // crisp, square wedges styles({ defaults: [style.geom.bar({ cornerRadius: 'lg' })] }); // pronounced petals ``` ## Related * [Styling](/sdk-next/config/styling) — the stylesheet, its cascade and every target * [Headline numbers](/sdk-next/config/headline-numbers) — a metric in the donut's hole * [Radar & radial](/sdk-next/graph-types/radial) — the `theta: 'x'` polar charts * [Coordinate systems](/sdk-next/concepts/coordinate-systems) — the polar plane, `theta`, `startAngle` and `innerRadius` # Radar & radial Source: https://docs.graphy.dev/sdk-next/graph-types/radial Radial charts bend the plane into a circle with `coord.polar`, just like [pie & donut](/sdk-next/graph-types/pie), but here the category sweeps around the circle instead of the value. With `theta: 'x'`, each category takes a spoke and its value grows outward along the radius. The geom then sets the shape: `geom.line()` traces a radar, `geom.area()` fills it, `geom.bar()` draws a rose. With `theta: 'y'`, the value sweeps the angle instead, and each category becomes its own concentric track — a radial bar. They read best for a small, fixed set of categories compared across a few groups: a skills profile, a weekly cycle, activity by hour. ## Radar A radar (or spider) chart is a `geom.line()` under `coord.polar({ theta: 'x' })`. Map the axis category to `x`, the value to `y`, and a group column to `color`. Two scale settings carry the chart. A discrete `x` scale gives each category an evenly spaced spoke. `domainMin: 0` on `y` is all but required for a line: it puts zero at the center of the circle, so the radius stays proportional to the value. Without it the domain starts at the data minimum, which lands at the center and exaggerates the differences. An area or bar holds zero on its own. A non-interactive `geom.point()` layer marks each vertex: ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, coord, } from '@graphysdk/react'; const data = { columns: [ { key: 'skill', label: 'Skill' }, { key: 'player', label: 'Player' }, { key: 'score', label: 'Score' }, ], rows: [ { skill: 'Speed', player: 'Alice', score: 8 }, { skill: 'Power', player: 'Alice', score: 6 }, { skill: 'Defense', player: 'Alice', score: 7 }, // ... { skill: 'Speed', player: 'Bob', score: 6 }, { skill: 'Power', player: 'Bob', score: 9 }, { skill: 'Defense', player: 'Bob', score: 5 }, // ... ], }; export function SkillsRadar() { const spec = useMemo( () => pipe( createSpec({ x: 'skill', y: 'score', color: 'player' }), geom.line(), geom.point({ interactive: false }), coord.polar({ theta: 'x' }), scale.x.discrete(), scale.y({ domainMin: 0 }), scale.color.discrete() ), [] ); return ( ); } ``` ### Time around the circle A datetime `x` scale in place of the discrete one turns the radar into a clock or a calendar. See [Coordinate systems](/sdk-next/concepts/coordinate-systems#time-around-the-circle) for how the turn is divided: ```tsx theme={null} const spec = pipe( createSpec({ x: 'month', y: 'temperature' }), geom.line(), geom.point({ interactive: false }), coord.polar({ theta: 'x' }), scale.x.datetime(), scale.y() ); ``` ## Filled radar Swap the line for `geom.area({ position: 'identity' })` to fill each group's polygon. Keep `position: 'identity'` so the areas overlap at their true values rather than stacking; the vertex points still sit on top: ```tsx theme={null} const spec = pipe( createSpec({ x: 'skill', y: 'score', color: 'player' }), geom.area({ position: 'identity' }), geom.point({ interactive: false }), coord.polar({ theta: 'x' }), scale.x.discrete(), scale.y(), scale.color.discrete() ); ``` Area fills draw at `fillAlpha: 0.3`. With more than two or three groups they still obscure each other. Use the outline radar above, or take the fill down further through the [stylesheet](/sdk-next/config/styling): ```tsx theme={null} styles({ defaults: [style.geom.area({ fillAlpha: 0.15 })] }); ``` The vertex dots draw at the built-in `size: 8`, which is heavy for a radar. Give the point layer an `id` and scope a style entry to it: ```tsx theme={null} const spec = pipe( createSpec({ x: 'skill', y: 'score', color: 'player' }), geom.area({ position: 'identity' }), geom.point({ id: 'vertices', interactive: false }), coord.polar({ theta: 'x' }), scale.x.discrete(), scale.y(), scale.color.discrete(), styles({ defaults: [ style.geom.area({ fillAlpha: 0.15 }), style.geom.point({ size: 5 }, { layer: 'vertices' }), ], }) ); ``` ## Rose / coxcomb A rose (or coxcomb) chart keeps `theta: 'x'` but draws bars instead of a line: each category is a wedge, and the value sets how far it reaches. Map a group to `color` and pick a [position](/sdk-next/graph-types/index#position-modes): `dodge` splits each wedge into side-by-side petals, `stack` grows the groups outward along the radius: ```tsx theme={null} const spec = pipe( createSpec({ x: 'day', y: 'signups', color: 'channel' }), geom.bar({ position: 'dodge' }), // 'stack' to stack the groups along the radius coord.polar({ theta: 'x' }), scale.x.discrete(), scale.y(), scale.color.discrete() ); ``` `params.width` sets a petal's angular thickness as a fraction of its band, in `(0, 1]`. Under polar it defaults to `1`, so neighboring petals touch; drop it to open a gap between wedges. ## Radial bar A radial bar (or race-track) chart puts the value back on the angle with `theta: 'y'`: each category is its own concentric track and the value sweeps around it. An `innerRadius` opens the center so the innermost track is not crushed into a point. Stack the groups to read each track as consecutive colored segments: ```tsx theme={null} const spec = pipe( createSpec({ x: 'day', y: 'signups', color: 'channel' }), geom.bar({ position: 'stack' }), coord.polar({ theta: 'y', innerRadius: 0.15 }), scale.x.discrete(), scale.y(), scale.color.discrete() ); ``` Here `params.width` is the radial thickness of each track. `cornerRadius: 'full'` gives a racetrack its capsule ends by rounding each arc to half its radial span: ```tsx theme={null} styles({ defaults: [style.geom.bar({ cornerRadius: 'full' })] }); ``` On a stacked track only the outer corners of the whole arc round, so the segments inside it stay flush. A border in the background color separates them instead: ```tsx theme={null} styles({ defaults: [style.geom.bar({ stroke: '#ffffff', strokeWidth: 2 })], }); ``` ## Where the first spoke lands `coord.polar` takes a `startAngle` in degrees, `0` by default. On a rose it decides which spoke sits at the top; on a racetrack, where every arc starts: ```tsx theme={null} coord.polar({ theta: 'x', startAngle: 45 }); ``` ## Axes Unlike a pie or donut, these charts map a real column to `x`, so both axes render by default. Turn the `x` axis off when the legend already names the spokes: ```tsx theme={null} config({ axes: { x: { visible: false } } }); ``` ## Related * [Styling](/sdk-next/config/styling) — fill opacity, marker size, corner radius and per-layer scoping * [Coordinate systems](/sdk-next/concepts/coordinate-systems) — the polar plane, `theta`, `startAngle` and `innerRadius` * [Pie & donut](/sdk-next/graph-types/pie) — the `theta: 'y'` parts-of-a-whole family * [Chart types overview](/sdk-next/graph-types/index) — geoms, positions and the full recipe list # Sankey Source: https://docs.graphy.dev/sdk-next/graph-types/sankey A sankey diagram draws flows between nodes, each flow as wide as its amount. What runs through a system, and where it ends up, reads at a glance: energy from its sources to its uses, a budget from income to spending, visitors from a landing page to a sale. > [!WARNING] > > The sankey ships as its own package, `@graphysdk/geom-sankey`, on a `0.x` > version line. While it is on `0.x`, a minor release may change its aesthetics, > params, style or look; pin a caret range (`^0.1.0`) to take only patches. Tell > us what works and what doesn't on [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-sankey ``` It peer-depends on `@graphysdk/react` and nothing else. Register the plugin once with `createGraphyKit`, which adds `kit.geom.sankey()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { sankey } from '@graphysdk/geom-sankey'; const kit = createGraphyKit({ plugins: [sankey] }); ``` ## A sankey diagram Pass one row per flow. Map the node it leaves to `source`, the node it reaches to `target` and its amount to `value`: ```tsx theme={null} const data = { columns: [ { key: 'from', label: 'From' }, { key: 'to', label: 'To' }, { key: 'twh', label: 'Energy (TWh)' }, ], rows: [ { from: 'Coal', to: 'Electricity', twh: 110 }, { from: 'Gas', to: 'Electricity', twh: 90 }, { from: 'Gas', to: 'Heat', twh: 120 }, { from: 'Electricity', to: 'Homes', twh: 120 }, // … ], }; const spec = kit.pipe( kit.createSpec({ source: 'from', target: 'to', value: 'twh', }), kit.geom.sankey() ); export function Energy() { return ( ); } ``` The nodes are the names the flows share, each as tall as the larger of what flows into it and what flows out. The flows run from left to right, a node in the column after every node that flows into it, and each flow is as wide at both ends as its amount. The geom lays itself out across the panel, so it takes no scales and draws no axes. A missing, zero or negative amount draws no flow and adds nothing to either node. A flow from a node to itself, or one that would close a loop with the rows before it, is left out: a sankey's flows run one way. ## Color Each node takes its own color from the palette, and each flow the color of the node it leaves, as above. Each node already carries its name, so the chart draws no legend for it unless you set `config.legend.position`. Color always groups by node. Map it to your source or target column to choose which node a flow takes its color from: | `color` | A flow is colored as… | | - | - | | unmapped, or the source | The node it leaves | | the target (`color: 'to'`) | The node it reaches | A node keeps its own color either way. Any other column is refused with `UNSUPPORTED_MAPPING`: a node has no value in it, so every node would fall into one group. Set `color: { value: '#…' }` to paint every node and flow one color. ## Alignment `params.align` chooses the column a node sits in: | `align` | A node sits… | | - | - | | `'justify'` (default) | As far left as its inflows allow, except a node no flow leaves: the last column | | `'left'` | As far left as its inflows allow | | `'right'` | As far right as its outflows allow | | `'center'` | As far left as its inflows allow, except a node no flow reaches: as far right as its outflows allow | ```tsx theme={null} kit.geom.sankey({ params: { align: 'left' } }); ``` ## Styling A sankey paints three things, each with its own builder: | Builder | Paints | Properties | Default | | - | - | - | - | | `kit.style.geom.sankey()` | The flows | `fill`, `fillAlpha`, `alpha`, `stroke`, `strokeAlpha`, and the rest of the shared geom vocabulary | The `color` scale's color at `fillAlpha` 0.4, and 0.65 while hovered | | `kit.style.geom.sankey.node()` | The nodes | `fill`, `fillAlpha`, `alpha`, `stroke`, `strokeWidth` | The `color` scale's color, with no border | | `kit.style.geom.sankey.label()` | The node names | `fontFamily`, `fontSize`, `fontWeight`, `textColor`, `textOutlineColor`, `textOutlineWidth` | 12px, weight 500, in the primary text color, outlined in the background's color | The outline keeps a name legible where it crosses a flow. The flows and the nodes read the `color` scale, and the scale beats an entry in `defaults`, so an entry that replaces their color is an override. Every entry takes a `where`, which tests the observation it paints: ```tsx theme={null} const spec = kit.pipe( // … kit.scale.color.discrete({ range: ['#0b3c49', '#115e67', '#16808a', '#2aa1a6', '#5cbfbf', '#8fd6d2'], }), styles({ defaults: [ kit.style.geom.sankey({ fillAlpha: 0.5 }), kit.style.geom.sankey.node({ stroke: '#ffffff', strokeWidth: 1 }), kit.style.geom.sankey.label({ fontWeight: 700, fontSize: 13 }), ], overrides: [kit.style.geom.sankey.node({ fill: '#1f2933' })], }) ); ``` A flow's `fill` takes a pattern (`diagonal`, `crosshatch` or `dots`) or a gradient as well as a color, and so does a node's. ## Hover and highlight Hovering a node raises it with every flow into and out of it; hovering a flow raises it with the two nodes it joins. Everything else dims. A node's tooltip is headed by its name and gives its total; a flow's is headed by the node it leaves, and names where it goes: ``` Gas Electricity To Electricity Energy (TWh) 360 Energy (TWh) 90 ``` A highlight raises each matched node and flow. A node holds its own name in your source column, so matching a source raises the node with the flows leaving it: ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'from', eq: 'Gas' }) ); ``` A node's total is written into your amount column, so `highlight({ variable: 'twh', gt: 100 })` matches the nodes and the flows over 100. ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the sankey renders and takes annotations on a node's center or a flow's middle, but it is authored in code. * **Color by a column other than the source or target.** Color groups by node. * **A flow colored from its source to its target.** A flow takes one color; a gradient between its two nodes' colors is not drawn. * **Node width and spacing.** The nodes and the gaps between them are sized to the panel, and are not yet options. * **Loops.** A flow that would close a loop is left out rather than drawn back around. * **Names that would overlap.** A name is drawn however short its node is, so the names of two short nodes in one column can meet. # Scatter & bubble Source: https://docs.graphy.dev/sdk-next/graph-types/scatter A scatter plot draws a `geom.point()` per observation against two continuous axes. It shows correlation and distribution. Add a `size` mapping and it becomes a bubble chart, encoding a third variable. ## Scatter Map two numeric columns to `x` and `y`, and declare continuous scales: ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, } from '@graphysdk/react'; const data = { columns: [ { key: 'weight', label: 'Weight' }, { key: 'height', label: 'Height' }, ], rows: [ { weight: 60, height: 160 }, { weight: 65, height: 165 }, { weight: 70, height: 175 }, { weight: 75, height: 170 }, { weight: 80, height: 180 }, { weight: 85, height: 178 }, { weight: 90, height: 185 }, ], }; export function WeightVsHeight() { const spec = useMemo( () => pipe( createSpec({ x: 'weight', y: 'height' }), geom.point(), scale.x.continuous(), scale.y.continuous() ), [] ); return ( ); } ``` ## Coloring by category Map a categorical column to `color` to tell groups of points apart: ```tsx theme={null} const spec = pipe( createSpec({ x: 'weight', y: 'height', color: 'gender' }), geom.point(), scale.x.continuous(), scale.y.continuous(), scale.color.discrete() ); ``` ## Bubble Map a third numeric column to `size` and add a size scale. The default size scale uses a square-root transform, so bubble area is proportional to the value: ```tsx theme={null} const spec = pipe( createSpec({ x: 'gdp', y: 'lifeExpectancy', size: 'population', color: 'continent', }), geom.point(), scale.x.continuous(), scale.y.continuous(), scale.size.continuous(), scale.color.discrete() ); ``` Each mapped aesthetic needs its own scale. `scale.size.continuous()` sizes the bubbles and `scale.color.discrete()` colors them; without it every bubble takes the built-in geom color. ## Missing values `geom.point()` has no `missingValues` param. An observation whose `x` or `y` is `null` is not drawn, and the rest of the cloud is unaffected. ## Painting the markers `geom.point()` takes no params. A marker's whole appearance comes from the [stylesheet](/sdk-next/config/styling), through the `style.geom.point` target: `fill`, `stroke`, `strokeWidth`, `size`, `symbol`, `alpha` and `saturation`: ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'weight', y: 'height' }), geom.point(), scale.x.continuous(), scale.y.continuous(), styles({ defaults: [style.geom.point({ size: 6, alpha: 0.5 })] }) ); ``` Markers draw at `size: 8` with a 1px white border by default. Lower `alpha` for a dense cloud, where overlapping markers would otherwise merge into a solid mass. One cascade rule matters here. A `defaults` entry applies only where no mapped aesthetic decided the value, so on a bubble chart `style.geom.point({ size: 6 })` is ignored: the `size` mapping decides. Put the entry in `overrides`, which outranks the mapping: ```tsx theme={null} styles({ overrides: [style.geom.point({ size: 6 })] }); ``` ## Related * [Styling](/sdk-next/config/styling) — marker paint, size and the cascade * [Scales](/sdk-next/concepts/scales) — continuous and size scales * [Coordinate systems](/sdk-next/concepts/coordinate-systems) — `coord.flip()` swaps the two axes * [Statistics](/sdk-next/advanced/statistics) — add a `smooth` trendline over the points # Waterfall Source: https://docs.graphy.dev/sdk-next/graph-types/waterfall A waterfall shows how a running total gets from its start to its end. Each change is a bar floating from the total before it to the total after, colored by whether it adds or takes away. Totals stand on zero, and a connector carries the total from each bar to the next. > [!WARNING] > > The waterfall ships as its own package, `@graphysdk/geom-waterfall`, on a > `0.x` version line. While it is on `0.x`, a minor release may change its > aesthetics, style parts or look; pin a caret range (`^0.1.0`) to take only > patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/geom-waterfall ``` Its only peer dependencies are `@graphysdk/react` and React. Register the plugin once with `createGraphyKit`, which adds `kit.geom.waterfall()` and binds `kit.GraphProvider` to the same plugin: ```tsx theme={null} import { createGraphyKit, GraphRenderer, highlight, styles, } from '@graphysdk/react'; import { waterfall } from '@graphysdk/geom-waterfall'; const kit = createGraphyKit({ plugins: [waterfall] }); ``` ## One bar per change Pass one row per bar, in the order the total runs. Map the category to `x` and the change to `y`, and map `measure` to a column saying which rows are totals: ```tsx theme={null} const data = { columns: [{ key: 'item' }, { key: 'amount' }, { key: 'measure' }], rows: [ { item: 'Revenue', amount: 1250, measure: 'absolute' }, { item: 'Cost of sales', amount: -480 }, { item: 'Gross profit', measure: 'total' }, { item: 'Sales & marketing', amount: -210 }, // … { item: 'Net income', measure: 'total' }, ], }; const spec = kit.pipe( kit.createSpec({ x: 'item', y: 'amount' }), kit.geom.waterfall({ aes: { measure: 'measure' } }), kit.scale.x.discrete(), kit.scale.y.continuous() ); export function ProfitBridge() { return ( ); } ``` The value axis fits the running totals, not the changes, and holds zero, since a bar is a length. The category axis is always discrete. The total runs in data order, and each connector joins a bar to the next row's bar. Sort the rows, not the axis: a discrete `domain` that reorders the categories moves the bars but not the total, so the connectors then skip across bars. ## Totals `measure` reads each row as one of three kinds, the names Plotly uses: | Measure | The bar | | - | - | | `'relative'` | Moves the running total by `y`. An empty cell reads so. | | `'total'` | Draws the running total so far, from zero. Its own `y` may be empty. | | `'absolute'` | Draws `y` from zero, and sets the running total to it. | Without `measure`, every row is a step. Any other value is an error. Use `'absolute'` for an opening balance, and `'total'` for each subtotal and the closing figure: ## Running totals already in the data When the data already holds each bar's running total before and after, as a finance tool exports a bridge, set the layer's stat to `identity` and map `start` and `end` instead of `y`. `measure` still marks the totals, which color apart from the steps: ```tsx theme={null} kit.geom.waterfall({ stat: kit.stat.identity(), aes: { start: 'opening', end: 'closing', measure: 'measure' }, }); ``` Under `identity`, a missing `start` or `end` is an error. Under the default stat, mapping either is an error too, since the stat computes them from `y`. ## Horizontal Add `kit.coord.flip()`. The categories move to the vertical axis and the bars run across. ## Bar width and connectors `width` sets how much of its band each bar spans, as a fraction from just above `0` up to `1`. It defaults to `0.8`, which leaves a gap for the connector; at `1` neighboring bars touch. A width above `1` is clamped to the full band, and one at or below `0` falls back to the default; the compiler warns in both cases. `connector` sets how the running total is carried from each bar to the next: `'spanning'` (the default) runs the line across both bars, along their edges, and `'between'` runs it across the gap only. To hide connectors, style them: `kit.style.geom.waterfall.connector({ alpha: 0 })` hides every one, and a `where` hides only those leaving the bars it picks. An unknown value falls back to `'spanning'` with a warning. ```tsx theme={null} kit.geom.waterfall({ aes: { measure: 'measure' }, params: { width: 0.6, connector: 'between' }, }); ``` ## Styling The bars are the observations, and each kind and the connectors have a builder of their own. A bar's color says what the step did, so a chart-wide `style.geom()` does not recolour it: | Builder | Properties | Default | | - | - | - | | `kit.style.geom.waterfall()` | `alpha`, `fillAlpha`, `strokeWidth` | Opaque, no border | | `kit.style.geom.waterfall.increase()` | `fill`, `stroke` | The positive trend color | | `kit.style.geom.waterfall.decrease()` | `fill`, `stroke` | The negative trend color | | `kit.style.geom.waterfall.total()` | `fill`, `stroke` | The neutral trend color | | `kit.style.geom.waterfall.connector()` | `stroke`, `alpha`, `strokeWidth`, `dashArray` | A 1px line | ```tsx theme={null} const spec = kit.pipe( // … styles({ defaults: [ kit.style.geom.waterfall({ fillAlpha: 0.85 }), kit.style.geom.waterfall.increase({ fill: '#7fb685' }), kit.style.geom.waterfall.decrease({ fill: '#d98a7e' }), kit.style.geom.waterfall.total({ fill: '#3a3833' }), kit.style.geom.waterfall.connector({ stroke: '#898373', dashArray: [3, 3], }), ], }) ); ``` A connector is styled against the bar it leaves, so an entry's `where` can pick connectors out by that bar's values. ### Color through a scale To color the kinds through the color scale, with a legend, map `color` to `waterfallKind`. A mapped color outranks the kind builders' `defaults` entries; an `overrides` entry outranks the mapped color. The connectors keep their own color. ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'item', y: 'amount' }), kit.geom.waterfall({ aes: { measure: 'measure', color: 'waterfallKind' } }), kit.scale.x.discrete(), kit.scale.y.continuous(), kit.scale.color.discrete({ domain: ['increase', 'decrease', 'total'], range: ['#2a9d8f', '#e76f51', '#264653'], }) ); ``` ## The running-total variables The geom writes four variables you can name in a `highlight` or an entry's `where`: | Variable | Value | | - | - | | `waterfallStart` | The running total the bar starts from, in `y`'s units | | `waterfallEnd` | The running total the bar ends on, in `y`'s units | | `waterfallKind` | `'increase'`, `'decrease'` or `'total'` | | `waterfallValue` | The figure the bar shows: its change, or the total it draws | Under `stat: 'identity'` your own `start` and `end` columns hold the running totals, so the geom writes only `waterfallKind` and `waterfallValue`. `y` keeps the values you passed. A pinned number or a difference arrow reads `waterfallValue`. An absolute bar is a `'total'` kind; highlight on your `measure` column to pick it out. ## Hover and highlight Hovering anywhere in a category's band emphasizes its bar, and the tooltip lists its value and the running total it lands on. A highlight raises each matched bar. The connectors stay with the dimmed layer: ```tsx theme={null} const spec = kit.pipe( // … highlight({ variable: 'waterfallKind', eq: 'total' }) ); ``` ## Data labels `dataLabels.showDataLabels` prints each bar's figure: the change for a step, the running total for a total. Under the default `position: 'auto'` each label sits just past the end the bar lands on, above an increase and below a decrease, clear of the connector. An explicit `position` puts every label on the side `justify` names. `showStackTotals` and `showCategoryLabels` are bar settings and a waterfall places neither. ```tsx theme={null} const spec = kit.pipe( // … kit.geom.waterfall({ aes: { measure: 'measure' }, dataLabels: { showDataLabels: true }, }) ); ``` ## Not yet supported * **The editor's chart-type picker and the chart agent.** In editable mode the waterfall renders, hovers, selects and takes annotations on each bar's running total, but it is authored in code. * **Dodged groups.** Each group runs its own total, but groups on the same category overlap rather than sitting side by side. # Introduction Source: https://docs.graphy.dev/sdk-next/index The Graphy SDK is a charting library for React, built on a **grammar of graphics**. You do not pick a chart from a fixed menu. You describe one from a few parts: which columns map to which aesthetics, what shapes to draw, and how values become positions and colors. The same parts combine into anything from a line chart to a polar racetrack. ## One package Start with `@graphysdk/react`. You describe a chart as a **spec**; `GraphProvider` compiles it with your data and `GraphRenderer` paints it. The same spec can be stored, reused with other data, or compiled outside React — see [How a chart is built](/sdk-next/concepts/how-a-chart-is-built). ## Anatomy of a spec A spec is assembled from a few kinds of part, folded together with `pipe`: ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style, config, } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue', color: 'product' }), // mapping — columns → aesthetics geom.line(), // geom — the shape to draw scale.x(), // scale — values → positions scale.y(), scale.color.discrete(), // scale — groups → colors styles({ defaults: [style.geom.line({ strokeWidth: 3 })] }), // styles — how the geoms are painted config({ legend: { position: 'bottom' } }) // config — chart chrome ); ``` Each part has its own page: | Part | Builder | Concept | | - | - | - | | **Mapping** | `mapping` / `createSpec` | [Mappings & aesthetics](/sdk-next/concepts/mappings) | | **Geom** | `geom.*` | [Geoms & layers](/sdk-next/concepts/geoms) | | **Scale** | `scale.*` | [Scales](/sdk-next/concepts/scales) | | **Coord** | `coord.*` | [Coordinate systems](/sdk-next/concepts/coordinate-systems) | | **Stat** | `stat.*` | [Statistics](/sdk-next/advanced/statistics) | | **Transform** | `transform.*` | [Transforms](/sdk-next/advanced/transforms) | | **Styles** | `styles` / `style.*` / `token` | [Styling](/sdk-next/config/styling) | | **Config** | `config` | [Configuration](/sdk-next/config/index) | Two more parts call things out: `highlight(...)` for [highlights](/sdk-next/emphasis/highlights) and `annotation.*` for [annotations](/sdk-next/emphasis/annotations). The builders only assemble a plain JSON object. You can store a spec in a database and load it back to render, with no builder in sight. See [Serializable spec](/sdk-next/concepts/serializable-spec). ## Where to start > **[Quickstart](/sdk-next/quickstart)** > > Install `@graphysdk/react` and render your first chart. > **[How a chart is built](/sdk-next/concepts/how-a-chart-is-built)** > > The spec pipeline and the compile → render model. > **[Data structure](/sdk-next/data-structure)** > > The data table and how mappings reference it. > **[Chart types](/sdk-next/graph-types/index)** > > Recipes for line, bar, pie, scatter and more. # Quickstart Source: https://docs.graphy.dev/sdk-next/quickstart Start with `@graphysdk/react`. One package builds the spec and renders the chart. The "Made with Graphy" mark is on by default. ### 1. Install the package ```shell npm theme={null} npm install @graphysdk/react ``` ```shell pnpm theme={null} pnpm add @graphysdk/react ``` ```shell yarn theme={null} yarn add @graphysdk/react ``` ```shell bun theme={null} bun add @graphysdk/react ``` What the install expects from your project: * **React 19** — `react@^19.0.0` is a peer of the package. * **Nothing else for a read-only chart** — the editing surface's rich-text editor is an optional peer, so a chart you only paint pulls in no `@tiptap/*` package. See [Editing](#editing). * **`moduleResolution: "bundler"`** (or `node16` / `nodenext`) in your `tsconfig.json` — the packages ship an `exports` map, which `moduleResolution: "node"` skips. * **A bundler that processes CSS imports from `node_modules`** — Vite, Next.js and webpack with `css-loader` all do. There is no stylesheet to import: each entry point imports its own CSS file. * **A client component** — the components use the DOM. In the Next.js app router, put `'use client'` at the top of the file that renders them. ### 2. Render your first chart A chart needs three things: your data, a spec that says how to draw it, and the React components that paint it. All come from `@graphysdk/react`. ```tsx theme={null} import { useMemo } from 'react'; import { GraphProvider, GraphRenderer, createSpec, pipe, geom, scale, } from '@graphysdk/react'; // 1. Your data — a table of columns and rows. const data = { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: 'Jan', revenue: 12000 }, { month: 'Feb', revenue: 15000 }, { month: 'Mar', revenue: 18000 }, { month: 'Apr', revenue: 17000 }, { month: 'May', revenue: 21000 }, ], }; export function App() { // 2. The spec — map columns to aesthetics, pick a geom, declare the scales. const spec = useMemo( () => pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), scale.x(), scale.y() ), [] ); // 3. Render it. return (
); } ``` `GraphRenderer` fills its parent, so the parent needs a height. A container with no height paints nothing and logs `ZERO_SIZE_CONTAINER` once. Build the spec once or memoize it: a new spec object recompiles the chart. ### 3. Customize the chart The chart's chrome — titles, axes, legend, layout — lives in `config`. Pipe a `config(...)` item onto the spec: ```tsx theme={null} import { createSpec, pipe, geom, scale, config } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue', color: 'product' }), geom.line({ params: { curve: 'smooth' } }), scale.x(), scale.y.continuous({ domainMin: 0, nice: true }), scale.color.discrete(), config({ content: { title: 'Monthly revenue', showTitle: true, subtitle: 'By product, 2026', showSubtitle: true, }, axes: { y: { label: 'Revenue (£)' }, }, legend: { position: 'bottom' }, }) ); ``` ### 4. Restyle it Paint — colors, stroke widths, corner rounding, fonts — lives in a **stylesheet** on the spec. Pipe a `styles(...)` item in the same way. Redefining a built-in token restyles every default that reads it; a `style.*` entry targets one thing: ```tsx theme={null} import { createSpec, pipe, geom, scale, styles, style } from '@graphysdk/react'; const spec = pipe( createSpec({ x: 'month', y: 'revenue' }), geom.bar(), scale.x(), scale.y(), styles({ tokens: { geom: { light: '#0B5FFF', dark: '#6AA1FF' } }, defaults: [style.geom.bar({ cornerRadius: 'full' })], }) ); ``` The stylesheet sits on the spec, so it serializes and travels with the chart. See [Styling](/sdk-next/config/styling) for every target and the cascade that resolves them. ### 5. Color scheme and locale `GraphProvider` accepts a `colorScheme` (`'light'` or `'dark'`, defaulting to `'light'`) and a `formattingLocale` (`'en-GB'`, `'en-US'`, `'pt-PT'` or `'ar'`) for number, date and currency formatting: ```tsx theme={null} ``` The provider's other props: * `theme` — a [theme](/sdk-next/themes/overview) that sits under the spec's own styles and config. A theme's `colorScheme` overrides the prop. * `plugins` — custom geoms, stats and transforms. See [Extending](/sdk-next/extending/index). * `onError`, `onWarnings` — called with a compile's errors or warnings. Both also go to the console, and a failed compile shows its errors in place of the chart. * `onSpecChange`, `handleRef` — for editing and driving the chart from outside the tree. See [Provider and renderer](/sdk-next/rendering/provider-and-renderer). `theme` and `plugins` are read once at mount; change the React `key` to swap them. `data`, `spec` and `colorScheme` recompile on every change. `GraphRenderer` has props of its own: * `sizing` — how the chart takes space. `responsive` (the default) fills its container; `fixed` takes a `width` and `height`; `keepAspectRatio` scales to the container width from `intrinsicWidth` and `intrinsicHeight`, or either one plus an `aspectRatio`. * `onResize` — called with the container size whenever it changes, in every sizing mode. * `animation` — `true` by default. `false` turns off the intro and the data transitions. An object `{ intro, transitions, maxAnimatedGeoms }` tunes each; `intro` takes `enabled`, `trigger` (`'mount'` or `'inView'`), `durationScale`, `stagger` and `staggerOrder`. A reduced-motion preference turns animation off regardless, as does a chart with more than `maxAnimatedGeoms` geoms (default 1500). * `showTooltips` — `true` by default. `false` hides the tooltip whatever `config.tooltip` says. * `mode` — `'readonly'` (the default), `'editable'` or `'point-and-edit'`. The editing modes need `EditableGraphRenderer` from `@graphysdk/react/editable`. * `slots` — per-region component overrides. See [Slots](/sdk-next/extending/slots). ## Editing `@graphysdk/react/editable` exports `EditableGraphRenderer`. It mounts inside the `GraphProvider` from the main entry and adds a rich-text editor over the chart's title, caption and text annotations. The editor is built on TipTap v3. Those packages are optional peers, so a read-only install skips them. Add them yourself when you import this entry: ```shell theme={null} npm install @tiptap/core @tiptap/pm @tiptap/react @tiptap/extensions \ @tiptap/extension-bold @tiptap/extension-document @tiptap/extension-hard-break \ @tiptap/extension-heading @tiptap/extension-italic @tiptap/extension-link \ @tiptap/extension-paragraph @tiptap/extension-text @tiptap/extension-text-align \ @tiptap/extension-text-style @tiptap/extension-underline ``` ## Using the engine and renderer separately `@graphysdk/react-renderer` exports the same `GraphProvider` and `GraphRenderer` with the mark off by default. `@graphysdk/viz-engine` holds the builders and the compiler, with no React. Import them directly when you want the mark off or you are not in React: ```tsx theme={null} import { createSpec, pipe, geom, scale } from '@graphysdk/viz-engine'; import { GraphProvider, GraphRenderer } from '@graphysdk/react-renderer'; ``` A spec that sets `config.content.brandMark.enabled` keeps that setting in every package. ### Next steps * Understand [how a chart is built](/sdk-next/concepts/how-a-chart-is-built) — the spec pipeline and the compile → render model * Learn the [data structure](/sdk-next/data-structure) and how mappings reference columns * Work through the [core concepts](/sdk-next/concepts/mappings) — mappings, geoms, scales, coords * Browse the [chart types](/sdk-next/graph-types/index) and build a [line chart](/sdk-next/graph-types/line) end to end * Read [license and evaluation](/licence/evaluation) before you put a chart on a public page # Formatting & locale Source: https://docs.graphy.dev/sdk-next/rendering/formatting The renderer turns raw values into display text: numbers, dates and currencies, in the chart's locale. The engine emits raw values; formatting happens here, at paint time. ## Two locales Two locales are in play: * The parsing locale decides how ambiguous input is read (is `01/02/2024` Jan 2 or Feb 1?). The data's rows are read in [`data._metadata.parsingLocale`](/sdk-next/data-structure#schema), which defaults to `en-GB`. Values written inside the spec, such as a date in a highlight predicate or an annotation anchor, are read in `config({ parsingLocale })`, which defaults to `en-US`. Set the two to match when the data's locale differs. * The formatting locale decides how output is displayed (separators, month names, currency symbols). Set it with the `formattingLocale` prop on `GraphProvider`. Without `formattingLocale`, display falls back to the spec's `config({ parsingLocale })`. ```tsx theme={null} ``` ## Supported locales `en-GB`, `en-US`, `ar`, `pt-PT`. ```tsx theme={null} import type { Locale } from '@graphysdk/viz-engine'; ``` ## What a locale drives The resolved locale, `formattingLocale ?? config.parsingLocale`, reaches two separate layers: * Value formatting: numbers, dates and currency symbols in axis labels, tooltips, data labels and headline numbers. * UI strings: the renderer's own phrases, such as the headline's comparison wording. Translations ship for `en-GB`, `en-US`, `ar` and `pt-PT`. Two independent fallbacks apply to the strings. A locale outside the supported set resolves to `en-GB` as a whole, and within a resolved locale a phrase with no translation falls back to the `en-GB` wording for that phrase alone. > [!NOTE] > > Dates always render in UTC. A host in UTC+13 reading a timestamp near midnight > sees the UTC calendar day, not the local one. Shift timestamps in your data > first if you need local days. The `duration` value format is always English, whatever the locale. ## Number formatting Locale governs separators and symbols. The [`numberFormat` config](/sdk-next/config/number-format) governs the rest, and can override the locale's separators when you need to: **decimals** `number | 'auto'` (default: auto) Fixed decimal places, or `'auto'` to choose from the value's magnitude. **abbreviation** `'auto' | 'k' | 'm' | 'b' | 'none'` `'auto'` abbreviates from magnitude (1234567 → `1.23m`); `'k'`, `'m'` and `'b'` force a unit; `'none'` writes the number in full. Applies to tooltips, data labels, legends and headlines. Axis ticks always abbreviate by their own magnitude. **thousandsSeparator** `string` Overrides the locale's grouping separator. **decimalSeparator** `string` Overrides the locale's decimal separator. ```tsx theme={null} config({ numberFormat: { decimals: 0, abbreviation: 'auto' }, }); ``` ## Currencies and dates Currency and date columns detected during [value-format inference](/sdk-next/data-structure#value-format-detection) are formatted for the active locale without extra configuration: a `£1,250` column renders with the right symbol and grouping, and a date axis with the locale's month names. ## Formatting a value yourself `createValueFormatter` is the formatter the renderer uses, for a custom [slot](/sdk-next/extending/slots) or [geom renderer](/sdk-next/extending/geom-renderers) that paints its own text. Give it a value format, a locale and the number-format config, and it returns a function from a value to a string: ```tsx theme={null} import { createValueFormatter } from '@graphysdk/react'; const formatRevenue = createValueFormatter({ valueFormat: { type: 'currency', iso: 'usd' }, locale: 'en-US', numberFormat: { decimals: 'auto', abbreviation: 'auto' }, }); formatRevenue(2500000); // "$2.50m" ``` A geom renderer reads the `valueFormat` from its layer or axis rather than writing one by hand, so its text matches the tooltip and the ticks. ## Related * [Number format](/sdk-next/config/number-format) — the `numberFormat` options in full * [Data structure](/sdk-next/data-structure) — parsing locale and value formats # Overview Source: https://docs.graphy.dev/sdk-next/rendering/index Import `` and `` from `@graphysdk/react`. They take the [scene](/sdk-next/concepts/how-a-chart-is-built#compile-then-render) and turn it into an interactive SVG chart: layout, formatting, interactivity, animation and the chrome tokens. The engine owns everything else. ## What the renderer owns * Layout: measuring text and packing the header, legend, axes and plot into pixel rectangles. * Formatting: turning raw values into locale-aware numbers, dates and currencies. * Chrome tokens: the CSS custom properties behind the HTML around the plot (legend, tooltip, headline, footer). * Interactivity: pointer handling, hover paint and tooltips. * Animation: transitions between compiled states. The [engine](/sdk-next/concepts/how-a-chart-is-built) owns the rest: mappings, scales, statistics, positions, hit-testing and the style cascade. The spec decides paint. Geom colors, grid lines, tick marks, the panel border, the graph background and the type on axis, tick and data labels all resolve from the [stylesheet](/sdk-next/config/styling) piped onto it. A [theme](/sdk-next/themes/overview) passed to the provider sits beneath the spec's own styles. ## The two components Rendering is always a ``, which holds the data and spec and compiles them, wrapping a ``, which paints the result: ```tsx theme={null} import { GraphProvider, GraphRenderer } from '@graphysdk/react'; ; ``` The split gives the scene one owner: commands and updates act on the provider's spec, and every renderer beneath it reflects them. [Provider & renderer](/sdk-next/rendering/provider-and-renderer) covers both in full. ## In this section > **[Provider & renderer](/sdk-next/rendering/provider-and-renderer)** > > The two components and their props. > **[Sizing](/sdk-next/rendering/sizing)** > > Responsive, fixed and aspect-ratio modes. > **[Theming](/sdk-next/rendering/theming)** > > Color scheme, the `theme` prop and the chrome tokens around the plot. > **[Formatting & locale](/sdk-next/rendering/formatting)** > > Locale-aware numbers, dates and currencies, UI strings and text direction. > **[Interactivity](/sdk-next/rendering/interactivity)** > > Hover, tooltips and animation. > **[Styling](/sdk-next/config/styling)** > > The stylesheet on the spec — every geom and chrome element the chart draws. To replace how a region renders (a custom header, legend or tooltip), see [Slots](/sdk-next/extending/slots) under Extending. # Interactivity Source: https://docs.graphy.dev/sdk-next/rendering/interactivity Charts are interactive by default. Hovering highlights the nearest observation and shows a tooltip, with no configuration. ```tsx theme={null} {/* hover + tooltips on by default */} ``` How the highlight and the dimming around it are painted is a [style](/sdk-next/config/styling) question; see [Restyling hover and dimming](#restyling-hover-and-dimming) below. ## How hover works Hit-testing runs in the engine, and the query depends on the geom: * Banded and bucketed cartesian geoms (bars, lines, areas) snap along the main axis: the coordinate the bands or buckets run along, which is x for a vertical bar or line chart and y for a horizontal bar chart. The cursor never has to land exactly on an observation, which is why a line chart responds smoothly between vertices. Everything at that main-axis position highlights together, every segment in a stack and every group in a multi-line chart, and the tooltip lists their values in legend order. * Point layers (scatter) use the nearest neighbor in 2D, so the closest point is picked rather than the nearest column. * Polar charts (pie and donut) hit-test the enclosing arc: the slice the cursor is inside. * Grid layers ([heatmaps](/sdk-next/graph-types/heatmap)) hit-test the enclosing cell, over its full band rather than the inset tile painted inside it. Cells the data leaves empty answer no hover. [Custom geoms](/sdk-next/extending/geom-renderers) answer their own hit-test queries. Layers can opt out with `interactive: false`, for example a decorative point overlay that should not compete with the line beneath it: ```tsx theme={null} geom.point({ interactive: false }); ``` A non-interactive layer is filtered out before both hit-testing and grouping, so it never takes a hover and never appears as a companion row in another layer's tooltip. As far as hover is concerned it is decoration. A [rule](/sdk-next/emphasis/reference-lines) layer is non-interactive by default. A layer that describes another, such as a marker for each bar's target or an error bar over its bar, attaches to it with `attachTo`. An attached layer never takes the hover itself. It joins the hover of the layer it attaches to, and its tooltip rows sit under that layer's, each under the observation it describes. `'previous'` attaches to the nearest earlier layer that stands alone, a layer `id` names one, and `'none'` keeps the layer on its own, which is the default for every built-in geom: ```tsx theme={null} pipe( createSpec({ x: 'region', y: 'actual', color: 'region' }), geom.bar({ id: 'actual' }), geom.point({ aes: { y: 'target' }, attachTo: 'actual' }) ); ``` A layer can only attach to an interactive layer that stands alone. An `id` that names no such layer, or a `'previous'` with nothing before it, leaves the layer on its own and the chart still renders. Marker diameter is a style, not a geom parameter: ```tsx theme={null} styles({ defaults: [style.geom.point({ size: 6 })] }); ``` ## Restyling hover and dimming Hover and dimming are painted through two style states, `hovered` and `dimmed`, that any geom entry can scope to. `dimmed` covers both cases where an element steps back: a [highlight](/sdk-next/emphasis/highlights) matched something else, or the pointer is on another element. States only paint. They never feed layout, so nothing moves when the pointer does. ```tsx theme={null} styles({ overrides: [ style.geom({ alpha: 0.15 }, { state: 'dimmed' }), style.geom.bar({ stroke: '#0B5FFF', strokeWidth: 2 }, { state: 'hovered' }), ], }); ``` By default `dimmed` drops a geom to `alpha: 0.4`. A hovered geom gains a soft shadow, and hovered bars and tiles also draw a white outline. See [Styling](/sdk-next/config/styling) for the full cascade. ## Tooltips Tooltips are on by default. `config.tooltip.mode` chooses which hovered observations the tooltip lists: * `'band'` (default) lists everything at the hovered position: every segment of a stack, every bar of a group, and the related observation on each other layer. A range row, such as an error bar's, lists every group's range in legend order, each named after its group. * `'observation'` lists only what was hovered, and from each other layer the observation in the same group. In a grouped bar chart with error bars, hovering one bar shows that bar and its interval. * `'none'` shows no tooltip. Hover, highlight and the hover guide still respond. ```ts theme={null} config({ tooltip: { mode: 'observation' } }); ``` `showTooltips={false}` on the renderer hides the tooltip whatever the spec says. The `'editable'` and `'point-and-edit'` [modes](/sdk-next/rendering/provider-and-renderer#graphrenderer) hide it too: ```tsx theme={null} ``` ### A layer's rows Each layer contributes its geom's rows: the hovered value, labeled by its color group, or the fields a geom declares (a candlestick's open, high, low and close). A layer's `tooltip` adds, removes, renames, reorders and formats them. Its keys name an aesthetic the layer maps, a variable the geom derives, or one of the geom's own fields; a layer's default rows are named `y`. Your entries come first, in their order, then the geom's remaining rows: ```ts theme={null} geom.point({ aes: { size: 'population' }, tooltip: { size: { title: 'Population', format: { type: 'integer' } }, y: { format: { type: 'currency', iso: 'usd' } }, color: { heading: true }, }, }); ``` * `false` removes a row and `true` adds or moves it. * `title` replaces the row's label, and `format` its value format. Values are formatted in the chart's locale. The default rows keep their color group's label, so a `title` on `y` names them only on a layer without color groups. * `heading: true` heads the tooltip with the field's value instead of the hovered main-axis value. A key the layer cannot read is dropped with an `UNKNOWN_TOOLTIP_FIELD` warning, and the chart still renders. ### Appearance To change how the tooltip looks, replace it with the `Tooltip` [slot](/sdk-next/extending/slots). It receives `TooltipSlotProps`: a single `content` prop carrying the formatted, render-ready rows the default tooltip gets. Positioning stays built in. `DefaultTooltip` is exported, so a custom tooltip can wrap it rather than rebuild it: ```tsx theme={null} import { DefaultTooltip, type TooltipSlotProps } from '@graphysdk/react'; const MyTooltip = ({ content }: TooltipSlotProps) => (
); ; ``` ## Animation A graph animates by default: an intro entrance on first mount (or, with `trigger: 'inView'`, when its panel comes into view), and transitions between compiled states when the data or a config value changes. Pass `animation` to turn either kind off, or to tune the intro: ```tsx theme={null} ``` The intro takes these settings: **enabled** `boolean` (default: true) Whether the entrance plays at all. Passing `intro: false` is the same thing. **trigger** `'mount' | 'inView'` (default: mount) When the entrance starts. `inView` waits until 40% of the panel is on screen, so a graph below the fold plays it as the reader scrolls to it. Until then only the title, axes and pill legends draw. It plays once, and on mount where the browser has no `IntersectionObserver`. Use `mount` for a panel that may never be 40% on screen: one clipped by its container, one taller than two and a half screens, or a printed page. **durationScale** `number` (default: 1) Multiplier applied to every entrance duration and stagger delay. **stagger** `boolean` (default: true) Whether geoms that support a staggered entrance enter one after another rather than all at once. **staggerOrder** `IntroStaggerOrder` (default: main-axis) The order staggered point geoms enter in. Bars and slices always enter in visual order. A viewer who prefers reduced motion gets no animation whatever this asks for. ### Transitions **transitions** `boolean` (default: true) Whether geoms animate to their new position when the data or a config value changes. Set on `animation` itself, beside `intro`: turning one off leaves the other running. ### Dense charts **maxAnimatedGeoms** `number` (default: 1500) Total geom count across all layers above which the graph plays no animation, intro or transitions. A line or area is one geom however many points it draws. Set on `animation` itself, beside `intro` and `transitions`. ## Related * [Styling](/sdk-next/config/styling) — the `hovered` and `dimmed` states, and everything else the chart paints * [Slots](/sdk-next/extending/slots) — replace the tooltip or other regions * [Provider & renderer](/sdk-next/rendering/provider-and-renderer) — `showTooltips`, `animation`, `mode` # Live data Source: https://docs.graphy.dev/sdk-next/rendering/live-data A chart fed by a live source updates by receiving new `data`. Keep `spec` the same object across pushes. The provider compares it by reference, and a new one means a full compile, spec resolution included, and clears the undo history. A data push alone reuses the resolved spec and keeps every layer's id, which is what lets a geom transition to its new position rather than redraw. Each source message supplies a complete data object (`columns` and `rows`) containing the current rolling window. ```tsx theme={null} import { useEffect, useMemo, useState } from 'react'; import { createSpec, type Data, geom, GraphProvider, GraphRenderer, pipe, scale, } from '@graphysdk/react'; function RollingLine({ source }: { source: EventSource }) { // Built once. Building it inside the render would make every push a full compile. const spec = useMemo( () => pipe( createSpec({ x: 'second', y: 'reading' }), geom.line(), scale.x.continuous({ nice: false }), scale.y.continuous() ), [] ); const [data, setData] = useState({ columns: [{ key: 'second' }, { key: 'reading' }], rows: [{ second: 0, reading: 50 }], }); useEffect(() => { source.onmessage = (event) => setData(JSON.parse(event.data)); }, [source]); return ( ); } ``` Transitions are off here because a source that pushes faster than a spring settles gives the spring nothing to settle toward. Above `maxAnimatedGeoms` (1500 by default, set on `animation`) the graph plays no animation on its own, but that counts geoms, not points. A live bar chart past the ceiling needs no such setting, while a line is one geom however many points it draws, so it keeps transitioning until you turn transitions off. ## Related * [Interactivity](/sdk-next/rendering/interactivity) — every `animation` setting * [Provider and renderer](/sdk-next/rendering/provider-and-renderer) — what `spec` and `data` are # Provider & renderer Source: https://docs.graphy.dev/sdk-next/rendering/provider-and-renderer Every chart is a `` wrapping a ``. The provider owns the data and spec and compiles them; the renderer paints the result. ```tsx theme={null} import { GraphProvider, GraphRenderer } from '@graphysdk/react'; ; ``` ## GraphProvider Holds the inputs and produces the scene. It recompiles when `spec`, `data` or `colorScheme` change by reference. A data or scheme change keeps layer ids; a spec change is a full compile. The provider from `@graphysdk/react` turns the "Made with Graphy" brand mark on unless the spec says otherwise. `config({ content: { brandMark: { enabled: false } } })` turns it off. See [Content](/sdk-next/config/content). **data** `Data` *required* The [data table](/sdk-next/data-structure) the spec draws from. **spec** `Spec` *required* The chart to compile — a [spec](/sdk-next/concepts/how-a-chart-is-built) from `createSpec`/`pipe`. **colorScheme** `ColorScheme` (default: light) `'light'` or `'dark'`. Selects the chrome token set, decides which half of every `{ light, dark }` [stylesheet](/sdk-next/config/styling) color and `token()` pair paints, and picks the tones of the default palette. See [Theming](/sdk-next/rendering/theming). **theme** `Theme` A [theme](/sdk-next/themes/overview): a stylesheet, palettes, config defaults and geom repaints that sit beneath the spec's own. Read once at mount; change the React `key` to switch themes. A theme that names a `colorScheme` overrides the `colorScheme` prop. **formattingLocale** `Locale` Locale for formatting displayed values. See [Formatting & locale](/sdk-next/rendering/formatting). **onError** `(errors: VizDiagnostic[]) =\> void` Fires with the diagnostics whenever a compile, recompile or dispatch produces errors, and again for a render-time throw caught by the error boundary. The chart shows an error panel in place rather than going blank. **onWarnings** `(warnings: VizDiagnostic[]) =\> void` Fires with any warnings a successful compile produced. **onSpecChange** `(next: Spec) =\> void` Fires with the new spec whenever a command changes it; persist the result here. See [Commands & history](/sdk-next/advanced/commands-and-history). **handleRef** `Ref\` Filled with this graph's handle, for callers mounted outside the provider where the hooks cannot reach. The handle carries `commands`, `undo`, `redo`, `subscribe`, `getScene`, `getSelection`, `setSelection` and `subscribeSelection`. See [Commands & history](/sdk-next/advanced/commands-and-history). **plugins** `readonly Plugin[]` Custom geoms, stats and transforms, with their render halves, registered for this graph. The compiler and the render resolver are built from the same array, so what can render cannot differ from what can compile. Frozen at mount: change the set by remounting with a new React `key`. **telemetry** `boolean` (default: true) A published production browser build sends one anonymous count the first time this provider has a scene. Set this to `false` to turn that off. ## GraphRenderer Paints the provider's scene. It must be a descendant of a ``. **sizing** `GraphSizing` (default: \{ mode: 'responsive' }) How the chart claims space. See [Sizing](/sdk-next/rendering/sizing). **mode** `GraphMode` (default: readonly) `'readonly'`, `'editable'` or `'point-and-edit'`. `'editable'` mounts whatever fills the `EditorSurface` slot over the chart frame; on a plain `GraphRenderer` that slot is empty, so no editor surface renders. `'point-and-edit'` mounts the same surface and makes the chart itself pressable: a press selects the observations under it, the band around them, or the chart. Neither editing mode shows a tooltip, and `'point-and-edit'` draws no annotations and no hover paint. Use `EditableGraphRenderer` from `@graphysdk/react/editable` for an editing UI. **showTooltips** `boolean` (default: true) `false` hides the tooltip whatever `config.tooltip.mode` says; `true` lets the spec decide. See [Interactivity](/sdk-next/rendering/interactivity). **animation** `GraphAnimation` (default: true) `boolean | GraphAnimationProps`. `false` disables every animation. An object tunes the intro entrance (`intro`), data transitions (`transitions`) and the geom count above which nothing animates (`maxAnimatedGeoms`). A reduced-motion preference disables everything. See [Interactivity](/sdk-next/rendering/interactivity#animation). **slots** `GraphSlots` Per-region component overrides. `Legend`, `Headline`, `AxisTicks` and `AxisLabel` take a `SlotOverride`, a `{ render, measure }` pair with a stable `measure` reference, rather than a bare component. See [Slots](/sdk-next/extending/slots). **onResize** `ResizeObserverOnResize` Called when the container resizes, in every sizing mode. Reports the graph container, not the panel. A geom renderer that needs the data rectangle reads `panelRect` from its `render` input. `EditableGraphRenderer` takes the same props, plus what only the editor reads: **palettePresets** `PalettePresets` Named color lists, `Record`, that the Chart menu's Palette row offers in point and edit after the theme's own colors. Picking one writes its colors as the color scale's `range`, so the spec keeps them whatever the host offers later, and a chart with no groups takes its first color. Without any, the row is hidden. **editMenus** `EditMenus` A host's edit menus, searched before Graphy's in point and edit. `null` removes that menu. A missing entry leaves Graphy's in place. Graphy's own menus (`BarEditMenu`, `LineEditMenu`, `AxisEditMenu`, `GridEditMenu`, `LegendEditMenu`) and rows (`ColorRow`, `PaletteRow`) are exported, so a host's menu can draw them and add its own rows. ## Keep inputs stable The provider recompiles when `spec` or `data` change by reference. If you build the spec or data inline in a component, memoize them so an unrelated re-render does not recompile for nothing: ```tsx theme={null} function Chart({ rows }) { const spec = useMemo( () => pipe( createSpec({ x: 'month', y: 'revenue' }), geom.line(), scale.x(), scale.y() ), [] ); const data = useMemo(() => ({ columns, rows }), [rows]); return ( ); } ``` A new `spec` costs more than the recompile. It is a new baseline, so once it compiles it also [clears the undo history](/sdk-next/advanced/commands-and-history#history-lifecycle). Watch the memo's dependencies as well as the memo itself: a spec setting the host drives from its own state, like a palette dropdown or a highlight-style toggle, produces a new `spec` every time it moves and drops the user's undo stack with it. Drive settings you want undoable through [commands](/sdk-next/advanced/commands-and-history) instead. ## Errors never blank the page A compile failure or a render-time throw is caught and shown as an error panel in place of the chart, so a bad spec does not take down the surrounding UI. Use `onError` to log or surface the diagnostics yourself. ## Related * [How a chart is built](/sdk-next/concepts/how-a-chart-is-built) — the compile → render model * [Commands & history](/sdk-next/advanced/commands-and-history) — editing the spec in place, with undo/redo * [Sizing](/sdk-next/rendering/sizing) · [Theming](/sdk-next/rendering/theming) · [Styling](/sdk-next/config/styling) · [Slots](/sdk-next/extending/slots) # Sizing Source: https://docs.graphy.dev/sdk-next/rendering/sizing The `sizing` prop on `GraphRenderer` decides how the chart claims space in its container. It defaults to `responsive`. ```tsx theme={null} ``` ## Responsive The chart fills its parent container and re-lays out when the container resizes. Give the parent a size: ```tsx theme={null}
``` > [!NOTE] > > The chart paints once the first measurement lands, and a measurement only > counts when both dimensions are above zero. Give the parent a real height. In > a container with no height the renderer measures 0, paints nothing, and logs a > `ZERO_SIZE_CONTAINER` warning to the console once. ## Fixed Explicit pixel dimensions, ignoring the container: ```tsx theme={null} ``` Useful for image export or thumbnails where the output size must be exact. The graph frame's border is drawn inside the sizing box, so a chart with a `style.graph({ strokeWidth })` [border](/sdk-next/config/styling) lays out in slightly less than the size you asked for: twice the border width in each axis. ## Keep aspect ratio The chart scales to the container's width while holding a ratio, which suits cards in a responsive grid. Provide an intrinsic size, or one dimension plus an `aspectRatio`: ```tsx theme={null} ``` The chart lays out at its intrinsic size and a CSS `transform` scales the result to the measured container width. Type scales with it rather than re-flowing: a card at half width shows the same layout at half the type size, not a chart laid out again for a narrower box. Before the first measurement the outer element reserves space with the CSS `aspect-ratio` property. Every dimension must resolve to a positive, finite number. A zero, negative or `NaN` width or height throws rather than rendering an empty box. ## Reacting to resize `onResize` fires with the graph container size in every mode, not the panel (the data rectangle inside the axes): ```tsx theme={null} console.log(width, height)} /> ``` A [geom renderer](/sdk-next/extending/geom-renderers) that needs the panel's pixel size reads `panelRect` from its `render` input instead. That rect is already inset from axes and chrome, and it updates on every layout. ## Related * [Provider & renderer](/sdk-next/rendering/provider-and-renderer) — the other renderer props * [Layout](/sdk-next/config/layout) — chrome `margin` and outer `style.graph({ padding })` # Theming Source: https://docs.graphy.dev/sdk-next/rendering/theming Two things decide how a chart looks beyond its spec. A theme package, passed as `theme` to `GraphProvider` or to `createGraphyKit`, restyles every graph it draws: its stylesheet, color palettes and config defaults sit beneath the spec's own. The [Themes](/sdk-next/themes/overview) section covers the themes that ship. The chrome tokens are the CSS custom properties the renderer writes onto the chart wrapper for the HTML around the plot (legend, tooltip, headline, footer, editor UI), and `colorScheme` selects them. Everything the chart itself draws, its font families included, is on the [Styling](/sdk-next/config/styling) page. | What you want to change | Where it lives | | - | - | | Geoms, grid lines, tick marks, panel border, graph background, base and heading font families, axis/tick/data-label type | `styles()` on the spec — [Styling](/sdk-next/config/styling) | | The whole look of every graph a host draws | `theme` on `GraphProvider` — [Themes](/sdk-next/themes/overview) | | Legend, tooltip, headline, footer, editor UI | the chrome tokens, selected by `colorScheme` | | The overlay menu in `'point-and-edit'` mode | editor menu tokens, [below](#overlay-menu) | | Editor tooltips | editor tooltip tokens, [below](#editor-tooltips) | | Group colors | the color scale's `range`, else the theme's `palettes.categorical` | ## Color scheme `colorScheme` picks the chrome token set: ```tsx theme={null} ``` **colorScheme** `ColorScheme` (default: light) `'light'` or `'dark'`; the type is exported from `@graphysdk/react`. A `theme` that names its own `colorScheme` overrides this prop. The same value also decides which half of every `{ light, dark }` stylesheet color and every `token()` pair resolves inside the plot, and which tones the [default palette](/sdk-next/concepts/scales#categorical-color) spreads over touching geoms. A chart switches scheme in one place. The token contract is exported as `vars`, for hosts that write their own CSS against the custom properties: ```tsx theme={null} import { vars } from '@graphysdk/react'; ``` ## Fonts Font families reach the chart from the stylesheet: * `style.graph({ fontFamily })` is the chart's base family. Every text region without a family of its own follows it, in paint and in measurement. * `style.heading({ fontFamily })` is the title and subtitle in the content region (both levels; `style.heading.h1` and `.h2` address one). Undeclared, they follow the graph entry. Size, weight, color and line height live here too. `style.caption` and `style.source` are the footer type targets. * `style.axisLabel`, `style.tickLabel`, `style.dataLabel`, `style.legendItem`, `style.directLabel` and `style.geom.rule.label` each take a `fontFamily`: the type inside the plot and on legend pills. See [Styling](/sdk-next/config/styling). > [!NOTE] > > The SDK references fonts; it does not load them. Load them on the page (with > `@font-face` or a font service) before the first measurement. Text measured > while a family is still loading is measured with fallback metrics, and the > layout keeps those numbers. ## What the chrome tokens cover | Category | Tokens | | - | - | | Base typography | `fontFamily`, `fontFamilyHeading`, `fontWeight*`, `fontSize*`, `fontLineHeight*`, plus per-element tokens such as `fontButton` (chart families and content type are stylesheet-owned: `style.graph`, `style.heading`, `style.caption`, `style.source`) | | Semantic colors | `textPrimary`, `textSecondary`, `textDisabled`, `iconPrimary`, `iconSecondary`, `border100/50/10`, `defaultBackground`, `raisedBackground`, `sunkenBackground`, `overlayBackground` | | Elevation, radii, space | `elevationXs…Lg`, `radiiXs…Lg`, `spaceXxs…Xl`, `zIndex*` | | Editor UI | `toolbarBackgroundColor`, `toolbarButtonBackgroundColor*`, `toolbarSeparatorColor`, `editorControlHeight`, `fontEditor*` | ## Overlay menu The overlay menu, which `'point-and-edit'` mode opens beside a selection, has Graphy's own look in light and dark, separate from the chrome tokens, and keeps the chrome font and text scale whatever the stylesheet sets. A host restyles the menu alone through the editor menu tokens: CSS variables no theme declares, so a value set on `:root` reaches the menu. A page that places a `
`, where the chart then portals its menus, can set them on any element holding it instead. Set literal values: a `var(--graphy-*)` set on `:root` resolves there, before any theme, and the menu falls back to its own. The controls in the menu's rows are compact and take their colors from the [editor look](#editor-look). The editor menu tokens do not restyle them, and neither does `editorControlHeight`: the menu sets its own control height, so that token reaches the editor panel alone. Every portal the chart opens carries its color scheme as `data-graphy-color-scheme`, `light` or `dark`, so a rule on it gives one scheme's menus values of their own. This is the only way to tell charts apart: every chart on the page portals its menu into the same container, so a value set on a chart's own element never reaches its menu. ```css theme={null} :root { --graphy-editor-menu-background: #fbfaf4; --graphy-editor-menu-text: #1f1e1e; --graphy-editor-menu-border: #1f1e1e; --graphy-editor-menu-radius: 0; --graphy-editor-menu-shadow: 4px 4px 0 #1f1e1e; } [data-graphy-color-scheme='dark'] { --graphy-editor-menu-background: #1f1e1e; --graphy-editor-menu-text: #fbfaf4; --graphy-editor-menu-border: #fbfaf4; --graphy-editor-menu-shadow: 4px 4px 0 #fbfaf4; } ``` | Token (`--graphy-editor-menu-*`) | Takes | Unset, the menu reads | | - | - | - | | `background` | a color | the editor look's `surface` | | `text` | a color | the editor look's `text` | | `border` | a color | the editor look's `text` at 12% | | `radius` | a length | `radiiMd` | | `shadow` | a `box-shadow` | `elevationMd` and a float shadow | | `padding` | a length | `spaceMd` | | `title-font` | a `font` shorthand | `600 13px/1.3` in the theme's font | | `min-width`, `max-width` | a length | 252px | A value set without a scheme holds in light and dark alike, so set `text` whenever you set `background`, and `border` whenever you set `text`: the border follows the editor look's text, not the menu's. An unset `min-width` takes `max-width`, so setting the max alone sets the menu's width, narrower or wider. The room beside the selection still caps both widths. ## Editor look The editor panel's controls and the overlay menu share one look: a warm dark text color on a light surface, or a light one on a dark surface, with that text color as the accent of a switch, a slider and a selected option. The chrome tokens keep it apart from the chart's own tokens, so the footer, the headline and the difference arrows do not move with it. A host changes it for both with the editor tokens, CSS variables no theme declares. Set them on `:root`, or on an element that holds both the panel and `#graphy_root`, where the menu portals. An element that holds only the chart or only the panel reaches one of them. ```css theme={null} .host-editor { --graphy-editor-accent: #0b5fff; --graphy-editor-surface: #fff8e6; --graphy-editor-text: #2b2b2b; --graphy-editor-border: #d9b86c; --graphy-editor-font-family: Georgia, serif; --graphy-editor-padding-x: 12px; --graphy-editor-padding-y: 8px; } ``` | Token (`--graphy-editor-*`) | Takes | Unset, the editor reads | | - | - | - | | `accent` | a color | `#3B372F`, or `#ECE7DF` in dark | | `surface` | a color | `#FFFFFF`, or `#26241F` in dark | | `text` | a color | `#3B372F`, or `#ECE7DF` in dark | | `border` | a color | `text` at 12% for dividers, fields and the menu; the panel draws none | | `font-family` | a font family | the theme's | | `padding-x`, `padding-y` | a length | `spaceMd`, 8px, for the panel and the menu | The controls' text, their sunken and raised surfaces, their borders and the focus outline are worked out from `surface` and `text`, so set the two together. `border` outlines the panel only when you set it. The panel pads its content by 8px; a host whose own box already pads it sets `padding-x` and `padding-y` to `0px`. A React host sets the tokens with a `style` on the element that holds `#graphy_root`. A select's list, which opens in a popup of its own, takes the look's colors too, its highlighted and selected rows included. ## Editor tooltips Every tooltip the editor draws, on a control's option or on a toolbar button, reads the editor tooltip tokens the way the overlay menu reads its own: set them on `:root`, around `#graphy_root` or on one `data-graphy-color-scheme`, as literal values. | Token (`--graphy-editor-tooltip-*`) | Takes | Unset, the tooltip reads | | - | - | - | | `background` | a color | the [editor look](#editor-look)'s `text` | | `text` | a color | the editor look's `surface` | | `radius` | a length | `radiiXs` | | `font` | a `font` shorthand | `fontUITooltipSecondary`, in the editor look's `font-family` | ## Tokens only paint The layout measures text in JS from the built-in token values and the stylesheet, not from the CSS cascade. A host-page override of a font token such as `--graphy-font-family` changes what paints but not the space the layout reserved for it, so the two disagree. Set type through the stylesheet instead: `style.graph({ fontFamily })` reaches paint and measurement alike. ## Tokens that carry no paint A few token names describe paint the stylesheet owns. Use the style entry instead. | Token | What actually paints it | | - | - | | `graphBackground` | `style.graph({ fill })` | The renderer resolves the stylesheet's `graph` background and writes it into the `graphBackground` CSS variable, so the stylesheet value is what a chart paints. ```tsx theme={null} // paints the background styles({ defaults: [style.graph({ fill: '#0B1020' })] }); ``` ## Two token namespaces `textPrimary` and `textSecondary` each name a chrome token and a [stylesheet](/sdk-next/config/styling) token. They are independent tables: the chrome pair colors HTML chrome text, while the stylesheet's `textPrimary` paints axis and data labels and its `textSecondary` paints tick labels. If you set one by name and nothing moves, check which namespace you reached for. ## Group colors Group colors are not chrome tokens. They come from the chart's color scale: an explicit `range` on `scale.color.discrete`, else the theme's `palettes.categorical` list, else the built-in palette. ```tsx theme={null} scale.color.discrete({ range: ['#0B5FFF', '#E5484D'] }); // explicit colors, in group order scale.color.discrete(); // the theme's categorical list, else the built-in palette ``` A stylesheet `overrides` entry that declares `fill` or `stroke` sits above the scale, so it decides over the palette. See [Styling](/sdk-next/config/styling). ## Related * [Styling](/sdk-next/config/styling) — the stylesheet on the spec: geoms, grid, axes, background * [Themes](/sdk-next/themes/overview) — the theme packages and how to pass one * [Provider & renderer](/sdk-next/rendering/provider-and-renderer) — where `theme` and `colorScheme` are set # Bauhaus Source: https://docs.graphy.dev/sdk-next/themes/bauhaus Bauhaus draws graphs as a 1920s design-school poster: warm off-white paper, flat red, blue, yellow and black, heavy black axes, and a geometric face with spaced capitals. Each group takes the form of its color, the red square, the blue circle or the yellow triangle, so groups stay apart even without color. > [!WARNING] > > Bauhaus ships as its own package, `@graphysdk/theme-bauhaus`, still on `0.x`. > A minor release may change its look, so pin a caret range (`^0.1.0`) to take > only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-bauhaus ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { bauhaus } from '@graphysdk/theme-bauhaus'; import '@graphysdk/theme-bauhaus/fonts.css'; const kit = createGraphyKit({ theme: bauhaus }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Bauhaus sets | | - | - | | Stylesheet | Off-white paper with no frame, heavy black axes on the left and along the bottom, no grid, square bars parted by a thin line in the paper, spaced capitals on the axes | | Colors | Red, blue, yellow and black, then green, orange and violet, for groups, a ramp from yellow through red to black, and blue against red either side of a midpoint | | Config | Values on the left, the legend above the plot | | Geoms | Bars as flat planes, square at both ends; points as forms; lines as heavy rules with a form at each reading; slices as flat fields of color, ending in a heavy black rule on a rose; areas and tiles keep the built-in renderer | | Color scheme | Light, whatever `colorScheme` the provider is given | ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the poster and draws its groups in blue and black only: ```tsx theme={null} kit.pipe(spec, kit.scale.color.discrete({ range: ['#1B4FA3', '#141414'] })); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Bauhaus repaints the built-in geoms only. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), keeps its own shapes and is drawn in the theme's colors and type. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: bauhaus, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Jost from Google Fonts, in weights 400, 500 and 700. To host the font yourself, leave the import out and load a family named `Jost` before the first graph is drawn. # Blueprint Source: https://docs.graphy.dev/sdk-next/themes/blueprint Blueprint draws graphs as an architect's drawing: a deep blue sheet under a fine drafting grid, shapes outlined in white and hatched in their own color, dashed construction lines, and monospaced capitals. > [!WARNING] > > Blueprint ships as its own package, `@graphysdk/theme-blueprint`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-blueprint ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { blueprint } from '@graphysdk/theme-blueprint'; import '@graphysdk/theme-blueprint/fonts.css'; const kit = createGraphyKit({ theme: blueprint }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Blueprint sets | | - | - | | Stylesheet | A deep blue sheet ruled in squares inside a thin frame, a dashed grid, a ruled baseline with ticks, bars and tiles hatched in their own color | | Colors | White, yellow, sky blue, red, aqua and lilac for groups, a ramp from a blue a step off the sheet to white, and the sky pencil against the red either side of a midpoint | | Config | The legend above the plot | | Geoms | None repainted | | Color scheme | Dark, whatever `colorScheme` the provider is given | ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the sheet and fills its bars solid, with no hatching: ```tsx theme={null} kit.pipe( spec, kit.styles({ defaults: [kit.style.geom.bar({ overpaint: 'none' })] }) ); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Blueprint repaints no geom. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), is drawn in the theme's colors and type like a built-in one. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: blueprint, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads IBM Plex Mono from Google Fonts, in weights 400, 500 and 600. To host the font yourself, leave the import out and load a family named `IBM Plex Mono` before the first graph is drawn. # Botanical Source: https://docs.graphy.dev/sdk-next/themes/botanical Botanical draws graphs as a page from a gardener's sketchbook: grained paper taped down at its corners with a sprig along its foot, every shape outlined by hand in sepia with a watercolour wash inside, and handwriting throughout. > [!WARNING] > > Botanical ships as its own package, `@graphysdk/theme-botanical`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-botanical ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { botanical } from '@graphysdk/theme-botanical'; import '@graphysdk/theme-botanical/fonts.css'; const kit = createGraphyKit({ theme: botanical }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Botanical sets | | - | - | | Stylesheet | Grained paper taped down at its corners with a sprig drawn along its foot, hairline rules for the values and none for the categories, a sepia baseline, notes in the gardener's hand with no box | | Colors | Eight colors for groups, leaf greens set between a petal pink, a bark brown, a berry red, a muted gold and a gentian blue, a ramp from a pale sage to a deep forest green, and gentian against berry either side of a midpoint | | Geoms | Bars as leaf blades with a rib and veins; lines as vines with a leaf at each reading; points as fallen leaves; slices as petals round a seed head; areas as washes; heatmap cells as a seed tray | | Color scheme | Light, whatever `colorScheme` the provider is given | ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the sketchbook and draws its groups in two greens: ```tsx theme={null} kit.pipe(spec, kit.scale.color.discrete({ range: ['#2E5E3B', '#6E8A5A'] })); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Theme and plugins go to the same kit. A geom package built from the shared shapes, such as the [dumbbell](/sdk-next/graph-types/dumbbell), draws its dots washed inside a sepia outline too, and takes the theme's colors, type and axes. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: botanical, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Kalam for the handwriting and Cormorant Garamond for the italic notes from Google Fonts. To host the fonts yourself, leave the import out and load families named `Kalam` and `Cormorant Garamond` before the first graph is drawn. # Chalkboard Source: https://docs.graphy.dev/sdk-next/themes/chalkboard Chalkboard draws graphs as a teacher draws them on a classroom board: a green-gray board with the smears of a wiped duster in a wooden frame, shapes outlined and shaded in chalk, and handwriting throughout. > [!WARNING] > > Chalkboard ships as its own package, `@graphysdk/theme-chalkboard`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-chalkboard ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { chalkboard } from '@graphysdk/theme-chalkboard'; import '@graphysdk/theme-chalkboard/fonts.css'; const kit = createGraphyKit({ theme: chalkboard }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Chalkboard sets | | - | - | | Stylesheet | A green-gray board with wiped smears in a wooden frame, stubs of chalk and a duster on the ledge along its foot, a dashed grid, no shadows | | Colors | Six chalks for groups, white, yellow, pink, sky blue, green and orange, a ramp from a dusty teal through sky blue to white, and blue chalk against orange either side of a midpoint | | Config | Values on the left, the legend above the plot | | Geoms | Bars outlined twice and shaded with the side of the chalk; pies drawn in one sweep with spokes ruled out; lines as chalk strokes through a cross at each reading; points as chalk circles; areas and heatmap cells shaded | | Color scheme | Dark, whatever `colorScheme` the provider is given | ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the board and draws its groups in white and yellow chalk only: ```tsx theme={null} kit.pipe(spec, kit.scale.color.discrete({ range: ['#F4F1E8', '#F7DC6F'] })); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Theme and plugins go to the same kit. A geom package built from the shared shapes, such as the [dumbbell](/sdk-next/graph-types/dumbbell), draws its dots as chalk circles too, and takes the theme's colors, type and axes. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: chalkboard, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Gochi Hand from Google Fonts. To host the font yourself, leave the import out and load a family named `Gochi Hand` before the first graph is drawn. The face has one weight. # Comic Source: https://docs.graphy.dev/sdk-next/themes/comic Comic draws graphs as a panel of a printed comic: newsprint in a black frame, the flat colors of a four-color press under its dots, a black outline and a hard shadow for every shape, and a title in capitals. > [!WARNING] > > Comic ships as its own package, `@graphysdk/theme-comic`, still on `0.x`. A > minor release may change its look, so pin a caret range (`^0.1.0`) to take > only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-comic ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { comic } from '@graphysdk/theme-comic'; import '@graphysdk/theme-comic/fonts.css'; const kit = createGraphyKit({ theme: comic }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Comic sets | | - | - | | Stylesheet | Newsprint with a patch of screen tone in a square black frame, printed dots over every shape, a black outline and a hard black shadow under each shape, thick round lines, text notes lettered as sound effects | | Colors | Vermillion, cyan, yellow, magenta, green and purple for groups, a ramp from yellow through orange to red, and cyan against vermillion either side of a midpoint | | Geoms | Points as little bursts; bars, pies, lines, areas and tiles keep the built-in renderer | | Color scheme | Light, whatever `colorScheme` the provider is given | ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the panel and prints its bars flat, with no dots and no shadow: ```tsx theme={null} kit.pipe( spec, kit.styles({ defaults: [kit.style.geom.bar({ overpaint: 'none', shadow: 'none' })], }) ); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Comic repaints only points. Every other geom, and a geom from a package such as the [dumbbell](/sdk-next/graph-types/dumbbell), keeps its own renderer and takes the theme's colors, outline, shadow and type. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: comic, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Bangers for the title and Comic Neue for the rest from Google Fonts. To host the fonts yourself, leave the import out and load families named `Bangers` and `Comic Neue` before the first graph is drawn. # Financial Source: https://docs.graphy.dev/sdk-next/themes/financial This theme draws graphs the way a newspaper's business pages print them: salmon paper, solid rules with the values at their right-hand end, a ruled baseline, and flat color. > [!WARNING] > > The theme ships as its own package, `@graphysdk/theme-financial`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-financial ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { financial } from '@graphysdk/theme-financial'; import '@graphysdk/theme-financial/fonts.css'; const kit = createGraphyKit({ theme: financial }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'week', y: 'level', color: 'index' }), kit.geom.line(), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What the theme sets | | - | - | | Stylesheet | Salmon paper with no frame, a ruled baseline with ticks, square bars with a pale divider in a stack, lines 2.5 pixels wide | | Grid | Solid rules for the values, none for the categories | | Colors | Ten muted colors for groups, blue, red and aqua first, a ramp from pale to dark blue, and navy against claret either side of a midpoint | | Config | Values on the right of the plot, and the legend above it, aligned to the left | | Geoms | None repainted | | Color scheme | Light, whatever `colorScheme` the provider is given | ## Changing one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph puts its values back on the left and takes its own colors: ```tsx theme={null} const customizedSpec = kit.pipe( spec, kit.config({ axes: { y: { position: 'left' } } }), kit.scale.color.discrete({ range: ['#14466B', '#DDA63A'] }) ); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package The theme repaints no geom. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), is drawn in the theme's colors and type like a built-in one. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: financial, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Source Sans 3 from Google Fonts, in weights 400 and 600. To host the font yourself, leave the import out and load a family named `Source Sans 3` before the first graph is drawn. # Gamma Source: https://docs.graphy.dev/sdk-next/themes/gamma Gamma is two themes drawn from Gamma's own look. Gamma Night puts graphs in the starred night sky behind Gamma's wordmark, every shape a pale glass that fades as it falls. Gamma Mushroom puts them in Gamma's mushroom forest under a summer sky, where bars are mushrooms whose caps end at their values. > [!WARNING] > > Both themes ship in one package, `@graphysdk/theme-gamma`, still on `0.x`. A > minor release may change their look, so pin a caret range (`^0.1.0`) to take > only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-gamma ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass one of the themes to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { gammaNight } from '@graphysdk/theme-gamma'; import '@graphysdk/theme-gamma/fonts.css'; const kit = createGraphyKit({ theme: gammaNight }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the themes decide | Part | Gamma Night | Gamma Mushroom | | - | - | - | | Stylesheet | A starred night sky fading from navy to indigo, rounded corners and no frame, a faint grid, the rule under the wordmark as baseline | A summer sky fading from blue to white, rounded corners and no frame, white grid lines, a navy baseline the mushrooms stand on | | Colors | The wordmark's lavender, then pink, mint, gold, sky blue and coral for groups, a ramp from the deep sky to the palest glass, and sky blue against coral either side of a midpoint | The caps' coral, violet, pink, orange, purple and teal for groups, a ramp from a pale pink through coral to violet, and violet against coral either side of a midpoint | | Geoms | Bars, slices and areas as glass that fades as it falls; lines as glass ribbons; a star at each reading; tiles keep the built-in renderer | Bars as mushrooms; a pie as the gilled underside of a cap; areas speckled like a cap; a cap at each reading; tiles keep the built-in renderer | | Color scheme | Dark, whatever `colorScheme` the provider is given | Light, whatever `colorScheme` the provider is given | ## Night or Mushroom The two themes are separate exports of the same package, each drawn for one scheme. Pick the one that suits the page, or follow the reader's scheme. The provider reads its theme once, so pass the matching theme and remount the provider when the scheme changes: ```tsx theme={null} import { gammaMushroom, gammaNight } from '@graphysdk/theme-gamma'; const theme = colorScheme === 'dark' ? gammaNight : gammaMushroom; ; ``` ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the night sky and draws its groups in the gold and mint only: ```tsx theme={null} kit.pipe(spec, kit.scale.color.discrete({ range: ['#FFD66B', '#5FE0C1'] })); ``` The themes are drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales their type for a larger or smaller one. ## With a geom package The themes repaint the built-in geoms only. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), keeps its own shapes and is drawn in the theme's colors and type. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: gammaNight, plugins: [dumbbell] }); ``` ## Fonts Gamma's own faces, ES Build for headings and PP Mori for text, cannot be served freely. The themes name them first and fall back to Plus Jakarta Sans and Inter. `fonts.css` loads Plus Jakarta Sans in weights 500, 700 and 800 and Inter in weights 400, 500 and 600 from Google Fonts. To host the fonts yourself, leave the import out and load families named `Plus Jakarta Sans` and `Inter`, or `ESBuild` and `PPMori` if you have Gamma's own, before the first graph is drawn. # Graphite Source: https://docs.graphy.dev/sdk-next/themes/graphite Graphite draws graphs in one hue, a cool gray like pencil lead: groups are told apart by lightness alone, shapes are parted by a thin line in the background, and nothing casts a shadow. > [!WARNING] > > Graphite ships as its own package, `@graphysdk/theme-graphite`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-graphite ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { graphite } from '@graphysdk/theme-graphite'; import '@graphysdk/theme-graphite/fonts.css'; const kit = createGraphyKit({ theme: graphite }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Graphite sets | | - | - | | Stylesheet | A near-white or near-black background, flat shapes parted by a thin line in the background, no shadows | | Colors | Six grays for groups, a ramp from a pale gray to a near-black one, and a steel blue against an umber either side of a midpoint | | Config | The legend above the plot | | Geoms | None repainted | | Color scheme | Light and dark. The provider's `colorScheme` picks which | ## Light and dark Graphite is drawn for both schemes. The provider's `colorScheme` picks the one a graph is shown in. A host that follows the reader's system setting passes it through: ```tsx theme={null} ``` ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the grays and picks out one group in a color: ```tsx theme={null} kit.pipe( spec, kit.scale.color.discrete({ range: ['#D62718', '#878D95', '#ABB2BA'] }) ); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Graphite repaints no geom. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), is drawn in the theme's colors and type like a built-in one. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: graphite, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads IBM Plex Sans from Google Fonts, in weights 400, 500 and 600. To host the font yourself, leave the import out and load a family named `IBM Plex Sans` before the first graph is drawn. # Halloween Source: https://docs.graphy.dev/sdk-next/themes/halloween Halloween draws graphs in a churchyard on a moonlit night: columns are headstones, candles or railing posts, a pie is a rose window lit by the moon, a line is a glowing trail with a ghost riding its head, and a scatter's readings are jack-o'-lanterns. > [!WARNING] > > Halloween ships as its own package, `@graphysdk/theme-halloween`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-halloween ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { halloween } from '@graphysdk/theme-halloween'; import '@graphysdk/theme-halloween/fonts.css'; const kit = createGraphyKit({ theme: halloween }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Halloween sets | | - | - | | Stylesheet | A night sky with a moon, bats, stars and a cobweb, a bone baseline, titles in Creepster, a goal line spun as spider silk, notes on paper tags | | Grid | Faint rules for the values, none for the categories | | Colors | Six churchyard stones for groups (marble, slate, sandstone, moss, rose and gray granite), a ramp from embers up to candlelight, and slate against sandstone either side of a midpoint | | Geoms | Columns as headstones, candles or railing posts; slices as stained glass; lines as glowing trails; points as jack-o'-lanterns; areas as mist | | Color scheme | Dark, whatever `colorScheme` the provider is given | ## Headstones, candles and railings Every column in a graph is the same object, chosen once from the room the columns have: a headstone where they are broad, a wax candle where they are slender, and a wrought-iron railing post where they are packed close. One graph never mixes them, so a change of object never reads as a change of meaning. Each object ends exactly at its value: the crown of a headstone's arch, the top of a candle's wax (only its wick and flame stand above), and the tip of a railing's spear. A stacked bar is a pile of carved courses and a horizontal bar a ledger slab, however close the columns are packed. A pie, donut, rose or radial bar is a rose window. Each slice keeps its sector as a pane of stained glass in its group's color, and the stone tracery is centerd on the edges between panes. A lantern is centerd on its reading, not on its stem. A heatmap keeps the built-in tile, which reads by its colors and labels. ## Motion In the intro, each column rises from below its foot, a line draws in from its first reading with a ghost riding its head, lanterns light up in turn, and a rose window's panes light one after another. A candle's flame keeps flickering while the graph is shown. A graph drawn with `animation={false}`, or for a viewer who prefers reduced motion, is drawn in its final state at once, with no flicker. ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the churchyard, draws its groups in pumpkin and plum, and scales its type up: ```tsx theme={null} const customizedSpec = kit.pipe( spec, kit.scale.color.discrete({ range: ['#FF7A1A', '#A23C78'] }), kit.styles({ defaults: [kit.style.graph({ textScale: 1.5 })] }) ); ``` The theme is drawn for a graph about 600 pixels wide. `textScale` scales its type for a larger or smaller one. ## With a geom package Halloween repaints the built-in geoms only. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), keeps its own shapes and is drawn in the theme's colors and type. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: halloween, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads three families from Google Fonts: Creepster for titles, Cinzel in weights 600 and 700 for the figures carved into headstones, and Nunito in weights 400, 600, 700 and 800 for everything else. To host the fonts yourself, leave the import out and load families named `Creepster`, `Cinzel` and `Nunito` before the first graph is drawn. # Neo Brutalist Source: https://docs.graphy.dev/sdk-next/themes/neo-brutalist Neo Brutalist draws graphs as blunt blocks: a cream sheet in a thick black border, square shapes in loud flat color outlined in black, and a solid black shadow under everything that stands off the sheet. > [!WARNING] > > Neo Brutalist ships as its own package, `@graphysdk/theme-neo-brutalist`, > still on `0.x`. A minor release may change its look, so pin a caret range > (`^0.1.0`) to take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-neo-brutalist ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { neoBrutalist } from '@graphysdk/theme-neo-brutalist'; import '@graphysdk/theme-neo-brutalist/fonts.css'; const kit = createGraphyKit({ theme: neoBrutalist }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Neo Brutalist sets | | - | - | | Stylesheet | A cream sheet in a thick black border, square shapes outlined in black, a solid black shadow under the graph and its shapes, headings in capitals | | Colors | Pink, purple and lime first, then orange, blue, teal and green, for groups, a ramp from a pale yellow through pink to a deep purple, and blue against pink either side of a midpoint | | Config | Values on the left, the legend above the plot | | Geoms | None repainted | | Color scheme | Light, whatever `colorScheme` the provider is given | ## The shadow The graph's own shadow falls outside its frame, to the right and below. Leave room for it around the renderer, or whatever holds the graph cuts it off. ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the blocks and draws its groups in pink and black: ```tsx theme={null} kit.pipe(spec, kit.scale.color.discrete({ range: ['#FF3D8B', '#111111'] })); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Neo Brutalist repaints no geom. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), is drawn in the theme's colors and type like a built-in one. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: neoBrutalist, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Space Grotesk from Google Fonts, in weights 500 and 700. To host the font yourself, leave the import out and load a family named `Space Grotesk` before the first graph is drawn. # Overview Source: https://docs.graphy.dev/sdk-next/themes/overview Every theme at a glance A theme decides how every graph in a kit looks: its stylesheet, colors, config defaults and, for some, the shapes its geoms are drawn in. Each theme ships as its own package. Pass it to `createGraphyKit({ theme })`, or to `GraphProvider` as its `theme` prop. A spec names no theme, so the same spec draws in any of them, and a spec's own `styles()`, scales and `config()` sit above the theme. The provider reads the theme once, when it mounts. Pick one below to install it and see it on more graphs. > **[Bauhaus](/sdk-next/themes/bauhaus)** > > A 1920s design-school poster: flat primaries, heavy black axes and a form > for each group. > **[Blueprint](/sdk-next/themes/blueprint)** > > An architect's drawing: a blue sheet, a drafting grid and hatched shapes > outlined in white. > **[Botanical](/sdk-next/themes/botanical)** > > A gardener's sketchbook: sepia outlines, watercolour washes and leaves on > grained paper. > **[Chalkboard](/sdk-next/themes/chalkboard)** > > A classroom board: shapes outlined and shaded in chalk, and handwriting > throughout. > **[Comic](/sdk-next/themes/comic)** > > A printed comic panel: four-color dots, black outlines and a hard shadow > under every shape. > **[Financial](/sdk-next/themes/financial)** > > A newspaper's business pages: salmon paper, solid rules and flat color. > **[Gamma](/sdk-next/themes/gamma)** > > Two themes from Gamma's own look: a starred night sky, and a mushroom > forest. > **[Graphite](/sdk-next/themes/graphite)** > > One cool gray, groups told apart by lightness alone. Light and dark. > **[Halloween](/sdk-next/themes/halloween)** > > A moonlit churchyard: headstones, candles, a rose window and a ghost on the > line. > **[Neo Brutalist](/sdk-next/themes/neo-brutalist)** > > Blunt blocks of loud color, outlined in black over a solid black shadow. > **[Phosphor](/sdk-next/themes/phosphor)** > > A cathode-ray tube: a dark screen, glowing green shapes and a terminal face. > **[Shiny](/sdk-next/themes/shiny)** > > A dark starred card in a holographic foil border, every shape one clean flat > color. > **[Solarized](/sdk-next/themes/solarized)** > > Ethan Schoonover's Solarized palette, a quiet grid and thin strokes. Light > and dark. > **[Spreadsheet](/sdk-next/themes/spreadsheet)** > > The graphs of a spreadsheet or slide deck: white ground, light gray rules, > familiar colors. > **[Typewriter](/sdk-next/themes/typewriter)** > > An office typescript: manila paper, a grid of hyphens and every shape typed > on it. > **[Watercolor](/sdk-next/themes/watercolor)** > > Washes of paint, colored-pen lines and handwritten labels on white paper. # Phosphor Source: https://docs.graphy.dev/sdk-next/themes/phosphor Phosphor draws graphs on a cathode-ray tube: a dark screen, shapes and type that glow green, and a terminal face throughout. > [!WARNING] > > Phosphor ships as its own package, `@graphysdk/theme-phosphor`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-phosphor ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { phosphor } from '@graphysdk/theme-phosphor'; import '@graphysdk/theme-phosphor/fonts.css'; const kit = createGraphyKit({ theme: phosphor }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'country', y: 'emissions' }), kit.geom.bar(), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Phosphor sets | | - | - | | Stylesheet | A dark screen under scanlines, a bright line under the plot, a dotted grid, a glow on shapes and labels | | Colors | Five tube colors (green, amber, cyan, pink, white) for groups, a ramp from a dim to a lit green, and cyan against amber either side of a midpoint | | Geoms | None repainted. Filled shapes are lit faintly and outlined in their own color | | Color scheme | Dark, whatever `colorScheme` the provider is given | ## The screen The scanlines and the dark corners are an `overpaint`, paint drawn over what lies under it. The graph has one over everything it draws, and each geom has one over its own shapes, so lit shapes show the lines more than the dark around them. Both are in the stylesheet, and a spec can turn either off: ```tsx theme={null} kit.styles({ defaults: [ kit.style.graph({ overpaint: 'none' }), kit.style.geom({ overpaint: 'none' }), ], }); ``` ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the screen and takes amber for its bars, with no glow: ```tsx theme={null} kit.pipe( spec, kit.styles({ defaults: [ kit.style.geom.bar({ fill: '#FFC247', stroke: '#FFC247', shadow: 'none', }), ], }) ); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Phosphor repaints no geom. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), is lit the same way as a built-in one. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: phosphor, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads VT323 from Google Fonts. To host the font yourself, leave the import out and load a family named `VT323` before the first graph is drawn. The face has one weight, and the theme sets every label in it. # Shiny Source: https://docs.graphy.dev/sdk-next/themes/shiny Shiny draws graphs on a dark card scattered with faint stars, inside a holographic foil border: every shape is one clean flat color, slices and stacked bars are parted by a hairline of the card, and lines carry a soft glow. > [!WARNING] > > Shiny ships as its own package, `@graphysdk/theme-shiny`, still on `0.x`. A > minor release may change its look, so pin a caret range (`^0.1.0`) to take > only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-shiny ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { shiny } from '@graphysdk/theme-shiny'; import '@graphysdk/theme-shiny/fonts.css'; const kit = createGraphyKit({ theme: shiny }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Shiny sets | | - | - | | Stylesheet | A dark starred card with rounded corners inside a holographic foil border, a faint grid for the values over one baseline, headings in Inter Tight and small type in JetBrains Mono, notes set in terminal windows | | Colors | Pulse pink, Sirius cyan, Lagoon violet, Nebula, a mint and Starlight for groups, a ramp from a deep violet through Pulse to a pale blush, and Sirius against Nebula either side of a midpoint | | Config | None | | Geoms | Every built-in geom repainted as flat shapes parted by a hairline of the card; lines glow softly; a hovered shape takes a bright line inside its edge | | Color scheme | Dark, whatever `colorScheme` the provider is given | ## The border The foil is the theme's one holographic part: a band of lilac, pink, cyan and mint round the card's edge, with a fine diamond plate and streaks of light across it. Nothing inside the card is holographic, so shapes keep their own colors, a slice its angle and a bar its value. ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the card and the border and draws its groups in Pulse and Sirius only: ```tsx theme={null} kit.pipe(spec, kit.scale.color.discrete({ range: ['#E94FF3', '#9AF2FF'] })); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Shiny repaints the built-in geoms only. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), keeps its own shapes and is drawn on the card in the theme's colors and type. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: shiny, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Inter Tight, in weights 500, 700 and 800, and JetBrains Mono, in weights 400, 500, 600 and 700, from Google Fonts. To host the fonts yourself, leave the import out and load families named `Inter Tight` and `JetBrains Mono` before the first graph is drawn. # Solarized Source: https://docs.graphy.dev/sdk-next/themes/solarized Solarized draws graphs in Ethan Schoonover's Solarized palette: a cream background in the light scheme and a deep teal one in the dark, a quiet grid, thin strokes and flat shapes. > [!WARNING] > > Solarized ships as its own package, `@graphysdk/theme-solarized`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-solarized ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { solarized } from '@graphysdk/theme-solarized'; import '@graphysdk/theme-solarized/fonts.css'; const kit = createGraphyKit({ theme: solarized }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Solarized sets | | - | - | | Stylesheet | A cream or deep teal background, a quiet grid in the raised tone, thin strokes, flat shapes | | Colors | The palette's eight accents, blue, orange and cyan first, for groups in both schemes, a ramp from yellow through cyan to a deep blue, and blue against orange either side of a midpoint | | Geoms | None repainted | | Color scheme | Light and dark. The provider's `colorScheme` picks which | ## Light and dark Solarized is drawn for both schemes. The provider's `colorScheme` picks the one a graph is shown in. A host that follows the reader's system setting passes it through: ```tsx theme={null} ``` ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the background and draws its groups in two of the accents: ```tsx theme={null} kit.pipe(spec, kit.scale.color.discrete({ range: ['#268bd2', '#cb4b16'] })); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Solarized repaints no geom. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), is drawn in the theme's colors and type like a built-in one. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: solarized, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Source Code Pro from Google Fonts, in weights 400, 500 and 600. To host the font yourself, leave the import out and load a family named `Source Code Pro` before the first graph is drawn. # Spreadsheet Source: https://docs.graphy.dev/sdk-next/themes/spreadsheet Spreadsheet draws graphs the way a spreadsheet or a slide deck charts them: a white ground in a thin gray edge, a gray title, light gray rules for the values over one darker line under the plot, and square shapes in flat color. > [!WARNING] > > Spreadsheet ships as its own package, `@graphysdk/theme-spreadsheet`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-spreadsheet ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit`; it is set in Arial, so there are no fonts to import: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { spreadsheet } from '@graphysdk/theme-spreadsheet'; const kit = createGraphyKit({ theme: spreadsheet }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Spreadsheet sets | | - | - | | Stylesheet | A white ground in a thin gray edge, light gray rules for the values and one darker line under the plot, a gray title, square shapes with no border, a white tooltip | | Colors | Blue, red, yellow, green and orange for groups, a ramp from the palest blue to a deep one, and blue against red either side of a midpoint | | Config | Values on the left, the legend centerd above the plot | | Geoms | None repainted | | Color scheme | Light, whatever `colorScheme` the provider is given | ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. Yellow and orange sit under 3:1 contrast against white, but keep their place in the palette because that order is the look. This graph keeps the look and draws its groups in blue, red and green only: ```tsx theme={null} kit.pipe( spec, kit.scale.color.discrete({ range: ['#5384EC', '#D75140', '#58A55D'] }) ); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Spreadsheet repaints no geom. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), is drawn in the theme's colors and type like a built-in one. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: spreadsheet, plugins: [dumbbell] }); ``` ## Fonts The theme is set in Arial, which every system has, so it loads no font and ships no `fonts.css`. # Typewriter Source: https://docs.graphy.dev/sdk-next/themes/typewriter Typewriter draws graphs as an office typed them: manila paper, a title in underlined capitals, a grid of typed hyphens, and every shape typed on the grid a typewriter moves on. > [!WARNING] > > Typewriter ships as its own package, `@graphysdk/theme-typewriter`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-typewriter ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { typewriter } from '@graphysdk/theme-typewriter'; import '@graphysdk/theme-typewriter/fonts.css'; const kit = createGraphyKit({ theme: typewriter }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Typewriter sets | | - | - | | Stylesheet | Manila paper, a grid of typed hyphens, a line ruled down the left of the plot, notes typed onto the page with no box | | Colors | The two colors of the ribbon, black and red, then carbon blue, pencil gray, stamp green and duplicator violet, for groups, a ramp from a pale manila to black, and carbon blue against red either side of a midpoint | | Config | Values on the left | | Geoms | Bars as blocks of X's closed at the value by underscores; pies, roses and areas ruled round and shaded in typed slashes; lines ruled through an x at each reading; points as typed o's | | Color scheme | Light, whatever `colorScheme` the provider is given | ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the paper and types its groups in the ribbon's black and red only: ```tsx theme={null} kit.pipe(spec, kit.scale.color.discrete({ range: ['#1F1B16', '#B3271E'] })); ``` The theme is drawn for a graph about 600 pixels wide. `style.graph({ textScale })` scales its type for a larger or smaller one. ## With a geom package Typewriter repaints the built-in geoms only. A geom from a package, such as the [dumbbell](/sdk-next/graph-types/dumbbell), keeps its own shapes and is drawn in the theme's colors and type. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: typewriter, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Special Elite from Google Fonts. To host the font yourself, leave the import out and load a family named `Special Elite` before the first graph is drawn. The face has one weight, and the theme sets every label in it. # Watercolor Source: https://docs.graphy.dev/sdk-next/themes/watercolor Watercolor draws graphs by hand: filled shapes are washes of paint, lines are drawn in colored pen, and every label is handwritten, on white paper. > [!WARNING] > > Watercolor ships as its own package, `@graphysdk/theme-watercolor`, still on > `0.x`. A minor release may change its look, so pin a caret range (`^0.1.0`) to > take only patches. Tell us what works and what doesn't on > [Discord](https://discord.gg/yGmYCjkSr). ## Install ```bash theme={null} npm install @graphysdk/theme-watercolor ``` Its peer dependencies are `@graphysdk/react` and React 19. Pass the theme to `createGraphyKit` and import its fonts once: ```tsx theme={null} import { createGraphyKit, GraphRenderer } from '@graphysdk/react'; import { watercolor } from '@graphysdk/theme-watercolor'; import '@graphysdk/theme-watercolor/fonts.css'; const kit = createGraphyKit({ theme: watercolor }); ``` Every graph drawn through `kit.GraphProvider` takes the theme. The spec is written as usual and names no theme: ```tsx theme={null} const spec = kit.pipe( kit.createSpec({ x: 'quarter', y: 'revenue', color: 'region' }), kit.geom.bar({ position: 'dodge' }), kit.scale.x(), kit.scale.y() ); ; ``` Without a kit, pass the theme to the provider instead: ``. The provider reads the theme once, when it mounts. To switch themes, remount the provider, for example with a React `key`. ## What the theme decides | Part | What Watercolor sets | | - | - | | Stylesheet | White paper, heavy left and bottom axes, handwriting, tooltips and annotations to match | | Grid | None on a cartesian graph; faint circles and spokes on a polar one | | Colors | Five pens (green, blue, purple, yellow, red) for groups, a ramp from a faint blue to the blue pen, and the blue pen against the red either side of a midpoint | | Geoms | Bars, slices, tiles and areas as washes; lines as pen strokes; points as dabs of paint | | Color scheme | Light, whatever `colorScheme` the provider is given | ## Changing the look of one graph A spec's own `styles()`, scales and `config()` sit above the theme. This graph keeps the washes and the handwriting and takes its own colors and a larger type: ```tsx theme={null} const customizedSpec = kit.pipe( spec, kit.scale.color.discrete({ range: ['#0B7285', '#E8590C'] }), kit.styles({ defaults: [kit.style.graph({ textScale: 1.5 })] }) ); ``` The theme is drawn for a graph about 600 pixels wide. `textScale` scales its type for a larger or smaller one. ## With a geom package Theme and plugins go to the same kit. A geom package built from the shared shapes, such as the [dumbbell](/sdk-next/graph-types/dumbbell), is drawn in Watercolor's paint too, and takes the theme's colors, type and axes. ```tsx theme={null} import { dumbbell } from '@graphysdk/geom-dumbbell'; const kit = createGraphyKit({ theme: watercolor, plugins: [dumbbell] }); ``` ## Fonts `fonts.css` loads Caveat from Google Fonts. To host the font yourself, leave the import out and load a family named `Caveat` before the first graph is drawn. # Error handling Source: https://docs.graphy.dev/sdk/advanced/error-handling Both `GraphProvider` and `Graph` support error handling to gracefully manage failures. ## GraphProvider error handling Use the `onError` prop to track errors in data processing, configuration, or provider setup: ```tsx theme={null} import { GraphProvider, Graph } from '@graphysdk/core'; { // Log to your error tracking service console.error('Error:', error); }} > ; ``` By default, `GraphProvider` re-throws errors (allowing your app's error boundary to handle them). Provide a `fallbackComponent` to show custom UI instead: ```tsx theme={null} import type { GraphProviderErrorFallbackProps } from '@graphysdk/core'; function ErrorFallback({ error }: GraphProviderErrorFallbackProps) { return (

Something went wrong

{error.message}

); } logError(error)} fallbackComponent={ErrorFallback} > ; ``` ## Graph error handling `Graph` also accepts `onError` and `errorFallback` props for handling chart rendering errors. Unlike `GraphProvider`, `Graph` always shows a fallback UI when rendering fails. If you don't provide a custom `errorFallback`, it displays a default "Unable to render chart" message. ```tsx theme={null} import type { GraphErrorBoundaryFallbackProps } from '@graphysdk/core'; function ChartErrorFallback({ error }: GraphErrorBoundaryFallbackProps) { return (

Chart failed to load

{error.message}

); } logError(error)} errorFallback={ChartErrorFallback} /> ; ``` # Internationalization Source: https://docs.graphy.dev/sdk/advanced/i18n Graphy uses two separate locale systems: | Type | Purpose | How to set | | - | - | - | | **Data Locale** | Parses dates and numbers in your dataset | `data._metadata.parsingLocale` | | **UI Locale** | Renders text in the visualization and editor | `uiLocale` prop on `GraphProvider` | These are independent — you can parse data in one format while displaying the UI in another language. ## Data Locale The data locale controls how Graphy interprets dates and numbers in your dataset. | Locale | Date format | Example | | - | - | - | | `en-US` (default) | MM/DD/YYYY | `03/15/2024` → 15th March | | `en-GB` | DD/MM/YYYY | `15/03/2024` → 15th March | Set it in your data configuration: ```tsx theme={null} const config: GraphConfig = { data: { columns: [...], rows: [...], _metadata: { parsingLocale: 'en-GB' } } }; ``` ## UI Locale The UI locale controls all text displayed in the visualization and editor, including labels, tooltips, and menu items. It also determines the text direction (LTR or RTL). ### Available locales | Locale | Language | Text Direction | | - | - | - | | `en-GB` | English (UK) | LTR | | `en-US` | English (US) | LTR | | `pt-PT` | Portuguese | LTR | | `ar` | Arabic | RTL | ### Setting the UI locale ```tsx theme={null} import { GraphProvider } from '@graphy/core'; ; ``` ### Example: Different data and UI locales You might have data formatted for a UK audience but want to display the UI in Arabic: ```tsx theme={null} const config: GraphConfig = { data: { columns: [...], rows: [...], _metadata: { parsingLocale: 'en-GB' // Parse dates as DD/MM/YYYY } } }; {/* Display UI in Arabic (RTL) */} ``` ## Runtime overrides The `i18nOverrides` prop on `GraphProvider` allows you to customize translations and text direction at runtime, without switching the entire locale. ### Overriding strings Override any UI text by providing translation keys: ```tsx theme={null} ``` Some strings accept parameters. Provide a function to handle dynamic values: ```tsx theme={null} `Columna ${number}`, 'common.confirmDelete': ({ item }) => `¿Eliminar ${item}?`, }} > ``` ### Overriding text direction (RTL support) Text direction is automatically determined by the selected locale (e.g., Arabic uses RTL). You can override it manually using the `dir` property: ```tsx theme={null} ``` Valid values are `'ltr'` (left-to-right) and `'rtl'` (right-to-left). This is useful when you want to: * Force RTL layout while using an LTR locale's translations * Force LTR layout while using an RTL locale's translations * Test RTL behavior during development ### Combining overrides You can combine string overrides with text direction in a single configuration: ```tsx theme={null} ``` ## String reference ### Common | Key | Default | Parameters | | - | - | - | | `common.save` | "Save" | - | | `common.cancel` | "Cancel" | - | | `common.confirm` | "Confirm" | - | | `common.delete` | "Delete" | - | | `common.confirmDelete` | "Are you sure you want to delete \$?" | `{ item }` | | `common.abbreviations.thousands` | "k" | - | | `common.abbreviations.millions` | "m" | - | | `common.abbreviations.billions` | "b" | - | ### Graph - General | Key | Default | Parameters | | - | - | - | | `graph.loading` | "Loading" | - | | `graph.error` | "Something went wrong" | - | | `graph.axisLabels.placeholder` | "Add label" | - | | `graph.waterfall.totalLabel` | "Total" | - | ### Graph - Default property labels | Key | Default | Parameters | | - | - | - | | `graph.defaultPropertyLabels.date` | "Date" | - | | `graph.defaultPropertyLabels.year` | "Year" | - | | `graph.defaultPropertyLabels.quarter` | "Quarter" | - | | `graph.defaultPropertyLabels.month` | "Month" | - | | `graph.defaultPropertyLabels.week` | "Week" | - | | `graph.defaultPropertyLabels.series` | "Series" | - | | `graph.defaultPropertyLabels.category` | "Category" | - | | `graph.defaultPropertyLabels.columnNumber` | "Column \$" | `{ number }` | | `graph.defaultPropertyLabels.rowNumber` | "Row \$" | `{ number }` | | `graph.defaultPropertyLabels.seriesNumber` | "Series \$" | `{ number }` | ### Graph - Trend comparisons | Key | Default | Parameters | | - | - | - | | `graph.trendComparison.fromPrevious.long` | "\$ vs previous" | `{ value }` | | `graph.trendComparison.fromPrevious.short` | "\$ vs prev" | `{ value }` | | `graph.trendComparison.fromPreviousYear.long` | "\$ vs previous year" | `{ value }` | | `graph.trendComparison.fromPreviousYear.short` | "\$ vs prev year" | `{ value }` | | `graph.trendComparison.fromPreviousMonth.long` | "\$ vs previous month" | `{ value }` | | `graph.trendComparison.fromPreviousMonth.short` | "\$ vs prev month" | `{ value }` | | `graph.trendComparison.fromPreviousWeek.long` | "\$ vs previous week" | `{ value }` | | `graph.trendComparison.fromPreviousWeek.short` | "\$ vs prev week" | `{ value }` | | `graph.trendComparison.fromPreviousDay.long` | "\$ vs previous day" | `{ value }` | | `graph.trendComparison.fromPreviousDay.short` | "\$ vs prev day" | `{ value }` | | `graph.trendComparison.fromPreviousHour.long` | "\$ vs previous hour" | `{ value }` | | `graph.trendComparison.fromPreviousHour.short` | "\$ vs prev hour" | `{ value }` | ### Graph - Headline metrics | Key | Default | Parameters | | - | - | - | | `graph.headlineMetrics.averageLong` | "Average \$" | `{ value }` | | `graph.headlineMetrics.averageShort` | "Avg. \$" | `{ value }` | | `graph.headlineMetrics.total` | "Total \$" | `{ value }` | | `graph.headlineMetrics.totalConversionRate` | "Total conversion rate \$" | `{ value }` | | `graph.headlineMetrics.current` | "\$" | `{ value }` | ### Graph - Title editor | Key | Default | | - | - | | `graph.titleEditor.titlePlaceholder` | "Untitled" | | `graph.titleEditor.subtitlePlaceholder` | "Add a subtitle" | ### Graph - Text toolbar formatting | Key | Default | | - | - | | `graph.toolbar.formatting.bold` | "Bold" | | `graph.toolbar.formatting.italic` | "Italic" | | `graph.toolbar.formatting.underline` | "Underline" | | `graph.toolbar.formatting.link` | "Link" | ### Graph - Text toolbar link | Key | Default | | - | - | | `graph.toolbar.link.ariaLabel` | "Create link" | | `graph.toolbar.link.placeholder` | "Type or paste a link..." | | `graph.toolbar.link.submit` | "Submit" | ### Graph - Text toolbar font | Key | Default | | - | - | | `graph.toolbar.font.label` | "Font" | ### Graph - Text toolbar heading | Key | Default | | - | - | | `graph.toolbar.heading.label` | "Heading level" | | `graph.toolbar.heading.text` | "Text" | | `graph.toolbar.heading.caption` | "Caption" | | `graph.toolbar.heading.heading1` | "Heading 1" | | `graph.toolbar.heading.heading2` | "Heading 2" | | `graph.toolbar.heading.heading3` | "Heading 3" | ### Graph - Text toolbar alignment | Key | Default | | - | - | | `graph.toolbar.textAlign.left` | "Align left" | | `graph.toolbar.textAlign.center` | "Align center" | | `graph.toolbar.textAlign.right` | "Align right" | ### Graph - Text toolbar color | Key | Default | | - | - | | `graph.toolbar.color.label` | "Text color" | ### Graph - Color dropdown | Key | Default | | - | - | | `graph.colorDropdown.colors` | "Colors" | | `graph.colorDropdown.chartColors` | "Chart colors" | | `graph.colorDropdown.custom` | "Custom" | | `graph.colorDropdown.opacity` | "Opacity" | | `graph.colorDropdown.transparent` | "Transparent" | ### Graph - Annotations base menu | Key | Default | Parameters | | - | - | - | | `graph.annotations.baseMenu.pinNumber` | "Pin number" | - | | `graph.annotations.baseMenu.annotate` | "Annotate" | - | | `graph.annotations.baseMenu.highlight` | "Highlight" | - | | `graph.annotations.baseMenu.highlightWithLabel` | "Highlight \$" | `{ label }` | | `graph.annotations.baseMenu.removeHighlight` | "Remove highlight" | - | | `graph.annotations.baseMenu.differenceArrow` | "Difference arrow" | - | | `graph.annotations.baseMenu.addSticker` | "Add sticker" | - | ### Graph - Annotations text menu | Key | Default | | - | - | | `graph.annotations.textMenu.title` | "Annotate" | | `graph.annotations.textMenu.placeholder` | "Add a comment" | | `graph.annotations.textMenu.addButton` | "Add" | ### Graph - Annotations highlights | Key | Default | | - | - | | `graph.annotations.highlights.menuTitle` | "Highlight" | | `graph.annotations.highlights.labels.bar` | "Bar" | | `graph.annotations.highlights.labels.barGroup` | "Group" | | `graph.annotations.highlights.labels.barStack` | "Stack" | | `graph.annotations.highlights.labels.barSeries` | "Series" | | `graph.annotations.highlights.labels.line` | "Line" | | `graph.annotations.highlights.labels.linePoint` | "Point" | | `graph.annotations.highlights.labels.lineSeries` | "Series" | | `graph.annotations.highlights.labels.scatterPoint` | "Point" | | `graph.annotations.highlights.labels.scatterSeries` | "Series" | | `graph.annotations.highlights.labels.pieSlice` | "Slice" | | `graph.annotations.highlights.labels.pointOrBar` | "Point / bar" | | `graph.annotations.highlights.labels.lineOrSeries` | "Line / series" | ### Graph - Annotations sticker menu | Key | Default | | - | - | | `graph.annotations.stickerMenu.title` | "Add sticker" | ### Graph - Annotations delete | Key | Default | | - | - | | `graph.annotations.delete` | "Delete" | ### Graph - Annotations arrow | Key | Default | | - | - | | `graph.annotations.arrow.thickness.label` | "Thickness" | | `graph.annotations.arrow.thickness.thin` | "Thin" | | `graph.annotations.arrow.thickness.medium` | "Medium" | | `graph.annotations.arrow.thickness.thick` | "Thick" | | `graph.annotations.arrow.arrowhead.startPoint` | "Start point" | | `graph.annotations.arrow.arrowhead.endPoint` | "End point" | | `graph.annotations.arrow.arrowhead.lineArrow` | "Line arrow" | | `graph.annotations.arrow.arrowhead.none` | "None" | | `graph.annotations.arrow.lineStyle.label` | "Line style" | | `graph.annotations.arrow.lineStyle.solid` | "Solid" | | `graph.annotations.arrow.lineStyle.dotted` | "Dotted" | | `graph.annotations.arrow.stickerEffect` | "Sticker effect" | | `graph.annotations.arrow.toolbarAriaLabel` | "Free-form arrow toolbar" | | `graph.annotations.arrow.editorAriaLabel` | "Arrow editor" | ### Graph - Annotations difference arrow | Key | Default | Parameters | | - | - | - | | `graph.annotations.differenceArrow.type.proportion` | "Proportion" | - | | `graph.annotations.differenceArrow.type.difference` | "Difference (\$)" | `{ absoluteDifference }` | | `graph.annotations.differenceArrow.type.increase` | "Increase (\$)" | `{ absoluteDifference }` | | `graph.annotations.differenceArrow.type.decrease` | "Decrease (\$)" | `{ absoluteDifference }` | | `graph.annotations.differenceArrow.type.percentageChange` | "Percentage change (\$)" | `{ percentageChange }` | | `graph.annotations.differenceArrow.type.percentIncrease` | "Percent increase (\$)" | `{ percentageChange }` | | `graph.annotations.differenceArrow.type.percentDecrease` | "Percent decrease (\$)" | `{ percentageChange }` | | `graph.annotations.differenceArrow.size.label` | "Size" | - | | `graph.annotations.differenceArrow.size.small` | "Small" | - | | `graph.annotations.differenceArrow.size.medium` | "Medium" | - | | `graph.annotations.differenceArrow.size.large` | "Large" | - | | `graph.annotations.differenceArrow.flipArrow` | "Flip arrow" | - | | `graph.annotations.differenceArrow.color` | "Color" | - | | `graph.annotations.differenceArrow.toolbarAriaLabel` | "Difference arrow toolbar" | - | | `graph.annotations.differenceArrow.editorAriaLabel` | "Difference arrow editor" | - | ### Graph - Annotations shape | Key | Default | Parameters | | - | - | - | | `graph.annotations.shape.color` | "Color" | - | | `graph.annotations.shape.border.label` | "Border" | - | | `graph.annotations.shape.border.none` | "None" | - | | `graph.annotations.shape.border.thin` | "Thin (\$)" | `{ value }` | | `graph.annotations.shape.border.medium` | "Medium (\$)" | `{ value }` | | `graph.annotations.shape.border.thick` | "Thick (\$)" | `{ value }` | ### Editor - General | Key | Default | | - | - | | `editor.title` | "Graphy Editor" | | `editor.description` | "Edit your content here" | ### Editor - Graph types | Key | Default | | - | - | | `editor.graphTypes.column` | "Column" | | `editor.graphTypes.columnStacked` | "Stacked" | | `editor.graphTypes.columnStackedFill` | "100% Stacked" | | `editor.graphTypes.bar` | "Bar" | | `editor.graphTypes.barStacked` | "Stacked" | | `editor.graphTypes.barStackedFill` | "100% Stacked" | | `editor.graphTypes.line` | "Line" | | `editor.graphTypes.areaStacked` | "Stacked Area" | | `editor.graphTypes.pie` | "Pie" | | `editor.graphTypes.donut` | "Donut" | | `editor.graphTypes.scatter` | "Scatter" | | `editor.graphTypes.bubble` | "Bubble" | | `editor.graphTypes.funnel` | "Funnel" | | `editor.graphTypes.combo` | "Combo" | | `editor.graphTypes.heatmap` | "Heatmap" | | `editor.graphTypes.waterfall` | "Waterfall" | | `editor.graphTypes.mekko` | "Mekko" | | `editor.graphTypes.table` | "Table" | ### Editor - Size panel | Key | Default | Parameters | | - | - | - | | `editor.sizePanel.toolbarButton` | "Size" | - | | `editor.sizePanel.presetsSection.title` | "Presets" | - | | `editor.sizePanel.customSizeSection.title` | "Custom size" | - | | `editor.sizePanel.presets.googleSlides` | "Google Slides / PowerPoint" | - | | `editor.sizePanel.presets.webEmail` | "Web / email" | - | | `editor.sizePanel.presets.linkedIn` | "LinkedIn post" | - | | `editor.sizePanel.presets.instagram` | "Instagram post" | - | | `editor.sizePanel.presets.tiktok` | "TikTok / Instagram story" | - | | `editor.sizePanel.presets.twitter` | "X (Twitter)" | - | | `editor.sizePanel.presets.mobile` | "Mobile" | - | | `editor.sizePanel.inputs.unitLabel` | "px" | - | | `editor.sizePanel.validation.maxSizeError` | "≤ \$px" | `{ max }` | | `editor.sizePanel.validation.minSizeError` | "≥ \$px" | `{ min }` | ### Editor - Graph panel | Key | Default | | - | - | | `editor.graphPanel.toolbarButton` | "Graph" | | `editor.graphPanel.graphTypeSection.title` | "Graph type" | | `editor.graphPanel.graphOptionsSection.title` | "Graph options" | | `editor.graphPanel.graphOptions.sortBars` | "Sort high → low" | | `editor.graphPanel.graphOptions.gridLines` | "Grid lines" | | `editor.graphPanel.graphOptions.showPoints` | "Show points" | | `editor.graphPanel.graphOptions.smoothLines` | "Smooth lines" | | `editor.graphPanel.graphOptions.stackTotals` | "Stack totals" | | `editor.graphPanel.graphOptions.dataLabels` | "Data labels" | | `editor.graphPanel.graphOptions.showPercentages` | "Show percentages" | | `editor.graphPanel.graphOptions.categoryLabels` | "Category labels" | | `editor.graphPanel.legendSection.title` | "Legend" | | `editor.graphPanel.legendSection.right` | "Right" | | `editor.graphPanel.legendSection.top` | "Top" | | `editor.graphPanel.legendSection.none` | "None" | | `editor.graphPanel.numberFormatSection.title` | "Number format" | | `editor.graphPanel.numberFormat.abbreviationLabel` | "Abbreviation" | | `editor.graphPanel.numberFormat.valueLabel` | "Value" | | `editor.graphPanel.numberFormat.decimalPlacesLabel` | "Decimal places" | | `editor.graphPanel.numberFormat.options.auto` | "Auto" | | `editor.graphPanel.numberFormat.options.custom` | "Custom" | | `editor.graphPanel.numberFormat.options.none` | "None" | | `editor.graphPanel.headlineNumberSize.sizeLabel` | "Size" | | `editor.graphPanel.headlineNumberSize.valueLabel` | "Value" | | `editor.graphPanel.headlineNumberSize.sizes.small` | "S" | | `editor.graphPanel.headlineNumberSize.sizes.medium` | "M" | | `editor.graphPanel.headlineNumberSize.sizes.large` | "L" | | `editor.graphPanel.lineThickness.label` | "Line thickness" | | `editor.graphPanel.pointSize.label` | "Point size" | | `editor.graphPanel.pointSize.options.auto` | "Auto" | | `editor.graphPanel.pointSize.options.custom` | "Custom" | ### Editor - Headline number section | Key | Default | | - | - | | `editor.graphPanel.headlineNumberSection.title` | "Headline number" | | `editor.graphPanel.headlineNumberSection.toggle` | "Visible" | | `editor.graphPanel.headlineNumberSection.metricLabel` | "Metric" | | `editor.graphPanel.headlineNumberSection.compareWithLabel` | "Compare with" | | `editor.graphPanel.headlineNumberSection.metrics.total` | "Total" | | `editor.graphPanel.headlineNumberSection.metrics.average` | "Avg." | | `editor.graphPanel.headlineNumberSection.metrics.current` | "Last" | | `editor.graphPanel.headlineNumberSection.metrics.conversion` | "Conversion" | | `editor.graphPanel.headlineNumberSection.metrics.left` | "Left" | | `editor.graphPanel.headlineNumberSection.comparison.first` | "First" | | `editor.graphPanel.headlineNumberSection.comparison.previous` | "Previous" | | `editor.graphPanel.headlineNumberSection.pieTotalPosition.left` | "Left" | | `editor.graphPanel.headlineNumberSection.pieTotalPosition.center` | "Center" | ### Editor - Treat empty values | Key | Default | | - | - | | `editor.graphPanel.treatEmptyValues.leaveGap.label` | "Leave gap in chart" | | `editor.graphPanel.treatEmptyValues.leaveGap.description` | "Leaves a visible break in the line where data is missing" | | `editor.graphPanel.treatEmptyValues.fillZero.label` | "Fill with zero" | | `editor.graphPanel.treatEmptyValues.fillZero.description` | "Displays missing data points as zero" | | `editor.graphPanel.treatEmptyValues.connectGaps.label` | "Connect across gaps" | | `editor.graphPanel.treatEmptyValues.connectGaps.description` | "Joins the line between available points, skipping the gap" | ### Editor - Combo chart appearance | Key | Default | | - | - | | `editor.graphPanel.comboChartAppearance.title` | "Combo chart appearance" | | `editor.graphPanel.comboChartAppearance.groupedBars` | "Grouped bars" | | `editor.graphPanel.comboChartAppearance.stackedBars` | "Stacked bars" | | `editor.graphPanel.comboChartAppearance.linesOnly` | "Lines only" | ### Editor - Column mapping | Key | Default | | - | - | | `editor.columnMapping.title` | "Data" | | `editor.columnMapping.xAxis` | "X-axis" | | `editor.columnMapping.yAxis` | "Y-axis" | | `editor.columnMapping.leftYAxis` | "Y-axis (left)" | | `editor.columnMapping.rightYAxis` | "Y-axis (right)" | | `editor.columnMapping.slices` | "Values (slices)" | | `editor.columnMapping.labels` | "Labels" | | `editor.columnMapping.size` | "Size" | | `editor.columnMapping.shape` | "Shape" | | `editor.columnMapping.reset` | "Reset to auto" | | `editor.columnMapping.addSeries` | "Add series" | | `editor.columnMapping.removeSeries` | "Remove series" | ### Editor - Axes panel | Key | Default | | - | - | | `editor.axesPanel.toolbarButton` | "Axes" | | `editor.axesPanel.mainAxisSection.yAxis` | "Y-axis" | | `editor.axesPanel.mainAxisSection.xAxis` | "X-axis" | | `editor.axesPanel.crossAxisSection.xAxis` | "X-axis" | | `editor.axesPanel.crossAxisSection.yAxis` | "Y-axis" | | `editor.axesPanel.controls.visible` | "Visible" | | `editor.axesPanel.controls.labels` | "Labels" | | `editor.axesPanel.controls.position` | "Position" | | `editor.axesPanel.controls.scale` | "Scale" | | `editor.axesPanel.controls.startFrom` | "Start from" | | `editor.axesPanel.controls.endAt` | "End at" | | `editor.axesPanel.controls.value` | "Value" | | `editor.axesPanel.controls.numberOfAxes` | "Number of axes" | | `editor.axesPanel.controls.reverse` | "Reverse" | | `editor.axesPanel.labelMode.auto` | "Auto" | | `editor.axesPanel.labelMode.edges` | "Edges" | | `editor.axesPanel.scale.auto` | "Auto" | | `editor.axesPanel.scale.log` | "Log" | | `editor.axesPanel.startFrom.auto` | "Auto" | | `editor.axesPanel.startFrom.zero` | "Zero" | | `editor.axesPanel.startFrom.custom` | "Custom" | | `editor.axesPanel.endAt.auto` | "Auto" | | `editor.axesPanel.endAt.custom` | "Custom" | | `editor.axesPanel.axisCount.single` | "Single" | | `editor.axesPanel.axisCount.double` | "Double" | | `editor.axesPanel.position.left` | "Left" | | `editor.axesPanel.position.right` | "Right" | | `editor.axesPanel.position.top` | "Top" | | `editor.axesPanel.position.bottom` | "Bottom" | | `editor.axesPanel.yesNo.yes` | "Yes" | | `editor.axesPanel.yesNo.no` | "No" | ### Editor - Color panel | Key | Default | | - | - | | `editor.colorPanel.toolbarButton` | "Color" | | `editor.colorPanel.themeSection.title` | "Theme" | | `editor.colorPanel.paletteSection.title` | "Palette" | | `editor.colorPanel.paletteSection.colorScheme` | "Color scheme" | | `editor.colorPanel.paletteSection.colors` | "Colors" | | `editor.colorPanel.paletteSection.colorPalettes` | "Color palettes" | | `editor.colorPanel.paletteMode.preset` | "Preset" | | `editor.colorPanel.paletteMode.brand` | "Brand" | | `editor.colorPanel.paletteMode.freestyle` | "Freestyle" | | `editor.colorPanel.paletteThemes.colorful` | "Colorful" | | `editor.colorPanel.paletteThemes.pastel` | "Pastel" | | `editor.colorPanel.paletteThemes.neon` | "Neon" | | `editor.colorPanel.backgroundSection.title` | "Background" | | `editor.colorPanel.backgroundSection.black` | "Black" | | `editor.colorPanel.backgroundSection.white` | "White" | | `editor.colorPanel.backgroundSection.grey` | "Grey" | | `editor.colorPanel.backgroundSection.tint` | "Tint" | | `editor.colorPanel.backgroundSection.custom` | "Custom" | | `editor.colorPanel.backgroundSection.none` | "None" | | `editor.colorPanel.borderSection.title` | "Border" | | `editor.colorPanel.borderSection.borderColor` | "Border color" | | `editor.colorPanel.borderSection.thickness` | "Thickness" | | `editor.colorPanel.borderSection.cornerRadius` | "Corner radius" | | `editor.colorPanel.borderType.solid` | "Solid" | | `editor.colorPanel.borderType.gradient` | "Gradient" | | `editor.colorPanel.borderType.grey` | "Grey" | | `editor.colorPanel.borderType.preset` | "Preset" | | `editor.colorPanel.borderType.custom` | "Custom" | | `editor.colorPanel.borderType.none` | "None" | | `editor.colorPanel.presetGradients.lilac` | "Lilac" | | `editor.colorPanel.presetGradients.neonPink` | "Neon Pink" | | `editor.colorPanel.presetGradients.blackberry` | "Blackberry" | | `editor.colorPanel.presetGradients.sun` | "Sun" | | `editor.colorPanel.presetGradients.iceland` | "Iceland" | | `editor.colorPanel.presetGradients.sunset` | "Sunset" | | `editor.colorPanel.presetGradients.ultraviolet` | "Ultraviolet" | | `editor.colorPanel.presetGradients.purple` | "Purple" | | `editor.colorPanel.presetGradients.iceCream` | "Ice Cream" | | `editor.colorPanel.presetGradients.mint` | "Mint" | | `editor.colorPanel.presetGradients.cool` | "Cool" | | `editor.colorPanel.presetGradients.fresh` | "Fresh" | ### Editor - Design panel | Key | Default | | - | - | | `editor.designPanel.toolbarButton` | "Design" | | `editor.designPanel.defaultExpanded` | "Palette" | ### Editor - Annotate panel | Key | Default | | - | - | | `editor.annotatePanel.toolbarButton` | "Annotate" | | `editor.annotatePanel.callOutSection.title` | "Call-out" | | `editor.annotatePanel.callOutSection.text` | "Text" | | `editor.annotatePanel.callOutSection.arrow` | "Arrow" | | `editor.annotatePanel.callOutSection.box` | "Box" | | `editor.annotatePanel.callOutSection.differenceArrows` | "Difference arrows" | | `editor.annotatePanel.highlightSection.title` | "Highlight" | | `editor.annotatePanel.highlightSection.button` | "Highlight" | | `editor.annotatePanel.highlightSection.colorLabel` | "Highlight color" | ### Editor - Annotations panel | Key | Default | | - | - | | `editor.annotationsPanel.toolbarButton` | "Annotate" | | `editor.annotationsPanel.freeformSection.title` | "Freeform" | | `editor.annotationsPanel.freeformSection.text` | "Text" | | `editor.annotationsPanel.freeformSection.arrow` | "Arrow" | | `editor.annotationsPanel.freeformSection.box` | "Box" | | `editor.annotationsPanel.freeformSection.difference` | "Difference" | | `editor.annotationsPanel.goalSection.title` | "Goal" | | `editor.annotationsPanel.goalSection.labelControl` | "Label" | | `editor.annotationsPanel.goalSection.labelPlaceholder` | "Goal" | | `editor.annotationsPanel.goalSection.labelAriaLabel` | "Custom goal label" | | `editor.annotationsPanel.goalSection.valueControl` | "Goal value" | | `editor.annotationsPanel.goalSection.valueAriaLabel` | "Goal value" | | `editor.annotationsPanel.goalSection.byDate` | "By date" | | `editor.annotationsPanel.goalSection.xAxisValue` | "X-axis value" | | `editor.annotationsPanel.goalSection.optional` | "(optional)" | | `editor.annotationsPanel.trendsAndAveragesSection.title` | "Trends and Averages" | | `editor.annotationsPanel.trendsAndAveragesSection.trend` | "Trend" | | `editor.annotationsPanel.trendsAndAveragesSection.average` | "Average" | | `editor.annotationsPanel.trendType.label` | "Trend type" | | `editor.annotationsPanel.trendType.placeholder` | "Trend type" | | `editor.annotationsPanel.trendType.options.linear` | "Linear" | | `editor.annotationsPanel.trendType.options.exponential` | "Exponential" | | `editor.annotationsPanel.trendType.options.quadratic` | "Quadratic" | | `editor.annotationsPanel.trendType.options.polynomial` | "Polynomial" | | `editor.annotationsPanel.trendType.options.logarithmic` | "Logarithmic" | | `editor.annotationsPanel.trendType.options.power` | "Power" | | `editor.annotationsPanel.trendType.options.loess` | "Loess" | | `editor.annotationsPanel.averageLineSeries.label` | "Average line series" | | `editor.annotationsPanel.averageLineSeries.placeholder` | "Select series" | | `editor.annotationsPanel.averageLineSeries.ariaLabel` | "Series dropdown for average line" | | `editor.annotationsPanel.highlightSection.title` | "Highlight" | | `editor.annotationsPanel.highlightSection.button` | "Highlight" | | `editor.annotationsPanel.highlightSection.fadeColorLabel` | "Fade color" | | `editor.annotationsPanel.titleAndSubtitleSection.title` | "Title & Subtitle" | | `editor.annotationsPanel.titleAndSubtitleSection.toggleTitle` | "Title" | | `editor.annotationsPanel.titleAndSubtitleSection.subtitle` | "Subtitle" | | `editor.annotationsPanel.captionAndSourceSection.title` | "Caption & Source" | | `editor.annotationsPanel.captionAndSourceSection.caption` | "Caption" | | `editor.annotationsPanel.captionAndSourceSection.source` | "Source" | | `editor.annotationsPanel.captionAndSourceSection.url` | "URL" | | `editor.annotationsPanel.captionAndSourceSection.name` | "Name" | ### Editor - Elements panel | Key | Default | Parameters | | - | - | - | | `editor.elementsPanel.toolbarButton` | "Elements" | - | | `editor.elementsPanel.headerSection.title` | "Header" | - | | `editor.elementsPanel.headerSection.toggleTitle` | "Title" | - | | `editor.elementsPanel.headerSection.subtitle` | "Subtitle" | - | | `editor.elementsPanel.footerSection.title` | "Footer" | - | | `editor.elementsPanel.footerSection.caption` | "Caption" | - | | `editor.elementsPanel.footerSection.source` | "Source" | - | | `editor.elementsPanel.footerSection.url` | "URL" | - | | `editor.elementsPanel.footerSection.name` | "Name" | - | | `editor.elementsPanel.textSizeSection.title` | "Text size" | - | | `editor.elementsPanel.textSizeSection.scaleFormat` | "\$x" | `{ value }` | | `editor.elementsPanel.fontSection.title` | "Font" | - | | `editor.elementsPanel.sourceSection.title` | "Source" | - | | `editor.elementsPanel.sourceSection.url` | "URL" | - | | `editor.elementsPanel.sourceSection.name` | "Name" | - | ### Editor - Power-ups panel | Key | Default | | - | - | | `editor.powerUpPanel.toolbarButton` | "Power-ups" | | `editor.powerUpPanel.goalSection.title` | "Goal" | | `editor.powerUpPanel.goalSection.toggle` | "Goal" | | `editor.powerUpPanel.goalSection.labelControl` | "Label" | | `editor.powerUpPanel.goalSection.labelPlaceholder` | "Goal" | | `editor.powerUpPanel.goalSection.labelAriaLabel` | "Custom goal label" | | `editor.powerUpPanel.goalSection.valueControl` | "Goal value" | | `editor.powerUpPanel.goalSection.byDate` | "By date" | | `editor.powerUpPanel.goalSection.xAxisValue` | "X-axis value" | | `editor.powerUpPanel.goalSection.optional` | " (optional)" | | `editor.powerUpPanel.goalSection.xAxisReferenceAriaLabel` | "X-axis reference value" | | `editor.powerUpPanel.goalSection.anyXAxisValue` | "Any x-axis value" | | `editor.powerUpPanel.goalSection.selectValuePlaceholder` | "Select value" | | `editor.powerUpPanel.trendSection.title` | "Trend" | | `editor.powerUpPanel.trendSection.toggle` | "Trend" | | `editor.powerUpPanel.averageSection.title` | "Average" | | `editor.powerUpPanel.averageSection.toggle` | "Average" | | `editor.powerUpPanel.averageSection.seriesLabel` | "Average line series" | | `editor.powerUpPanel.averageSection.seriesPlaceholder` | "Select series" | | `editor.powerUpPanel.averageSection.seriesAriaLabel` | "Series dropdown for average line" | | `editor.powerUpPanel.trendType.label` | "Trend type" | | `editor.powerUpPanel.trendType.placeholder` | "Trend type" | | `editor.powerUpPanel.trendType.options.linear` | "Linear" | | `editor.powerUpPanel.trendType.options.exponential` | "Exponential" | | `editor.powerUpPanel.trendType.options.quadratic` | "Quadratic" | | `editor.powerUpPanel.trendType.options.polynomial` | "Polynomial" | | `editor.powerUpPanel.trendType.options.logarithmic` | "Logarithmic" | | `editor.powerUpPanel.trendType.options.power` | "Power" | | `editor.powerUpPanel.trendType.options.loess` | "Loess" | | `editor.powerUpPanel.valueInput.percentagePlaceholder` | "Percentage" | | `editor.powerUpPanel.valueInput.numberPlaceholder` | "Number" | | `editor.powerUpPanel.valueInput.percentageSymbol` | "%" | ### Editor - Highlighting | Key | Default | | - | - | | `editor.highlighting.modeHelper.title` | "Highlight mode" | | `editor.highlighting.modeHelper.hover` | "Hover" | | `editor.highlighting.modeHelper.toHighlight` | "to highlight" | | `editor.highlighting.modeHelper.anyElement` | "any element" | | `editor.highlighting.modeHelper.escToExit` | "to exit" | | `editor.highlighting.modeHelper.esc` | "ESC" | | `editor.highlighting.emptyState` | "No highlight options available" | | `editor.highlighting.deleteAriaLabel` | "Delete highlight" | | `editor.highlighting.highlightStyle.tint` | "Tint" | | `editor.highlighting.highlightStyle.gray` | "Grey" | ### Editor - Fine tune panel | Key | Default | | - | - | | `editor.fineTunePanel.toolbarButton` | "Fine tune" | | `editor.fineTunePanel.detailSection.title` | "Detail" | | `editor.fineTunePanel.detailSection.missingValues` | "Missing values" | | `editor.fineTunePanel.lineStyleSection.title` | "Line style" | | `editor.fineTunePanel.lineStyleSection.lineCurve` | "Line curve" | | `editor.fineTunePanel.lineStyleSection.sharp` | "Sharp" | | `editor.fineTunePanel.lineStyleSection.smooth` | "Smooth" | ### Editor - Custom theme editor | Key | Default | | - | - | | `editor.customThemeEditor.patternDropdownAriaLabel` | "Pattern dropdown for series" | | `editor.customThemeEditor.patterns.solid` | "Solid" | | `editor.customThemeEditor.patterns.pattern` | "Pattern" | | `editor.customThemeEditor.patterns.dotted` | "Dotted" | | `editor.customThemeEditor.patterns.dashed` | "Dashed" | | `editor.customThemeEditor.patterns.hatched` | "Hatched" | | `editor.customThemeEditor.heatmapColorLabel` | "Color" | ### Editor - Accessibility | Key | Default | Parameters | | - | - | - | | `editor.accessibility.toggleSection` | "Toggle \$ section" | `{ title }` | ### Editor - Graphy defaults | Key | Default | | - | - | | `editor.graphyDefaults.themeOptions.light` | "Light" | | `editor.graphyDefaults.themeOptions.dark` | "Dark" | # Portal provider Source: https://docs.graphy.dev/sdk/advanced/portal-provider Graphy renders some elements outside of its root container using React portals. These include the annotations menu, text editor toolbar, tooltips and other overlay components. By default, these portal elements render into the document body. Use `PortalProvider` to specify a custom container instead, which is useful for modals, z-index control or CSS containment. ## Basic usage Wrap your `GraphProvider` with `PortalProvider`. All portal elements will be rendered as children of the `PortalProvider`. ```tsx theme={null} import { GraphProvider, Graph, PortalProvider } from '@graphysdk/core'; {/* portal elements will be rendered here */} ; ``` ## Props | Prop | Description | | - | - | | `children` | React children to render inside the provider | | `as` | Element type to render as (defaults to `div`) | | `className` | CSS class name for styling | ## Styling By default, `PortalProvider` applies `display: contents` to avoid affecting layout. When you pass a `className`, you control the styling completely: ```tsx theme={null} ``` ## Common use cases **Modals and dialogs** – Prevent portal elements from appearing behind modal overlays: ```tsx theme={null} ``` **Custom containers** – Render portals in a specific DOM element: ```tsx theme={null} ``` # Version management Source: https://docs.graphy.dev/sdk/advanced/versioning Graphy SDK follows [Semantic Versioning](https://semver.org/) (SemVer). This page describes our release lifecycle and what you can expect as a consumer of the SDK. ## Packages The Graphy SDK is composed of three packages: * `@graphysdk/core` * `@graphysdk/i18n` * `@graphysdk/editor` All three packages are versioned together — every release bumps them to the same version number. You must ensure that all Graphy packages in your project use matching versions. > [!WARNING] > > Mixing different versions of Graphy packages (e.g. `@graphysdk/core@1.3.0` with `@graphysdk/editor@1.2.0`) is not > supported and may cause unexpected behaviour. ## Version format Every release follows the `MAJOR.MINOR.PATCH` convention: * **Major** — contains breaking changes (aligned with Graphy customers beforehand) * **Minor** — new features, fully backwards-compatible * **Patch** — bug fixes and performance improvements ## Release lifecycle Each change merged to the main branch goes through the following stages before reaching a stable release: **1. Beta** New features and fixes are first published as beta releases (e.g. `1.2.0-beta.20260219191134`) which are appropriate for testing in staging environments. **2. Stable** Once a beta version has been tested and validated it is promoted to a stable release (e.g. `1.2.0`), which is the version recommended for production use. ### Installing a specific release channel ```bash Beta theme={null} npm install @graphysdk/core@beta @graphysdk/i18n@beta @graphysdk/editor@beta ``` ```bash Stable (default) theme={null} npm install @graphysdk/core@latest @graphysdk/i18n@latest @graphysdk/editor@latest ``` ## Deprecation policy We follow a deliberate deprecation cycle so that you always have time to migrate: 1. A feature is marked as **deprecated** using TypeScript's `@deprecated` tag, and noted in the release notes and documentation. 2. The deprecated feature continues to work for the remainder of the current major version. 3. The deprecated feature is **removed** in the next major release. > [!WARNING] > > Deprecated features are never removed in minor or patch releases. You can safely upgrade within a major version > without worrying about removed functionality. # Annotations Source: https://docs.graphy.dev/sdk/config/annotations Annotations allow users to add visual elements like arrows, shapes and text to graphs to highlight important data points or provide additional context. ## Annotation types Graphy supports seven types of annotations. ### Sticker Adds an emoji sticker to a specific data point on the chart. **type** `'sticker'` *required* Annotation type identifier. **sticker** `string` *required* The sticker to display. Available options: `'rocket'`, `'clapping-hands'`, `'thumbs-up'`, `'thumbs-down'`, `'grinning-face'`. **rowIndex** `number` Zero-based index of the row this annotation targets. **columnKey** `string` Key of the column this annotation targets. **rowValue** `string | number | null` Optional categorical value to pin the annotation to. Used to remap the annotation if the dataset changes. ```tsx theme={null} const config: GraphConfig = { annotations: [ { id: 'sticker-1', type: 'sticker', sticker: 'rocket', rowIndex: 2, columnKey: 'sales', }, ], }; ``` ### Tooltip Displays a custom tooltip with rich text content at a specific data point. **type** `'tooltip'` *required* Annotation type identifier. **caption** `JSONContent` *required* Rich text (TipTap JSON) shown inside the tooltip. **rowIndex** `number` Zero-based index of the row this annotation targets. **columnKey** `string` Key of the column this annotation targets. **rowValue** `string | number | null` Optional categorical value to pin the annotation to. ```tsx theme={null} const config: GraphConfig = { annotations: [ { id: 'tooltip-1', type: 'tooltip', caption: { type: 'doc', content: [ { type: 'paragraph', content: [{ type: 'text', text: 'Peak sales period' }], }, ], }, rowIndex: 5, columnKey: 'revenue', }, ], }; ``` ### Highlight Highlights a data point, entire series or all data points at a specific x-value. **type** `'highlight'` *required* Annotation type identifier. **highlight** `string` *required* What to highlight: - `'data-point'` - Highlight a single data point - `'series'` - Highlight an entire series - `'x-value'` - Highlight all data points at a specific x-value **rowIndex** `number` Zero-based index of the row this annotation targets. **columnKey** `string` Key of the column this annotation targets. **rowValue** `string | number | null` Optional categorical value to pin the annotation to. ```tsx theme={null} const config: GraphConfig = { annotations: [ { id: 'highlight-1', type: 'highlight', highlight: 'x-value', rowIndex: 3, }, ], }; ``` ### Text Adds a text box with rich text content positioned relative to the plot area. **type** `'text'` *required* Annotation type identifier. **content** `JSONContent` *required* Rich text content (TipTap JSON). **x** `number` *required* Horizontal position relative to the plot area (0 to 1). Represents the midpoint of the text box. **y** `number` *required* Vertical position relative to the plot area (0 to 1). Represents the midpoint of the text box. **width** `number` *required* Width relative to the plot area (0 to 1). **backgroundColor** `string` Background color for the text box (hex color). **backgroundColorStyle** `'fade' | 'opaque'` Background style: `'fade'` for semi-transparent or `'opaque'` for solid. ```tsx theme={null} const config: GraphConfig = { annotations: [ { id: 'text-1', type: 'text', content: { type: 'doc', content: [ { type: 'paragraph', content: [{ type: 'text', text: 'Notable trend' }], }, ], }, x: 0.5, y: 0.3, width: 0.2, backgroundColor: '#ffffff', backgroundColorStyle: 'opaque', }, ], }; ``` ### Arrow Draws an arrow between two points on the chart with customizable styling. **type** `'arrow'` *required* Annotation type identifier. **startX** `number` *required* Starting horizontal position relative to the plot area (0 to 1). **startY** `number` *required* Starting vertical position relative to the plot area (0 to 1). **endX** `number` *required* Ending horizontal position relative to the plot area (0 to 1). **endY** `number` *required* Ending vertical position relative to the plot area (0 to 1). **color** `string | null` *required* Arrow color (hex color or null for default). **thickness** `'thin' | 'medium' | 'thick'` *required* Arrow line thickness. **startArrowheadStyle** `'none' | 'line-arrow'` *required* Arrowhead style at the start of the line. **lineStyle** `'solid' | 'dashed'` *required* Line style. **endArrowheadStyle** `'none' | 'line-arrow'` *required* Arrowhead style at the end of the line. **hasStickerStyle** `boolean` *required* Whether to use a sticker outline style for the arrow. ```tsx theme={null} const config: GraphConfig = { annotations: [ { id: 'arrow-1', type: 'arrow', startX: 0.2, startY: 0.8, endX: 0.6, endY: 0.3, color: '#ef4444', thickness: 'medium', startArrowheadStyle: 'none', lineStyle: 'solid', endArrowheadStyle: 'line-arrow', hasStickerStyle: false, }, ], }; ``` ### Difference arrow Shows the difference between two data points with an automatic label displaying the calculated difference. **type** `'difference-arrow'` *required* Annotation type identifier. **show** `string` *required* What to display: - `'absolute-difference'` - Show the numeric difference - `'relative-difference'` - Show the percentage change - `'proportion'` - Show the ratio between values **start** `object` *required* Starting data point for the difference calculation. **properties** **rowIndex** `number` Zero-based row index. **columnKey** `string` Column key. **rowValue** `string | number | null` Optional categorical value. **end** `object` *required* Ending data point for the difference calculation. Same structure as `start`. **color** `string | null` *required* Arrow color (hex color or null for default). **size** `'small' | 'medium' | 'large'` *required* Arrow size. **labelPosition** `number` Position of the label along the arrow (0 to 1): - `0` - Label at the start (default) - `0.5` - Label in the middle - `1` - Label at the end ```tsx theme={null} const config: GraphConfig = { annotations: [ { id: 'diff-1', type: 'difference-arrow', show: 'relative-difference', start: { rowIndex: 0, columnKey: 'sales', }, end: { rowIndex: 5, columnKey: 'sales', }, color: '#10b981', size: 'medium', labelPosition: 0.5, }, ], }; ``` ### Shape Adds a rectangular shape to the chart, either below or above the plot area. **type** `'shape'` *required* Annotation type identifier. **shape** `'rectangle'` *required* Shape type. Currently only rectangles are supported. **layer** `'belowPlot' | 'abovePlot'` *required* Whether to render the shape below or above the plot area. **x** `number` *required* Horizontal position relative to the plot area (0 to 1). **y** `number` *required* Vertical position relative to the plot area (0 to 1). **width** `number` *required* Width relative to the plot area (0 to 1). **height** `number` *required* Height relative to the plot area (0 to 1). **fillColor** `string` *required* Fill color for the shape (hex color). **fillOpacity** `number` *required* Fill opacity (0 to 1). **strokeWidth** `number` *required* Border stroke width in pixels. ```tsx theme={null} const config: GraphConfig = { annotations: [ { id: 'shape-1', type: 'shape', shape: 'rectangle', layer: 'belowPlot', x: 0.1, y: 0.2, width: 0.3, height: 0.4, fillColor: '#fef3c7', fillOpacity: 0.5, strokeWidth: 2, }, ], }; ``` ## Canvas colors Canvas colors are the default options displayed in the color picker for annotations that allow the user to customize strokes, fills and text colors. Each canvas color is defined as an object with the following properties: | Property | Type | Description | | - | - | - | | `id` | `string` | A unique identifier for the color | | `label` | `string` (optional) | A human-readable name for the color | | `value` | `string` | The hex color value | ```tsx theme={null} import { graphyLightTheme } from '@graphysdk/core'; const customTheme = { ...graphyLightTheme, canvasColors: [ { id: 'default', label: 'Black', value: '#000000' }, { id: 'blue', label: 'Blue', value: '#3b82f6' }, { id: 'green', label: 'Green', value: '#10b981' }, { id: 'red', label: 'Red', value: '#ef4444' }, ], }; ``` ### Color persistence across themes The `id` property is important for maintaining color consistency when switching between themes. When a user applies a canvas color to an annotation (such as text color or arrow stroke), the color's `id` is stored rather than the hex value itself. When the theme changes: 1. **Matching ID found**: If the new theme contains a canvas color with the same `id`, the annotation automatically uses the color value from the new theme. This allows colors to adapt appropriately—for example, a "default" color might be black in a light theme but white in a dark theme. 2. **No matching ID**: If the new theme does not contain a canvas color with the same `id`, the annotation falls back to the first canvas color in the new theme's `canvasColors` array. This behavior ensures that annotations remain visible and appropriately styled regardless of theme changes, while giving you control over how colors translate between your light and dark themes. ## Default annotation colors When users create new annotations, Graphy applies default colors automatically. You can configure these defaults via the `defaultAnnotationColorIds` property in your theme, which references the `id` values from your `canvasColors` array. See [`GraphThemeAnnotationColorIds`](/sdk/reference/graph-theme#graphthemeannotationcolorids) for the available properties. ```tsx theme={null} import { graphyLightTheme } from '@graphysdk/core'; const customTheme = { ...graphyLightTheme, canvasColors: [ { id: 'default', label: 'Black', value: '#000000' }, { id: 'blue', label: 'Blue', value: '#3b82f6' }, { id: 'accent', label: 'Accent', value: '#ef4444' }, ], defaultAnnotationColorIds: { arrowStroke: 'accent', shapeFill: 'blue', }, }; ``` ### Fallback behavior If a default annotation color is not configured or references an invalid `id`, Graphy automatically falls back to the first canvas color in the `canvasColors` array. This ensures annotations always render with a valid color, even if the configuration is incomplete. # Appearance Source: https://docs.graphy.dev/sdk/config/appearance Configure visual styling for your graph via the `appearance` field. ## Colors **appearance.paletteId** `string` ID of a palette from `customPalettes` passed to `GraphProvider`. This determines which colors are used for series in your graph. See [Custom palettes](/sdk/customisation/series-colors#custom-palettes). ```tsx theme={null} // Register custom palettes with GraphProvider const myPalette = { id: 'my-palette-id', name: 'My Palette', colors: [ { id: '1', hex: '#3b82f6', name: 'Blue' }, { id: '2', hex: '#ef4444', name: 'Red' }, { id: '3', hex: '#10b981', name: 'Green' }, ], }; ; ``` **appearance.seriesStyles** `Record\` Style overrides for individual series. Each series is identified by a key that starts with `series` and is followed by a number (e.g. `series1`, `series2` etc). For a waterfall chart, the keys are `waterfallStart`, `waterfallPositive`, `waterfallNegative` and `waterfallTotal`. Each `SeriesStyle` can have: * `paletteColorId` - Color ID from the active palette * `customColor` - Hex color (takes precedence over `paletteColorId`) * `fillStyle` - `'solid'` | `'hatched'` (for bars/areas) * `lineStyle` - `'solid'` | `'dashed'` | `'dotted'` (for lines) **appearance.useSingleColorForBars** `boolean` (default: false) Use the same color for all bars in single-series categorical bar/column charts. ## Border **appearance.border** `object` Border configuration. No border by default. - `style` - `'none'` | `'custom'` | `'tinted'` | `'gradient'` | `'preset'` \| `'gray'` (default: `'none'`) - `color` - Hex color or preset name (when `style` is `'preset'`) - `width` - Stroke width in pixels, 0-64 (default: `0`) **appearance.hasRoundedCorners** `boolean` (default: true) Round the corners of the border. See [Borders](/sdk/theming/borders) for more info. ## Background **appearance.backgroundModifier** `'none' | 'tint'` (default: none) Set to `'tint'` to mix the background with the first palette color. ## Text styles **appearance.textStyle** `object` Font and color overrides for `heading` and `body` text. Each accepts `fontId` and `color` properties. See [Fonts](/sdk/customisation/graph-fonts). **appearance.textScale** `number` (default: 1) Text size multiplier (0.5-5). See [text sizes in pixels](#text-sizes-in-pixels) below for the sizes it produces. ### Text sizes in pixels Every text element in a chart derives its rendered size from `textScale`: ``` rendered size = base size × textScale × responsive multiplier ``` Base sizes (at `textScale: 1`): | Element | Base size | | - | - | | Axis tick labels and axis titles | 12px | | Legend labels and series labels | 12px | | Data labels, stack totals, pie labels, goal line labels | 11px | | Tooltip labels and values | 14px | | Tooltip heading, footer and caption | 12px | | Headline number values | 19px (small), 23px (medium), 26px (large) | | Headline number labels | 10px (small and medium), 12px (large) | | Chart title | 18px | | Chart subtitle | 15px | The responsive multiplier shrinks text on narrow charts: `0.9` when the chart is 600px wide or narrower, `0.8` at 400px or narrower and `1` otherwise. `textScale` scales all elements together. To pin an exact pixel size on individual elements, use [CSS hooks](/sdk/customisation/css-hooks#text-size). ## Number formatting **appearance.numberFormat** `object` Number display settings: - `decimalPlaces` - `'auto'` or 0-10 - `abbreviation` - `'none'` | `'auto'` | `'k'` | `'m'` | `'b'` ## Interactivity **appearance.highlightStyle** `'gray' | 'fade-color'` (default: gray) How non-highlighted items appear on hover: - `'gray'` = grey out non-highlighted items - `'fade-color'` = fade the color of non-highlighted items **appearance.showTooltips** `boolean` (default: true) Show tooltips on hover. **appearance.animateTransitions** `boolean` (default: true) Animate on initial render and data/config changes. # Axes Source: https://docs.graphy.dev/sdk/config/axes ## Overview The `axes` property provides comprehensive control over chart axes. Configure labels, ranges, scales and visibility for x, y and secondary y axes. ```tsx theme={null} const config: GraphConfig = { axes: { x: { label: 'Month', }, y: { label: 'Revenue (£)', min: 0, }, }, }; ``` > [!NOTE] > > Axes configuration only applies to chart types that support axes (line, bar, column, combo, scatter, etc.). Pie and > donut charts don't use axes. ## X-axis The x-axis typically represents the independent variable (categories, time periods or continuous values). ### Label **axes.x.label** `string` Custom label for the x-axis. Displayed below the axis. ```tsx theme={null} axes: { x: { label: 'Quarter'; } } ``` ### Visibility **axes.x.isHidden** `boolean` Whether to hide the x-axis. When `true`, the axis line, ticks and labels are hidden. The axis label is also hidden. ```tsx theme={null} axes: { x: { isHidden: true; } } ``` ### Orientation **axes.x.isReversed** `boolean` Flip the x-axis position (top↔bottom). ```tsx theme={null} axes: { x: { isReversed: true; // Move x-axis to opposite side } } ``` ### Numeric x-axis options When the x-axis contains numeric values (not categories), additional options are available: **axes.x.scaleType** `'linear' | 'logarithmic'` (default: linear) Scale type for numeric x-axes. Use `'logarithmic'` for data that spans several orders of magnitude. **axes.x.min** `number` Minimum value for the x-axis. If not set, automatically calculated from the data. **axes.x.max** `number` Maximum value for the x-axis. If not set, automatically calculated from the data. **axes.x.tickDisplayMode** `'auto' | 'edges'` (default: auto) How to display ticks on numeric x-axes: - `'auto'` - Display all ticks - `'edges'` - Display only the first and last ticks ```tsx theme={null} axes: { x: { scaleType: 'linear', min: 0, max: 100, tickDisplayMode: 'auto' } } ``` ## Y-axis The y-axis typically represents the dependent variable (measurements, values, quantities). ### Label **axes.y.label** `string` Custom label for the y-axis. Displayed beside the axis. ```tsx theme={null} axes: { y: { label: 'Sales (£000s)'; } } ``` ### Visibility **axes.y.isHidden** `boolean` Whether to hide the y-axis. When `true`, the axis line, ticks and labels are hidden. The axis label is also hidden. ```tsx theme={null} axes: { y: { isHidden: true; } } ``` ### Orientation **axes.y.isReversed** `boolean` Flip the y-axis position (left↔right). ```tsx theme={null} axes: { y: { isReversed: true; // Move y-axis to opposite side } } ``` ### Scale type **axes.y.scaleType** `'linear' | 'logarithmic'` (default: linear) Scale type for the y-axis: - `'linear'` - Standard linear scale with evenly spaced ticks - `'logarithmic'` - Logarithmic scale, useful for data spanning multiple orders of magnitude ```tsx theme={null} axes: { y: { scaleType: 'logarithmic'; // Better for data like: 1, 10, 100, 1000 } } ``` ### Range **axes.y.min** `number` Minimum value for the y-axis. If not set, automatically calculated from the data. Setting this to `0` is common to ensure bars start from zero. **axes.y.max** `number` Maximum value for the y-axis. If not set, automatically calculated from the data. ```tsx theme={null} axes: { y: { min: 0, // Start at zero max: 10000 // Cap at 10,000 } } ``` ### Tick display **axes.y.tickDisplayMode** `'auto' | 'edges'` (default: auto) How to display ticks on the y-axis: - `'auto'` - Display all ticks at appropriate intervals - `'edges'` - Display only the minimum and maximum values ```tsx theme={null} axes: { y: { tickDisplayMode: 'edges'; // Show only min and max } } ``` ## Secondary y-axis (y2) Combo charts can display two y-axes to accommodate series with different scales or units. ### Label **axes.y2.label** `string` Custom label for the secondary y-axis. Only used when dual y-axes are enabled. ```tsx theme={null} const config: GraphConfig = { type: 'combo', axes: { y: { label: 'Revenue (£)', }, y2: { label: 'Units sold', }, hasDualYAxis: true, }, }; ``` > [!NOTE] > > The secondary y-axis (y2) only supports the `label` property. Other axis options (min, max, scale) are not > configurable for y2. ### Dual y-axis mode **axes.hasDualYAxis** `boolean` (default: true) Whether to show two y-axes on a combo chart. When `true`, the primary y-axis appears on the left and the secondary on the right (or reversed if `isReversed` is set). ```tsx theme={null} const config: GraphConfig = { type: 'combo', axes: { hasDualYAxis: true, // Show both y-axes }, }; ``` When `hasDualYAxis` is `false`, all series share a single y-axis. ### Swapping y-axes For combo charts with dual y-axes, use `isReversed` to swap the positions: ```tsx theme={null} axes: { y: { label: 'Revenue (£)', isReversed: true // Move primary y-axis to the right }, y2: { label: 'Units sold' // This will now appear on the left }, hasDualYAxis: true } ``` ## Grid lines **axes.showGridLines** `boolean` Whether to show horizontal grid lines across the chart. Also controls the plot area's outer outline: hiding grid lines hides the outline too. ```tsx theme={null} axes: { showGridLines: true; // Show horizontal grid lines } ``` ## Complete example ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { /* ... */ }, axes: { x: { label: 'Date', isHidden: false, }, y: { label: 'Revenue (£)', min: 0, max: 50000, scaleType: 'linear', tickDisplayMode: 'auto', isHidden: false, isReversed: false, }, showGridLines: true, }, }; ``` ## Combo chart example ```tsx theme={null} const config: GraphConfig = { type: 'combo', data: { /* ... */ }, axes: { y: { label: 'Revenue (£)', min: 0, }, y2: { label: 'Units sold', }, hasDualYAxis: true, showGridLines: true, }, }; ``` # Content Source: https://docs.graphy.dev/sdk/config/content ## Overview The `content` property controls the title, subtitle, caption, data-source attribution, and the optional Made with Graphy provenance badge for your chart. ```tsx theme={null} const config: GraphConfig = { content: { title: 'Quarterly revenue', subtitle: 'FY 2024', caption: 'Excluding one-off adjustments', source: { label: 'Finance team', url: 'https://example.com/data', }, // Opt in to the Graphy provenance mark (hidden by default). isBrandMarkHidden: false, }, }; ``` ## Title **content.title** `string | JSONContent` Graph title. Can be plain text or TipTap JSON content for rich text formatting. ```tsx theme={null} // Plain text content: { title: 'Monthly sales performance' } // Rich text with formatting content: { title: { type: 'doc', content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Sales growth: ' }, { type: 'text', text: '+15%', marks: [{ type: 'bold' }] } ] } ] } } ``` ## Subtitle **content.subtitle** `string | JSONContent` Subtitle displayed under the title. Can be plain text or TipTap JSON content for rich text formatting. ```tsx theme={null} content: { subtitle: 'Across all regions'; } ``` ## Caption **content.caption** `string | JSONContent` Caption displayed at the bottom of the chart. Can be plain text or TipTap JSON content for rich text formatting. Use captions for notes, methodology or context about the data. ```tsx theme={null} // Plain text content: { caption: 'Data reflects confirmed transactions only' } // Rich text with link content: { caption: { type: 'doc', content: [ { type: 'paragraph', content: [ { type: 'text', text: 'Methodology: ' }, { type: 'text', text: 'See our data guide', marks: [ { type: 'link', attrs: { href: 'https://example.com/guide' } } ] } ] } ] } } ``` ## Data source **content.source** `object` Your data-source attribution shown under the caption (for example "Office for National Statistics"). This is **your** attribution of where the data came from — distinct from the Graphy provenance mark below. **source.label** `string` Text label describing the data source (e.g., "Office for National Statistics", "Internal sales data"). **source.url** `string` URL link to the original data source. Must be a valid URL. If provided, the `label` will be clickable. ```tsx theme={null} content: { source: { label: 'UK Office for National Statistics', url: 'https://www.ons.gov.uk' } } ``` ## Provenance badge ("Made with Graphy") A translucent capsule badge with the Graphy glyph and “Made with Graphy” text. Discovery signal (not a lock). Linked to `graphy.app`. Size ladder: full pill → circular mini under 200 px → hidden under 120 × 80. **content.brandMark** `\{ enabled?: boolean; placement?: 'footer' | 'header'; variant?: 'full' | 'mini' \}` Structured badge config. Prefer this over `isBrandMarkHidden`. Defaults to disabled at the low-level renderer; `@graphysdk/react` seeds it on. **content.isBrandMarkHidden** `boolean` Legacy opt-out (`true` = hide). Ignored when `brandMark.enabled` is set explicitly. ```tsx theme={null} // Show the badge (footer-right, full pill) content: { brandMark: { enabled: true }, } // Header top-right, circular mini content: { brandMark: { enabled: true, placement: 'header', variant: 'mini' }, } // Legacy opt-out content: { isBrandMarkHidden: true, } ``` > [!NOTE] > > `source` is **your** data attribution. `brandMark` controls Graphy's own provenance badge. They can > appear together (source left, badge right), either alone, or neither. ## Content visibility Use the visibility flags (`isTitleHidden`, `isSubtitleHidden`, `isCaptionHidden`, `isSourceHidden`, `isBrandMarkHidden`) to programmatically control which content appears: ```tsx theme={null} // Show only title and source; keep the Graphy mark hidden (default) content: { title: 'Revenue trends', subtitle: 'This will be hidden', isSubtitleHidden: true, caption: 'This will also be hidden', isCaptionHidden: true, source: { label: 'Finance department' }, } // Opt in to the provenance mark as well content: { title: 'Revenue trends', source: { label: 'Finance department' }, isBrandMarkHidden: false, } ``` > [!NOTE] > > Hiding content with visibility flags is different from omitting it entirely. Hidden content is still part of the > config but not rendered. This is useful when you want to preserve content but temporarily hide it. The provenance > mark has no separate text value — only the visibility flag. # Data labels Source: https://docs.graphy.dev/sdk/config/data-labels ## Overview Data labels display numeric values directly on chart elements like bars, columns, lines and pie slices. They make it easier to read exact values without relying on tooltips or axes. ```tsx theme={null} const config: GraphConfig = { dataLabels: { showDataLabels: true, dataLabelFormat: 'absolute', }, }; ``` ## Show data labels **dataLabels.showDataLabels** `boolean` (default: false) Whether to show numeric data labels on the chart. When `true`, values appear directly on bars, columns, line points and pie slices. ```tsx theme={null} const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'product', label: 'Product' }, { key: 'sales', label: 'Sales' }, ], rows: [ { product: 'A', sales: 100 }, { product: 'B', sales: 150 }, { product: 'C', sales: 120 }, ], }, dataLabels: { showDataLabels: true, // Show "100", "150", "120" on top of columns }, }; ``` > [!NOTE] > > Data labels respect the number formatting configured in `appearance.numberFormat`, including decimal places and > abbreviations. ## Label format **dataLabels.dataLabelFormat** `'absolute' | 'percentage'` (default: absolute) Format to use for the data labels: - `'absolute'` - Show the actual value from the data - `'percentage'` - Show values as percentages (calculated differently for each graph type) ### Absolute values Show the actual numeric values: ```tsx theme={null} dataLabels: { showDataLabels: true, dataLabelFormat: 'absolute' // Shows: 100, 150, 120 } ``` ### Percentage values Show values as percentages. The calculation method depends on the chart type: **For stacked charts:** * Shows each segment as a percentage of the total stack **For pie/donut charts:** * Shows each slice as a percentage of the total **For other charts:** * Shows each value as a percentage of the total sum ```tsx theme={null} const config: GraphConfig = { type: 'columnStacked', dataLabels: { showDataLabels: true, dataLabelFormat: 'percentage', // Shows each segment as % of total }, }; ``` ## Stack totals **dataLabels.showStackTotals** `boolean` (default: false) Whether to show total values above stacked bar or column stacks. Only applicable to stacked bar and column charts. ```tsx theme={null} const config: GraphConfig = { type: 'columnStacked', data: { columns: [ { key: 'quarter', label: 'Quarter' }, { key: 'online', label: 'Online' }, { key: 'retail', label: 'Retail' }, ], rows: [ { quarter: 'Q1', online: 100, retail: 50 }, { quarter: 'Q2', online: 120, retail: 60 }, { quarter: 'Q3', online: 140, retail: 55 }, ], }, dataLabels: { showStackTotals: true, // Shows "150", "180", "195" above each stack }, }; ``` > [!TIP] > > `showStackTotals` and `showDataLabels` can be used together. This shows both the individual segment values and the > total for each stack. ## Category labels **dataLabels.showCategoryLabels** `boolean` (default: false) Whether to show category names next to each pie / donut slice or each scatter / bubble point. On scatter and bubble charts, the category label replaces the value label when both are enabled. ```tsx theme={null} const config: GraphConfig = { type: 'pie', data: { columns: [ { key: 'department', label: 'Department' }, { key: 'budget', label: 'Budget' }, ], rows: [ { department: 'Marketing', budget: 50000 }, { department: 'Sales', budget: 75000 }, { department: 'Engineering', budget: 120000 }, ], }, dataLabels: { showCategoryLabels: true, // Shows "Marketing", "Sales", "Engineering" }, }; ``` On scatter and bubble charts, category labels identify each point: ```tsx theme={null} const config: GraphConfig = { type: 'scatter', data: { columns: [ { key: 'student', label: 'Student' }, { key: 'hours_studied', label: 'Hours studied' }, { key: 'exam_score', label: 'Exam score' }, ], rows: [ { student: 'Amara', hours_studied: 2, exam_score: 55 }, { student: 'Boris', hours_studied: 5, exam_score: 74 }, { student: 'Chen', hours_studied: 8, exam_score: 90 }, ], }, dataLabels: { showCategoryLabels: true, // Shows "Amara", "Boris", "Chen" next to each point }, }; ``` ## Combining data labels You can combine different data label options for comprehensive labelling: ### Stacked chart with totals and segment labels ```tsx theme={null} const config: GraphConfig = { type: 'barStacked', dataLabels: { showDataLabels: true, // Show values on each segment dataLabelFormat: 'absolute', // Show actual values (not percentages) showStackTotals: true, // Also show the total for each bar }, }; ``` ### Pie chart with categories and percentages ```tsx theme={null} const config: GraphConfig = { type: 'pie', dataLabels: { showDataLabels: true, // Show values on each slice dataLabelFormat: 'percentage', // Show as percentages showCategoryLabels: true, // Also show category names }, }; ``` ## Chart type compatibility Different data label options work with different chart types: | Option | Compatible chart types | | - | - | | `showDataLabels` | All chart types except table | | `dataLabelFormat: 'percentage'` | All chart types except table | | `showStackTotals` | `barStacked`, `barStackedFill`, `columnStacked`, `columnStackedFill` | | `showCategoryLabels` | `pie`, `donut`, `scatter`, `bubble` | ## Complete example ```tsx theme={null} const config: GraphConfig = { type: 'columnStacked', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'new', label: 'New customers' }, { key: 'returning', label: 'Returning customers' }, ], rows: [ { month: 'Jan', new: 45, returning: 120 }, { month: 'Feb', new: 52, returning: 135 }, { month: 'Mar', new: 48, returning: 142 }, ], }, dataLabels: { showDataLabels: true, // Show segment values dataLabelFormat: 'absolute', // Show actual numbers showStackTotals: true, // Show totals above stacks }, appearance: { numberFormat: { decimalPlaces: 0, // No decimals for data labels abbreviation: 'none', // Don't abbreviate }, }, }; ``` # Headline numbers Source: https://docs.graphy.dev/sdk/config/headline-numbers ## Overview Headline numbers display key summary metrics prominently at the top of your chart. They're ideal for highlighting the most important value in your data at a glance. ```tsx theme={null} const config: GraphConfig = { headlineNumbers: { show: 'total', compareWith: 'previous', size: 'large', }, }; ``` ## Display mode **headlineNumbers.show** `'current' | 'average' | 'total' | 'conversion' | 'none'` (default: none) How to calculate the headline number: - `'current'` - The last value in the series - `'average'` - The arithmetic mean of the series - `'total'` - The sum of all values in the series - `'conversion'` - The ratio between the first and last values (shown as a percentage) - `'none'` - Don't show any headline numbers ### Current value Show the most recent value in your data: ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'users', label: 'Users' }, ], rows: [ { month: 'Jan', users: 1000 }, { month: 'Feb', users: 1200 }, { month: 'Mar', users: 1450 }, ], }, headlineNumbers: { show: 'current', // Shows 1,450 (the March value) }, }; ``` **Best for:** Time series showing current state, latest measurements ### Average Show the mean of all values: ```tsx theme={null} headlineNumbers: { show: 'average'; // Shows the arithmetic mean } ``` **Best for:** Understanding typical values, smoothing out volatility ### Total Show the sum of all values: ```tsx theme={null} headlineNumbers: { show: 'total'; // Shows the sum } ``` **Best for:** Cumulative data, total revenue, aggregate counts ### Conversion rate Show the percentage change from first to last value: ```tsx theme={null} const config: GraphConfig = { type: 'funnel', data: { columns: [ { key: 'stage', label: 'Stage' }, { key: 'count', label: 'Count' }, ], rows: [ { stage: 'Visitors', count: 10000 }, { stage: 'Signups', count: 2500 }, { stage: 'Purchases', count: 500 }, ], }, headlineNumbers: { show: 'conversion', // Shows 5% (500/10000) }, }; ``` > [!NOTE] > > Conversion rate is designed for funnel charts and calculates the ratio between the first and last values as a > percentage. **Best for:** Funnel conversions, efficiency metrics ## Comparison **headlineNumbers.compareWith** `'previous' | 'first' | 'none'` (default: none) Show a trend arrow comparing the headline value to another value: - `'previous'` - Compare to the previous value in the series - `'first'` - Compare to the first value in the series - `'none'` - Don't show a comparison ### Compare with previous Show how the headline value has changed from the previous data point: ```tsx theme={null} const config: GraphConfig = { data: { columns: [ { key: 'quarter', label: 'Quarter' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { quarter: 'Q1', revenue: 100000 }, { quarter: 'Q2', revenue: 120000 }, { quarter: 'Q3', revenue: 135000 }, ], }, headlineNumbers: { show: 'current', // Show Q3 value (135,000) compareWith: 'previous', // Compare to Q2 (120,000) // Shows: 135,000 ↑ +12.5% }, }; ``` ### Compare with first Show how the headline value has changed from the first data point: ```tsx theme={null} headlineNumbers: { show: 'current', compareWith: 'first' // Compare current value to the very first value } ``` **Best for:** Growth since start, year-over-year changes ## Size **headlineNumbers.size** `'auto' | 'small' | 'medium' | 'large'` (default: auto) Size of the headline metric: - `'auto'` - Automatically choose the appropriate size based on chart dimensions - `'small'` - Small font size - `'medium'` - Medium font size - `'large'` - Large font size ```tsx theme={null} headlineNumbers: { show: 'total', size: 'large' // Extra prominent } ``` ## Multiple series When your chart has multiple data series, a headline number is shown for each series: ```tsx theme={null} const config: GraphConfig = { data: { columns: [ { key: 'month', label: 'Month' }, { key: 'productA', label: 'Product A' }, { key: 'productB', label: 'Product B' }, ], rows: [ { month: 'Jan', productA: 100, productB: 150 }, { month: 'Feb', productA: 120, productB: 160 }, ], }, headlineNumbers: { show: 'total', // Shows totals for both Product A and Product B }, }; ``` ## Complete example ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { columns: [ { key: 'week', label: 'Week' }, { key: 'signups', label: 'Signups' }, ], rows: [ { week: 'Week 1', signups: 45 }, { week: 'Week 2', signups: 52 }, { week: 'Week 3', signups: 48 }, { week: 'Week 4', signups: 61 }, ], }, content: { title: 'Weekly signups', }, headlineNumbers: { show: 'current', // Show current week (61) compareWith: 'previous', // Compare to last week (48) size: 'large', // Make it prominent }, // Display shows: "61 ↑ +27.1%" }; ``` # Legend Source: https://docs.graphy.dev/sdk/config/legend ## Overview The `legend` property controls where the legend appears and whether it's visible. The legend shows which color or pattern represents each data series. ```tsx theme={null} const config: GraphConfig = { legend: { position: 'right', }, }; ``` ## Position **legend.position** `'auto' | 'top' | 'right' | 'none'` (default: auto) Where to position the legend: - `'auto'` - Automatically determine the appropriate position based on chart size and type - `'top'` - Show the legend above the chart - `'right'` - Show the legend to the right of the chart - `'none'` - Hide the legend completely ```tsx theme={null} legend: { position: 'right'; } ``` > [!NOTE] > > Even when the legend is hidden with `position: 'none'`, series colors are still applied to the chart. The legend > position only affects visibility and placement, not the styling. ## Legend interaction When visible, the legend is interactive: * **Click** a legend item to toggle the series visibility (in supported contexts) ## Complete example ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'productA', label: 'Product A' }, { key: 'productB', label: 'Product B' }, { key: 'productC', label: 'Product C' }, ], rows: [ { month: 'Jan', productA: 100, productB: 150, productC: 120 }, { month: 'Feb', productA: 120, productB: 160, productC: 140 }, { month: 'Mar', productA: 140, productB: 180, productC: 130 }, ], }, legend: { position: 'right', // Show series labels on the right }, }; ``` # Reference lines Source: https://docs.graphy.dev/sdk/config/reference-lines ## Goal line A goal line displays a target value as a horizontal line across your chart. Use it to show targets, thresholds or benchmarks. **referenceLines.goalLine** `object` Goal line configuration. **goalLine.target** `number` *required* Goal target value along the y-axis. This is where the horizontal line will be drawn. **goalLine.marker** `string | number` Optional marker position on the x-axis. Use this to indicate when the goal applies or when it was set. **goalLine.label** `string` Custom label for the goal line. Defaults to "Goal" if not provided. ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: 'Jan', revenue: 850 }, { month: 'Feb', revenue: 920 }, { month: 'Mar', revenue: 1050 }, { month: 'Apr', revenue: 1100 }, ], }, referenceLines: { goalLine: { target: 1000, label: 'Monthly target', }, }, }; ``` ### Goal line with marker Add a marker to show when a goal was set or when it changes: ```tsx theme={null} referenceLines: { goalLine: { target: 1000, marker: 'Mar', // Mark the x-position where goal applies label: 'Q2 target' } } ``` ## Trendline A trendline shows the general direction and pattern in your data using statistical methods. Choose from multiple trendline types depending on your data characteristics. **referenceLines.trendline** `TrendlineType` The type of trendline to display. Each type fits a different mathematical model to your data. ### Trendline types **linear** `trendline` Straight line. Best for data that moves up or down at a steady pace. Use when: Your data shows consistent growth or decline **loess** `trendline` Smooth line that gently follows the overall pattern in the data. Good for bumpy or noisy trends. Use when: You want to see the general direction without being distracted by short-term volatility **exponential** `trendline` Line that bends upward or downward more and more over time. Good for growth rates or compounding changes. Use when: Your data is accelerating or decelerating exponentially **logarithmic** `trendline` Line that changes quickly at first, then levels off. Good for patterns with diminishing returns. Use when: Your data grows rapidly initially then plateaus **quadratic** `trendline` Gently curved line with one clear peak or valley. Good for data that changes direction once. Use when: Your data has a single turning point **power** `trendline` Curved line for scaling relationships where one value grows as a power of another (e.g., area vs radius). Use when: You're showing power law relationships **polynomial** `trendline` Flexible curved line that can have several peaks and valleys. Use when: Your data has multiple turning points ### Using trendlines ```tsx theme={null} const config: GraphConfig = { type: 'scatter', data: { /* ... */ }, referenceLines: { trendline: 'linear', // Add a linear trendline }, }; ``` ## Average line An average line shows the arithmetic mean of a data series as a horizontal line. Useful for identifying values that are above or below average. **referenceLines.averageLine** `object` Average line configuration. **averageLine.columnKey** `string` *required* Column key for the series to calculate the average from. Must match a column key from your data. ```tsx theme={null} const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'product', label: 'Product' }, { key: 'sales', label: 'Sales' }, ], rows: [ { product: 'A', sales: 100 }, { product: 'B', sales: 150 }, { product: 'C', sales: 120 }, { product: 'D', sales: 180 }, ], }, referenceLines: { averageLine: { columnKey: 'sales', // Show average of sales values (137.5) }, }, }; ``` # Type options Source: https://docs.graphy.dev/sdk/config/type-options ## Overview The `options` property contains chart type-specific settings. Different options are available depending on which chart type you're using. ```tsx theme={null} const config: GraphConfig = { type: 'line', options: { isSmoothLine: true, showPoints: true, }, }; ``` ## Line chart options ### Smooth lines **isSmoothLine** `boolean` (default: false) Whether to use smooth (curved) lines for line charts. When `true`, lines are drawn with curves. When `false`, lines are straight between data points. ```tsx theme={null} const config: GraphConfig = { type: 'line', options: { isSmoothLine: true, }, }; ``` ### Line thickness **lineThickness** `number | 'auto'` (default: auto) Line thickness in pixels. Set to `'auto'` to automatically determine the appropriate thickness based on chart size, or provide a specific pixel value. ```tsx theme={null} const config: GraphConfig = { type: 'line', options: { lineThickness: 3, // 3px thick lines }, }; ``` ### Show points **showPoints** `boolean` (default: false) Whether to show data point markers on the line. Useful for emphasising individual data points or when you have sparse data. ```tsx theme={null} const config: GraphConfig = { type: 'line', options: { showPoints: true, }, }; ``` ### Missing values **missingValues** `'gap' | 'connect' | 'zero'` (default: gap) How to handle missing values (nulls) in line charts: - `'gap'` - Break the line where values are missing - `'connect'` * Connect the line across missing values - `'zero'` - Treat missing values as zero ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'Jan', sales: 100 }, { month: 'Feb', sales: null }, // Missing data { month: 'Mar', sales: 150 }, ], }, options: { missingValues: 'connect', // Draw line across the gap }, }; ``` ## Bar and column chart options ### Sort bars **sortBars** `boolean` (default: false) Whether to sort bars in descending order by value. Only applies to categorical bar and column charts with a single series. ```tsx theme={null} const config: GraphConfig = { type: 'bar', options: { sortBars: true, // Sort from highest to lowest }, }; ``` ## Scatter plot options ### Point size **pointSize** `number | 'auto'` (default: auto) Custom point size for scatter plots in pixels. Set to `'auto'` to automatically determine the appropriate size, or provide a specific pixel value. ```tsx theme={null} const config: GraphConfig = { type: 'scatter', options: { pointSize: 8, // 8px diameter points }, }; ``` ## Combo chart options ### Combo type **comboType** `'grouped-bars' | 'stacked-bars' | 'lines'` (default: grouped-bars) Determines the type of combo plot to show: - `'grouped-bars'` - Display series as grouped bars alongside each other - `'stacked-bars'` - Stack series bars on top of each other - `'lines'` - Display series as lines ```tsx theme={null} const config: GraphConfig = { type: 'combo', options: { comboType: 'lines', // Show all series as lines }, }; ``` ## Pie and donut chart options ### Pie total position **pieTotalPosition** `'center' | 'outside'` (default: outside) Where to show the sum of all values in a pie or donut chart: - `'center'` - Show the total in the center of the chart (particularly useful for donut charts) - `'outside'` - Show the total outside the chart area ```tsx theme={null} const config: GraphConfig = { type: 'donut', options: { pieTotalPosition: 'center', // Show total in donut center }, }; ``` ## Combining options You can combine multiple options for the same chart type: ```tsx theme={null} const config: GraphConfig = { type: 'line', options: { isSmoothLine: true, showPoints: true, lineThickness: 2, missingValues: 'connect', }, }; ``` > [!NOTE] > > Options that don't apply to your current chart type are ignored. For example, setting `isSmoothLine` on a bar chart > has no effect. # Data structure Source: https://docs.graphy.dev/sdk/core/data-structure ## Basic structure Data is structured as a table with explicitly defined columns and rows: ```tsx theme={null} const config: GraphConfig = { data: { columns: [ { key: 'category', label: 'Category' }, { key: 'value', label: 'Value' }, ], rows: [ { category: 'A', value: 100 }, { category: 'B', value: 200 }, { category: 'C', value: 150 }, ], }, }; ``` The `columns` array defines your data structure, while `rows` contains the actual values. Each row object must have keys matching the column keys. ## Automatic column mapping Graphy automatically assigns your data columns to x-axis and y-axis, and creates series based on the detected value formats. **Default mapping** for most graph types (bar, column, line, area): * The first text or date column becomes the **x-axis** * All numeric columns become the **y-axis**, with each numeric column creating a separate **series** ```tsx theme={null} const config: GraphConfig = { data: { columns: [ { key: 'month', label: 'Month' }, // date column → x-axis { key: 'revenue', label: 'Revenue' }, // numeric → series 1 { key: 'expenses', label: 'Expenses' }, // numeric → series 2 ], rows: [ { month: 'January 2026', revenue: 5000, expenses: 3200 }, { month: 'February 2026', revenue: 6200, expenses: 3800 }, { month: 'March 2026', revenue: 5800, expenses: 3500 }, ], }, }; ``` **Graph-specific behaviour:** | Graph type | X-axis | Y-axis / values | | - | - | - | | Bar, column, line, area | First text or date column | All numeric columns (one series each) | | Combo | First text or date column | All numeric columns (last defaults to line) | | Funnel, waterfall | First text column | First numeric column | **Pie and donut** use the first text column for slices and the first numeric column for values. Additional columns are ignored. **Scatter** requires at least 2 numeric columns: the first becomes the x-axis and the remaining become y-axis series. If a text column is present, it is used as a label for each point. **Bubble** requires at least 3 numeric columns: the first is the x-axis, the second is the y-axis and the third controls the bubble size. Like scatter, the first text column is used as a label. If your data contains only numeric columns, the first numeric column is used as the x-axis and the rest become series. ## Value format detection Graphy automatically detects the format of values in each column. The first match wins: ### Numbers Numbers and numeric strings are detected as quantitative values. Thousands separators, decimals and magnitude suffixes are supported: ```tsx theme={null} { category: 'A', value: 100 } { category: 'B', value: '1,250.50' } { category: 'C', value: '2.5m' } // k, m, b, t suffixes supported ``` ### Dates Date-like strings are automatically parsed. Graphy recognises a wide range of formats, including ISO dates, named months and locale-specific orderings: ```tsx theme={null} { date: '2024-01-15', value: 100 } // YYYY-MM-DD { date: 'January 2024', value: 200 } // month name + year { date: '15/01/2024', value: 150 } // DD/MM/YYYY (when data._metadata.parsingLocale is en-GB) { date: '01/15/2024', value: 150 } // MM/DD/YYYY (en-US, the default locale) { date: '2024-01-15T12:30:00Z', value: 300 } // ISO datetime { date: 'Q1 2024', value: 400 } // quarter ``` Short month names (`Jan`, `Feb`) are recognised alongside full names. Set `data._metadata.parsingLocale` to control whether ambiguous dates like `01/02/2024` are interpreted as MM/DD (en-US) or DD/MM (en-GB). **Weekly date ranges** are also detected — values like `1 Feb – 7 Feb` or `1 Feb 2024 – 7 Feb 2024` representing 7-day spans. ### Percentages Values with a `%` suffix are detected as percentages: ```tsx theme={null} { category: 'Desktop', share: '64.5%' } { category: 'Mobile', share: '28.3%' } ``` ### Currencies Currency-formatted strings are parsed with automatic symbol recognition. Supported symbols: `$`, `€`, `£`, `¥`, `₹`, `₱`, `₩`, `₪`, `₫`, `₽`, `฿`, `₦`, `₺`, `zł`, `kr`, `Fr`, `R`, `R$`, `Rp`, `RM`, `د.إ`, `﷼`, `Ch$`, `NT$`, `HK$`, `S$`, `A$`, `C$`, `NZ$`, `MX$`. ```tsx theme={null} { product: 'A', price: '$1,250' } { product: 'B', price: '£2,450.50' } { product: 'C', price: '€3,000' } ``` Symbols can appear as prefix or suffix, and negative values are supported (`-$100`, `$-100`). ### Text Any value that doesn't match the above formats is treated as text (categorical data). ## Schema **data.columns** `Column[]` *required* Array of column definitions. Each column defines the structure and metadata for a data field. **columns\[].key** `string` *required* Unique, stable identifier for the column. Must match the keys used in `rows` objects. **columns\[].label** `string` Human-readable label displayed in the UI (axis labels, legends, tooltips). Defaults to the `key` if not provided. **columns\[].\_metadata** `object` Internal column metadata. Managed automatically by the editor. **properties** **isHidden** `boolean` Whether the column is hidden from visualization. **aggregation** `'count' | 'distinct' | 'max' | 'min' | 'sum' | 'mean' | 'mode' | 'median'` Aggregation function applied to this column. **data.rows** `Row[]` *required* Array of data rows. Each row is an object with keys matching the column keys. Values can be strings, numbers or null. **data.\_metadata** `object` Optional metadata for advanced data processing. **properties** **parsingLocale** `'en-GB' | 'en-US'` Locale for parsing dates and numbers. Defaults to `'en-US'`. * `'en-US'`: MM/DD/YYYY date format * `'en-GB'`: DD/MM/YYYY date format **isDataTransposed** `boolean` Whether the data is transposed. Used by the data table component. **isAggregated** `boolean` Whether aggregation is active. Managed automatically by the editor. **groupByTimeUnit** `'year' | 'quarter' | 'month' | 'week' | 'day'` Time unit for grouping time-based data. **sortBy** `object` Sort configuration. **properties** **columnKey** `string` *required* Column key to sort by. **direction** `'asc' | 'desc'` *required* Sort direction. **rollingDateFilter** `object` Rolling date filter configuration. **properties** **timeUnit** `'year' | 'quarter' | 'month' | 'week' | 'day'` *required* Time unit for the filter. **value** `number` *required* Number of time units (e.g., last 30 days: `{ timeUnit: 'day', value: 30 }`). > [!NOTE] > > Row object keys must exactly match the `key` values in your columns array. Data with keys that don't have a > corresponding column definition will be ignored. # Quickstart Source: https://docs.graphy.dev/sdk/core/quickstart ### 1. Install the Graphy SDK ```shell npm theme={null} npm install @graphysdk/core ``` ```shell yarn theme={null} yarn add @graphysdk/core react react-dom styled-components @tiptap/core @tiptap/extension-blockquote @tiptap/extension-bold @tiptap/extension-document @tiptap/extension-hard-break @tiptap/extension-heading @tiptap/extension-italic @tiptap/extension-link @tiptap/extension-list @tiptap/extension-paragraph @tiptap/extension-strike @tiptap/extension-text @tiptap/extension-text-align @tiptap/extension-text-style @tiptap/extension-underline @tiptap/extensions @tiptap/pm @tiptap/react ``` ```shell pnpm theme={null} pnpm add @graphysdk/core ``` ```shell bun theme={null} bun add @graphysdk/core ``` Because `@graphysdk/core` is a private package, you'll need to configure your npm auth token. Create an `.npmrc` in your repository root (or user-level) with: ```ini .npmrc theme={null} //registry.npmjs.org/:_authToken=${NPM_TOKEN} @graphysdk:registry=https://registry.npmjs.org/ ``` ### 2. Create your first graph There are two main components required to render a Graphy chart: **`GraphProvider`** is the state manager for Graphy charts and can be placed anywhere in the React tree. It provides state to the charting components as well as any components used to edit these charts. **`Graph`** is the component used to render the graph itself. ```tsx theme={null} import { GraphProvider, Graph } from '@graphysdk/core'; export function App() { const config: GraphConfig = { type: 'column', // Graph type (column, bar, line, pie, etc.) data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'January', sales: 1000 }, { month: 'February', sales: 1200 }, { month: 'March', sales: 1450 }, { month: 'April', sales: 1500 }, ], }, }; return ( ); } ``` Graphy will automatically select sensible defaults for any properties that are not provided. See the [graph types reference](/sdk/graph-types/index) for all available chart types. ### 3. Customize your graph You have fine-grained control over every aspect of your chart. Here are some common customizations: ```tsx theme={null} const config: GraphConfig = { data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'January', sales: 1000 }, { month: 'February', sales: 1200 }, { month: 'March', sales: 1450 }, { month: 'April', sales: 1500 }, ], }, type: 'line', options: { isSmoothLine: true, showPoints: true, }, axes: { y: { label: 'Revenue (£)', min: 0, }, }, appearance: { numberFormat: { abbreviation: 'auto', decimalPlaces: 0, }, }, content: { title: 'Monthly sales', subtitle: 'Q1 2024', source: { label: 'Internal sales data', url: 'https://example.com/data', }, }, }; ``` ### 4. Make it editable By default, graphs are read-only. Use `mode="editor"` on the `Graph` component to allow users to edit titles, add annotations and make other modifications. Use the `onChange` handler on `GraphProvider` to persist these changes: ```tsx theme={null} function MyChart() { const [config, setConfig] = useState({ ... }); return ( setConfig((current) => ({ ...current, ...update }))} > ); } ``` The `mode` prop has three options: * `readonly` (default) - Full interactivity with tooltips and hover effects, but no editing * `editor` - Same as readonly, plus the ability to edit the graph * `static` - No interactivity, useful for image exports ### Next steps * Learn about the [data structure](/sdk/core/data-structure) and how columns and rows work * Explore all available [graph types](/sdk/graph-types/index) * Discover [configuration options](/sdk/config/type-options) to customize your charts * See all [GraphProvider props](/sdk/reference/graph-provider) and [Graph props](/sdk/reference/graph) * Read the complete [schema reference](/sdk/reference/graph-config) # CSS hooks Source: https://docs.graphy.dev/sdk/customisation/css-hooks Every part of a chart you might want to style (bars, lines, grid lines, axes, the legend) has a fixed classname, and useful context like the chart type and the series index is exposed as data attributes. ARIA attributes (`role`, `aria-label`, `aria-roledescription`) remain in the DOM for accessibility, but they are not a styling API; selectors should use the hooks below. ```css theme={null} .graphy-bar { rx: 4; } .graphy-bar[data-graphy-series-index='1'] { fill: red; } ``` ## Parts Each part carries a classname; the data attributes present on a part are listed in the [next section](#data-attributes). | Classname | Element | What it is | | - | - | - | | `.graphy-chart` | `div` | Figure root (`role="figure"`) | | `.graphy-bar` | `rect` | A bar, in bar/column/combo/waterfall/funnel/mekko charts | | `.graphy-line` | `path` | A line | | `.graphy-area` | `path` | The area fill under a line | | `.graphy-pie-slice` | `path` | A pie or donut slice | | `.graphy-point` | `circle` | A point: dots on lines, scatter and bubble marks | | `.graphy-tile` | `rect` | A heatmap cell | | `.graphy-grid-line` | `line` | Grid line | | `.graphy-tick-line` | `line` | Axis tick mark | | `.graphy-origin-line` | `line` | Zero/origin line | | `.graphy-plot-outline` | `rect` | Plot area outline; fill it to color the area behind the data. Not rendered when grid lines are disabled (`axes.showGridLines: false`) | | `.graphy-axis` | `g` | Axis group (ticks and label) | | `.graphy-axis-label` | `text` | Axis title | | `.graphy-tick-label` | `text` | Axis tick label | | `.graphy-legend` | `div` | Legend container (`role="list"`) | | `.graphy-legend-item` | `div` | Legend item | | `.graphy-legend-swatch` | `div` | Color swatch wrapper inside an item | | `.graphy-legend-label` | `div` | Label text inside an item | | `.graphy-data-label` | `div` | Value label on a bar or slice | | `.graphy-stack-total` | `div` | Total label above a stacked bar | | `.graphy-series-label` | `text` | Inline series name next to a line | | `.graphy-headline` | `div` | Headline metrics row | | `.graphy-headline-item` | `div` | One headline metric | | `.graphy-tooltip` | `div` | Tooltip box (hover and pinned) | | `.graphy-tooltip-heading` | `div` | Tooltip heading | | `.graphy-tooltip-item` | `div` | Row inside a tooltip | | `.graphy-tooltip-swatch` | `div` | Color swatch inside a tooltip row | | `.graphy-tooltip-label` | `div`/`span` | Label text inside a tooltip | | `.graphy-tooltip-value` | `div` | Value text inside a tooltip | | `.graphy-tooltip-caption` | `div` | Tooltip caption | | `.graphy-tooltip-footer` | `div`/`span` | Tooltip footer | > [!WARNING] > > Tooltips render outside `.graphy-chart`, so don't nest tooltip selectors under a chart selector. Write them at > the top level, and note they apply to every chart on the page. ## Data attributes | Attribute | On | Values | | - | - | - | | `data-graphy-chart-type` | `.graphy-chart` | `line`, `areaStacked`, `bar`, `barStacked`, `barStackedFill`, `column`, `columnStacked`, `columnStackedFill`, `combo`, `pie`, `donut`, `funnel`, `heatmap`, `scatter`, `bubble`, `waterfall`, `table`, `mekko` | | `data-graphy-orientation` | `.graphy-chart` | `horizontal` (bar charts) or `vertical`; absent for `pie`, `donut`, `funnel`, `heatmap`, `table` | | `data-graphy-series-index` | bars, lines, areas, slices, points, legend items | 0-based series position, see below | | `data-graphy-axis-side` | axes, axis labels, tick labels, grid lines, tick lines | `top`, `bottom`, `left`, `right` | | `data-graphy-hidden` | `.graphy-legend-item` | present (value `true`) only while the series is toggled off in the legend | | `data-graphy-theme` | a wrapper element around `.graphy-chart` | `light` or `dark`; use it to scope rules to the chart's color scheme, e.g. `[data-graphy-theme='dark'] .graphy-grid-line` | Prefer `data-graphy-orientation` over enumerating bar types when a rule only cares about direction. On grid lines, the axis side tells you the line's direction: `left`/`right` grid lines run horizontally, `top`/`bottom` ones vertically. ## Series identity `data-graphy-series-index` is the 0-based position of the series in **data order**: the order series first appear in the data, honoring an explicit series order when one is configured. The same value is stamped on a series' shapes (bars, lines, areas, slices, points) and on its legend item, so match them **by attribute, not by visual position**: * Combo charts move line series to the end of the legend visually; the attribute still matches the shapes. * Hiding a series does not shift the other series' indices. * Hover highlighting reorders line z-position without touching the attribute. ```css theme={null} /* The first series (index 0) is the hero: a thicker line with a soft glow… */ .graphy-line[data-graphy-series-index='0'] { stroke-width: 3.5; filter: drop-shadow(0 2px 6px rgb(37 99 235 / 0.4)); } /* …with its legend entry bolded to match */ .graphy-legend-item[data-graphy-series-index='0'] .graphy-legend-label { font-weight: 700; } /* The second series (index 1) reads as a projection: dashed and lighter */ .graphy-line[data-graphy-series-index='1'] { stroke-dasharray: 6 4; stroke-linecap: round; opacity: 0.7; } ``` ## Examples ```css theme={null} /* Dashed grid lines, horizontal ones only */ .graphy-grid-line[data-graphy-axis-side='left'] { stroke: #dcd7fe; stroke-dasharray: 2 4; stroke-linecap: round; } /* Fade legend items for series that are toggled off */ .graphy-legend-item[data-graphy-hidden] { opacity: 0.4; text-decoration: line-through; } /* Rounder corners on horizontal bar charts only */ .graphy-chart[data-graphy-orientation='horizontal'] .graphy-bar { rx: 6; } /* Outlined bars */ .graphy-bar { stroke: #1a1523; stroke-width: 1.5; } /* Tint the plot area and hide its outline */ .graphy-plot-outline { fill: #faf9ff; stroke: none; } /* Emphasize the bottom axis labels */ .graphy-tick-label[data-graphy-axis-side='bottom'] { font-weight: 600; text-transform: uppercase; } /* Rounded, borderless tooltips (top-level rule, see the warning above) */ .graphy-tooltip { border-radius: 12px; border: none; box-shadow: 0 8px 24px rgb(0 0 0 / 0.16); } ``` ## Text size Chart text is sized in `em`, relative to a base controlled by [`appearance.textScale`](/sdk/config/appearance#text-sizes-in-pixels). Your CSS can pin exact pixel sizes per element: ```css theme={null} .graphy-tick-label, .graphy-legend-label, .graphy-data-label { font-size: 23px !important; } ``` Use `!important` so your rule wins over the chart's own styles. > [!WARNING] > > The chart reserves layout space for labels based on `textScale`. It doesn't know about sizes set in CSS. If labels > look clipped or overlap, adjust `appearance.textScale` until the layout fits your CSS sizes. ## Programmatic access If you generate CSS or selectors in code, import the names instead of hardcoding strings. `GRAPHY_PART_CLASSNAMES` maps part keys to classnames, `GRAPHY_DATA_ATTRIBUTES` maps value keys to attribute names, and `getChartOrientation(chartType)` returns the same `'horizontal' | 'vertical' | undefined` the chart stamps as `data-graphy-orientation`. That's useful when you know a chart's type and want to branch on its orientation without touching the DOM. ```ts theme={null} import { GRAPHY_PART_CLASSNAMES, GRAPHY_DATA_ATTRIBUTES, getChartOrientation } from '@graphysdk/core'; GRAPHY_PART_CLASSNAMES.bar; // 'graphy-bar' GRAPHY_DATA_ATTRIBUTES.seriesIndex; // 'data-graphy-series-index' const firstSeriesBars = `.${GRAPHY_PART_CLASSNAMES.bar}[${GRAPHY_DATA_ATTRIBUTES.seriesIndex}='0']`; // '.graphy-bar[data-graphy-series-index='0']' getChartOrientation('barStacked'); // 'horizontal' getChartOrientation('column'); // 'vertical' getChartOrientation('pie'); // undefined (orientation doesn't apply) ``` # Custom appearance Source: https://docs.graphy.dev/sdk/customisation/custom-appearance Graphy is designed to look good out of the box, but we know that a charting library embedded in your product needs to feel like it belongs there. This page explains the layers of customisation available and when to reach for each one. ## Ways to customise We think about customisation in three tiers, each offering progressively more control: 1. [**Themes**](#themes) — the right tool for the majority of cases. Adjust colours, typography, and spacing through a structured token system without touching any markup. 2. [**Config**](#config) — targeted appearance and behaviour overrides for specific chart types or elements. 3. [**Custom components**](#custom-components) — replace Graphy's internal UI components with your own when you need a completely native feel. ## Themes The `theme` prop on `GraphProvider` is the primary way to make Graphy match your brand. Themes use a layered token system: **base tokens** define raw values, **semantic tokens** map those values to purpose, and **element tokens** target specific components. The SDK includes two built-in themes: `graphyLightTheme` (default) and `graphyDarkTheme` which can be extended to match your preffered style. Override a semantic token to update many elements at once: ```tsx theme={null} import { graphyDarkTheme } from '@graphylib/core'; ; ``` Override an element token when you need surgical control over a single component without affecting anything else: ```tsx theme={null} values: { tooltipBackground: '#1A1A2E', tooltipHeadingTextColor: '#FFFFFF', } ``` Individual properties can also be overriden on `GraphConfig`. This can be useful in situations where you want to enable customisation on a per chart basis. With this approach you can simply store the whole `GraphConfig` object after a change, and restore it later without needing to reconstruct the entire theme each time. ```tsx theme={null} const config: GraphConfig = { data: { ... }, themeOverrides: { graphBackground: '#1e293b', textPrimary: '#f8fafc', gridLineColor: '#334155', }, }; ``` For a full reference of available tokens see the [`GraphTheme`](/sdk/reference/graph-theme) API reference. ## Config The `GraphConfig` object is the primary way to control the appearance and behaviour of individual charts. Where themes set the visual language across your entire application, config is chart-specific. Config covers a broad surface area. Some of the key properties to be aware of are: **`axes`**, **`legend`**, and **`options`** handle the structural and chart type specific configuration such as axis labels, tick formatting, line smoothing, bar sorting, legend position, and similar. **`appearance`** controls visual properties that persist across chart types such as series colours, border style, number formatting and tooltip styles. **`content`** controls the content and visiblility of additional elements around a chart, such as the title, subtitle and caption. **`themeOverrides`** allows specific theme tokens to be overridden at the chart level, for cases where a single chart needs to depart from the global theme (a different background colour, for instance) without requiring a separate theme definition. For a complete reference of all available properties and their types, see the [`GraphConfig`](/sdk/reference/graph-config) API reference. ## Custom components When themes and props aren't enough, you can replace individual internal components with your own implementations. ### How it works Graphy defines a typed interface for each overridable component. You implement a component that satisfies that interface, then register it at the provider level. **1. Graphy's component interface** ```tsx theme={null} export interface SwitchComponentProps { isChecked?: boolean; isDisabled?: boolean; isInvalid?: boolean; onCheckedChange?: (checked: boolean) => void; ref?: AnyRef; } export type SwitchComponent = React.ComponentType; ``` **2. Your custom implementation** ```tsx theme={null} import type { SwitchComponent } from '@graphysdk/core'; import Switch from '@mui/material/Switch'; // An example implementation using Material UI const CustomSwitch: SwitchComponent = (props) => { return ( props.onCheckedChange?.(event.target.checked)} /> ); }; ``` **3. Register at the provider level** ```tsx theme={null} ``` For a full list of components that can be customised see the [`EditorComponentRegistry`](/sdk/reference/editor-component-registry) API reference. # Graph borders Source: https://docs.graphy.dev/sdk/customisation/graph-borders The border properties in `appearance.border` allow you to customize the style, width and color of borders around your graphs. You can create subtle strokes, bold frames, gradient effects or completely custom border styles. ## Quick start Borders are controlled by three properties that work together: ```tsx theme={null} import type { GraphConfig } from '@graphysdk/core'; const config: GraphConfig = { appearance: { border: { width: 12, // Width in pixels style: 'gradient', // Visual style color: '#6366f1', // Color or preset name }, }, }; ``` ## Border width Control the thickness of your border using `width`. Set to `0` to remove the border completely. ```tsx theme={null} appearance: { border: { width: 0; // No border } } appearance: { border: { width: 12; // 12px border } } appearance: { border: { width: 1; // 1px thin border } } ``` **appearance.border.width** `number` (default: 0) Border width in pixels (0-64). Set to `0` for no border. ## Border styles Use `style` to control how your border appears. Each style treats `color` differently, helping frame your data without stealing the spotlight: **appearance.border.style** `'none' | 'custom' | 'tinted' | 'gradient' | 'preset' | 'gray'` (default: none) Border style: - `'none'` - No border - `'custom'` - Apply the custom hex color from `color` as-is - `'tinted'` - Tint the `color` based on the color scheme (darkens in dark mode, lightens in light mode) - `'gradient'` - Generate a gradient starting from `color`, adjusted based on the color scheme - `'preset'` - Use a predefined border style - `'gray'` - Legacy grey border style ### Tinted A color border that's subtly tinted based on your chosen color and the graph's theme: ```tsx theme={null} appearance: { border: { width: 12, style: 'tinted', color: '#3b82f6' // Automatically tinted for contrast } } ``` ### Gradient Automatically generates a linear gradient based on your chosen color and the graph's theme: ```tsx theme={null} appearance: { border: { width: 12, style: 'gradient', color: '#8b5cf6' // Creates theme-adjusted gradient } } ``` ### Grey A neutral border that adapts to light and dark themes. Perfect for subtle definition without drawing attention: ```tsx theme={null} appearance: { border: { width: 1, style: 'gray' // color not required } } ``` ### Preset Use one of twelve predefined gradient styles. Set `color` to one of these preset names: ```tsx theme={null} appearance: { border: { width: 12, style: 'preset', color: 'sunset' // or any preset name below } } ``` **Available presets:** * `'lilac'` - Soft purple to pink * `'neon_pink'` - Vibrant pink gradient * `'blackberry'` - Purple to light blue * `'sun'` - Yellow to orange * `'iceland'` - Teal gradient * `'sunset'` - Yellow to pink * `'ultraviolet'` - Purple gradient * `'purple'` - Rich purple tones * `'ice_cream'` - Pink to peach * `'mint'` - Green to cyan * `'cool'` - Blue to cyan * `'fresh'` - Purple to cyan ### Custom Use your exact color with no adjustments. Unlike `tinted` and `gradient`, `custom` applies your color as-is with no theme-based tinting: ```tsx theme={null} appearance: { border: { width: 12, style: 'custom', color: '#ff6b6b' // Exact color used as-is } } ``` ## Border colors **appearance.border.color** `string` The color to use for the border. Accepts: - **Hex colors**: `"#3b82f6"`, `"#ff6b6b"` - **Preset names** (when `style: 'preset'`): `"lilac"`, `"sunset"`, `"ultraviolet"`, etc. - **Series references**: `"series1"`, `"series2"` (uses color from series styles) The color may be adjusted based on theme and `style` for optimal visual appearance. ### Hex colors Standard hex color values work with `tinted`, `gradient` and `custom` styles: ```tsx theme={null} appearance: { border: { style: 'tinted', color: '#3b82f6' } } ``` ### Preset names When using `style: 'preset'`, reference one of the predefined gradients: ```tsx theme={null} appearance: { border: { style: 'preset', color: 'sunset' } } ``` ### Series references Match your border to a series color by referencing the series key: ```tsx theme={null} const config: GraphConfig = { appearance: { border: { width: 12, style: 'tinted', color: 'series1', }, seriesStyles: { series1: { customColor: '#10b981' }, }, }, }; ``` ## Rounded corners **appearance.hasRoundedCorners** `boolean` (default: true) Whether to round the corners of the border. Ignored if `border.width` is 0. ```tsx theme={null} appearance: { border: { width: 12, style: 'gradient', color: '#6366f1' }, hasRoundedCorners: true // Rounded corners } ``` # Graph fonts Source: https://docs.graphy.dev/sdk/customisation/graph-fonts Customize the font families in your graphs ## Setup ### 1. Load your fonts Ensure your fonts are loaded via `@font-face` or a web font service (e.g. Google Fonts). ### 2. Register fonts with GraphProvider ```tsx theme={null} {/* ... */} ``` ### 3. Apply fonts in config ```tsx theme={null} ``` ## Reference ### fontList Array of font definitions passed to `GraphProvider`. **id** `string` *required* Unique identifier referenced by `fontId` in the config. **label** `string` *required* Display name shown in the font picker UI. **fontFamily** `string` *required* CSS font-family value with fallbacks (e.g. `'Helvetica Neue', Arial, sans-serif`). ### appearance.textStyle **heading** `object` Font and color for titles and subtitles. Accepts `fontId` and `color`. **body** `object` Font and color for axis labels, legends, captions, and annotations. Accepts `fontId` and `color`. # Series colors Source: https://docs.graphy.dev/sdk/customisation/series-colors By default, series are colored using a built-in palette. There are two ways to customize series colors. ## Custom palettes Register palettes with `GraphProvider` and reference them by ID: ```tsx theme={null} const palettes = [ { id: 'brand', name: 'Brand Colors', colors: [ { id: 'blue', hex: '#1e40af' }, { id: 'green', hex: '#059669' }, { id: 'amber', hex: '#d97706' }, ], }, ]; ; ``` Palettes appear in the Editor's color picker, allowing users to switch between them. If a chart has more series than palette colors, colors cycle from the beginning. ## Series style overrides Override individual series via `appearance.seriesStyles` using keys `series1` through `series20`: ```tsx theme={null} appearance: { seriesStyles: { series1: { paletteColorId: 'blue' }, // Reference a palette color series2: { customColor: '#ef4444' }, // Or use a custom hex color series3: { customColor: '#10b981', fillStyle: 'hatched', // 'solid' | 'hatched' lineStyle: 'dashed', // 'solid' | 'dashed' | 'dotted' }, }, } ``` You can combine both approaches: use a palette as the base and override specific series. ## Waterfall chart colors Waterfall charts have special series keys for their four bar types: | Key | Description | | - | - | | `waterfallStart` | Starting value bar | | `waterfallPositive` | Positive change bars | | `waterfallNegative` | Negative change bars | | `waterfallTotal` | Final total bar | When using a custom palette without overrides, colors are assigned in order: 1st color → start, 2nd → positive, 3rd → negative, 4th → total. To customize waterfall colors explicitly: ```tsx theme={null} appearance: { seriesStyles: { waterfallStart: { customColor: '#6b7280' }, waterfallPositive: { customColor: '#10b981' }, waterfallNegative: { customColor: '#ef4444' }, waterfallTotal: { customColor: '#3b82f6' }, }, } ``` # Data table Source: https://docs.graphy.dev/sdk/editor/data-table The `DataTable` component provides a spreadsheet-like interface for viewing and editing the data in your graph. It automatically connects to `GraphProvider` and syncs all changes with your graph configuration. ## Basic usage Simply render the `DataTable` component inside a `GraphProvider`: ```tsx theme={null} import type { GraphConfig } from '@graphysdk/core'; import { GraphProvider } from '@graphysdk/core'; import { DataTable } from '@graphysdk/editor'; import { useState } from 'react'; function MyEditor() { const [config, setConfig] = useState({ data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'Jan', sales: 1000 }, { month: 'Feb', sales: 1200 }, { month: 'Mar', sales: 1450 }, ], }, }); return ( setConfig({ ...config, ...update })}>
); } ``` ## Props The `DataTable` component accepts optional configuration props: | Prop | Type | Default | Description | | - | - | - | - | | `minCols` | `number` | `26` | The minimum number of columns | | `minRows` | `number` | `50` | The minimum number of rows | | `additionalEmptyCols` | `number` | `1` | The number of empty columns to maintain at the end of the table | | `additionalEmptyRows` | `number` | `1` | The number of empty rows to maintain at the end of the table | | `readOnly` | `boolean` | `false` | Makes the table read only | | `inert` | `boolean` | `false` | Makes the table inert (no focus, clicks or hover events) | ## Features * **Responsive** – the table resizes to fit the container * **Cell editing** – click, press Enter or start typing to edit * **Range selection** – select multiple cells with mouse or Shift + arrows * **Keyboard navigation** – arrow keys, Tab, Enter, Escape * **Clipboard** – copy (Cmd/Ctrl+C), paste (Cmd/Ctrl+V), cut (Cmd/Ctrl+X) * **Undo/redo** – Cmd/Ctrl+Z to undo, Cmd/Ctrl+Shift+Z to redo * **Delete** – Backspace or Delete to clear cell values * **State sync** – changes automatically update the graph config ## Integration with editor components The `DataTable` works seamlessly with other editor components. You can combine it with editor panels and other controls: ```tsx theme={null} setConfig({ ...config, ...update })}>
``` ## Low-level primitive For more control, you can use `DataTablePrimitive` which doesn't connect to `GraphProvider` and requires manual state management. This is useful when you need a standalone table outside of the chart editing context. # Editor panels Source: https://docs.graphy.dev/sdk/editor/editor-panels Easy-to-use panels to change how your graph looks and works ## Overview All editor panels must be rendered inside `EditorProvider` and `GraphProvider` to function correctly: ```tsx theme={null} import { GraphProvider, graphyLightTheme } from '@graphysdk/core'; import { EditorProvider, GraphPanel } from '@graphysdk/editor'; import { useState } from 'react'; function MyEditor() { const [config, setConfig] = useState(initialConfig); return ( setConfig({ ...config, ...update })}> {/* ... or other panels ... */} ); } ``` ## Size panel Controls for setting graph size using presets or custom values. ```tsx theme={null} import { SizePanel } from '@graphysdk/editor'; ; ``` ### Props | Prop | Type | Default | Description | | - | - | - | - | | `defaultExpanded` | `string` | – | The title of the section to expand by default | ### Sections * **`SizePresetsSection`** – Predefined size options (e.g., social media formats) * **`CustomSizeSection`** – Custom width and height inputs ### Custom composition ```tsx theme={null} import { SizePanel, SizePresetsSection, CustomSizeSection } from '@graphysdk/editor'; ; ``` ## Graph panel Core graph configuration including type, legend and number formatting. ```tsx theme={null} import { GraphPanel } from '@graphysdk/editor'; ; ``` ### Props | Prop | Type | Default | Description | | - | - | - | - | | `defaultExpanded` | `string` | `"Graph type"` | The title of the section to expand by default | ### Sections * **`GraphTypeSection`** – Choose chart type (column, bar, line, pie, etc.) * **`GraphOptionsSection`** – Chart-specific options * **`LegendPositionSection`** – Control legend visibility and position * **`HeadlineNumberSection`** – Configure headline number display * **`NumberFormatSection`** – Number formatting options (abbreviation, decimals) ### Custom composition ```tsx theme={null} import { GraphPanel, GraphTypeSection, GraphOptionsSection, LegendPositionSection, HeadlineNumberSection, NumberFormatSection, } from '@graphysdk/editor'; ; ``` ## Axes panel Customize the main and cross axes. ```tsx theme={null} import { AxesPanel } from '@graphysdk/editor'; ; ``` ### Props | Prop | Type | Default | Description | | - | - | - | - | | `defaultExpanded` | `string` | – | The title of the section to expand by default | ### Sections * **`MainAxisSection`** – Configure the primary axis (usually y-axis) * **`CrossAxisSection`** – Configure the secondary axis (usually x-axis) ### Custom composition ```tsx theme={null} import { AxesPanel, MainAxisSection, CrossAxisSection } from '@graphysdk/editor'; ; ``` ## Color panel Manage graph colors, backgrounds and border styles. ```tsx theme={null} import { ColorPanel } from '@graphysdk/editor'; ; ``` ### Props | Prop | Type | Default | Description | | - | - | - | - | | `defaultExpanded` | `string` | `"Palette"` | The title of the section to expand by default | ### Sections * **`ThemeSection`** – Select from dark or light mode * **`PaletteSection`** – Pick a graph color palette * **`ChartBackgroundSection`** – Set graph background color * **`ChartBorderSection`** – Set graph border style * **`HighlightColorSection`** – Set highlight color ### Custom composition ```tsx theme={null} import { ColorPanel, ThemeSection, PaletteSection, ChartBackgroundSection, ChartBorderSection, HighlightColorSection, } from '@graphysdk/editor'; ; ``` ## Elements panel Control visibility and styling of chart text elements. ```tsx theme={null} import { ElementsPanel } from '@graphysdk/editor'; ; ``` ### Props | Prop | Type | Default | Description | | - | - | - | - | | `defaultExpanded` | `string` | – | The title of the section to expand by default | ### Sections * **`TextVisibilitySection`** – Toggle visibility of titles, labels and annotations * **`SourceSection`** – Add and edit data source attribution * **`TextSizeSection`** – Adjust font sizes for graph elements ### Custom composition ```tsx theme={null} import { ElementsPanel, TextVisibilitySection, SourceSection, TextSizeSection } from '@graphysdk/editor'; ; ``` ## Annotate panel Add annotations like call-outs and highlights to draw attention to specific data points. ```tsx theme={null} import { AnnotatePanel } from '@graphysdk/editor'; ; ``` ### Sections * **`CallOutSection`** – Text annotations, arrows, boxes and difference arrows * **`HighlightSection`** – Highlight specific data points, lines or groups of data points ### Custom composition ```tsx theme={null} import { AnnotatePanel, CallOutSection, HighlightSection } from '@graphysdk/editor'; ; ``` ### Props #### AnnotatePanel | Prop | Type | Default | Description | | - | - | - | - | | `defaultExpanded` | `string` | – | The title of the section to expand by default | | `closePanel` | `() => void` | – | Optional callback invoked when panel should close | | `callOutProps` | `CallOutSectionProps` | – | Optional props to pass to the CallOutSection | #### CallOutSection The `CallOutSection` accepts an optional `hiddenButtons` prop to hide specific buttons: | Prop | Type | Description | | - | - | - | | `hiddenButtons` | `CallOutButton[]` | Array of button names to hide. Options: `'text'`, `'arrow'`, `'shape'`, `'differenceArrows'` | ```tsx theme={null} import { AnnotatePanel } from '@graphysdk/editor'; // Hide the shape and difference arrows buttons ; ``` Or use `CallOutSection` directly: ```tsx theme={null} import { CallOutSection } from '@graphysdk/editor'; ; ``` ## Power-ups panel Add goals, trend lines and average lines. ```tsx theme={null} import { PowerUpPanel } from '@graphysdk/editor'; ; ``` ### Props | Prop | Type | Default | Description | | - | - | - | - | | `defaultExpanded` | `string` | – | The title of the section to expand by default | ### Sections * **`GoalPowerUpSection`** – Add goal lines to set targets * **`TrendPowerUpSection`** – Add trend lines to show the trend of a series * **`AveragePowerUpSection`** – Add average lines to show the average value of a series ### Custom composition ```tsx theme={null} import { PowerUpPanel, GoalPowerUpSection, TrendPowerUpSection, AveragePowerUpSection } from '@graphysdk/editor'; ; ``` ## Panel composition All panels support custom composition by passing children. This allows you to: * Reorder sections within a panel * Include only specific sections * Add custom controls alongside built-in sections ```tsx theme={null} import { GraphPanel, GraphTypeSection, NumberFormatSection } from '@graphysdk/editor'; // Custom "Graph" panel with only graph type and number formatting sections {/* GraphOptionsSection, LegendPositionSection and HeadlineNumberSection are excluded */} ; ``` ## Building a custom panel You can build completely custom panels and sections using the `EditorPanel` layout primitives. ### Custom panel structure A custom panel is built using `EditorPanel.Root` and `EditorPanel.Section`: ```tsx theme={null} import { EditorPanel } from '@graphysdk/editor'; export const CustomPanel = () => { return ( {/* import existing sections */} {/* or build your own custom sections from scratch */} {/* controls go here */} ); }; ``` ### EditorPanel.Root props | Prop | Type | Description | | - | - | - | | `defaultExpanded` | `string` | The title of the section to expand by default. If not provided, all collapsible sections start collapsed. | ```tsx theme={null} // Expand "Number format" section by default ``` ### Creating custom sections Sections can have three layouts: `fixed`, `collapsible` or `inline`: ```tsx theme={null} import { EditorPanel } from "@graphysdk/editor"; // Fixed section (always expanded) {/* Controls go here */} // Collapsible section (can be expanded/collapsed) {/* Controls go here */} // Inline section (controls shown inline with title) {/* Controls go here */} ``` # Graph type icon Source: https://docs.graphy.dev/sdk/editor/graph-type-icon The `GraphTypeIcon` component renders SVG icons representing different graph types. ## Basic usage ```tsx theme={null} import { GraphTypeIcon } from '@graphysdk/editor'; ; ``` ## Props | Prop | Description | | - | - | | `type` | The graph type to display (required) | | `className` | Optional CSS class name for styling | ## Supported icons The component supports all graph types: * `column` – Column graph * `columnStacked` – Stacked column graph * `columnStackedFill` – 100% stacked column graph * `bar` – Bar graph * `barStacked` – Stacked bar graph * `barStackedFill` – 100% stacked bar graph * `line` – Line graph * `areaStacked` – Stacked area graph * `combo` – Combo graph * `pie` – Pie graph * `donut` – Donut graph * `funnel` – Funnel * `heatmap` – Heatmap * `table` – Table * `scatter` – Scatter plot * `bubble` – Bubble plot * `mekko` – Mekko * `waterfall` – Waterfall ## Styling Icons inherit the `currentColor` for their fill, making them easy to theme. You can also pass a `className` for custom styling: ```tsx theme={null}
``` # Editor quick start Source: https://docs.graphy.dev/sdk/editor/index The Graphy Editor provides ready-to-use UI components that let users customize graphs visually. It includes panels for editing data, styling, annotations and more. ## Installation Install the editor package alongside the core SDK: ```bash npm theme={null} npm install @graphysdk/core @graphysdk/editor ``` ```bash yarn theme={null} yarn add @graphysdk/core @graphysdk/editor react react-dom styled-components @tiptap/core @tiptap/extension-blockquote @tiptap/extension-bold @tiptap/extension-document @tiptap/extension-hard-break @tiptap/extension-heading @tiptap/extension-italic @tiptap/extension-link @tiptap/extension-list @tiptap/extension-paragraph @tiptap/extension-strike @tiptap/extension-text @tiptap/extension-text-align @tiptap/extension-text-style @tiptap/extension-underline @tiptap/extensions @tiptap/pm @tiptap/react ``` ```bash pnpm theme={null} pnpm add @graphysdk/core @graphysdk/editor ``` ```bash bun theme={null} bun add @graphysdk/core @graphysdk/editor ``` ## Basic setup The editor requires two providers: `GraphProvider` from `@graphysdk/core` and `EditorProvider` from `@graphysdk/editor`. ```tsx theme={null} import { useState } from 'react'; import { GraphProvider, Graph } from '@graphysdk/core'; import { EditorProvider, GraphPanel } from '@graphysdk/editor'; function MyEditor() { const [config, setConfig] = useState({ data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'Jan', sales: 1000 }, { month: 'Feb', sales: 1200 }, { month: 'Mar', sales: 1100 }, ], }, type: 'column', }); return ( { setConfig(currentValues); }} >
{/* The graph - use mode="editor" for inline editing */} {/* Editor panel */}
); } ``` ## How it works 1. **GraphProvider** wraps your entire editor and receives the `config` and `onChange` handler 2. **EditorProvider** provides context for all editor components 3. **Graph** with `mode="editor"` renders an editable chart (inline title editing, annotations etc.) 4. **Editor panels** (like `GraphPanel`) let users modify the config When users interact with editor panels, the `onChange` callback receives the updated config, which you can save to your state or database. ## Available editor panels The editor includes several pre-built panels: * **[GraphPanel](/sdk/editor/editor-panels#graph-panel)** - Chart type, legend, headline numbers, number formatting * **[AxesPanel](/sdk/editor/editor-panels#axes-panel)** - Axis labels, ranges, tick marks * **[ColorPanel](/sdk/editor/editor-panels#color-panel)** - Themes, color palettes, borders * **[ElementsPanel](/sdk/editor/editor-panels#elements-panel)** - Text visibility, source attribution, text size * **[AnnotatePanel](/sdk/editor/editor-panels#annotate-panel)** - Annotations, highlights, call-outs * **[PowerUpPanel](/sdk/editor/editor-panels#power-ups-panel)** - Goal lines, trend lines, average lines * **[SizePanel](/sdk/editor/editor-panels#size-panel)** - Graph dimensions and presets ## Complete example Here's a more complete editor with multiple panels: ```tsx theme={null} import { useState } from 'react'; import { GraphProvider, Graph } from '@graphysdk/core'; import { EditorProvider, GraphPanel, AxesPanel, ColorPanel, DataTable } from '@graphysdk/editor'; function MyEditor() { const [config, setConfig] = useState({ data: { columns: [ { key: 'category', label: 'Category' }, { key: 'value', label: 'Value' }, ], rows: [ { category: 'A', value: 100 }, { category: 'B', value: 150 }, { category: 'C', value: 120 }, ], }, type: 'column', content: { title: 'Sales by category', }, }); return ( { setConfig(currentValues); }} >
{/* Main content area */}
{/* Editor sidebar */}
); } ``` ## Next steps * See all [GraphProvider props](/sdk/reference/graph-provider) and [EditorProvider props](/sdk/reference/editor-provider) * Explore [editor panels](/sdk/editor/editor-panels) to learn about all available components * Use the [DataTable](/sdk/editor/data-table) component to let users edit data * Learn about [custom panel composition](/sdk/editor/editor-panels#panel-composition) to build custom editors # Stacked area Source: https://docs.graphy.dev/sdk/graph-types/area-stacked Stacked area charts display multiple series as filled areas stacked on top of each other, showing both individual series values and the cumulative total. ## When to use * **Part-to-whole over time** - Show how components contribute to a total * **Cumulative values** - Display running totals * **Multiple series composition** - Compare proportions across time periods ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'areaStacked', data: { columns: [ { key: 'quarter', label: 'Quarter' }, { key: 'product_a', label: 'Product A' }, { key: 'product_b', label: 'Product B' }, { key: 'product_c', label: 'Product C' }, ], rows: [ { quarter: 'Q1', product_a: 30, product_b: 20, product_c: 15 }, { quarter: 'Q2', product_a: 35, product_b: 25, product_c: 20 }, { quarter: 'Q3', product_a: 40, product_b: 30, product_c: 25 }, { quarter: 'Q4', product_a: 45, product_b: 35, product_c: 30 }, ], }, }; ``` ## Related * [Type options](/sdk/config/type-options) - Line chart options also apply * [Legend](/sdk/config/legend) - Show series labels * [Data labels](/sdk/config/data-labels) - Display values on areas # Bar Source: https://docs.graphy.dev/sdk/graph-types/bar Bar charts display data as horizontal bars. They support three variants: grouped, stacked and 100% stacked. ## When to use * **Comparing categories** - Show differences between discrete groups * **Ranking data** - Display ordered comparisons * **Long category names** - Horizontal orientation accommodates longer labels * **Large number of categories** - Easier to read than vertical columns * **Part-to-whole comparisons** - Use stacked variants to show composition ## Grouped bars Display multiple series side-by-side for easy comparison. ```tsx theme={null} const config: GraphConfig = { type: 'bar', data: { columns: [ { key: 'region', label: 'Region' }, { key: 'q1', label: 'Q1' }, { key: 'q2', label: 'Q2' }, ], rows: [ { region: 'North', q1: 150, q2: 180 }, { region: 'South', q1: 120, q2: 145 }, { region: 'East', q1: 200, q2: 220 }, { region: 'West', q1: 175, q2: 190 }, ], }, }; ``` ### Single series ```tsx theme={null} const config: GraphConfig = { type: 'bar', data: { columns: [ { key: 'country', label: 'Country' }, { key: 'population', label: 'Population' }, ], rows: [ { country: 'China', population: 1412 }, { country: 'India', population: 1408 }, { country: 'United States', population: 333 }, { country: 'Indonesia', population: 275 }, { country: 'Pakistan', population: 231 }, ], }, }; ``` ### Sorted bars Sort bars by value in descending order (single-series only): ```tsx theme={null} const config: GraphConfig = { type: 'bar', options: { sortBars: true, // Sort from highest to lowest }, data: { /* ... */ }, }; ``` ## Stacked bars Stack multiple series end-to-end to show both individual values and cumulative totals. ```tsx theme={null} const config: GraphConfig = { type: 'barStacked', data: { columns: [ { key: 'department', label: 'Department' }, { key: 'full_time', label: 'Full time' }, { key: 'part_time', label: 'Part time' }, { key: 'contract', label: 'Contract' }, ], rows: [ { department: 'Engineering', full_time: 45, part_time: 5, contract: 10 }, { department: 'Sales', full_time: 30, part_time: 15, contract: 5 }, { department: 'Marketing', full_time: 20, part_time: 8, contract: 12 }, { department: 'Support', full_time: 25, part_time: 20, contract: 5 }, ], }, }; ``` ### Stack totals Show the total value at the end of each stacked bar: ```tsx theme={null} const config: GraphConfig = { type: 'barStacked', dataLabels: { showStackTotals: true, }, data: { /* ... */ }, }; ``` ## 100 percent stacked bars Normalise each bar to 100% to show relative proportions rather than absolute values. ```tsx theme={null} const config: GraphConfig = { type: 'barStackedFill', data: { columns: [ { key: 'product', label: 'Product' }, { key: 'online', label: 'Online' }, { key: 'retail', label: 'Retail' }, { key: 'wholesale', label: 'Wholesale' }, ], rows: [ { product: 'Product A', online: 5000, retail: 3000, wholesale: 2000 }, { product: 'Product B', online: 1500, retail: 2500, wholesale: 1000 }, { product: 'Product C', online: 4000, retail: 2000, wholesale: 4000 }, ], }, }; ``` ### Show percentages Display percentage values instead of absolute values: ```tsx theme={null} const config: GraphConfig = { type: 'barStackedFill', dataLabels: { showDataLabels: true, dataLabelFormat: 'percentage', }, data: { /* ... */ }, }; ``` ## Options **options.sortBars** `boolean` (default: false) Sort bars in descending order by value. Only applies to single-series grouped bar charts (`type: 'bar'`). ## Related * [Column](/sdk/graph-types/column) - Vertical bars * [Type options](/sdk/config/type-options) - Bar chart options * [Data labels](/sdk/config/data-labels) - Display values and totals * [Axes](/sdk/config/axes) - Customize axes # Bubble Source: https://docs.graphy.dev/sdk/graph-types/bubble Bubble charts are scatter plots where point size represents a third dimension, allowing you to compare three variables simultaneously. ## When to use * **Three-variable comparisons** - Show relationships between three metrics * **Weighted scatter plots** - Size indicates importance or magnitude * **Market analysis** - Compare products across multiple dimensions * **Portfolio visualization** - Display risk, return and size ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'bubble', data: { columns: [ { key: 'product', label: 'Product' }, { key: 'marketing_spend', label: 'Marketing spend (£)' }, { key: 'revenue', label: 'Revenue (£)' }, { key: 'profit', label: 'Profit (£)' }, ], rows: [ { product: 'Product A', marketing_spend: 50000, revenue: 200000, profit: 75000 }, { product: 'Product B', marketing_spend: 30000, revenue: 150000, profit: 60000 }, { product: 'Product C', marketing_spend: 80000, revenue: 350000, profit: 120000 }, { product: 'Product D', marketing_spend: 40000, revenue: 180000, profit: 50000 }, ], }, }; ``` ## Data structure Bubble charts require four columns: 1. **Category column** - Labels for each bubble (optional but recommended) 2. **X-axis column** - First numeric variable (horizontal position) 3. **Y-axis column** - Second numeric variable (vertical position) 4. **Size column** - Third numeric variable (bubble size) ## Multiple series Group bubbles by adding more numeric columns: ```tsx theme={null} const config: GraphConfig = { type: 'bubble', data: { columns: [ { key: 'country', label: 'Country' }, { key: 'gdp', label: 'GDP per capita' }, { key: 'life_expectancy', label: 'Life expectancy' }, { key: 'population_europe', label: 'Europe' }, { key: 'population_asia', label: 'Asia' }, ], rows: [ // Each series (Europe, Asia) creates separate bubble groups ], }, }; ``` ## Point labels Label each bubble with its row's label column using `dataLabels.showCategoryLabels`: ```tsx theme={null} const config: GraphConfig = { type: 'bubble', dataLabels: { showCategoryLabels: true, // Shows "Product A", "Product B", ... next to each bubble }, data: { /* ... */ }, }; ``` When both `showCategoryLabels` and `showDataLabels` are enabled, the category label takes precedence. ## Related * [Scatter](/sdk/graph-types/scatter) - Standard scatter plot * [Type options](/sdk/config/type-options) - Bubble chart options # Column Source: https://docs.graphy.dev/sdk/graph-types/column Column charts display data as vertical bars. They support three variants: grouped, stacked and 100% stacked. ## When to use * **Comparing values across categories** - Show differences between discrete groups * **Time-based comparisons** - Track changes over time periods * **Small to moderate number of categories** - Works best with 3-12 categories * **Multiple series comparison** - Compare several series side-by-side * **Part-to-whole relationships** - Use stacked variants to show composition ## Grouped columns Display multiple series side-by-side for easy comparison. ```tsx theme={null} const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, { key: 'costs', label: 'Costs' }, ], rows: [ { month: 'Q1', revenue: 150000, costs: 95000 }, { month: 'Q2', revenue: 180000, costs: 105000 }, { month: 'Q3', revenue: 210000, costs: 115000 }, { month: 'Q4', revenue: 240000, costs: 125000 }, ], }, }; ``` ### Single series ```tsx theme={null} const config: GraphConfig = { type: 'column', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'Jan', sales: 12000 }, { month: 'Feb', sales: 15000 }, { month: 'Mar', sales: 18000 }, { month: 'Apr', sales: 16000 }, { month: 'May', sales: 21000 }, { month: 'Jun', sales: 24000 }, ], }, }; ``` ### Sorted columns Sort columns by value in descending order (single-series only): ```tsx theme={null} const config: GraphConfig = { type: 'column', options: { sortBars: true, // Sort from highest to lowest }, data: { /* ... */ }, }; ``` ## Stacked columns Stack multiple series on top of each other to show both individual values and cumulative totals. ```tsx theme={null} const config: GraphConfig = { type: 'columnStacked', data: { columns: [ { key: 'quarter', label: 'Quarter' }, { key: 'desktop', label: 'Desktop' }, { key: 'mobile', label: 'Mobile' }, { key: 'tablet', label: 'Tablet' }, ], rows: [ { quarter: 'Q1', desktop: 45000, mobile: 28000, tablet: 12000 }, { quarter: 'Q2', desktop: 48000, mobile: 35000, tablet: 15000 }, { quarter: 'Q3', desktop: 50000, mobile: 42000, tablet: 18000 }, { quarter: 'Q4', desktop: 52000, mobile: 48000, tablet: 20000 }, ], }, }; ``` ### Stack totals Show the total value at the top of each stacked column: ```tsx theme={null} const config: GraphConfig = { type: 'columnStacked', dataLabels: { showStackTotals: true, }, data: { /* ... */ }, }; ``` ## 100 percent stacked columns Normalise each column to 100% to show relative proportions rather than absolute values. ```tsx theme={null} const config: GraphConfig = { type: 'columnStackedFill', data: { columns: [ { key: 'year', label: 'Year' }, { key: 'renewable', label: 'Renewable' }, { key: 'natural_gas', label: 'Natural gas' }, { key: 'coal', label: 'Coal' }, ], rows: [ { year: '2020', renewable: 200, natural_gas: 500, coal: 300 }, { year: '2021', renewable: 250, natural_gas: 480, coal: 270 }, { year: '2022', renewable: 320, natural_gas: 450, coal: 230 }, { year: '2023', renewable: 400, natural_gas: 420, coal: 180 }, ], }, }; ``` ### Show percentages Display percentage values instead of absolute values: ```tsx theme={null} const config: GraphConfig = { type: 'columnStackedFill', dataLabels: { showDataLabels: true, dataLabelFormat: 'percentage', }, data: { /* ... */ }, }; ``` ## Options **options.sortBars** `boolean` (default: false) Sort columns in descending order by value. Only applies to single-series grouped column charts (`type: 'column'`). ## Related * [Bar](/sdk/graph-types/bar) - Horizontal bars * [Type options](/sdk/config/type-options) - Column chart options * [Data labels](/sdk/config/data-labels) - Display values and totals * [Axes](/sdk/config/axes) - Customize axes # Combo Source: https://docs.graphy.dev/sdk/graph-types/combo Combo charts combine multiple chart types with two y-axes, allowing you to compare metrics with different units or scales. ## When to use * **Different units** - Compare metrics measured in different units (e.g., revenue in £ vs conversion rate in %) * **Different scales** - Display values with vastly different magnitudes * **Correlation analysis** - Show relationships between two different metrics ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'combo', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue (£)' }, { key: 'conversion', label: 'Conversion rate (%)' }, ], rows: [ { month: 'Jan', revenue: 50000, conversion: 2.5 }, { month: 'Feb', revenue: 55000, conversion: 2.8 }, { month: 'Mar', revenue: 62000, conversion: 3.1 }, { month: 'Apr', revenue: 58000, conversion: 2.9 }, { month: 'May', revenue: 68000, conversion: 3.4 }, ], }, }; ``` ## Dual y-axes By default, combo charts show two y-axes: * **Left axis** (primary) - First numeric series * **Right axis** (secondary) - Second numeric series ### Configuring axes ```tsx theme={null} const config: GraphConfig = { type: 'combo', axes: { y: { label: 'Revenue (£)', }, y2: { label: 'Conversion rate (%)', }, }, data: { /* ... */ }, }; ``` ### Single y-axis Disable the dual y-axis mode if your series share the same scale: ```tsx theme={null} const config: GraphConfig = { type: 'combo', axes: { hasDualYAxis: false, }, data: { /* ... */ }, }; ``` ## Options **options.comboType** `'grouped-bars' | 'stacked-bars' | 'lines'` (default: grouped-bars) Determines how to display the combo chart: - `'grouped-bars'` - Show series as grouped bars side-by-side - `'stacked-bars'` - Stack bars on top of each other - `'lines'` - Display all series as lines ### Display as lines ```tsx theme={null} const config: GraphConfig = { type: 'combo', options: { comboType: 'lines', }, data: { /* ... */ }, }; ``` ## Related * [Type options](/sdk/config/type-options) - Combo chart options * [Axes](/sdk/config/axes) - Configure dual y-axes * [Line](/sdk/graph-types/line) - Line chart options apply # Donut Source: https://docs.graphy.dev/sdk/graph-types/donut Donut charts are pie charts with a hollow center, ideal for displaying a central metric alongside part-to-whole relationships. ## When to use * **Part-to-whole with central metric** - Show breakdown with total or key value in center * **2-6 categories** - Works best with a small number of slices * **Percentage comparison** - Emphasise relative proportions * **Single data series** - Show one breakdown at a time ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'donut', data: { columns: [ { key: 'category', label: 'Category' }, { key: 'sales', label: 'Sales' }, ], rows: [ { category: 'Electronics', sales: 45000 }, { category: 'Clothing', sales: 32000 }, { category: 'Home & Garden', sales: 28000 }, { category: 'Sports', sales: 18000 }, { category: 'Books', sales: 12000 }, ], }, }; ``` ## Data labels Show category labels and percentages on slices: ```tsx theme={null} const config: GraphConfig = { type: 'donut', dataLabels: { showCategoryLabels: true, showDataLabels: true, dataLabelFormat: 'percentage', }, data: { /* ... */ }, }; ``` ## Related * [Pie](/sdk/graph-types/pie) - Standard pie chart * [Type options](/sdk/config/type-options) - Donut chart options * [Data labels](/sdk/config/data-labels) - Display values and percentages # Funnel Source: https://docs.graphy.dev/sdk/graph-types/funnel Funnel charts display progressively decreasing values across stages, ideal for visualising conversion processes and multi-stage workflows. ## When to use * **Conversion funnels** - Track user journey from awareness to conversion * **Process stages** - Show drop-off at each step * **Progressive reduction** - Display sequential filtering or narrowing ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'funnel', data: { columns: [ { key: 'stage', label: 'Stage' }, { key: 'users', label: 'Users' }, ], rows: [ { stage: 'Website visits', users: 10000 }, { stage: 'Product views', users: 5000 }, { stage: 'Add to cart', users: 2000 }, { stage: 'Checkout', users: 800 }, { stage: 'Purchase', users: 500 }, ], }, }; ``` ## Conversion rate Show the conversion rate between the first and last stages as a headline number: ```tsx theme={null} const config: GraphConfig = { type: 'funnel', headlineNumbers: { show: 'conversion', }, data: { /* ... */ }, }; ``` > [!NOTE] > > The `'conversion'` headline number type is specifically designed for funnel charts. It calculates the ratio between > the first and last values. ## Stage data The funnel automatically calculates drop-off between stages. Your data should include: * **First column**: Stage names (categorical) * **Second column**: Values (numeric), ordered from highest to lowest ## Related * [Headline numbers](/sdk/config/headline-numbers) - Show conversion rate * [Data labels](/sdk/config/data-labels) - Display stage values # Heatmap Source: https://docs.graphy.dev/sdk/graph-types/heatmap Heatmaps display data as a grid of colored cells where color intensity represents values, making patterns and correlations easy to spot. ## When to use * **Patterns across two dimensions** - Show relationships between two categorical variables * **Correlation matrices** - Display strength of relationships * **Time-based patterns** - Reveal trends across days, weeks or months * **Large datasets** - Summarise many data points visually ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'heatmap', data: { columns: [ { key: 'day', label: 'Day' }, { key: 'hour_0', label: '00:00' }, { key: 'hour_6', label: '06:00' }, { key: 'hour_12', label: '12:00' }, { key: 'hour_18', label: '18:00' }, ], rows: [ { day: 'Monday', hour_0: 45, hour_6: 120, hour_12: 350, hour_18: 280 }, { day: 'Tuesday', hour_0: 52, hour_6: 135, hour_12: 380, hour_18: 295 }, { day: 'Wednesday', hour_0: 48, hour_6: 140, hour_12: 420, hour_18: 310 }, { day: 'Thursday', hour_0: 55, hour_6: 145, hour_12: 390, hour_18: 285 }, { day: 'Friday', hour_0: 60, hour_6: 150, hour_12: 450, hour_18: 380 }, ], }, }; ``` ## Data structure Heatmaps require: * **First column**: Row labels (categorical) * **Remaining columns**: Numeric values for each cell Each data column becomes a column in the heatmap grid. ## Data labels Show values in each cell: ```tsx theme={null} const config: GraphConfig = { type: 'heatmap', dataLabels: { showDataLabels: true, }, data: { /* ... */ }, }; ``` ## Related * [Appearance](/sdk/config/appearance) - Customize color palette * [Data labels](/sdk/config/data-labels) - Display cell values # Overview Source: https://docs.graphy.dev/sdk/graph-types/index Set the `type` property on your `GraphConfig`: ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { /* ... */ }, }; ``` ## All types | Type | Description | Best for | | - | - | - | | `line` | Line chart | Time series, trends | | `areaStacked` | Stacked area chart | Part-to-whole over time | | `bar` | Horizontal bar chart | Comparing categories, ranking | | `barStacked` | Stacked horizontal bars | Composition across categories | | `barStackedFill` | 100% stacked horizontal bars | Percentage breakdowns | | `column` | Vertical column chart | Comparisons, time series | | `columnStacked` | Stacked columns | Composition over time | | `columnStackedFill` | 100% stacked columns | Proportions over time | | `combo` | Dual y-axis chart | Different units/scales | | `pie` | Pie chart | Simple part-to-whole (2-6 slices) | | `donut` | Donut chart | Part-to-whole with central metric | | `funnel` | Funnel chart | Conversion funnels, stages | | `heatmap` | Heatmap | Patterns across two dimensions | | `scatter` | Scatter plot | Correlation, distribution | | `bubble` | Bubble chart | Three-variable comparisons | | `waterfall` | Waterfall chart | Financial analysis, variance | | `mekko` | Mekko chart | Market share analysis | | `table` | Data table | Precise values | ## TypeScript ```tsx theme={null} type Type = | 'line' | 'areaStacked' | 'bar' | 'barStacked' | 'barStackedFill' | 'column' | 'columnStacked' | 'columnStackedFill' | 'combo' | 'pie' | 'donut' | 'funnel' | 'heatmap' | 'scatter' | 'bubble' | 'waterfall' | 'mekko' | 'table'; ``` See [type options](/sdk/config/type-options) for chart-specific configuration. # Line Source: https://docs.graphy.dev/sdk/graph-types/line Line charts display data as a series of points connected by straight or curved lines. They're ideal for showing trends and changes over time. ## When to use * **Time series data** - Track changes over time periods * **Trends** - Show upward or downward patterns * **Continuous measurements** - Display data with no gaps * **Multiple series comparison** - Compare several trends on the same chart ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: 'Jan', revenue: 12000 }, { month: 'Feb', revenue: 15000 }, { month: 'Mar', revenue: 18000 }, { month: 'Apr', revenue: 17000 }, { month: 'May', revenue: 21000 }, ], }, }; ``` ## Multiple series ```tsx theme={null} const config: GraphConfig = { type: 'line', data: { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, { key: 'profit', label: 'Profit' }, ], rows: [ { month: 'Jan', revenue: 12000, profit: 3000 }, { month: 'Feb', revenue: 15000, profit: 4500 }, { month: 'Mar', revenue: 18000, profit: 5400 }, ], }, }; ``` ## Related * [Type options](/sdk/config/type-options) - All line chart options * [Axes](/sdk/config/axes) - Customize axes * [Reference lines](/sdk/config/reference-lines) - Add trendlines and goal lines # Mekko Source: https://docs.graphy.dev/sdk/graph-types/mekko Mekko charts (also called marimekko or market map charts) display stacked bars with variable widths, showing both size and composition simultaneously. ## When to use * **Market share analysis** - Show market size and segment composition * **Portfolio breakdown** - Display size and composition of groups * **Two-dimensional part-to-whole** - Compare both totals and proportions * **Strategic positioning** - Visualise competitive landscape ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'mekko', data: { columns: [ { key: 'region', label: 'Region' }, { key: 'product_a', label: 'Product A' }, { key: 'product_b', label: 'Product B' }, { key: 'product_c', label: 'Product C' }, ], rows: [ { region: 'North America', product_a: 120, product_b: 80, product_c: 50 }, { region: 'Europe', product_a: 90, product_b: 110, product_c: 40 }, { region: 'Asia Pacific', product_a: 200, product_b: 150, product_c: 100 }, { region: 'Latin America', product_a: 40, product_b: 30, product_c: 20 }, ], }, }; ``` ## Data structure Mekko charts require: * **First column**: Categories (becomes variable-width bars) * **Remaining columns**: Numeric values (stacked segments within each bar) ## Related * [Bar](/sdk/graph-types/bar) - Fixed-width stacked bars * [Column](/sdk/graph-types/column) - Percentage composition # Pie Source: https://docs.graphy.dev/sdk/graph-types/pie Pie charts display data as slices of a circle, showing parts of a whole as proportions. ## When to use * **Simple part-to-whole relationships** - Show how categories contribute to a total * **2-6 categories** - Works best with a small number of slices * **Percentage comparison** - Emphasise relative proportions * **Single data series** - Show one breakdown at a time ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'pie', data: { columns: [ { key: 'browser', label: 'Browser' }, { key: 'users', label: 'Users' }, ], rows: [ { browser: 'Chrome', users: 12500 }, { browser: 'Safari', users: 8200 }, { browser: 'Firefox', users: 3400 }, { browser: 'Edge', users: 2100 }, { browser: 'Other', users: 1800 }, ], }, }; ``` ## Data labels Show category labels and percentages on slices: ```tsx theme={null} const config: GraphConfig = { type: 'pie', dataLabels: { showCategoryLabels: true, showDataLabels: true, dataLabelFormat: 'percentage', }, data: { /* ... */ }, }; ``` ## Related * [Donut](/sdk/graph-types/donut) - Donut chart with hollow center * [Type options](/sdk/config/type-options) - Pie chart options * [Data labels](/sdk/config/data-labels) - Display values and percentages # Scatter Source: https://docs.graphy.dev/sdk/graph-types/scatter Scatter plots display individual data points on x and y axes, revealing correlations, distributions and outliers. ## When to use * **Correlation analysis** - Identify relationships between two variables * **Distribution patterns** - See how data is spread across ranges * **Outlier detection** - Spot unusual data points * **Large datasets** - Display hundreds or thousands of points ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'scatter', data: { columns: [ { key: 'hours_studied', label: 'Hours studied' }, { key: 'exam_score', label: 'Exam score' }, ], rows: [ { hours_studied: 2, exam_score: 55 }, { hours_studied: 3, exam_score: 62 }, { hours_studied: 4, exam_score: 68 }, { hours_studied: 5, exam_score: 74 }, { hours_studied: 6, exam_score: 80 }, { hours_studied: 7, exam_score: 85 }, { hours_studied: 8, exam_score: 90 }, ], }, }; ``` ## Multiple series Compare correlations across different groups: ```tsx theme={null} const config: GraphConfig = { type: 'scatter', data: { columns: [ { key: 'age', label: 'Age' }, { key: 'income_male', label: 'Male income' }, { key: 'income_female', label: 'Female income' }, ], rows: [ { age: 25, income_male: 35000, income_female: 33000 }, { age: 30, income_male: 45000, income_female: 43000 }, { age: 35, income_male: 55000, income_female: 54000 }, // ... more data points ], }, }; ``` ## Options **options.pointSize** `number | 'auto'` (default: auto) Point size in pixels. Set to `'auto'` to automatically determine appropriate size. ### Custom point size ```tsx theme={null} const config: GraphConfig = { type: 'scatter', options: { pointSize: 8, // 8px diameter points }, data: { /* ... */ }, }; ``` ## Point labels Label each point with its row's label column (the first text column, or the column mapped as the label) using `dataLabels.showCategoryLabels`: ```tsx theme={null} const config: GraphConfig = { type: 'scatter', dataLabels: { showCategoryLabels: true, // Label each point with its row's label column }, data: { /* ... */ }, }; ``` When both `showCategoryLabels` and `showDataLabels` are enabled, the category label takes precedence. ## Trendlines Add a trendline to show the overall correlation: ```tsx theme={null} const config: GraphConfig = { type: 'scatter', referenceLines: { trendline: 'linear', }, data: { /* ... */ }, }; ``` ## Related * [Bubble](/sdk/graph-types/bubble) - Scatter plot with sized points * [Reference lines](/sdk/config/reference-lines) - Add trendlines * [Type options](/sdk/config/type-options) - Scatter plot options # Table Source: https://docs.graphy.dev/sdk/graph-types/table Tables display data in a traditional tabular format with rows and columns, ideal for precise value lookup and detailed data exploration. ## When to use * **Precise values** - When exact numbers are important * **Detailed data exploration** - Allow users to scan specific values * **Supporting charts** - Provide detailed breakdown alongside visual charts * **Many columns** - Display data with numerous variables * **Reference material** - Create data that can be easily copied or referenced ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'table', data: { columns: [ { key: 'product', label: 'Product' }, { key: 'units_sold', label: 'Units sold' }, { key: 'revenue', label: 'Revenue' }, { key: 'profit_margin', label: 'Profit margin' }, ], rows: [ { product: 'Laptop Pro', units_sold: 1245, revenue: 1867500, profit_margin: 0.32 }, { product: 'Phone X', units_sold: 3521, revenue: 2816800, profit_margin: 0.41 }, { product: 'Tablet Mini', units_sold: 892, revenue: 267600, profit_margin: 0.28 }, { product: 'Watch Smart', units_sold: 2104, revenue: 841600, profit_margin: 0.45 }, ], }, }; ``` ## Formatting Tables automatically format values based on data type: * **Numbers** - Formatted with thousands separators * **Currency** - Detected from symbols (£, \$, €) * **Percentages** - Displayed with % symbol * **Dates** - Formatted according to locale ### Number formatting Control how numbers are displayed: ```tsx theme={null} const config: GraphConfig = { type: 'table', appearance: { numberFormat: { decimalPlaces: 2, abbreviation: 'k', }, }, data: { /* ... */ }, }; ``` ## Column visibility Hide specific columns by setting `_metadata.isHidden`: ```tsx theme={null} const config: GraphConfig = { type: 'table', data: { columns: [ { key: 'id', label: 'ID', _metadata: { isHidden: true } }, { key: 'product', label: 'Product' }, { key: 'sales', label: 'Sales' }, ], rows: [ { id: 1, product: 'Product A', sales: 1000 }, { id: 2, product: 'Product B', sales: 1500 }, ], }, }; ``` ## Combining with charts Tables work well alongside visual charts: ```tsx theme={null} const config: GraphConfig = { type: 'column', // Main visualization // ... chart configuration }; // Render both chart and table <> ; ``` ## Related * [Data structure](/sdk/core/data-structure) - Learn about columns and rows * [Appearance](/sdk/config/appearance) - Number formatting options # Waterfall Source: https://docs.graphy.dev/sdk/graph-types/waterfall Waterfall charts show running totals with up and down bars, ideal for visualising how sequential positive and negative values contribute to a cumulative result. ## When to use * **Financial analysis** - Show income, expenses and profit * **Explaining variance** - Break down differences between starting and ending values * **Incremental changes** - Display step-by-step contributions * **Budget vs actual** - Analyze deviations ## Basic example ```tsx theme={null} const config: GraphConfig = { type: 'waterfall', data: { columns: [ { key: 'category', label: 'Category' }, { key: 'amount', label: 'Amount' }, ], rows: [ { category: 'Starting balance', amount: 10000 }, { category: 'Revenue', amount: 25000 }, { category: 'Operating costs', amount: -8000 }, { category: 'Marketing', amount: -3000 }, { category: 'Tax', amount: -4000 }, { category: 'Ending balance', amount: 20000 }, ], }, }; ``` ## How it works Waterfall charts automatically: * Start with the first value as the base * Show positive values as upward bars (increases) * Show negative values as downward bars (decreases) * Connect bars to show the running total * Display the final cumulative value ## Data structure Provide: * **First column**: Category labels * **Second column**: Numeric values (positive for increases, negative for decreases) The chart calculates running totals automatically. ## Data labels Show values on each bar: ```tsx theme={null} const config: GraphConfig = { type: 'waterfall', dataLabels: { showDataLabels: true, }, data: { /* ... */ }, }; ``` ## Total label The final cumulative bar is labelled "Total" (localised to the chart's locale). Customise it with the `i18nOverrides` prop on `GraphProvider`: ```tsx theme={null} ``` The label appears on the x-axis tick and in the total bar's tooltip. ## Related * [Column](/sdk/graph-types/column) - Standard column chart * [Data labels](/sdk/config/data-labels) - Display bar values # EditorComponentRegistry Source: https://docs.graphy.dev/sdk/reference/editor-component-registry `EditorComponentRegistry` is a list of custom components that can be passed to the `components` prop of [`EditorProvider`](/sdk/reference/editor-provider) ## Example ```tsx theme={null} ``` ## Properties | Key | Interface | | - | - | | `Select` | [SelectComponent](/sdk/reference/components/select-component) | | `Switch` | [SwitchComponent](/sdk/reference/components/switch-component) | > The set of overridable components will grow over time. If you need a component that isn't listed here, [let us know on Discord](https://discord.gg/yGmYCjkSr). # EditorProvider Source: https://docs.graphy.dev/sdk/reference/editor-provider `EditorProvider` is the context provider for editor components. It manages editor-specific state and must wrap all editor panels, sections and other controls. It should be placed inside a `GraphProvider`. ## Basic usage ```tsx theme={null} import { GraphProvider } from '@graphysdk/core'; import { EditorProvider } from '@graphysdk/editor'; {/* Editor panels / sections / controls go here */} ; ``` ## Props **children** `React.ReactNode` *required* Child components to be wrapped by the provider. **components** `EditorComponentRegistry` List of overrides for internal components. See [`EditorComponentRegistry`](/sdk/reference/editor-component-registry) **customColorInputs** `CustomColorInputConfig[]` Custom color configuration for series styling in the editor. By default, the editor automatically generates color inputs based on the data series. Use this prop to override that behavior with custom configurations. **Type:** `Array<{ text: string, seriesConfigKey: string, symbol?: LegendSymbolType, color?: string }>` ```tsx theme={null} const customColorInputs = [ { text: 'Revenue', seriesConfigKey: 'revenue' }, { text: 'Profit', seriesConfigKey: 'profit', symbol: 'bar' }, ]; {/* Editor panels */}; ``` **themeOptions** `GraphThemeOption[]` Available theme options to display in the editor theme selector. Each option has a `label` (string or function receiving a translate function), a `value` (`GraphTheme`), and an optional `renderIcon` callback. You can use the pre-built `defaultThemeOptions` which provides light and dark options with icons. ```tsx theme={null} import { graphyLightTheme, graphyDarkTheme, SunIcon, MoonIcon } from '@graphysdk/core'; const themeOptions = [ { label: 'Light', value: graphyLightTheme, renderIcon: () => }, { label: 'Dark', value: graphyDarkTheme, renderIcon: () => }, { label: 'Custom', value: myCustomTheme }, ]; {/* Editor panels */}; ``` Or use the built-in defaults: ```tsx theme={null} import { defaultThemeOptions } from '@graphysdk/editor'; {/* Editor panels */}; ``` **onThemeChange** `(value: GraphTheme) =\> void` Callback invoked when the user changes the graph theme in the editor. ```tsx theme={null} { console.log('Theme changed to:', theme.id); }} > {/* Editor panels */} ``` **theme** `EditorTheme` Customize the editor UI theme (typography, colors, font weights, etc.). Defaults to `editorLightTheme`. You can use the pre-built `editorLightTheme` or `editorDarkTheme`, or create a custom theme. See [EditorTheme](/sdk/reference/editor-theme) for the full type definition and available font tokens. ```tsx theme={null} import { editorLightTheme } from '@graphysdk/editor'; // or editorDarkTheme const customEditorTheme = { ...editorLightTheme, values: { ...editorLightTheme.values, fontSectionTitle: '600 14px Inter', }, }; {/* Editor panels with custom fonts */}; ``` ## Complete example ```tsx theme={null} import { useState } from 'react'; import { GraphProvider, Graph, graphyLightTheme, graphyDarkTheme } from '@graphysdk/core'; import { EditorProvider, GraphPanel, defaultThemeOptions, editorLightTheme } from '@graphysdk/editor'; import type { GraphConfig, GraphTheme } from '@graphysdk/core'; function MyEditor() { const [config, setConfig] = useState({ data: { columns: [ { key: 'category', label: 'Category' }, { key: 'value', label: 'Value' }, ], rows: [ { category: 'A', value: 100 }, { category: 'B', value: 200 }, ], }, type: 'column', }); return ( setConfig({ ...config, ...update })}> { console.log('User selected theme:', theme.id); }} theme={editorLightTheme} >
); } ``` ## Type definitions ```tsx theme={null} interface EditorProviderProps { children: React.ReactNode; customColorInputs?: CustomColorInputConfig[]; themeOptions?: GraphThemeOption[]; onThemeChange?: GraphThemeChangeHandler; theme?: EditorTheme; } interface CustomColorInputConfig { text: string; seriesConfigKey: SeriesConfigKey; symbol?: LegendSymbolType; color?: string; } interface GraphThemeOption { label: string | ((t: TranslateFunction) => string); value: GraphTheme; renderIcon?: () => React.ReactNode; } type GraphThemeChangeHandler = (value: GraphTheme) => void; ``` ## Related * [GraphProvider](/sdk/reference/graph-provider) - State management and configuration * [EditorTheme](/sdk/reference/editor-theme) - Editor theme type and font tokens * [Editor panels](/sdk/editor/editor-panels) - Available editor components * [Editor quick start](/sdk/editor/index) - Getting started with the editor # EditorTheme Source: https://docs.graphy.dev/sdk/reference/editor-theme ## Structure ```tsx theme={null} interface EditorTheme { values: EditorThemeValues; } ``` | Property | Type | Description | | - | - | - | | `values` | `EditorThemeValues` | Design tokens (see below) | ## EditorThemeValues All tokens are strings resolved to CSS values at render time. Any valid CSS value is supported, including CSS variables (e.g. `var(--my-brand-color)`). `EditorTheme` inherits the base and semantic tokens from [`GraphTheme`](/sdk/reference/graph-theme#semantic-font-tokens). Modify these tokens in the graph theme to update multiple elements at once. ## Color tokens | Token | Purpose | | - | - | | `graphTypeButtonIcon` | Icon color for buttons in the graph type picker | ## Font tokens Font tokens are passed to the CSS `font` property and use the CSS `font` [shorthand syntax](https://developer.mozilla.org/en-US/docs/Web/CSS/font), which sets multiple font properties in a single declaration. | Token | Purpose | | - | - | | `fontInputUnit` | Unit suffix in numeric inputs | | `fontInlineError` | Inline validation error messages | | `fontToolbarButton` | Toolbar button labels | | `fontGridButtonLabel` | Grid/layout button labels | | `fontTextScalePreview` | Text scale preview labels | | `fontSectionTitle` | Editor panel section titles | | `fontSectionBody` | Editor panel section body text | | `fontControlLabel` | Control and field labels | | `fontAccordionTitle` | Accordion section titles | | `fontAnnotationButton` | Annotation action button text | | `fontPaletteLabel` | Color palette item labels | | `fontHighlightSelected` | Selected highlight mode label | | `fontDataTableAccessory` | Data table accessory text | | `fontNumberControlValue` | Numeric control value display | | `fontTabButtonLabel` | Tab button labels | | `fontSelectedPropertyLabel` | Selected property indicator label | | `fontSwitchLabel` | Toggle switch labels | | `fontDataTableCell` | Data table cell text | | `fontDataTableHeaderCell` | Data table header cell text | # Graph Source: https://docs.graphy.dev/sdk/reference/graph `Graph` is the component that renders the chart. It must be used inside a `GraphProvider`. ## Basic usage ```tsx theme={null} import { GraphProvider, Graph } from '@graphysdk/core'; ; ``` ## Props All props are optional. ### Mode **mode** `'readonly' | 'editor' | 'static'` Shortcut prop for setting sensible defaults for the interactivity and behavior of the graph for common use cases. | Mode | `isEditable` | `showTooltips` | `showHoverEffects` | `animateTransitions` | `isExplorable` | | - | - | - | - | - | - | | `readonly` | ✗ | ✓ | ✓ | ✓ | ✓ | | `editor` | ✓ | ✓ | ✓ | ✓ | ✓ | | `static` | ✗ | ✗ | ✗ | ✗ | ✗ | > [!TIP] > > When no `mode` is provided, the graph behaves like `readonly` by default. ```tsx theme={null} // Readonly graph with full interactivity (default) // Editable graph // Useful for thumbnails or exports ``` ### Sizing **sizing** `GraphSizing` Controls how the graph responds to its container size. - `mode: 'fixed'` - graph has a fixed width and height (default) - `mode: 'responsive'` - graph automatically resizes to fit its parent container - `mode: 'keepAspectRatio'` * graph scales to fit container while maintaining aspect ratio (requires either intrinsic dimensions or aspect ratio) ```tsx theme={null} // Fixed sizing: specific dimensions // Responsive sizing: fills container // Keep aspect ratio: with intrinsic dimensions // Keep aspect ratio: with intrinsic width and aspect ratio // Keep aspect ratio: with intrinsic height and aspect ratio ``` ### Error handling **onError** `function` Callback invoked when the graph encounters an error. Useful for logging or analytics. **Signature:** `(info: { error: Error, errorInfo: React.ErrorInfo }) => void` **errorFallback** `React.ComponentType\<\{ error: Error \}\>` Component to render when the graph encounters an error. Receives the error as a prop. ```tsx theme={null} { console.error('Chart error:', error); }} errorFallback={({ error }) =>
Something went wrong: {error.message}
} /> ``` ### Callbacks **onResize** `function` Callback invoked when the chart container is resized. **Signature:** `(entries: ResizeObserverEntry[]) => void` ```tsx theme={null} { console.log('Chart resized:', entries[0].contentRect); }} /> ``` ### Custom rendering **hasBorder** `boolean` (default: true) If `false`, no border will be shown around the graph (this overrides the border styling set in the `GraphConfig`). ```tsx theme={null} // Without border ``` **footerContent** `React.ReactNode` Custom content to display in the chart footer area. ```tsx theme={null} Custom footer content
} /> ``` **renderTitle** `function` Custom render function for the chart title area. **Signature:** `(props: { isEditable: boolean, titleDocument: JSONContent, onChange: (newTitle: JSONContent) => void }) => React.ReactNode` ```tsx theme={null} } /> ``` **ref** `React.Ref\` Ref to the graph container element. ```tsx theme={null} const graphRef = useRef(null); ; ``` ### Advanced props These props allow fine-grained control over individual behaviors. In most cases, you should use the `mode` prop instead. > [!NOTE] > > **Precedence order:** explicit props → `mode` → `GraphConfig` settings → defaults. For example, `showTooltips={false}` > will override both the `mode` setting and `appearance.showTooltips` in your config. **isEditable** `boolean` (default: false) Whether the graph is editable. Changes are handled by the `GraphProvider.onChange` handler. **showTooltips** `boolean` (default: true) Whether to show tooltips on hover. Overrides the `appearance.showTooltips` setting in `GraphConfig`. **showHoverEffects** `boolean` (default: true) Whether to show hover effects (line markers, bar highlighting, etc.). **animateTransitions** `boolean` (default: true) Whether to animate the graph on initial render and when data changes. Overrides the `appearance.animateTransitions` setting in `GraphConfig`. **isExplorable** `boolean` (default: true) Whether to allow transient, in-memory interactions for exploring the data. These interactions do not modify the `GraphConfig`. Currently supports: - **Legend toggling** — click legend items to temporarily hide/show series ```tsx theme={null} // Fine-grained control example: editable with no animations // Readonly mode but without tooltips ``` ## Examples ### Responsive dashboard chart ```tsx theme={null} import { GraphProvider, Graph } from '@graphysdk/core'; import type { GraphConfig } from '@graphysdk/core'; const config: GraphConfig = { data: { columns: [ { key: 'month', label: 'Month' }, { key: 'revenue', label: 'Revenue' }, ], rows: [ { month: 'Jan', revenue: 12000 }, { month: 'Feb', revenue: 15000 }, { month: 'Mar', revenue: 18000 }, ], }, type: 'column', }; function DashboardChart() { return (
); } ``` ### Exportable chart with fixed dimensions ```tsx theme={null} function ExportableChart({ config }: { config: GraphConfig }) { const graphRef = useRef(null); const handleExport = async () => { // Use graphRef.current to capture the chart as an image }; return ( ); } ``` ## Type definitions > **GraphSizing** > > ```tsx theme={null} > type GraphSizing = > | { mode: 'responsive' } > | { mode: 'fixed'; width: number; height: number } > | { mode: 'keepAspectRatio'; intrinsicWidth: number; intrinsicHeight: number } > | { mode: 'keepAspectRatio'; intrinsicWidth: number; aspectRatio: number } > | { mode: 'keepAspectRatio'; intrinsicHeight: number; aspectRatio: number }; > ``` > **GraphTitleRenderProps** > > ```tsx theme={null} > interface GraphTitleRenderProps { > /** Whether the graph is in edit mode */ > isEditable: boolean; > /** The current title as a TipTap JSONContent document */ > titleDocument: JSONContent; > /** Callback to update the title */ > onChange: (newTitle: JSONContent) => void; > } > ``` ## Migrating from older versions ### `isInteractive` prop (removed in `0.0.62`) The `isInteractive` prop has been replaced by the `mode` prop. | Before | After | | - | - | | `` (no prop) | `` — no changes needed | | `` | `` — remove the prop | | `` | `` | ### `disableAnimation` prop (removed in `0.0.62`) The `disableAnimation` prop has been replaced by the `mode` prop or `animateTransitions`. | Before | After | | - | - | | `` (no prop) | `` — no changes needed | | `` | `` — remove the prop | | `` | `` or `` | ### `isEditable` prop The `isEditable` prop is still supported but we recommend using `mode` instead for clarity. | Before | After | | - | - | | `` | `` | | `` | `` | ## Related * [GraphProvider](/sdk/reference/graph-provider) - State management and configuration * [Error Handling](/sdk/advanced/error-handling) - Error handling patterns # GraphConfig Source: https://docs.graphy.dev/sdk/reference/graph-config ## Overview `GraphConfig` is the main configuration object for creating and customising graphs in the Graphy SDK. It combines data, visual styling, text content and interactive features into a single, strongly-typed configuration. ```tsx theme={null} import type { GraphConfig } from '@graphysdk/core'; const config: GraphConfig = { data: { /* ... */ }, type: 'column', // ... other properties }; ``` ## Schema structure ### Required properties **data** `Data` *required* Data to visualise. Contains columns and rows. See [data structure](/sdk/core/data-structure) for detailed documentation. ### Optional properties **type** `Type` Graph type to render. Defaults to `'column'` if not specified. See [graph types](/sdk/graph-types/index) for all available types. **options** `Options` Chart type-specific options (line thickness, smooth lines, bar sorting, etc.). See [type options](/sdk/config/type-options) for details. **axes** `Axes` Axis configuration for x, y and secondary y axes. See [axes configuration](/sdk/config/axes) for details. **legend** `Legend` Legend position and visibility. See [legend configuration](/sdk/config/legend) for details. **appearance** `Appearance` Visual styling including colors, borders, backgrounds, text styles and number formatting. See [appearance configuration](/sdk/config/appearance) for details. **themeOverrides** `GraphThemeOverrides` Theme-level customization. Overrides specific theme values like `graphBackground`, `textColor`, etc. See [GraphTheme schema](/sdk/reference/graph-theme) for available properties. **content** `Content` Text content for title, subtitle, caption, data source, and the optional Made with Graphy provenance mark. See [content configuration](/sdk/config/content) for details — notably `isBrandMarkHidden` (hidden by default; set `false` to opt in). **headlineNumbers** `HeadlineNumbers` Summary metrics displayed prominently on the chart. See [headline numbers](/sdk/config/headline-numbers) for details. **dataLabels** `DataLabels` Configuration for value labels displayed on chart elements. See [data labels](/sdk/config/data-labels) for details. **annotations** `Annotations` Visual annotations layered on top of the chart (stickers, tooltips, highlights, text, arrows, shapes). See [annotations](/sdk/config/annotations) for details. **referenceLines** `ReferenceLines` Goal lines, trendlines and average lines. See [reference lines](/sdk/config/reference-lines) for details. ## Complete example ```tsx theme={null} import type { GraphConfig } from '@graphysdk/core'; const config: GraphConfig = { // Data (required) data: { columns: [ { key: 'quarter', label: 'Quarter' }, { key: 'revenue', label: 'Revenue' }, { key: 'profit', label: 'Profit' }, ], rows: [ { quarter: 'Q1', revenue: 100000, profit: 25000 }, { quarter: 'Q2', revenue: 120000, profit: 30000 }, { quarter: 'Q3', revenue: 135000, profit: 35000 }, { quarter: 'Q4', revenue: 150000, profit: 42000 }, ], }, // Graph type type: 'line', // Type-specific options options: { isSmoothLine: true, showPoints: true, lineThickness: 2, }, // Axes configuration axes: { x: { label: 'Fiscal quarter', }, y: { label: 'Amount (£)', min: 0, }, showGridLines: true, }, // Legend legend: { position: 'right', }, // Appearance appearance: { paletteId: 'corporate', seriesStyles: { series1: { customColor: '#1e40af', lineStyle: 'solid', }, series2: { customColor: '#059669', lineStyle: 'solid', }, }, border: { style: 'tinted', color: '#1e40af', width: 8, }, hasRoundedCorners: true, textScale: 1, numberFormat: { decimalPlaces: 0, abbreviation: 'k', }, showTooltips: true, animateTransitions: true, }, // Text content content: { title: 'Quarterly financial performance', subtitle: 'FY 2024', caption: 'All figures are preliminary', source: { label: 'Finance department', url: 'https://company.com/finance', }, // Opt in to the Made with Graphy badge (hidden by default at low level). isBrandMarkHidden: false, }, // Headline numbers headlineNumbers: { show: 'current', compareWith: 'previous', size: 'large', }, // Data labels dataLabels: { showDataLabels: false, }, // Reference lines referenceLines: { goalLine: { target: 140000, label: 'Target', }, trendline: 'linear', }, }; ``` ## Type definitions The `GraphConfig` type is defined using Zod for runtime validation: ```tsx theme={null} export const GraphConfig = z.object({ data: Data, type: Type.optional(), options: Options.optional(), axes: Axes.optional(), legend: Legend.optional(), appearance: Appearance.optional(), themeOverrides: GraphThemeOverrides.optional(), content: Content.optional(), headlineNumbers: HeadlineNumbers.optional(), dataLabels: DataLabels.optional(), annotations: Annotations.optional(), referenceLines: ReferenceLines.optional(), }); export type GraphConfig = z.infer; ``` ## Using with GraphProvider Pass your config to `GraphProvider`: ```tsx theme={null} import { GraphProvider, Graph } from '@graphysdk/core'; function App() { return ( ); } ``` # GraphProvider Source: https://docs.graphy.dev/sdk/reference/graph-provider `GraphProvider` is the root component that wraps your graphs and provides configuration, theming and state management. ## Basic usage ```tsx theme={null} import { useState } from 'react'; import { GraphProvider, Graph } from '@graphysdk/core'; import type { GraphConfig } from '@graphysdk/core'; function MyGraph() { const [config, setConfig] = useState(); return ( { setConfig((currentValues) => ({ ...currentValues, ...update })); }} > ); } ``` ## Props **children** `React.ReactNode` Components to render within the provider. Typically includes `Graph` and optionally editor components. **config** `GraphConfig` The graph configuration object. See [GraphConfig schema](/sdk/reference/graph-config) for all available options. ```tsx theme={null} const config: GraphConfig = { data: { columns: [ { key: 'month', label: 'Month' }, { key: 'sales', label: 'Sales' }, ], rows: [ { month: 'Jan', sales: 1000 }, { month: 'Feb', sales: 1200 }, ], }, type: 'column', content: { title: 'Monthly sales', }, }; ; ``` **onChange** `function` Callback invoked when the graph configuration changes (typically from editor interactions). **Signature:** `(changedValues: Partial, currentValues: GraphConfig) => void` * `changedValues` - Only the properties that changed * `currentValues` - The complete updated configuration ```tsx theme={null} { console.log('Changed:', changedValues); console.log('Current:', currentValues); setConfig(currentValues); }} > ``` **theme** `GraphTheme` Custom theme for the graph. See [GraphTheme reference](/sdk/reference/graph-theme) for all available properties. Defaults to `graphyLightTheme` if not provided. ```tsx theme={null} import { graphyDarkTheme } from '@graphysdk/core'; ; ``` **fontList** `FontList` Array of custom fonts available for use in graphs. Each font definition includes an `id`, `label` and `fontFamily`. See [Fonts](/sdk/customisation/graph-fonts) for usage details. ```tsx theme={null} const fontList = [ { id: 'custom-serif', label: 'Custom Serif', fontFamily: 'Garamond, Baskerville, serif', }, ]; ; ``` **customPalettes** `CustomPaletteCatalog` Array of custom color palettes available for graphs. Each palette includes an `id`, `name` and array of `colors`. See [Custom color palettes](/sdk/customisation/series-colors#custom-palettes) for usage details. ```tsx theme={null} const customPalettes = [ { id: 'brand', name: 'Brand Palette', colors: [ { id: '1', hex: '#3b82f6', name: 'Blue' }, { id: '2', hex: '#ef4444', name: 'Red' }, ], }, ]; ; ``` **uiLocale** `Locale` Locale for UI text (editor labels, buttons, etc.). Accepts `'en-GB'` or `'en-US'`. Defaults to `'en-US'`. **formattingLocale** `Locale` Locale for formatting numbers and dates in the UI. Accepts `'en-GB'` or `'en-US'`. Defaults to `'en-US'`. > [!NOTE] > > This is different from `data._metadata.parsingLocale`, which controls how data is parsed. This prop controls how > values are displayed in the editor UI. ```tsx theme={null} ``` **canvasColorToVariableName** `CanvasColorToVariableName` Advanced: Maps canvas color IDs to CSS variable names for custom styling. **i18nOverrides** `object` Advanced: Override specific translation strings in the UI. Use this to customize text labels in the editor. **Type:** `Partial>>` ## Complete example ```tsx theme={null} import { useState } from 'react'; import { GraphProvider, Graph, graphyDarkTheme } from '@graphysdk/core'; import type { GraphConfig } from '@graphysdk/core'; const fontList = [ { id: 'custom-sans', label: 'Custom Sans', fontFamily: 'Helvetica Neue, Arial, sans-serif', }, ]; const customPalettes = [ { id: 'brand', name: 'Brand Palette', colors: [ { id: '1', hex: '#3b82f6', name: 'Blue' }, { id: '2', hex: '#10b981', name: 'Green' }, ], }, ]; function MyGraph() { const [config, setConfig] = useState({ data: { columns: [ { key: 'category', label: 'Category' }, { key: 'value', label: 'Value' }, ], rows: [ { category: 'A', value: 100 }, { category: 'B', value: 150 }, ], }, type: 'column', appearance: { paletteId: 'brand', textStyle: { body: { fontId: 'custom-sans', }, }, }, }); return ( { setConfig(currentValues); }} theme={graphyDarkTheme} fontList={fontList} customPalettes={customPalettes} uiLocale="en-GB" formattingLocale="en-GB" > ); } ``` ## Type definitions ```tsx theme={null} type Locale = 'en-GB' | 'en-US'; interface GraphProviderProps { children?: React.ReactNode; config?: GraphConfig; onChange?: (changedValues: Partial, currentValues: GraphConfig) => void; uiLocale?: Locale; formattingLocale?: Locale; fontList?: FontList; customPalettes?: CustomPaletteCatalog; canvasColorToVariableName?: CanvasColorToVariableName; theme?: GraphTheme; i18nOverrides?: Partial>>; } ``` ## Related * [Graph](/sdk/reference/graph) - Chart rendering component * [EditorProvider](/sdk/reference/editor-provider) - Editor state management # GraphTheme Source: https://docs.graphy.dev/sdk/reference/graph-theme ## Structure ```tsx theme={null} interface GraphTheme { id: string; colorScheme: 'light' | 'dark'; values: GraphThemeValues; canvasColors: GraphThemeCanvasColor[]; defaultAnnotationColorIds: GraphThemeAnnotationColorIds; } ``` | Property | Type | Description | | - | - | - | | `id` | `string` | Unique identifier for this theme | | `colorScheme` | `'light'` \| `'dark'` | Hint for deriving colors (tinted backgrounds, borders) | | `values` | `GraphThemeValues` | Design tokens (see below) | | `canvasColors` | `GraphThemeCanvasColor[]` | Colors available in annotation color picker | | `defaultAnnotationColorIds` | `GraphThemeAnnotationColorIds` | Default colors for new annotations | ## GraphThemeCanvasColor ```tsx theme={null} interface GraphThemeCanvasColor { id: string; // Unique ID, persists across theme changes label?: string; // Display name in color picker value: string; // Hex color } ``` ## GraphThemeAnnotationColorIds ```tsx theme={null} interface GraphThemeAnnotationColorIds { arrowStroke?: string; // Canvas color ID for arrow strokes shapeFill?: string; // Canvas color ID for shape fills } ``` ## GraphThemeValues All tokens are strings resolved to CSS values at render time. Any valid CSS value is supported, including CSS variables (e.g. `var(--my-brand-color)`). Tokens follow an inheritance hierarchy: **Base Tokens** define foundational primitives, **Semantic Tokens** map those primitives to contextual meaning and can be used to update multiple elements at once. **Element Tokens** target specific UI components for more fine-grained control. ## Colors ### Base colors | Token | | | | | - | - | - | - | | `white` | `black` | `transparent` | | | `gray100` | `gray95` | `gray90` | | | `gray85` | `gray80` | `gray75` | | | `gray70` | `gray60` | `gray50` | | | `gray0` | `grayGradient80` | | | | `green60` | `green50` | | | | `red60` | `red50` | | | | `amber70` | `amber50` | `amber40` | `amber30` | | `blue80` | `blue60` | | | | `purple50` | `purple30` | | | ### Semantic color tokens | Token | Purpose | | - | - | | `brand` | Brand accent color | | `success` | Success states | | `warning` | Warning states | | `alert` | Error/alert states | | `textPrimary` | Primary text | | `textSecondary` | Secondary text | | `textDisabled` | Disabled text | | `iconPrimary` | Primary icon color | | `iconSecondary` | Secondary icon color | | `iconStickerBackground` | Background color for icons with a "sticker" appearance | | `sunkenBackground` | Recessed surfaces | | `defaultBackground` | Default surfaces | | `raisedBackground` | Elevated surfaces | | `overlayBackground` | Overlay/modal backgrounds | | `overlayBorderGradient` | Overlay border gradient | | `border100` | Full opacity borders | | `border10` | Low opacity borders | ### Element color tokens | Token | Purpose | | - | - | | **Graph** | | | `graphBackground` | Chart background | | **Grid** | | | `gridLineColor` | Grid lines | | `hoverGuideLineColor` | Hover guide lines | | `originLineColor` | Origin/zero line | | `targetLineColor` | Goal/target line | | `targetLineMarkerColor` | Goal line marker | | **Legend** | | | `legendBackground` | Legend background | | `legendBorderColor` | Legend border | | `legendTextColor` | Legend text | | `dimmedSeriesLabelTextColor` | Dimmed series label text | | `dimmedSeriesLabelLineColor` | Dimmed series label line | | **Trends** | | | `trendPositiveColor` | Positive trend indicator | | `trendNegativeColor` | Negative trend indicator | | **Tooltips** | | | `tooltipBackground` | Tooltip background | | `tooltipBorderColor` | Tooltip border | | `tooltipHeadingTextColor` | Tooltip heading | | `tooltipLabelTextColor` | Tooltip labels | | `tooltipValueTextColor` | Tooltip values | | **Annotations** | | | `defaultArrowAnnotationColor` | Default arrow color | | `annotationOutlineColor` | Annotation outline | | `annotationMenuTriggerIconColor` | Annotation menu icon | | **Chart-specific** | | | `heatmapEmptyTileBackground` | Heatmap empty cells | ## Fonts Font tokens are passed to the CSS `font` property and use the CSS `font` [shorthand syntax](https://developer.mozilla.org/en-US/docs/Web/CSS/font), which sets multiple font properties in a single declaration. ### Base font tokens Foundational font properties. Combine these to build higher-level tokens. | Token | | - | | **Font families** | | `fontFamilyDefault` | | `fontFamilyHeading` | | **Font weights** | | `fontWeightRegular` | | `fontWeightMedium` | | `fontWeightSemibold` | | `fontWeightBold` | | **Font sizes** | | `fontXxs` | | `fontXs` | | `fontSm` | | `fontMd` | | `fontLg` | | `fontXl` | ### Semantic font tokens Named text styles built from base tokens. Override these to update the font style across all elements that use them. | Token | Purpose | | - | - | | `fontEditorBody` | Body text for editable text | | `fontHeadingSm` | Smallest heading size | | `fontHeadingMd` | Standard heading size | | `fontHeadingLg` | Large heading size | ### Element font tokens Per-component font tokens that reference semantic tokens. Override these to adjust typography for a specific element without affecting others. | Token | Purpose | | - | - | | **Axes** | | | `fontTickLabel` | Axis tick labels | | `fontAxisLabel` | Axis title labels | | **Data labels** | | | `fontDataLabel` | In-chart data labels | | `fontStackTotal` | Stacked bar total labels | | **Legend** | | | `fontLegendLabel` | Legend item labels | | `fontSeriesLabel` | Series labels | | **Trends** | | | `fontTrendTag` | Trend tag text | | `fontTrendTagSmall` | Small trend tag text | | **Goal lines** | | | `fontGoalLineLabel` | Goal/target line labels | | **Pie charts** | | | `fontPieLabel` | Pie segment labels | | `fontPieChartTotal` | Pie chart total text | | **Button** | | | `fontButton` | Button text | | **Forms** | | | `fontInput` | Input field text | | `fontInputLabel` | Input field labels | | `fontSelectLabel` | Select dropdown labels | | `fontSelectDescription` | Select dropdown descriptions | | `fontColorSelectLabel` | Color picker labels | | **Menus** | | | `fontMenuTitle` | Menu title | | `fontMenuGroupTitle` | Menu group heading | | `fontMenuItemLabel` | Menu item primary label | | `fontMenuItemLabelSecondary` | Menu item secondary label | | **UI tooltips** | | | `fontUITooltip` | UI tooltip text | | `fontUITooltipSecondary` | UI tooltip secondary text | | **Error boundary** | | | `fontErrorBoundaryTitle` | Error boundary title | | `fontErrorBoundaryMessage` | Error boundary message | | **Tables** | | | `fontTableCell` | Table cell text | | `fontTableHeaderCell` | Table header cell text | | **Footer** | | | `fontSourceLabel` | Source label text | | `fontSourceLink` | Source link text | | **Text editor** | | | `fontTextEditorH1` | Text editor H1 heading | | `fontTextEditorH2` | Text editor H2 heading | | `fontTextEditorH3` | Text editor H3 heading | | `fontTextEditorH6` | Text editor H6 heading | | `fontTextEditorBody` | Text editor body text | | `fontTextEditorLink` | Text editor link text | | **Highlight mode** | | | `fontHighlightModeTitle` | Highlight mode title | | `fontHighlightModeSubtitle` | Highlight mode subtitle | | **Editor panel** | | | `fontEditorSectionTitle` | Editor panel section title | | `fontEditorControlLabel` | Editor panel setting label | | `fontEditorControlValue` | Editor panel control's value | | `fontEditorCaption` | Editor panel unit or caption |