agentleFS
adobeadobe/react-spectrum-charts/.agents/skills/adding-chart-marks/SKILL.md

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>.

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
  1. Distinctive:

    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 …

    line 8
  2. Very specific:

    Read architecture/mark-internals.md, architecture/encoding-and-props.md, and architecture/file-creation.md — this is the most involved …

    line 10
  3. Very specific:

    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 …

    line 16
  4. Distinctive:

    If the implementation diverges from the issue, update the issue body.

    line 22
  5. Very specific:

    Add WidgetOptions to the MarkOptions union in packages/vega-spec-builder/src/types/chartSpec.types.ts.

    line 70
  6. Very specific:

    Create packages/vega-spec-builder/src/widget/widgetTestUtils.ts with a fully-populated defaultWidgetOptions: WidgetSpecOptions.

    line 74
  7. Very specific:

    Use toHaveProperty in tests — never direct property access on Vega encode objects due to ProductionRule<T> union types.

    line 99
  8. Very specific:

    Add to initializeComponentCounts(): widgetCount: -1

    line 158
  9. Very specific:

    Create the barrel index.ts.

    line 202
  10. Distinctive:

    Add the import and dispatch case:

    line 222
  11. Very specific:

    Add Widget.displayName to sanitizeRscChartChildren in utils.ts.

    line 229
  12. Distinctive:

    After writing tests, type-check once at task completion — not proactively after every file.

    line 245
  13. Very specific:

    If the mark is not ready for the public API, place the component in packages/react-spectrum-charts/src/alpha/components/Widget/ and import …

    line 249
  14. Distinctive:

    If the mark ships in S2:

    line 255
  15. Very specific:

    Create an S2 spec builder in packages/vega-spec-builder-s2/src/widget/ (often identical to S1 except using S2 color utilities)

    line 256
  16. Very specific:

    Create the S2 React component in the lab subpath, packages/react-spectrum-charts-s2/src/lab/components/Widget/, and export it from …

    line 258
  17. Very specific:

    Create packages/react-spectrum-charts-s2/src/dev/Widget/Dashboards/widget.dashboard.tsx modeled on the Donut dashboard, with variations and …

    line 261
  18. Very specific:

    Create packages/docs-s2/docs/widget.mdx and src/playgrounds/widget.tsx, register the page in sidebars.ts, and add a search card to …

    line 262

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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

  1. Add import: import { addWidget } from './widget/widgetSpecBuilder';
  2. Add to initializeComponentCounts(): widgetCount: -1
  3. 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:

  1. Create an S2 spec builder in packages/vega-spec-builder-s2/src/widget/ (often identical to S1 except using S2 color utilities)
  2. Register it in packages/vega-spec-builder-s2/src/chartSpecBuilder.ts
  3. Create the S2 React component in the lab subpath, packages/react-spectrum-charts-s2/src/lab/components/Widget/, and export it from lab/components/index.ts. Every new chart type starts in lab; see planning/README.md
  4. Register in packages/react-spectrum-charts-s2/src/rscToSbAdapter/childrenAdapter.ts
  5. Build with yarn build:s2
  6. Create packages/react-spectrum-charts-s2/src/dev/Widget/Dashboards/widget.dashboard.tsx modeled 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")
  7. Create packages/docs-s2/docs/widget.mdx and src/playgrounds/widget.tsx, register the page in sidebars.ts, and add a search card to src/search/chartExamples.tsx (see writing-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, SpecOptions
  • types/marks/index.ts — export added
  • types/chartSpec.types.ts — WidgetOptions in MarkOptions union
  • widget/widgetTestUtils.ts — fully-populated fixture
  • widget/widgetMarkUtils.ts — mark factory functions
  • widget/widgetSpecBuilder.ts — addWidget, addData, addSignals, setScales
  • widget/widgetMarkUtils.test.ts — mark structure tests
  • widget/widgetSpecBuilder.test.ts — per-function tests
  • chartSpecBuilder.ts — counter, import, switch case

react-spectrum-charts:

  • types/marks/widget.types.ts — WidgetProps, WidgetElement
  • types/marks/index.ts — export added
  • components/Widget/Widget.tsx — render-null component with displayName
  • components/Widget/index.ts — barrel
  • components/index.ts — export added
  • rscToSbAdapter/widgetAdapter.ts — getWidgetOptions
  • rscToSbAdapter/widgetAdapter.test.ts — adapter tests
  • rscToSbAdapter/childrenAdapter.ts — case added
  • rscToSbAdapter/chartAdapter.test.ts — snapshot updated
  • utils/utils.ts — sanitizeRscChartChildren updated
  • stories/components/Widget/Widget.story.tsx — created
  • stories/components/Widget/Widget.test.tsx — created
  • S2: dev/Widget/Dashboards/widget.dashboard.tsx — created with coverage map
  • S2: docs-s2 page, 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.