adding-chart-marks
Skill131 starsChanged 8 days agoApache-2.0View on GitHub
Adds a new top-level chart mark (e.g. Scatter, Combo, Heatmap) or supplemental visualization type to react-spectrum-charts, across the spec builder, React component, adapters, tests, stories, and S2. Use when creating a new mark type used directly inside <Chart>.
In this repository12 other files it gives its agents.
- AGENTS.mdAGENTS.md
- adding-child-componentsSkill
- adding-propsSkill
- chart-featuresSkill
- comparing-time-periodsSkill
- filing-issuesSkill
- fixing-bugsSkill
- implementing-milestonesSkill
- s2-design-tokensSkill
- summarizing-bugsSkill
- writing-rfcsSkill
- writing-s2-docs-pagesSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
18 rules. About 0 are common, 5 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 8
Use this skill when adding a new top-level chart mark (a new mark type like Scatter, Combo, or a hypothetical Heatmap) or a new …
- Very specific: line 10
Read architecture/mark-internals.md, architecture/encoding-and-props.md, and architecture/file-creation.md — this is the most involved …
- Very specific: line 16
If this work has a GitHub issue (gh issue view <number> --repo adobe/react-spectrum-charts) or comes from an accepted RFC in rfcs/, read it …
- Distinctive: line 22
If the implementation diverges from the issue, update the issue body.
- Very specific: line 70
Add WidgetOptions to the MarkOptions union in packages/vega-spec-builder/src/types/chartSpec.types.ts.
- Very specific: line 74
Create packages/vega-spec-builder/src/widget/widgetTestUtils.ts with a fully-populated defaultWidgetOptions: WidgetSpecOptions.
- Very specific: line 99
Use toHaveProperty in tests — never direct property access on Vega encode objects due to ProductionRule<T> union types.
- Very specific: line 158
Add to initializeComponentCounts(): widgetCount: -1
- Very specific: line 202
Create the barrel index.ts.
- Distinctive: line 222
Add the import and dispatch case:
- Very specific: line 229
Add Widget.displayName to sanitizeRscChartChildren in utils.ts.
- Distinctive: line 245
After writing tests, type-check once at task completion — not proactively after every file.
- Very specific: line 249
If the mark is not ready for the public API, place the component in packages/react-spectrum-charts/src/alpha/components/Widget/ and import …
- Distinctive: line 255
If the mark ships in S2:
- Very specific: line 256
Create an S2 spec builder in packages/vega-spec-builder-s2/src/widget/ (often identical to S1 except using S2 color utilities)
- Very specific: line 258
Create the S2 React component in the lab subpath, packages/react-spectrum-charts-s2/src/lab/components/Widget/, and export it from …
- Very specific: line 261
Create packages/react-spectrum-charts-s2/src/dev/Widget/Dashboards/widget.dashboard.tsx modeled on the Donut dashboard, with variations and …
- Very specific: line 262
Create packages/docs-s2/docs/widget.mdx and src/playgrounds/widget.tsx, register the page in sidebars.ts, and add a search card to …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
react-spectrum-charts: Adding a New Chart Mark
Use this skill when adding a new top-level chart mark (a new mark type like Scatter, Combo, or a hypothetical Heatmap) or a new supplemental visualization type. This is the most involved type of change in the library.
Read architecture/mark-internals.md, architecture/encoding-and-props.md, and architecture/file-creation.md — this is the most involved change type and touches most of the architecture docs. If the mark ships in S2, also read architecture/s2-parity.md.
Step 0: Check for a Linked Issue or Accepted RFC
If this work has a GitHub issue (gh issue view <number> --repo adobe/react-spectrum-charts)
or comes from an accepted RFC in rfcs/, read it and treat it as the requirements: the
design, defaults, cross-cutting concerns, and implementation plan. Re-verify file and
function references against the current code; the issue may have been written before other
changes landed.
If the implementation diverges from the issue, update the issue body. If it diverges from an accepted RFC's design, stop and tell the user. If neither exists, proceed as below.
The PR description says Closes #<number> so merging closes the issue.
Implementation Steps
Step 1: Spec Builder Types
Create packages/vega-spec-builder/src/types/marks/widgetSpec.types.ts:
export interface WidgetOptions {
markType: 'widget'; // required discriminant
name?: string;
metric?: string;
dimension?: string;
color?: ColorFacet;
// Children (populated by the adapter):
chartPopovers?: ChartPopoverOptions[];
chartTooltips?: ChartTooltipOptions[];
}
type WidgetOptionsWithDefaults =
| 'chartPopovers'
| 'chartTooltips'
| 'color'
| 'dimension'
| 'metric'
| 'name';
export interface WidgetSpecOptions
extends PartiallyRequired<WidgetOptions, WidgetOptionsWithDefaults> {
// Injected by chartSpecBuilder.ts via specOptions spread — always present:
colorScheme: ColorScheme;
highlightedItem?: HighlightedItem;
idKey: string;
s2?: boolean;
// Computed by addWidget:
index: number;
interactiveMarkName: string | undefined;
}Export from packages/vega-spec-builder/src/types/marks/index.ts.
Add WidgetOptions to the MarkOptions union in packages/vega-spec-builder/src/types/chartSpec.types.ts. Do this in Step 1 — the union must be updated before the spec builder can compile.
Step 2: Test Fixture
Create packages/vega-spec-builder/src/widget/widgetTestUtils.ts with a fully-populated defaultWidgetOptions: WidgetSpecOptions. Every required field must be present at its default value. This fixture is the baseline for all spec builder unit tests.
Step 3: Mark Utils
Create packages/vega-spec-builder/src/widget/widgetMarkUtils.ts:
export const addWidgetMarks = produce<Mark[], [WidgetSpecOptions]>((marks, options) => {
marks.push({
name: `${options.name}_group`,
type: 'group',
marks: [
getWidgetMark(options),
...getInteractiveMarks(options), // voronoi overlay when interactive
],
});
});
export const getWidgetMark = (options: WidgetSpecOptions): SymbolMark => {
// Build the primary Vega mark
// Use encode.enter for static encoding
// Use encode.update for dynamic encoding (opacity, color that reacts to hover state)
};Use toHaveProperty in tests — never direct property access on Vega encode objects due to ProductionRule<T> union types.
Step 4: Spec Builder
Create packages/vega-spec-builder/src/widget/widgetSpecBuilder.ts:
export const addWidget = produce<ScSpec, [WidgetOptions & { colorScheme?: ColorScheme; highlightedItem?: HighlightedItem; index?: number; idKey: string; s2?: boolean; }]>(
(spec, {
chartPopovers = [],
chartTooltips = [],
color = { value: 'categorical-100' },
index = 0,
metric = DEFAULT_METRIC,
name,
...options // colorScheme, highlightedItem, idKey, s2 arrive via ...specOptions spread in chartSpecBuilder
}) => {
const widgetName = toCamelCase(name || `widget${index}`);
const widgetOptions: WidgetSpecOptions = {
chartPopovers,
chartTooltips,
color,
index,
interactiveMarkName: getInteractiveMarkName({ chartPopovers, chartTooltips }, widgetName),
metric,
name: widgetName,
...options,
};
spec.usermeta = addUserMetaInteractiveMark(spec.usermeta, widgetOptions.interactiveMarkName);
spec.data = addData(spec.data ?? [], widgetOptions);
spec.signals = addSignals(spec.signals ?? [], widgetOptions);
spec.scales = setScales(spec.scales ?? [], widgetOptions);
spec.marks = addWidgetMarks(spec.marks ?? [], widgetOptions);
}
);
export const addData = produce<Data[], [WidgetSpecOptions]>((data, options) => {
// Add time transform if dimension is time-based
// Add filteredTableForTooltip when interactive
// Add ${name}_selectedData when has popover
});
export const addSignals = produce<Signal[], [WidgetSpecOptions]>((signals, options) => {
if (!isInteractive(options)) return;
addHoveredItemSignal(signals, options.name, options.interactiveMarkName);
addTooltipSignals(signals, options);
});
export const setScales = produce<Scale[], [WidgetSpecOptions]>((scales, options) => {
addContinuousDimensionScale(scales, { scaleType: options.scaleType, dimension: options.dimension });
addMetricScale(scales, [options.metric]);
addFieldToFacetScaleDomain(scales, COLOR_SCALE, options.color);
});Step 5: Register in chartSpecBuilder.ts
- Add import:
import { addWidget } from './widget/widgetSpecBuilder'; - Add to
initializeComponentCounts():widgetCount: -1 - Add switch case:
case 'widget':
widgetCount++;
return addWidget(acc, { ...mark, ...specOptions, index: widgetCount });Step 6: React Types
Create packages/react-spectrum-charts/src/types/marks/widget.types.ts:
type WidgetChildElement = ChartPopoverElement | ChartTooltipElement;
export interface WidgetProps
extends Omit<WidgetOptions, 'chartPopovers' | 'chartTooltips' | 'markType'> {
children?: Children<WidgetChildElement>;
}
export type WidgetElement = ReactElement<WidgetProps, JSXElementConstructor<WidgetProps>>;The Omit list contains only fields replaced at the React boundary. Plain value props from WidgetOptions flow through automatically.
Step 7: React Component
Create packages/react-spectrum-charts/src/components/Widget/Widget.tsx:
/* eslint-disable @typescript-eslint/no-unused-vars */
import { FC } from 'react';
import { WidgetProps } from '../../types';
const Widget: FC<WidgetProps> = ({
color = { value: 'categorical-100' },
metric = DEFAULT_METRIC,
children,
}) => { return null; };
Widget.displayName = 'Widget';
export { Widget };Create the barrel index.ts. Add to components/index.ts.
Step 8: Adapter
Create packages/react-spectrum-charts/src/rscToSbAdapter/widgetAdapter.ts:
export const getWidgetOptions = ({ children, ...widgetProps }: WidgetProps): WidgetOptions => {
const { chartPopovers, chartTooltips } = childrenToOptions(children);
return {
...widgetProps,
chartPopovers,
chartTooltips,
markType: 'widget',
};
};Step 9: Register in childrenAdapter.ts
Add the import and dispatch case:
case Widget.displayName:
marks.push(getWidgetOptions(child.props as WidgetProps));
break;Add Widget.displayName to sanitizeRscChartChildren in utils.ts.
Step 10: Tests
widgetSpecBuilder.test.ts — test addData, addSignals, setScales independently using initializeSpec() and defaultWidgetOptions fixture.
widgetMarkUtils.test.ts — test addWidgetMarks group structure, mark names, interactive mark additions.
widgetAdapter.test.ts — test getWidgetOptions with createElement(ChartTooltip) and createElement(ChartPopover) children, assert collections have correct length.
chartAdapter.test.ts — update the toStrictEqual snapshot to include the new mark type with all fields at default values.
Widget.story.tsx — at minimum: Basic, Color, WithTooltip stories.
Widget.test.tsx — integration test using findChart and findAllMarksByGroupName.
After writing tests, type-check once at task completion — not proactively after every file. See AGENTS.md's Test Completeness Checklist for when to run it.
Step 11: Alpha vs Stable
If the mark is not ready for the public API, place the component in packages/react-spectrum-charts/src/alpha/components/Widget/ and import it in childrenAdapter.ts from '../alpha/components/Widget'. The spec builder, types, and registration in chartSpecBuilder.ts are identical — only the component's export path changes.
To graduate from alpha to stable later: move the component to components/, update the import in childrenAdapter.ts. No spec builder changes needed.
Step 12: S2 Parity
If the mark ships in S2:
- Create an S2 spec builder in
packages/vega-spec-builder-s2/src/widget/(often identical to S1 except using S2 color utilities) - Register it in
packages/vega-spec-builder-s2/src/chartSpecBuilder.ts - Create the S2 React component in the lab subpath,
packages/react-spectrum-charts-s2/src/lab/components/Widget/, and export it fromlab/components/index.ts. Every new chart type starts in lab; seeplanning/README.md - Register in
packages/react-spectrum-charts-s2/src/rscToSbAdapter/childrenAdapter.ts - Build with
yarn build:s2 - Create
packages/react-spectrum-charts-s2/src/dev/Widget/Dashboards/widget.dashboard.tsxmodeled on the Donut dashboard, with variations and a coverage map for the mark, its child components, and relevant siblings (see AGENTS.md's "Variation Dashboards") - Create
packages/docs-s2/docs/widget.mdxandsrc/playgrounds/widget.tsx, register the page insidebars.ts, and add a search card tosrc/search/chartExamples.tsx(seewriting-s2-docs-pages)
S2 differences: uses getS2ColorValue instead of getColorValue, no s2 boolean prop (always S2), no Venn support.
Checklist
vega-spec-builder:
types/marks/widgetSpec.types.ts— Options, OptionsWithDefaults, SpecOptionstypes/marks/index.ts— export addedtypes/chartSpec.types.ts— WidgetOptions in MarkOptions unionwidget/widgetTestUtils.ts— fully-populated fixturewidget/widgetMarkUtils.ts— mark factory functionswidget/widgetSpecBuilder.ts— addWidget, addData, addSignals, setScaleswidget/widgetMarkUtils.test.ts— mark structure testswidget/widgetSpecBuilder.test.ts— per-function testschartSpecBuilder.ts— counter, import, switch case
react-spectrum-charts:
types/marks/widget.types.ts— WidgetProps, WidgetElementtypes/marks/index.ts— export addedcomponents/Widget/Widget.tsx— render-null component with displayNamecomponents/Widget/index.ts— barrelcomponents/index.ts— export addedrscToSbAdapter/widgetAdapter.ts— getWidgetOptionsrscToSbAdapter/widgetAdapter.test.ts— adapter testsrscToSbAdapter/childrenAdapter.ts— case addedrscToSbAdapter/chartAdapter.test.ts— snapshot updatedutils/utils.ts— sanitizeRscChartChildren updatedstories/components/Widget/Widget.story.tsx— createdstories/components/Widget/Widget.test.tsx— created- S2:
dev/Widget/Dashboards/widget.dashboard.tsx— created with coverage map - S2:
docs-s2page, playgrounds, sidebar entry, and search card yarn verify— passes
Key Things That Go Wrong
Counter initialized wrong — The count starts at -1 so the first mark gets index 0. Initializing at 0 shifts all names by one and breaks snapshot tests.
Snapshot in chartAdapter.test.ts — This test asserts the complete ChartOptions shape. Every field with a default must appear in the expected object. Missing any field (even chartPopovers: []) causes a strict equality failure.
removeUnusedScales removes empty scales — addFieldToFacetScaleDomain only adds domain fields when the value is a field reference (string), not a static value { value: ... }. If your mark always uses a static color, no color scale domain field gets added, and the color scale gets removed from the final spec. This is correct behavior — don't fight it.
interactiveMarkName vs mark name — The interactive mark name is what Vega event listeners attach to. For most marks it equals the mark name. For marks with a separate hover layer (like a voronoi overlay), it should reference the voronoi mark name so events fire on the overlay, not the data mark.
Failing TypeScript but not tests — yarn test doesn't type-check. Run yarn verify (includes tsc) once the whole task is complete, before pushing (see AGENTS.md's Test Completeness Checklist) — don't run it proactively after each change.
Cognitive complexity — SonarQube flags functions whose cognitive complexity exceeds the threshold. Spec builder functions with many conditionals are the most common trigger. When a function grows complex, extract inline conditional chains or loops into named helper functions rather than inlining them. The addData and addMarks functions are the most likely candidates.
Copyright header missing — Every new .ts/.tsx source file requires the Apache 2.0 copyright block at the top. ESLint enforces this as a hard error. See architecture/file-creation.md for the exact header text. Story files (.story.tsx) are exempt.
Multi-line JSDoc/comments narrating the change — New functions get at most a one-line JSDoc (description + @param/@returns), matching the length of sibling functions in the same file. Do not add paragraphs explaining why the mark was added or what was investigated while building it — that belongs in the PR description, never the code. See AGENTS.md's Code Style section for a worked example.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

