Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,16 @@ module.exports = {
'react/state-in-constructor': 'off',
},
},
{
files: './packages/visx-registry/registry/**',
rules: {
'import/no-unresolved': [
'error',
{
ignore: ['^@visx/(a11y|scale|theme)/react$'],
},
],
},
},
],
};
24 changes: 23 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ visx 4.1 adds the first hook and helper APIs used to build primitive charts for
registry. These APIs are additive and live in package roots or explicit React subpaths:

```tsx
import { useChartDimensions } from '@visx/chart';
import {
getPositiveDomain,
getResponsiveWidth,
getZeroBaselineDomain,
useChartDimensions,
} from '@visx/chart';
import { useAxis } from '@visx/axis/react';
import { useScale } from '@visx/scale/react';
import { arcPath, areaPath, linePath } from '@visx/shape';
Expand All @@ -103,6 +108,23 @@ non-React code.
**What you need to do:** nothing unless you want to author hook-based primitive charts. Continue
using existing components as before, or opt into the new hooks from the import paths above.

### The visx chart registry adds shadcn-compatible chart starters

visx 4.1 introduces a source-first chart registry for installing themed, responsive, accessible
primitive chart components into application code with the shadcn CLI.

```sh
npx shadcn@latest add airbnb/visx/line-chart
```

Registry items are copied into your app as editable source files and depend on public visx packages
such as `@visx/a11y`, `@visx/chart`, `@visx/responsive`, and `@visx/theme`. The registry itself is
distribution infrastructure and is not a runtime package your app imports.

**What you need to do:** nothing unless you want drop-in primitive chart starters. Existing visx
charts continue to work. Use registry installs only after the referenced 4.1 visx packages are
published.

## From visx 3.x

Upgrade every visx package in your application at the same time. Mixing visx 3 and visx 4 packages
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"test:watch": "yarn run vitest watch",
"vitest": "vitest",
"vitest:ui": "vitest --ui",
"registry:smoke": "yarn workspace @visx/registry smoke:install",
"registry:validate": "yarn workspace @visx/registry validate",
"ts": "tsx --tsconfig ./tsconfig.node.json",
"type": "yarn type:clean && yarn type:build",
"type:build": "lerna exec -- 'tsc --build'",
Expand Down
17 changes: 9 additions & 8 deletions packages/visx-a11y/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function RevenueChart({ data }) {

return (
<>
<svg {...a11y.svgProps}>
<svg {...a11y.svgProps} id={a11y.id}>
<desc id={a11y.descriptionId}>{a11y.description}</desc>
{/* chart marks */}
</svg>
Expand Down Expand Up @@ -139,7 +139,7 @@ const a11y = useChartA11y({

return (
<>
<svg {...a11y.svgProps}>
<svg {...a11y.svgProps} id={a11y.id}>
<desc id={a11y.descriptionId}>{a11y.description}</desc>
<g>
{data.map((datum) => (
Expand Down Expand Up @@ -174,7 +174,7 @@ const a11y = useChartA11y({

return (
<>
<svg {...a11y.svgProps}>
<svg {...a11y.svgProps} id={a11y.id}>
<desc id={a11y.descriptionId}>{a11y.description}</desc>
<g {...a11y.getSeriesProps(0)}>
{data.map((datum, index) => (
Expand Down Expand Up @@ -216,7 +216,7 @@ const a11y = useChartA11y({
});

return (
<svg {...a11y.svgProps}>
<svg {...a11y.svgProps} id={a11y.id}>
<desc id={a11y.descriptionId}>{a11y.description}</desc>
<g {...a11y.getSeriesProps(0)}>
{data.map((datum, index) => (
Expand All @@ -236,9 +236,10 @@ still wants the same roving-focus behavior, point focus state, and `onPointFocus

## Keyboard interaction model

`useChartA11y` starts in chart mode. The chart root is tabbable, and data marks become tabbable only
while keyboard exploration is active. Navigation is enabled by default for charts with at least one
point and no more than `pointDescriptionThreshold` points.
When keyboard navigation is enabled, `useChartA11y` starts in chart mode. The chart root is
tabbable, and data marks become tabbable only while keyboard exploration is active. Navigation is
enabled by default for charts with at least one point and no more than `pointDescriptionThreshold`
points.

| Key | Result |
| ------------------ | -------------------------------------------------------------------------------------------- |
Expand All @@ -264,7 +265,7 @@ rects, clipping helpers, and other non-data elements should usually be rendered
table.

