Axes provide scale context for your visualizations. Semiotic supports axes on all four sides of XY and Ordinal visualizations, with full control over labels, tick formatting, tick count, grid lines, and interactive axis behaviors. Both the simplified Chart components and the lower-level Frame components share the same axis configuration system.
With Charts
Chart components like LineChart and BarChart generate axes automatically based on the xLabel and yLabel props. This is the simplest way to add labeled axes to your visualization.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, sales: 4200 }, { month: 2, sales: 5800 }, { month: 3, sales: 4900 }, // ...more data points ], /* --- Process --- */ xAccessor: "month", yAccessor: "sales", /* --- Customize --- */ xLabel: "Month", yLabel: "Sales ($)" } export default () => { return <LineChart {...frameProps} /> }
For more control over axis behavior in Chart components, use the framePropsescape hatch to pass axis configuration directly:
JSX
<LineChart data={salesData} xAccessor="month" yAccessor="sales" frameProps={{ axes: [ { orient: "left", label: "Sales ($)", tickFormat: d => `${d.toLocaleString()}` }, { orient: "bottom", label: "Month", ticks: 6 } ] }} />
Exact vs. Nice Tick Endpoints
By default every Semiotic chart uses d3-scale's nice tick algorithm: tick values are rounded to readable numbers, but as a side effect the first and last tick may sit inside the data domain rather than at the actual min and max. For dashboards where the endpoints are the message — KPI dials, fixed score bands, regulatory-cutoff readouts — passaxisExtent="exact". The first and last tick pin to the literal data min and max, with equidistant intermediate ticks in between.
The prop is available on every XY and ordinal HOC and applies to:
- XY charts — both x and y axes (linear, time, log)
- Ordinal charts — the value axis (the categorical axis is a band scale and has no numeric ticks)
- Network / geo / hierarchy — no-op (no continuous axis)
Explicit tickValues still win over both modes. Use "exact" when you want the algorithm to do the work but the endpoints must read as the actual boundaries; reach for tickValues when you have hand-picked tick locations.
Trade-off: exact mode also pins the scale domain to the data extent — the usual 5% extent padding (which keeps symbols clear of the plot edge) is skipped. Data marks at the extremes will sit at the plot boundary. If you need both exact labels and visual breathing room, pass a hand-picked tickValues array viaframeProps.axes and leave the default "nice" mode in place.
Log-scale caveat: log scales always clamp both domain bounds to≥ 1e-6 (log of zero is undefined), so a dataset whose values include0 or negatives will render with that clamp in place even under exact mode — the first tick reads as max(dataMin, 1e-6), not the literal data minimum.
Time-series LineChart with exact endpoints
The default-nice version snaps to month boundaries — January 1st, February 1st, and so on. With axisExtent="exact", the first and last ticks read as the actual data dates (mid-January through end-of-August), and the y-axis pins to the actual revenue min and max rather than rounding to 4,000 / 12,000.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: axisExtentTemporalData, /* --- Size --- */ size: [600,320], /* --- Process --- */ xScaleType: "time", xAccessor: "date", yAccessor: "value", /* --- Customize --- */ xLabel: "Date", yLabel: "Revenue ($)", /* --- Annotate --- */ frameProps: { axes: [ { orient: "left", tickFormat: d => "$" + (d / 1000).toFixed(1) + "k" }, { orient: "bottom", tickFormat: d => new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric" }) }, ], }, /* --- Other --- */ axisExtent: "exact" } export default () => { return <LineChart {...frameProps} /> }
Scatterplot pinning both axes
With axisExtent="exact" the x-axis pins to 1.7 / 9.3 and the y-axis pins to 12 / 87 — the actual data ranges, not 0–10 and 0–100 like the nice algorithm would produce.
import { Scatterplot } from "semiotic" const frameProps = { /* --- Data --- */ data: axisExtentScatterData, /* --- Size --- */ size: [600,360], /* --- Process --- */ xAccessor: "x", yAccessor: "y", /* --- Customize --- */ xLabel: "Engagement score", yLabel: "Conversion rate (%)", pointRadius: 6, /* --- Other --- */ axisExtent: "exact" } export default () => { return <Scatterplot {...frameProps} /> }
SwarmPlot ordinal value axis
For ordinal charts the prop affects the r (value) axis only; the band-scaled categorical axis is unaffected. Here the value axis endpoints sit at the actual swarm min (4.2) and max (98.7) rather than rounding to 0 and 100.
import { SwarmPlot } from "semiotic" const frameProps = { /* --- Data --- */ data: axisExtentSwarmData, /* --- Size --- */ size: [600,360], /* --- Process --- */ valueAccessor: "score", categoryAccessor: "group", /* --- Customize --- */ colorBy: "group", /* --- Other --- */ axisExtent: "exact" } export default () => { return <SwarmPlot {...frameProps} /> }
Edge-Aligned Tick Labels
Default tick labels center on their tick mark, which is fine for middles but lets the first and last labels overflow past the plot's left and right edges. PassframeProps.axes[i].tickAnchor: "edges" to flip the first label totext-anchor: start and the last to text-anchor: end. On vertical axes the equivalent is dominant-baseline: hanging for the top tick anddominant-baseline: auto for the bottom tick — the labels nudge inward so they can't bleed past the chart bounds.
Pairs naturally with axisExtent="exact". Exact mode pins the ticks to the literal data min and max; edges mode keeps those edge labels readable.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ date: { }, value: 4200 }, { date: { }, value: 4900 }, ... ], /* --- Size --- */ margin: { left: 60, right: 20, top: 30, bottom: 40 }, /* --- Process --- */ xScaleType: "time", xAccessor: "date", yAccessor: "value", /* --- Customize --- */ xLabel: "Date", yLabel: "Revenue", curve: "monotoneX", /* --- Annotate --- */ frameProps: { axes: [ { orient: "bottom", tickAnchor: "edges" }, { orient: "left", tickAnchor: "edges" }, ], }, /* --- Other --- */ axisExtent: "exact" } export default () => { return <LineChart {...frameProps} /> }
Per-Axis CSS Targeting
Every axis renders as its own<g class="semiotic-axis semiotic-axis-{bottom|left|right|top}" data-orient="…">inside the .stream-axes wrapper. Tick text carries class="semiotic-axis-tick"; axis labels carryclass="semiotic-axis-label"; chart titles carryclass="semiotic-chart-title". Combine these for per-axis CSS without!important:
CSS
/* Make the y-axis labels larger than the x-axis */ .my-chart [data-orient="left"] .semiotic-axis-tick { font-size: 14px; } .my-chart [data-orient="bottom"] .semiotic-axis-tick { font-size: 11px; } /* Or override the font-size CSS variable on a wrapper — both axes will pick it up via the cascade. */ .dashboard-card { --semiotic-tick-font-size: 14px; --semiotic-axis-label-font-size: 16px; }
The default font sizes come from theme typography (tickSize,labelSize) and are emitted as --semiotic-tick-font-size and --semiotic-axis-label-font-size. SVG axes consume the vars via inline style={ fontSize: "var(--…)" }, so a CSS-var override on any DOM ancestor flows through the cascade without consumer code needing!important.
For chart HOCs such as LineChart, set those variables on a wrapper element. The axis labels below use --semiotic-axis-label-font-size: 20px; the tick labels use --semiotic-tick-font-size: 14px.
JSX
<div style={{ "--semiotic-tick-font-size": "14px", "--semiotic-axis-label-font-size": "20px", }} > <LineChart data={lineData} xAccessor="month" yAccessor="sales" xLabel="Month" yLabel="Sales ($)" /> </div>
With Frames
Frame components accept an axes prop — an array of axis configuration objects. Each object must include an orient property to specify where the axis appears.
Basic Left and Bottom Axes
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ label: "Revenue", coordinates: [ { step: 1, value: 4200 }, { step: 2, value: 5800 }, // ...more coordinates ] }], chartType: "line", /* --- Size --- */ margin: { top: 20, bottom: 60, left: 70, right: 20 }, /* --- Process --- */ xAccessor: "step", yAccessor: "value", lineDataAccessor: "coordinates", /* --- Customize --- */ lineStyle: { stroke: "#6366f1", strokeWidth: 2 }, showAxes: true, xLabel: "Month", yLabel: "Sales ($)" } export default () => { return <StreamXYFrame {...frameProps} /> }
Use tickFormat to control how tick labels render. This is especially useful for currencies, percentages, and dates.
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ label: "Revenue", coordinates: salesData }], chartType: "line", /* --- Size --- */ margin: { top: 20, bottom: 60, left: 80, right: 20 }, /* --- Process --- */ xAccessor: "step", yAccessor: "value", lineDataAccessor: "coordinates", /* --- Customize --- */ lineStyle: { stroke: "#6366f1", strokeWidth: 2 }, showAxes: true, xLabel: "Month", yLabel: "Revenue", axes: [ { orient: "left", label: "Revenue", tickFormat: d => `${(d / 1000).toFixed(0)}k` }, { orient: "bottom", label: "Month", tickFormat: d => ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][d - 1] } ] } export default () => { return <StreamXYFrame {...frameProps} /> }
Multiple Axes
You can place axes on all four sides. This is useful for dual-axis charts or adding reference scales.
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: frameLineData, chartType: "line", /* --- Size --- */ size: [500,300], margin: { top: 20, bottom: 50, left: 60, right: 60 }, /* --- Process --- */ xAccessor: "step", yAccessor: "value", lineDataAccessor: "coordinates", /* --- Customize --- */ lineStyle: { stroke: "#6366f1", strokeWidth: 2 }, showAxes: true, axes: [ { orient: "left", label: "Primary" }, { orient: "right", label: "Secondary" }, { orient: "bottom", label: "Step" }, ] } export default () => { return <StreamXYFrame {...frameProps} /> }
Jagged Base for Non-Zero Baselines
When your data does not start at zero, set jaggedBase to true to render a torn-edge tick at the minimum data point. This is a classic data visualization technique to signal that the axis has been truncated.
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ label: "Revenue", coordinates: salesData }], chartType: "line", /* --- Size --- */ margin: { top: 20, bottom: 60, left: 70, right: 20 }, /* --- Process --- */ xAccessor: "step", yAccessor: "value", yExtent: [4000, undefined], lineDataAccessor: "coordinates", /* --- Customize --- */ lineStyle: { stroke: "#6366f1", strokeWidth: 2 }, showAxes: true, axes: [ { orient: "left", baseline: false, jaggedBase: true }, { orient: "bottom" } ] } export default () => { return <StreamXYFrame {...frameProps} /> }
Custom Tick Lines
The tickLineGenerator prop lets you render custom tick line elements. This example draws dashed rectangular tick bands:
JSX
<StreamXYFrame axes={[ { orient: "left", baseline: "under", tickLineGenerator: ({ xy }) => ( <path style={{ fill: "#efefef", stroke: "#ccc", strokeDasharray: "2 2" }} d={`M${xy.x1},${xy.y1 - 5}L${xy.x2},${xy.y1 - 5}L${xy.x2},${xy.y1 + 5}L${xy.x1},${xy.y1 + 5}Z`} /> ) } ]} />
Landmark Ticks for Time Series
When displaying time series data, landmarkTicks applies semibold styling to tick labels at higher-level time boundaries (e.g., when the month or year changes). This provides hierarchical context that dramatically improves readability of long time axes. Notice how "Feb" and "Mar" are rendered bolder than regular day ticks:
JSX
<StreamXYFrame data={timeSeriesData} // 90 days, Jan–Mar 2024 xAccessor="date" xScaleType="time" // scaleTime for date-aware ticks yAccessor="value" axes={[ { orient: "left", label: "Value" }, { orient: "bottom", label: "Date", landmarkTicks: true, // bold at month boundaries tickFormat: d => { const date = new Date(d) return `${date.toLocaleString("en", {month:"short"})} ${date.getDate()}` }, ticks: 8, } ]} />
Landmark ticks render at fontSize: 11 with fontWeight: 600(semibold), while regular ticks remain at fontSize: 10 with normal weight. You can also pass a custom function:
JSX
// Custom landmark detection axes={[{ orient: "bottom", landmarkTicks: (d, i) => d.getMonth() === 0, // bold only January ticks }]}
Explicit Tick Values
Use tickValues when you need exact control over which ticks appear:
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: frameLineData, chartType: "line", /* --- Size --- */ size: [500,300], /* --- Process --- */ xAccessor: "step", yAccessor: "value", lineDataAccessor: "coordinates", /* --- Customize --- */ lineStyle: { stroke: "#6366f1", strokeWidth: 2 }, showAxes: true, axes: [ { orient: "left", tickValues: [3000, 5000, 7000, 9000], tickFormat: d => "$" + d.toLocaleString() }, { orient: "bottom", tickValues: [1, 4, 7, 10], tickFormat: d => "Step " + d }, ] } export default () => { return <StreamXYFrame {...frameProps} /> }
Include Max Tick
Set includeMax: true to ensure the domain maximum always appears as a labeled tick, even if d3's default tick generation skips it.
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: frameLineData, chartType: "line", /* --- Size --- */ size: [500,300], /* --- Process --- */ xAccessor: "step", yAccessor: "value", lineDataAccessor: "coordinates", /* --- Customize --- */ lineStyle: { stroke: "#6366f1", strokeWidth: 2 }, showAxes: true, axes: [ { orient: "left", label: "Value", includeMax: true }, { orient: "bottom", label: "Step", includeMax: true }, ] } export default () => { return <StreamXYFrame {...frameProps} /> }
Auto-Rotate Labels
Set autoRotate: true to automatically rotate bottom-axis labels 45° when horizontal spacing is too tight to fit them. Drag the handle to resize the chart and see labels switch between rotated and horizontal:
Drag the handle to resize — 300px wide
JSX
<StreamXYFrame xScaleType="time" axes={[ { orient: "bottom", autoRotate: true, tickFormat: d => new Date(d).toLocaleDateString("en-US", { weekday: "short", month: "long", day: "numeric", year: "numeric" }) }, ]} />
Dashed & Dotted Grid Lines
Use gridStyle to change grid lines to dashed or dotted. Accepts"dashed", "dotted", or a custom strokeDasharraystring. Requires showGrid to be enabled.
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: frameLineData, chartType: "line", /* --- Size --- */ size: [500,300], /* --- Process --- */ xAccessor: "step", yAccessor: "value", lineDataAccessor: "coordinates", /* --- Customize --- */ lineStyle: { stroke: "#6366f1", strokeWidth: 2 }, showAxes: true, showGrid: true, axes: [ { orient: "left", gridStyle: "dotted" }, { orient: "bottom", gridStyle: "dashed" }, ] } export default () => { return <StreamXYFrame {...frameProps} /> }
Configuration
Each axis object in the axes array accepts the following properties:
| Prop | Type | Required | Default | Description |
|---|
orient | string | Yes | — | Position of the axis: "left", "right", "top", or "bottom". |
label | string | object | — | — | Axis label text. Can be a string or an object with { name, locationDistance, position } for precise placement. |
ticks | number | — | — | Suggested number of ticks to display. The actual count may vary based on D3 tick algorithm. |
tickValues | array | — | — | Explicit array of tick values to use instead of auto-generated ticks. |
tickFormat | function | — | — | Function to format tick labels. Receives the tick value and returns a string. |
tickLineGenerator | function | — | — | Custom function to render tick lines. Receives { xy } with { x1, x2, y1, y2 } coordinates. |
baseline | boolean | string | — | true | Show the baseline. Set to false to hide, or "under" to draw beneath the visualization layer. |
jaggedBase | boolean | — | false | Renders the tick at the minimum data point with a "torn" appearance for non-zero baselines. |
showOutboundTickLines | boolean | — | false | Display tick lines outside the chart area to accompany tick labels. |
axisAnnotationFunction | function | — | — | Enables hover interaction on the axis. Called with { className, type, value } on click. |
glyphFunction | function | — | — | Custom hover glyph on axis. Receives { lineWidth, lineHeight, value } and returns JSX. |
marginalSummaryGraphics | object | — | — | Add an ordinal summary (histogram, violin, etc.) to the axis margin. |
landmarkTicks | boolean | function | — | false | Highlight ticks at time boundaries with semibold styling. Set to true for auto-detection (Date boundaries), or pass a function (d, i) => boolean for custom landmark logic. |
Baseline Options
The baseline prop controls the perpendicular line drawn at the axis edge. By default it renders above the visualization layer. Set it to "under" to draw it beneath, or false to hide it entirely.
JSX
// Baseline above visualization (default) { orient: "left", baseline: true } // Baseline beneath visualization { orient: "left", baseline: "under" } // No baseline { orient: "left", baseline: false }
Outbound Tick Lines
Set showOutboundTickLines to true to draw additional tick lines extending outside the chart area alongside the tick labels.
JSX
<StreamXYFrame axes={[ { orient: "left", label: "Value", showOutboundTickLines: true } ]} />
Axis Interactivity
The axisAnnotationFunction prop enables hover and click interaction on the axis. When set, hovering over the axis displays a guideline, and clicking fires the callback with the axis value. Use glyphFunction to customize the hover display.
JSX
<StreamXYFrame axes={[ { orient: "left", label: "Value", axisAnnotationFunction: ({ value }) => { console.log("Clicked axis at:", value) // Use this value to set a threshold annotation, filter data, etc. } } ]} />
- StreamXYFrame — the underlying Frame for line, area, and point visualizations
- StreamOrdinalFrame — the underlying Frame for bar, swarm, and categorical visualizations
- Annotations — adding callouts, highlights, and threshold lines
- Tooltips — hover-triggered data display
- Responsive — making frames resize with their container