import { AxisEnv, AxisPosition, CartesianAxisInstance, Insets, LayoutRect, MeasureText } from '../../../shared/kernel'; import { ColorValue, Degrees, FontOptions, Pixels, Switchable } from '../../../shared/options'; import { BandScale, AnyScale } from '../../../shared/scale'; import { Group } from '../../../shared/scene'; import { Bounds } from '../../../shared/util'; export interface AxisLabelFormatterParams { value: unknown; index: number; } export type AxisLabelPlacement = 'outside' | 'inside'; export type AxisInsideLabelAlign = 'element' | 'gap'; /** * What a label does with room it does not fit into: 'thin' drops the crowded * ones and draws the rest in full, 'ellipsis' keeps every label and cuts it. */ export type AxisLabelOverflow = 'thin' | 'ellipsis'; export interface AxisCrossLineOptions { type?: 'line' | 'range'; value?: unknown; range?: [unknown, unknown]; stroke?: ColorValue; strokeWidth?: Pixels; lineDash?: Pixels[]; fill?: ColorValue; fillOpacity?: number; label?: { text?: string; color?: ColorValue; fontSize?: Pixels; }; } export interface AxisBaseOptions { position?: AxisPosition; /** * Which series this axis carries, by their value field (`yField`, or the * low/high/OHLC fields of the multi-field series) or by series `id`. Only * meaningful with two value axes — it is what tells `left` from `right`. * A series matching no axis falls back to the first value axis without keys. */ keys?: string[]; title?: Switchable & FontOptions & { text?: string; }; /** The axis line itself, styled like the grid: colour, width and dash pattern. */ line?: Switchable & { stroke?: ColorValue; width?: Pixels; /** Dash pattern, as `gridLine.lineDash`; an empty array draws a solid line. */ lineDash?: Pixels[]; }; tick?: Switchable & { /** Tick length, px (6 by default). */ size?: Pixels; width?: Pixels; /** Tick colour; `color` is an alias of it. */ stroke?: ColorValue; color?: ColorValue; /** Dash pattern of a tick mark; solid by default. */ lineDash?: Pixels[]; }; label?: Switchable & FontOptions & { /** Gap from the axis line (from the tick, when ticks are on). Outside labels only. */ spacing?: Pixels; /** * 'outside' (default) — labels next to the axis line; 'inside' — inside * the plot area: on a vertical axis above the band (over the bar), on a * horizontal one along the inner edge. Inside labels reserve no space. */ placement?: AxisLabelPlacement; /** Inside placement: indent from the axis into the plot area. */ insideSpacing?: Pixels; /** Inside placement: gap to the element the label belongs to (and to the one before it). */ insideGap?: Pixels; /** * Inside placement on a band axis: 'element' (default) — the label hugs its * own element, `insideGap` away from it; 'gap' — the label is centred in the * gap between elements. */ insideAlign?: AxisInsideLabelAlign; /** Serializable format string (',.2f', '.0%', '%d %b'). */ format?: string; formatter?: (params: AxisLabelFormatterParams) => string; /** * Tilt of the labels, degrees clockwise. `-45` on a bottom axis slants * them up to the right, each name ending at its own tick — the classic * way of fitting long category names without dropping any of them; `45` * slants them the other way, `-90` stands them on end. The axis reserves * the room the tilted text asks for, and tilted labels clear each other * once a line of text fits between them, whatever their length, so far * fewer of them have to go. Outside labels only. * * `'auto'` leaves the angle to the axis: the labels stand level while * they all fit that way, and tilt — as gently as the step allows — the * moment one of them would otherwise have to go. */ rotation?: Degrees | 'auto'; /** Skip overlapping labels (true by default). */ avoidCollisions?: boolean; /** * What a label that does not fit its room does: 'thin' (default) — the * crowded ones are dropped and the rest are drawn whole; 'ellipsis' — * every label stays and is cut to the room between two ticks, so long * category names stop running into their neighbours and the tick lines * between them. Only the horizontal axis crowds this way; on a vertical * one the cut is what `maxWidth` asks for. */ overflow?: AxisLabelOverflow; /** * Widest a label may be, px. Anything longer is cut with `label.ellipsis` * whatever `overflow` says; on a vertical axis this also caps the room * the labels take away from the plot. */ maxWidth?: Pixels; /** The mark standing where the text was cut: '..' by default, '…' reads too. */ ellipsis?: string; }; gridLine?: Switchable & { stroke?: ColorValue; width?: Pixels; lineDash?: Pixels[]; }; interval?: { /** Explicit tick values. */ values?: unknown[]; /** Minimum spacing between labels, px. */ minSpacing?: Pixels; }; crossLines?: AxisCrossLineOptions[]; } /** Band padding shared by the categorical axes. */ export declare const DEFAULT_PADDING_INNER = 0.2; export declare const DEFAULT_PADDING_OUTER = 0.1; export declare abstract class BaseAxis implements CartesianAxisInstance { protected readonly options: O; protected readonly env: AxisEnv; abstract readonly type: string; abstract readonly scale: AnyScale; /** Set by setDomain when the domain it was handed made no sense; the chart says so out loud. */ domainError: string | undefined; constructor(options: O, env: AxisEnv); get position(): AxisPosition; get keys(): readonly string[] | undefined; protected get isHorizontal(): boolean; /** Labels drawn inside the plot area: they take no thickness from the plot rect. */ protected get labelsInside(): boolean; /** Ticks are off in most themes: the labels alone read the scale well enough. */ protected get ticksVisible(): boolean; /** Tick label size — the theme's base size unless the axis overrides it. */ protected get labelSize(): Pixels; /** Tick label colour — the axis option, then the theme, then the muted ink. */ protected get labelColor(): ColorValue; /** Indent of an inside label from the axis into the plot area. */ private get insideSpacing(); /** Gap an inside label keeps from its own element and from the one before it. */ private get insideGap(); /** Vertical space one inside label needs: the glyph row plus a gap on both sides. */ protected insideLabelSlot(): number; /** * Binds a band scale to the plot rect and resolves the gap between bands: * `gapPx` wins over the `paddingInner` fraction, and an inside label row is * added on top of whichever was asked for — the requested gap keeps its meaning * either way. A row is also reserved above the first band. */ protected layoutBandScale(scale: BandScale, plot: LayoutRect, paddingInner: number, gapPx?: Pixels): void; abstract setDomain(domain: unknown[]): void; abstract layout(plot: LayoutRect): void; /** Tick positions in pixels and their values. */ protected abstract tickInfo(): Array<{ value: unknown; coord: number; }>; /** * The ticks the layout measures itself against. Usually the ones on screen — * but partway through an update those are only the ones the frame has reached, * and a layout following them would give the plot back the room of a label * that has not arrived yet, then take it away the moment it does. So an axis * that walks between scales answers here with the ticks it is settling on, * at the places they will settle in. */ protected measurementTicks(): Array<{ value: unknown; coord: number; index: number; }>; /** * How much of its band a tick's category holds, 0..1 — whole for everything * an axis without bands carries, and less for a category on its way in or out * of an update. */ protected tickWeight(_value: unknown): number; protected formatTick(value: unknown, index: number): string; protected labelFont(): string; /** Labels are cut to their room instead of thinning out. */ protected get labelCuts(): boolean; /** Room labels keep between one another before one of them has to go. */ protected get minLabelSpacing(): number; /** Widest a label may be regardless of how much room its tick leaves it. */ protected get labelMaxWidth(): number; /** Tilt of the tick labels, degrees clockwise; 0 leaves them level. */ protected get labelRotation(): Degrees; /** * The tilt `'auto'` worked out, and what it was worked out against. The * answer feeds back into the question — tilted labels ask for room at the * ends of the axis, which moves the plot, which moves the step the tilt was * chosen for — so within one set of bounds a tilt only ever steepens, and the * passes of a layout settle instead of chasing each other. The domain the * chart hands the axis is new on every render, so a resize, or data of its * own, is decided from scratch. */ private settledTilt; /** * The angle `'auto'` settles on: none while every label fits standing level, * and otherwise the gentlest tilt the step leaves room for — the steepest one * where even that is not enough, with the ordinary thinning taking over from * there. Nothing along this path may go through the tilted-label helpers: * they are the ones asking for the angle. */ private get autoTilt(); /** * The gentlest tilt the room between two ticks leaves for a label — none, * where the labels fit level. A tilted label needs a line of text between * the strips rather than the length of its own name, which is why an axis * that cannot fit two names side by side still fits every one of them at 30°. */ private tiltForRoom; /** * The ticks a tilt is worked out against: every one the axis carries, before * any of them are thinned out, since what is being asked is whether they all * fit. An axis walking between two scales answers with the one it is settling * on — a tilt taken from the crowd of a frame partway through would come and * go with the animation. */ protected tiltTicks(): Array<{ value: unknown; coord: number; index: number; }>; /** Whether the labels are drawn at an angle. Inside labels are always level. */ protected get labelsRotated(): boolean; /** * How a label hangs off its anchor. A level label reads along the axis and is * centred on its tick; a tilted one ends at its tick — or starts there, where * the tilt runs the other way — and is centred across the text, so the same * anchor serves at every angle. Of the two ways to hang a tilted label off a * tick, the axis takes the one leaning away from the plot. */ protected labelAnchoring(): { align: CanvasTextAlign; baseline: CanvasTextBaseline; rotation: Degrees; }; /** * How far from the axis line the labels hang: past the ticks, past the gap * they keep, and past the lean a tilted label takes back over that gap. * Where the label rows begin, and where anything drawn among them belongs. */ protected labelAnchorDepth(): number; /** Box a label of that width covers around its anchor, tilt and all. */ protected labelBox(width: number): Bounds; /** * How far a label reaches from its anchor across the axis: `near` back * towards the axis line, `far` away from it. A tilted label is centred across * its text, so it leans back over the gap it was given — the anchor moves out * by `near` and the gap reads as the one that was asked for. */ protected labelReach(box: Bounds): { near: number; far: number; }; /** Room the labels take across the axis, the deepest of them deciding. */ protected labelSpan(ticks: Array<{ value: unknown; index: number; }>, room: number, measureText: MeasureText): number; /** * Room one label needs along the axis before the next may follow it. A level * label is as long as its own text; tilted labels lie in parallel strips, and * two strips clear each other as soon as the axis has stepped far enough for a * line of text to fit between them — whatever the names are, and which is why * a tilt keeps labels a level axis would have to drop. Neither ever needs more * than the width of the box the label covers. */ protected labelStride(ticks: Array<{ value: unknown; index: number; }>): number; /** * Room one label has along a horizontal axis: the step between neighbouring * ticks, less the spacing labels keep from each other — the same distance * that decides thinning, spent on cutting instead. A vertical axis reads its * labels across, so only `maxWidth` bounds them. */ protected labelRoom(ticks: Array<{ coord: number; }>): number; /** Tick label as it is drawn: the formatted value, cut to `room` when it overruns. */ protected tickLabel(value: unknown, index: number, room: number, measureText: MeasureText): string; /** Text measurement without a layout context, as a `MeasureText`. */ protected get ownMeasureText(): MeasureText; /** Ticks accounting for interval.values and skipping of overlapping labels. */ protected displayTicks(): Array<{ value: unknown; coord: number; index: number; }>; /** * Whether two ticks are allowed to read the same. A scale of numbers is a * ruler: every mark on it stands for a different amount, and a label that * repeats is a format too coarse for the step rather than a fact about the * data. Categories are not like that — two of them may honestly share a name. */ protected get labelsMustDiffer(): boolean; /** * Ticks the reader can tell apart. A step finer than the format prints the * same text several times over — «1M» once per 200 000, five in a row — and a * scale that repeats itself says nothing about where a value sits. So the * axis keeps every n-th tick instead, for the smallest n that leaves * neighbouring labels different: the step of the scale grows to the one its * own labels can carry, and the grid follows it. */ protected thinRepeats(ticks: T[]): T[]; /** * Every n-th item, for the smallest n whose neighbours carry different text. * Nothing is dropped when even the widest stride keeps repeating: a format * that says one thing for the whole scale is answered elsewhere, not by * thinning the axis down to a single mark. */ protected keepDistinct(items: T[], label: (item: T, index: number) => string): T[]; /** * Which ticks keep their labels when they cannot all fit: every `stride`-th * one, for the room `labelExtent` one label takes. Axes that group their * categories thin them run by run instead. */ protected thinTicks(ticks: T[], labelExtent: number): T[]; /** Every tick the axis carries, in order, before any of them are thinned out. */ protected allTicks(): Array<{ value: unknown; coord: number; index: number; }>; /** Coordinate of a value on the scale (for interval.values and crossLines). */ protected coordOf(value: unknown): number; measure(measureText: MeasureText): number; /** * A tick label is centred on its tick, so the outermost ones hang over the * ends of the plot rect — by half their width on a horizontal axis, by half * a line on a vertical one. That is the room the layout has to find for them. */ labelOverflow(measureText: MeasureText, plot: LayoutRect): Insets; render(axisLayer: Group, gridLayer: Group, plot: LayoutRect, foregroundLayer?: Group): void; /** Tick label with the label font applied; positioning is up to the caller. */ private labelNode; /** * Labels inside the plot area: on a vertical axis above the band (over the bar, * flush with the start of the value axis), on a horizontal one along the inner * edge of the plot rect. Continuous scales fall back to the tick coordinate. */ private renderInsideLabels; /** Gap between two bands in px; undefined on a continuous scale. */ private bandGap; /** Top (or left) edge of the band for a value; undefined on a continuous scale. */ private bandStart; private renderCrossLines; private appendCrossLineLabel; private measureCtx; /** Rough text measurement without a layout context. */ protected measureWithCanvasFallback(text: string, font: string): number; /** Coordinate of the axis line (the plot rect edge on the position side). */ private edgeCoordinate; /** Outward direction from the plot rect: +1 down/right, −1 up/left. */ private outwardSign; } /** Default look of a number on an axis: millions and thousands are shortened. */ export declare function formatAxisNumber(value: number): string;