```tsx
<svg {...a11y.svgProps}>
<svg {...a11y.svgProps} id={a11y.id}>
<rect aria-hidden="true" width={width} height={height} />
<GridRows aria-hidden="true" scale={yScale} width={innerWidth} />
<AxisBottom aria-hidden="true" scale={xScale} />
Expand Down
5 changes: 5 additions & 0 deletions packages/visx-a11y/src/useChartA11y.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { generateChartDescription } from './generators/description';
import type { ChartA11yMode, ChartA11yPointFocus } from './keyboard/stateMachine';
import type {
ChartA11yConfig,
ChartA11yIds,
ChartA11yPointProps,
ChartA11ySeriesProps,
ChartA11ySvgProps,
Expand All @@ -35,6 +36,8 @@ export type UseChartA11yDataTableProps<Datum> = Pick<
export type UseChartA11yAnnouncerProps = Omit<ChartA11yAnnouncerProps, 'message'>;

export type UseChartA11yResult<Datum> = {
id: string;
ids: ChartA11yIds;
svgProps: UseChartA11ySvgProps;
getSeriesProps: (seriesIndex: number) => ChartA11ySeriesProps;
getPointProps: (seriesIndex: number, index: number) => UseChartA11yPointProps;
Expand Down Expand Up @@ -127,6 +130,8 @@ export function useChartA11y<Datum>(config: ChartA11yConfig<Datum>): UseChartA11
const announce = useCallback((message: string) => setAnnouncement(message), []);

return {
id: ariaProps.ids.rootId,
ids: ariaProps.ids,
svgProps,
getSeriesProps,
getPointProps,
Expand Down
8 changes: 8 additions & 0 deletions packages/visx-a11y/test/useChartA11y.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ describe('useChartA11y', () => {
it('returns chart semantics and generated description metadata', () => {
const { result } = renderHook(() => useChartA11y(lineConfig));

expect(result.current.id).toBe('revenue-chart');
expect(result.current.ids).toEqual({
rootId: 'revenue-chart',
descriptionId: 'revenue-chart-description',
tableId: 'revenue-chart-table',
});
expect(result.current.descriptionId).toBe('revenue-chart-description');
expect(result.current.description).toBe(
'line chart of Revenue over 2 data points. Values start at $10 for Jan, end at $25 for Feb, range from $10 at Jan to $25 at Feb.',
Expand Down Expand Up @@ -66,6 +72,8 @@ describe('useChartA11y', () => {
useChartA11y({ ...lineConfig, id: undefined, idPrefix: 'chart' }),
);

expect(result.current.id).toMatch(/^chart-[\w-]+$/);
expect(result.current.ids.rootId).toBe(result.current.id);
expect(result.current.descriptionId).toMatch(/^chart-[\w-]+-description$/);
expect(result.current.descriptionId).not.toBe('chart-revenue-description');
});
Expand Down
99 changes: 95 additions & 4 deletions packages/visx-chart/Readme.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# @visx/chart

Chart-level React hooks for building primitive visx charts without adopting a chart framework. The
package starts with shared layout helpers that registry-style charts can use before composing
`@visx/axis`, `@visx/scale`, `@visx/shape`, and other low-level primitives.
Chart-level React hooks and helpers for building primitive visx charts without adopting a chart
framework. The package starts with shared layout and domain helpers that registry-style charts can
use before composing `@visx/axis`, `@visx/scale`, `@visx/shape`, and other low-level primitives.

## Installation

Expand All @@ -20,6 +20,8 @@ is the point, but every primitive chart still tends to rebuild the same chart-le
- margin defaults
- inner drawable width and height
- `xMax` and `yMax` aliases used throughout visx examples
- common numeric domains for primitive chart scales
- responsive width fallbacks while parent measurement initializes
- stable layout objects for downstream hooks and components

Those details are small, but they become repetitive in examples, dashboards, and registry items.
Expand Down Expand Up @@ -66,6 +68,92 @@ export function ChartFrame({ width, height, children }) {
The hook fills missing margin sides with `0`, treats non-finite dimensions as `0`, and clamps the
inner drawing area to `0` so marks never receive negative layout space.

## Numeric domains

Use the domain helpers when a primitive chart needs the usual safe defaults for empty, invalid, or
single-value data.

```tsx
import { getPositiveDomain, getZeroBaselineDomain } from '@visx/chart';

