@chipmobilesdk/rn-chart 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The shared datasource contract (FR-007, FR-007a).
3
+ *
4
+ * One contract spans every chart family: each point carries the same envelope —
5
+ * identity, value state, optional unit and format hint, opaque metadata — and
6
+ * only its **position** varies by kind. Families that accept the same position
7
+ * kind consume one another's datasources unchanged; anything else is a
8
+ * validation error rather than a rendering guess.
9
+ *
10
+ * See specs/007-rn-chart-package/data-model.md §2–§6.
11
+ */
12
+ import type { FormatHint } from './format';
13
+ import type { ReferenceMarker } from './marker';
14
+ import type { ValueState } from './value';
15
+
16
+ export type ChartFamily =
17
+ | 'progress'
18
+ | 'distribution'
19
+ | 'timeSeries'
20
+ | 'bar'
21
+ | 'heatmap';
22
+
23
+ /** A named bucket: a category axis, a distribution segment, a progress metric. */
24
+ export interface CategoryPosition {
25
+ readonly kind: 'category';
26
+ readonly category: string;
27
+ }
28
+
29
+ /**
30
+ * An absolute instant as epoch milliseconds, UTC.
31
+ *
32
+ * A number rather than a `Date` or an ISO string: cheap to compare and
33
+ * memoize, no per-render parsing across hundreds of points, no identity churn
34
+ * that would defeat the re-render guarantee, and no ambiguous offset. All
35
+ * date wording comes from app formatters (FR-016).
36
+ */
37
+ export interface InstantPosition {
38
+ readonly kind: 'instant';
39
+ readonly at: number;
40
+ }
41
+
42
+ /** A cell in a row-by-column matrix, for heatmaps. */
43
+ export interface MatrixPosition {
44
+ readonly kind: 'matrix';
45
+ readonly row: string;
46
+ readonly column: string;
47
+ }
48
+
49
+ export type Position = CategoryPosition | InstantPosition | MatrixPosition;
50
+ export type PositionKind = Position['kind'];
51
+
52
+ /** Non-colour encoding for a series, so meaning never rides on hue alone (FR-064). */
53
+ export type SeriesPattern = 'solid' | 'dashed' | 'dotted' | 'dashDot';
54
+
55
+ export interface DataPoint<M = unknown> {
56
+ /** Stable and unique within its series. Duplicates are a validation error. */
57
+ readonly id: string;
58
+ readonly position: Position;
59
+ readonly value: ValueState;
60
+ /** Overrides the series unit. */
61
+ readonly unit?: string;
62
+ /** Overrides the series format hint. */
63
+ readonly format?: FormatHint;
64
+ /** Opaque to the package: never read, never logged, returned unchanged. */
65
+ readonly metadata?: M;
66
+ }
67
+
68
+ export interface Series<M = unknown> {
69
+ /** Stable and unique within the datasource. */
70
+ readonly id: string;
71
+ readonly label: string;
72
+ readonly points: readonly DataPoint<M>[];
73
+ readonly unit?: string;
74
+ readonly format?: FormatHint;
75
+ /** Explicit colour override; otherwise the theme palette assigns one. */
76
+ readonly color?: string;
77
+ readonly pattern?: SeriesPattern;
78
+ /** Initial visibility. Runtime toggling is controlled separately. */
79
+ readonly hidden?: boolean;
80
+ readonly metadata?: M;
81
+ }
82
+
83
+ export interface ChartDatasource<M = unknown> {
84
+ /** Declared once, then validated against every point and the receiving family. */
85
+ readonly positionKind: PositionKind;
86
+ readonly series: readonly Series<M>[];
87
+ readonly markers?: readonly ReferenceMarker[];
88
+ readonly unit?: string;
89
+ readonly format?: FormatHint;
90
+ readonly metadata?: M;
91
+ }
92
+
93
+ /**
94
+ * Which position kinds each family accepts.
95
+ *
96
+ * Bar accepting both `category` and `instant` is what makes swapping a
97
+ * time-series chart for a bar chart free — the datasource does not change.
98
+ */
99
+ export const FAMILY_POSITION_KINDS: Readonly<
100
+ Record<ChartFamily, readonly PositionKind[]>
101
+ > = {
102
+ progress: ['category'],
103
+ distribution: ['category'],
104
+ bar: ['category', 'instant'],
105
+ timeSeries: ['instant'],
106
+ heatmap: ['matrix'],
107
+ };
108
+
109
+ export function familyAcceptsPositionKind(
110
+ family: ChartFamily,
111
+ kind: PositionKind,
112
+ ): boolean {
113
+ return FAMILY_POSITION_KINDS[family].includes(kind);
114
+ }
115
+
116
+ /**
117
+ * Resolves unit and format through the documented precedence:
118
+ * point -> series -> datasource -> undefined (caller falls back to its default).
119
+ */
120
+ export function resolveFormat<M>(
121
+ point: DataPoint<M> | undefined,
122
+ series: Series<M> | undefined,
123
+ datasource: ChartDatasource<M>,
124
+ ): FormatHint | undefined {
125
+ return point?.format ?? series?.format ?? datasource.format;
126
+ }
127
+
128
+ export function resolveUnit<M>(
129
+ point: DataPoint<M> | undefined,
130
+ series: Series<M> | undefined,
131
+ datasource: ChartDatasource<M>,
132
+ ): string | undefined {
133
+ return point?.unit ?? series?.unit ?? datasource.unit;
134
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Typed error taxonomy and failure reporting (FR-038, FR-038a, FR-038b).
3
+ *
4
+ * Messages come from a **fixed catalogue with no interpolated data**. That is
5
+ * what makes the redaction guarantee structural rather than a rule someone has
6
+ * to remember: because the package never builds a message out of a value or a
7
+ * label, a failure report cannot carry patient data or account balances into a
8
+ * consuming app's crash reporting.
9
+ */
10
+ import type { ChartFamily, PositionKind } from './datasource';
11
+
12
+ export type ChartErrorCategory = 'validation' | 'declared' | 'render';
13
+
14
+ export type ChartErrorCode =
15
+ | 'duplicateSeriesId'
16
+ | 'duplicatePointId'
17
+ | 'positionKindMismatch'
18
+ | 'familyPositionKindUnsupported'
19
+ | 'inconsistentMatrix'
20
+ | 'distributionNotWhole'
21
+ | 'declaredByApp'
22
+ | 'renderException';
23
+
24
+ /**
25
+ * Fixed message catalogue. Every entry is a constant string — no template
26
+ * holes, so no data can leak through a message.
27
+ */
28
+ const MESSAGES: Readonly<Record<ChartErrorCode, string>> = {
29
+ duplicateSeriesId:
30
+ 'Datasource contains duplicate series identifiers. Series ids must be unique.',
31
+ duplicatePointId:
32
+ 'A series contains duplicate point identifiers. Point ids must be unique within their series.',
33
+ positionKindMismatch:
34
+ 'A data point position kind does not match the datasource positionKind.',
35
+ familyPositionKindUnsupported:
36
+ 'This chart family does not accept the datasource position kind.',
37
+ inconsistentMatrix:
38
+ 'Matrix datasource rows and columns are inconsistent across points.',
39
+ distributionNotWhole:
40
+ 'Distribution segments do not sum to the whole. The chart marks the total incomplete rather than rescaling.',
41
+ declaredByApp: 'The application declared this chart to be in an error state.',
42
+ renderException:
43
+ 'The chart failed while rendering and was contained. See the report for details.',
44
+ };
45
+
46
+ export interface ChartError {
47
+ readonly category: ChartErrorCategory;
48
+ readonly code: ChartErrorCode;
49
+ readonly message: string;
50
+ readonly chartId?: string;
51
+ readonly family?: ChartFamily;
52
+ /**
53
+ * Structural detail that is safe to expose: counts and kinds only, never a
54
+ * value or a label.
55
+ */
56
+ readonly detail?: {
57
+ readonly expectedKind?: PositionKind;
58
+ readonly actualKind?: PositionKind;
59
+ readonly occurrences?: number;
60
+ };
61
+ }
62
+
63
+ export interface ChartFailureReport {
64
+ readonly chartId?: string;
65
+ readonly family: ChartFamily;
66
+ readonly error: ChartError;
67
+ readonly componentStack?: string;
68
+ }
69
+
70
+ export type ChartFailureHandler = (report: ChartFailureReport) => void;
71
+
72
+ export function chartError(
73
+ category: ChartErrorCategory,
74
+ code: ChartErrorCode,
75
+ extra?: Omit<ChartError, 'category' | 'code' | 'message'>,
76
+ ): ChartError {
77
+ return {
78
+ category,
79
+ code,
80
+ message: MESSAGES[code],
81
+ ...extra,
82
+ };
83
+ }
84
+
85
+ export function validationError(
86
+ code: ChartErrorCode,
87
+ extra?: Omit<ChartError, 'category' | 'code' | 'message'>,
88
+ ): ChartError {
89
+ return chartError('validation', code, extra);
90
+ }
91
+
92
+ export function isChartError(e: unknown): e is ChartError {
93
+ if (typeof e !== 'object' || e === null) {
94
+ return false;
95
+ }
96
+ const candidate = e as Partial<ChartError>;
97
+ return (
98
+ typeof candidate.code === 'string' &&
99
+ typeof candidate.message === 'string' &&
100
+ (candidate.category === 'validation' ||
101
+ candidate.category === 'declared' ||
102
+ candidate.category === 'render')
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Builds the report handed to the app after a contained render exception.
108
+ *
109
+ * A `ChartError` thrown by the package keeps its catalogue message. Anything
110
+ * else — a defect in the rendering library, say — is reported under the generic
111
+ * `renderException` message. We do **not** copy a third-party exception's own
112
+ * message into the report, because we cannot promise it is free of data.
113
+ */
114
+ export function toFailureReport(
115
+ family: ChartFamily,
116
+ thrown: unknown,
117
+ chartId?: string,
118
+ componentStack?: string,
119
+ ): ChartFailureReport {
120
+ const error: ChartError = isChartError(thrown)
121
+ ? { ...thrown, chartId, family }
122
+ : chartError('render', 'renderException', { chartId, family });
123
+
124
+ return { chartId, family, error, componentStack };
125
+ }
126
+
127
+ /**
128
+ * Stable identity for a failure, used to de-duplicate reports so an unrelated
129
+ * parent re-render does not re-report the same problem.
130
+ */
131
+ export function failureSignature(report: ChartFailureReport): string {
132
+ return `${report.chartId ?? 'anon'}:${report.family}:${report.error.code}`;
133
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Formatting contract (FR-055 to FR-059).
3
+ *
4
+ * The package never invents or converts a business unit. It accepts a hint,
5
+ * applies a documented locale-neutral default, and uses app-supplied formatter
6
+ * output verbatim whenever one is given.
7
+ */
8
+ import type { Position, Series } from './datasource';
9
+ import { isKnown, type ValueState } from './value';
10
+
11
+ export type FormatHint =
12
+ | { readonly kind: 'number'; readonly decimals?: number }
13
+ | { readonly kind: 'percent'; readonly decimals?: number }
14
+ | { readonly kind: 'currency'; readonly currency: string }
15
+ | {
16
+ readonly kind: 'duration';
17
+ readonly unit: 'ms' | 's' | 'min' | 'h';
18
+ }
19
+ | { readonly kind: 'count' }
20
+ | { readonly kind: 'custom'; readonly unit: string };
21
+
22
+ export interface FormatContext {
23
+ readonly hint?: FormatHint;
24
+ readonly unit?: string;
25
+ /** Where the text will appear, so a formatter can adapt its verbosity. */
26
+ readonly slot: 'axis' | 'value' | 'legend' | 'summary';
27
+ }
28
+
29
+ export interface SeriesRef {
30
+ readonly id: string;
31
+ readonly label: string;
32
+ }
33
+
34
+ export interface ChartFormatters {
35
+ readonly axisLabel?: (
36
+ v: ValueState | Position,
37
+ ctx: FormatContext,
38
+ ) => string;
39
+ readonly value?: (v: ValueState, ctx: FormatContext) => string;
40
+ readonly legendLabel?: (series: SeriesRef, ctx: FormatContext) => string;
41
+ readonly summary?: (v: ValueState, ctx: FormatContext) => string;
42
+ }
43
+
44
+ /**
45
+ * Placeholders for the three non-value classes. Deliberately not "0" and not
46
+ * an empty string, so a reader can tell a gap from a zero at a glance.
47
+ */
48
+ export const NON_VALUE_TEXT: Readonly<Record<string, string>> = {
49
+ unknown: '—',
50
+ notApplicable: 'n/a',
51
+ withheld: '••',
52
+ };
53
+
54
+ function trimTrailingZeros(text: string): string {
55
+ return text.includes('.') ? text.replace(/\.?0+$/, '') : text;
56
+ }
57
+
58
+ /**
59
+ * Locale-neutral default formatting (FR-057).
60
+ *
61
+ * Deliberately plain: no thousands separators, no currency symbol lookup, no
62
+ * month names. Anything locale-dependent belongs to the consuming app, which
63
+ * owns an i18n system the package does not.
64
+ */
65
+ export function formatValueDefault(
66
+ v: ValueState,
67
+ ctx: FormatContext = { slot: 'value' },
68
+ ): string {
69
+ if (!isKnown(v)) {
70
+ return NON_VALUE_TEXT[v.kind] ?? NON_VALUE_TEXT.unknown;
71
+ }
72
+
73
+ const hint = ctx.hint;
74
+ const n = v.value;
75
+
76
+ switch (hint?.kind) {
77
+ case 'percent': {
78
+ const decimals = hint.decimals ?? 0;
79
+ return `${(n * 100).toFixed(decimals)}%`;
80
+ }
81
+ case 'currency':
82
+ // Code rather than symbol: symbol placement is locale-dependent and
83
+ // therefore the app's decision.
84
+ return `${n.toFixed(2)} ${hint.currency}`;
85
+ case 'duration':
86
+ return `${trimTrailingZeros(n.toFixed(2))}${hint.unit}`;
87
+ case 'count':
88
+ return String(Math.round(n));
89
+ case 'custom':
90
+ return `${trimTrailingZeros(n.toFixed(2))} ${hint.unit}`;
91
+ case 'number':
92
+ return hint.decimals === undefined
93
+ ? trimTrailingZeros(n.toFixed(2))
94
+ : n.toFixed(hint.decimals);
95
+ default:
96
+ return trimTrailingZeros(n.toFixed(2));
97
+ }
98
+ }
99
+
100
+ export function formatPositionDefault(p: Position): string {
101
+ switch (p.kind) {
102
+ case 'category':
103
+ return p.category;
104
+ case 'matrix':
105
+ return `${p.row} / ${p.column}`;
106
+ case 'instant':
107
+ // Epoch milliseconds as-is. Any human-readable date belongs to an app
108
+ // formatter, which alone knows the locale and the display timezone
109
+ // (FR-016).
110
+ return String(p.at);
111
+ }
112
+ }
113
+
114
+ export function formatLegendDefault(series: SeriesRef): string {
115
+ return series.label;
116
+ }
117
+
118
+ /**
119
+ * Runs an app formatter, falling back to the package default for that single
120
+ * label if it throws (FR-059). One bad formatter must not take down a screen.
121
+ *
122
+ * The dev-only warning is intentionally value-free: it names the slot, never
123
+ * the data.
124
+ */
125
+ export function safeFormat(
126
+ run: () => string,
127
+ fallback: () => string,
128
+ slot: FormatContext['slot'],
129
+ onceKey?: string,
130
+ ): string {
131
+ try {
132
+ return run();
133
+ } catch (e) {
134
+ if (__DEV__) {
135
+ warnFormatterFailureOnce(slot, onceKey, e);
136
+ }
137
+ return fallback();
138
+ }
139
+ }
140
+
141
+ const warnedFormatters = new Set<string>();
142
+
143
+ function warnFormatterFailureOnce(
144
+ slot: FormatContext['slot'],
145
+ onceKey: string | undefined,
146
+ error: unknown,
147
+ ): void {
148
+ const key = `${slot}:${onceKey ?? 'default'}`;
149
+ if (warnedFormatters.has(key)) {
150
+ return;
151
+ }
152
+ warnedFormatters.add(key);
153
+ const reason = error instanceof Error ? error.message : 'unknown error';
154
+ console.warn(
155
+ `[rn-chart] A supplied "${slot}" formatter threw (${reason}). ` +
156
+ 'Falling back to the built-in default for that label. ' +
157
+ 'This warning appears once per formatter slot and never in production builds.',
158
+ );
159
+ }
160
+
161
+ /** Test-only reset so warning-once behaviour can be asserted repeatedly. */
162
+ export function __resetFormatterWarnings(): void {
163
+ warnedFormatters.clear();
164
+ }
165
+
166
+ export function seriesRef<M>(series: Series<M>): SeriesRef {
167
+ return { id: series.id, label: series.label };
168
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Interaction contract (FR-048 to FR-054a).
3
+ *
4
+ * The package reports what the user touched and does nothing else: no
5
+ * navigation, no fetching, no datasource mutation, no business rules.
6
+ *
7
+ * Selection is **uncontrolled by default and controlled when the app supplies a
8
+ * value** — the standard React pattern. The same callbacks fire either way, so
9
+ * a consumer can move between the two without rewriting handlers.
10
+ */
11
+ import type { Position } from './datasource';
12
+ import type { ValueState } from './value';
13
+
14
+ export interface SelectionRef {
15
+ readonly seriesId: string;
16
+ /** Absent for a whole-series selection. */
17
+ readonly pointId?: string;
18
+ }
19
+
20
+ export interface PointInteractionEvent<M = unknown> {
21
+ readonly type: 'pointSelect' | 'segmentPress';
22
+ readonly seriesId: string;
23
+ readonly pointId: string;
24
+ readonly value: ValueState;
25
+ readonly position: Position;
26
+ readonly metadata?: M;
27
+ }
28
+
29
+ export interface SeriesInteractionEvent<M = unknown> {
30
+ readonly type: 'seriesSelect' | 'visibilityChange';
31
+ readonly seriesId: string;
32
+ readonly visible?: boolean;
33
+ readonly metadata?: M;
34
+ }
35
+
36
+ export type ChartInteractionEvent<M = unknown> =
37
+ | PointInteractionEvent<M>
38
+ | SeriesInteractionEvent<M>;
39
+
40
+ export interface InteractionProps<M = unknown> {
41
+ /**
42
+ * Omit for uncontrolled selection. Supply — including `null` — to take
43
+ * ownership; the chart then renders exactly what this says.
44
+ */
45
+ readonly selection?: SelectionRef | null;
46
+ readonly onSelectionChange?: (
47
+ next: SelectionRef | null,
48
+ event: PointInteractionEvent<M> | null,
49
+ ) => void;
50
+ /**
51
+ * Fires when an app-controlled selection no longer resolves against the
52
+ * current datasource, so the app can correct its own state rather than
53
+ * having the package silently ignore it.
54
+ */
55
+ readonly onSelectionUnresolvable?: (ref: SelectionRef) => void;
56
+ /** Omit for uncontrolled visibility. Supply to take ownership. */
57
+ readonly hiddenSeriesIds?: readonly string[];
58
+ readonly onVisibilityChange?: (event: SeriesInteractionEvent<M>) => void;
59
+ /** Series toggling stays off unless the app opts in (FR-049). */
60
+ readonly seriesToggleEnabled?: boolean;
61
+ }
62
+
63
+ export function sameSelection(
64
+ a: SelectionRef | null | undefined,
65
+ b: SelectionRef | null | undefined,
66
+ ): boolean {
67
+ if (!a || !b) {
68
+ return !a && !b;
69
+ }
70
+ return a.seriesId === b.seriesId && a.pointId === b.pointId;
71
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Reference markers: targets, baselines, and thresholds (FR-008, FR-021, FR-022).
3
+ *
4
+ * The important rule lives here: a marker whose value is not `known`, or whose
5
+ * target is zero or negative, produces **no completion proportion**. A chart
6
+ * shows the current value alone rather than inventing a percentage.
7
+ */
8
+ import type { FormatHint } from './format';
9
+ import { isKnown, type ValueState } from './value';
10
+
11
+ export type MarkerKind = 'target' | 'baseline' | 'threshold';
12
+
13
+ export interface ReferenceMarker {
14
+ readonly id: string;
15
+ readonly kind: MarkerKind;
16
+ readonly value: ValueState;
17
+ readonly label?: string;
18
+ readonly format?: FormatHint;
19
+ }
20
+
21
+ /**
22
+ * A target only supports a proportion when it is known and strictly positive.
23
+ * Zero and negative targets make "percent complete" meaningless, so we decline
24
+ * to compute one rather than divide and produce a nonsense figure.
25
+ */
26
+ export function isUsableTarget(marker: ReferenceMarker | undefined): boolean {
27
+ if (!marker || marker.kind !== 'target') {
28
+ return false;
29
+ }
30
+ return isKnown(marker.value) && marker.value.value > 0;
31
+ }
32
+
33
+ export interface ProgressReading {
34
+ /** The current value, when known. */
35
+ readonly current: ValueState;
36
+ /** The target, when one was supplied. */
37
+ readonly target?: ReferenceMarker;
38
+ /**
39
+ * Completion as a ratio of current to target. `undefined` whenever no
40
+ * proportion can honestly be computed — this is never defaulted to 0 or 1.
41
+ */
42
+ readonly proportion?: number;
43
+ /** True when the current value exceeds the target: shown, never clamped. */
44
+ readonly overshoot: boolean;
45
+ }
46
+
47
+ export function readProgress(
48
+ current: ValueState,
49
+ target: ReferenceMarker | undefined,
50
+ ): ProgressReading {
51
+ if (!isKnown(current) || !isUsableTarget(target) || !target) {
52
+ return { current, target, proportion: undefined, overshoot: false };
53
+ }
54
+
55
+ // isUsableTarget already proved the target is known and positive.
56
+ const targetValue = (target.value as { value: number }).value;
57
+ const proportion = current.value / targetValue;
58
+
59
+ return {
60
+ current,
61
+ target,
62
+ proportion,
63
+ overshoot: proportion > 1,
64
+ };
65
+ }
66
+
67
+ export function findTarget(
68
+ markers: readonly ReferenceMarker[] | undefined,
69
+ id?: string,
70
+ ): ReferenceMarker | undefined {
71
+ if (!markers) {
72
+ return undefined;
73
+ }
74
+ return markers.find(m => m.kind === 'target' && (!id || m.id === id));
75
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Props shared by every chart family.
3
+ *
4
+ * See specs/007-rn-chart-package/contracts/public-api.md §2 for the contract
5
+ * behind each field.
6
+ */
7
+ import type { ReactNode } from 'react';
8
+ import type { StyleProp, ViewStyle } from 'react-native';
9
+
10
+ import type { ChartDatasource } from './datasource';
11
+ import type { ChartFailureHandler } from './errors';
12
+ import type { ChartFormatters, FormatContext } from './format';
13
+ import type { InteractionProps } from './interaction';
14
+ import type { ChartStateContent, DeclarableChartStatus } from './state';
15
+ import type { ChartTheme } from './theme';
16
+ import type { ValueState } from './value';
17
+
18
+ export interface ChartAccessibility<M = unknown> {
19
+ /** Replaces the composed summary entirely. */
20
+ readonly label?: string;
21
+ readonly summaryFormatter?: (
22
+ datasource: ChartDatasource<M>,
23
+ ctx: FormatContext,
24
+ ) => string;
25
+ /** Defaults to true when `onSelectionChange` is supplied. */
26
+ readonly interactive?: boolean;
27
+ }
28
+
29
+ export interface BaseChartProps<M = unknown> extends InteractionProps<M> {
30
+ /**
31
+ * Must be referentially stable across renders, or memoization is defeated.
32
+ * Hold it in state or a memo rather than building it inline.
33
+ */
34
+ readonly datasource: ChartDatasource<M>;
35
+
36
+ /**
37
+ * `empty` and `partial` are derived from the data and are not accepted here
38
+ * (FR-036).
39
+ */
40
+ readonly status?: DeclarableChartStatus;
41
+ readonly stateContent?: ChartStateContent;
42
+
43
+ readonly chartId?: string;
44
+ readonly onFailure?: ChartFailureHandler;
45
+
46
+ readonly theme?: ChartTheme;
47
+ readonly formatters?: ChartFormatters;
48
+ readonly title?: string;
49
+ readonly legend?: boolean | { readonly position: 'top' | 'bottom' };
50
+
51
+ readonly accessibility?: ChartAccessibility<M>;
52
+
53
+ /** Width always comes from the container. */
54
+ readonly height?: number;
55
+ readonly style?: StyleProp<ViewStyle>;
56
+ /** Rendered below the plot, inside the chart's own bounds. */
57
+ readonly footer?: ReactNode;
58
+ }
59
+
60
+ export type ValueFormatter = (value: ValueState) => string;