> ## Documentation Index
> Fetch the complete documentation index at: https://docs.graphy.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Combo

export const GraphEmbed = ({src, story, aspectRatio, borderRadius = '14px', args}) => {
  const STORYBOOK_BASE_URL = typeof window !== 'undefined' && window.location.hostname === 'localhost' ? 'http://localhost:6006' : 'https://storybook-sdk.vercel.app';
  function buildArgsString(args) {
    if (!args) return '';
    const encodeValue = value => {
      if (value === null) return '!null';
      if (value === undefined) return '!undefined';
      if (typeof value === 'boolean') return `!${value}`;
      if (typeof value === 'number') return String(value);
      return encodeURIComponent(String(value));
    };
    const properties = Object.entries(args).map(([key, value]) => `${key}:${encodeValue(value)}`).join(';');
    return `&args=${properties}`;
  }
  const resolvedSrc = story ? `${STORYBOOK_BASE_URL}/iframe.html?id=${story}&viewMode=story&embed=1&globals=mode:readonly${buildArgsString(args)}` : src;
  const resolvedAspectRatio = aspectRatio ?? (story ? '16 / 9' : '10 / 6');
  return <iframe src={resolvedSrc} loading="lazy" allowfullscreen="true" style={{
    width: '100%',
    aspectRatio: resolvedAspectRatio,
    border: 'none',
    borderRadius,
    colorScheme: 'light'
  }} />;
};

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's how you show two series whose units or magnitudes don't 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 resolve the same built-in mark color and neither earns a legend entry. A per-layer `transform.constant` synthesizes a categorical column holding that layer's series name, and mapping it to `color` gives the layer its own palette color and its own legend item:

<GraphEmbed story="chart-types-combo-graph--revenue-bars-and-growth-line" />

```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.palette()
      ),
    []
  );

  return (
    <GraphProvider spec={spec} data={data}>
      <GraphRenderer />
    </GraphProvider>
  );
}
```

Each scale trains only on the layers bound to it, so the two metrics keep independent ranges. `transforms` on a layer run on top of any spec-level transforms and apply to that layer alone — reach for it whenever one geom needs a different shape of the data than its siblings.

`scale.ySecondary()` is optional: the compiler infers one as soon as a layer declares `yScaleType: 'secondary'`. Declare it when you want to control the domain, the same as any other scale.

## Configuring the second axis

The secondary axis is keyed `ySecondary` under [`axes`](/sdk-next/config/axes), and it's a sparse override — every field you leave out is inherited. `position` lands on the side opposite `y`; `isVisible`, `grid` and `ticks` come from `y` itself:

```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 series:

```tsx theme={null}
styles({
  overrides: [
    style.geom.line({ strokeWidth: 4, dashArray: [4, 2] }, { layer: 'growth' }),
    style.geom.bar({ cornerRadius: 'md' }, { layer: 'revenue' }),
  ],
});
```

`overrides` rather than `defaults`, because `color` here is decided by a mapping and only an override outranks one. 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 drawn over the bars it summarises — omit `yScaleType` and let the line sit on the primary scale. A dual axis there silently decouples 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.palette()
);
```

For a flat reference line at a constant value, `geom.rule()` is the purpose-built layer — non-interactive by default, with an optional inline `label`, and painted through `style.geom.rule({ stroke, strokeWidth, lineType })`:

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.