const yDomain = getZeroBaselineDomain(data.map((datum) => datum.value));
const stackedYDomain = getPositiveDomain(
data.map((datum) => datum.desktop + datum.mobile + datum.tablet),
);
```

`getZeroBaselineDomain` includes zero and expands equal domains. `getPositiveDomain` returns a
positive `[0, max]` domain with an empty fallback of `[0, 1]`. `getPaddedDomain` is available for
plots such as scatter charts where the domain should wrap finite values with proportional padding.

## Axis ticks

Use the tick helpers when a chart axis may become too dense in responsive layouts.
`getVisibleTickValues` thins explicit category tick values while preserving the first and last tick
by default, and `getAxisTickCount` computes a conservative numeric tick count from the available
axis length.

```tsx
import { getAxisTickCount, getVisibleTickValues } from '@visx/chart';

const xTickValues = getVisibleTickValues(months, {
axisLength: dimensions.innerWidth,
minTickSpacing: 48,
});
const yNumTicks = getAxisTickCount({
axisLength: dimensions.innerHeight,
maxTicks: 5,
minTickSpacing: 48,
});
```

The helpers only choose visible axis labels. They do not remove data points, bars, bins, or table
fallback rows.

## Responsive width fallback

Use `getResponsiveWidth` when a responsive container may report `0`, `NaN`, or another
non-renderable width while layout measurement initializes.

```tsx
import { getResponsiveWidth } from '@visx/chart';

const chartWidth = getResponsiveWidth(measuredWidth, 640);
```

The helper returns the measured width only when it is finite and greater than `0`; otherwise it
returns the fallback width.

## Chart config

Use `ChartConfig` when labels, colors, icons, and theme-specific tokens should live outside the
chart data. This mirrors the way registry charts are usually copied into app code: the data can come
from an API while chart presentation stays colocated with the component.

```tsx
import { getChartConfigColor, getChartConfigLabel, type ChartConfig } from '@visx/chart';

const chartConfig = {
bookings: {
label: 'Bookings',
color: 'var(--chart-1)',
},
revenue: {
label: 'Revenue',
theme: {
light: 'oklch(0.646 0.222 41.116)',
dark: 'oklch(0.488 0.243 264.376)',
},
},
} satisfies ChartConfig;

const label = getChartConfigLabel(chartConfig, 'bookings');
const color = getChartConfigColor(chartConfig, 'bookings', 'currentColor');
```

`getChartCssVariables` can turn a config into `--color-*` CSS variables for chart shells that want
the same token style as copied registry components.

## Working with scales

`xMax` and `yMax` are aliases for `innerWidth` and `innerHeight`, matching the naming used in many
Expand Down Expand Up @@ -162,12 +250,15 @@ export function RevenueBars({ data, width, height }) {
`@visx/responsive` when a chart should fill available space.

```tsx
import { getResponsiveWidth } from '@visx/chart';
import { ParentSize } from '@visx/responsive';

export function ResponsiveRevenueBars({ data }) {
return (
<ParentSize>
{({ width, height }) => <RevenueBars data={data} width={width} height={height} />}
{({ width }) => (
<RevenueBars data={data} width={getResponsiveWidth(width, 640)} height={320} />
)}
</ParentSize>
);
}
Expand Down
50 changes: 50 additions & 0 deletions packages/visx-chart/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ComponentType } from 'react';

export type ChartTheme = 'light' | 'dark';

export type ChartConfigItem = {
label?: string;
color?: string;
theme?: Partial<Record<ChartTheme, string>>;
icon?: ComponentType<{ className?: string }>;
};

export type ChartConfig = Record<string, ChartConfigItem>;

export function getChartConfigEntry(config: ChartConfig | undefined, key: string) {
return config?.[key];
}

export function getChartConfigLabel(config: ChartConfig | undefined, key: string, fallback = key) {
return getChartConfigEntry(config, key)?.label ?? fallback;
}

export function getChartConfigColor(
config: ChartConfig | undefined,
key: string,
fallback?: string,
theme?: ChartTheme,
) {
const entry = getChartConfigEntry(config, key);

return (theme ? entry?.theme?.[theme] : undefined) ?? entry?.color ?? fallback;
}

export function getChartConfigIcon(config: ChartConfig | undefined, key: string) {
return getChartConfigEntry(config, key)?.icon;
}

export function getChartCssVariableName(key: string) {
return `--color-${key.replace(/[^\w-]/g, '-')}`;
}

export function getChartCssVariables(config: ChartConfig | undefined, theme?: ChartTheme) {
return Object.fromEntries(
Object.keys(config ?? {})
.map((key) => [
getChartCssVariableName(key),
getChartConfigColor(config, key, undefined, theme),
])
.filter((entry): entry is [string, string] => entry[1] != null),
);
}
Loading
Loading