@chipmobilesdk/rn-chart 0.3.0 → 0.4.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.
@@ -23,6 +23,19 @@ export interface HitRegion {
23
23
  readonly width: number;
24
24
  readonly height: number;
25
25
  };
26
+ /**
27
+ * An arc segment: containment by radius and angle. Angles are radians,
28
+ * clockwise from twelve o'clock, as the distribution geometry draws them.
29
+ * A region with an arc is hit only inside it, never by proximity.
30
+ */
31
+ readonly arc?: {
32
+ readonly cx: number;
33
+ readonly cy: number;
34
+ readonly innerRadius: number;
35
+ readonly outerRadius: number;
36
+ readonly startAngle: number;
37
+ readonly endAngle: number;
38
+ };
26
39
  /**
27
40
  * False for points that must never be selectable: withheld values, and
28
41
  * anything the chart is not currently drawing (FR-053).
@@ -47,6 +60,20 @@ function inBounds(region: HitRegion, x: number, y: number): boolean {
47
60
  return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
48
61
  }
49
62
 
63
+ const TAU = Math.PI * 2;
64
+
65
+ function inArc(arc: NonNullable<HitRegion['arc']>, x: number, y: number): boolean {
66
+ const dx = x - arc.cx;
67
+ const dy = y - arc.cy;
68
+ const radius = Math.sqrt(dx * dx + dy * dy);
69
+ if (radius < arc.innerRadius || radius > arc.outerRadius) {
70
+ return false;
71
+ }
72
+ // Clockwise from the top, the convention the arcs are drawn in.
73
+ const angle = (Math.atan2(dy, dx) + Math.PI / 2 + TAU) % TAU;
74
+ return angle >= arc.startAngle && angle < arc.endAngle;
75
+ }
76
+
50
77
  /**
51
78
  * Resolves a touch to a mark.
52
79
  *
@@ -70,6 +97,14 @@ export function hitTest(
70
97
  if (inBounds(region, x, y)) {
71
98
  return { seriesId: region.seriesId, pointId: region.pointId, distance: 0 };
72
99
  }
100
+ if (region.arc) {
101
+ // A large segment's edge is far from its midpoint, and a neighbour's
102
+ // midpoint can be nearer: only the segment under the finger counts.
103
+ if (inArc(region.arc, x, y)) {
104
+ return { seriesId: region.seriesId, pointId: region.pointId, distance: 0 };
105
+ }
106
+ continue;
107
+ }
73
108
 
74
109
  const dx = region.x - x;
75
110
  const dy = region.y - y;
@@ -17,7 +17,6 @@ import { rectSubpath } from './path';
17
17
  import type { GeometryInput, GeometryLabel, MarkerLine, MarkPath, ResolvedGeometry } from './types';
18
18
 
19
19
  export interface ProgressOptions {
20
- readonly orientation?: 'horizontal' | 'vertical';
21
20
  readonly trackRadius?: number;
22
21
  }
23
22
 
@@ -130,16 +130,3 @@ export function reserveAxisSpace(
130
130
  }
131
131
  return widest + LABEL_GUTTER;
132
132
  }
133
-
134
- /**
135
- * Shortens a label that cannot fit, signalling the truncation with an ellipsis.
136
- * Callers must expose the full value through an accessible label (FR-070).
137
- */
138
- export function truncateToWidth(text: string, fontSize: number, maxWidthPx: number): string {
139
- if (estimateTextWidth(text, fontSize) <= maxWidthPx) {
140
- return text;
141
- }
142
- const perChar = fontSize * AVERAGE_ADVANCE_RATIO;
143
- const fits = Math.max(1, Math.floor(maxWidthPx / perChar) - 1);
144
- return `${text.slice(0, fits)}…`;
145
- }
@@ -50,8 +50,13 @@ export function buildTimeSeriesGeometry<M>(
50
50
  const values: number[] = [];
51
51
  let excluded = 0;
52
52
 
53
+ // Sorted and windowed once per series; both passes below read the result.
54
+ const ordered = new Map(
55
+ datasource.series.map(series => [series, visiblePoints(series, options.range)] as const),
56
+ );
57
+
53
58
  for (const series of datasource.series) {
54
- for (const point of visiblePoints(series, options.range)) {
59
+ for (const point of ordered.get(series) ?? []) {
55
60
  if (point.position.kind === 'instant') {
56
61
  instants.push(point.position.at);
57
62
  }
@@ -104,7 +109,7 @@ export function buildTimeSeriesGeometry<M>(
104
109
 
105
110
  const pts: MaybePt[] = [];
106
111
 
107
- for (const point of visiblePoints(series, options.range)) {
112
+ for (const point of ordered.get(series) ?? []) {
108
113
  const at = point.position.kind === 'instant' ? point.position.at : 0;
109
114
  const px = x.project(at);
110
115
 
@@ -46,6 +46,13 @@ export interface GeometryLabel {
46
46
  readonly muted?: boolean;
47
47
  }
48
48
 
49
+ /** What a colour ramp runs from and to, for the legend that explains it. */
50
+ export interface ColorScale {
51
+ readonly min: number;
52
+ readonly max: number;
53
+ readonly colors: readonly string[];
54
+ }
55
+
49
56
  export interface ResolvedGeometry {
50
57
  readonly plot: PlotRect;
51
58
  readonly paths: readonly MarkPath[];
@@ -54,6 +61,8 @@ export interface ResolvedGeometry {
54
61
  readonly xTicks: readonly Tick[];
55
62
  readonly yTicks: readonly Tick[];
56
63
  readonly hitRegions: readonly HitRegion[];
64
+ /** Present when colour encodes a value, as in a heatmap. */
65
+ readonly colorScale?: ColorScale;
57
66
  /**
58
67
  * True when something was excluded from a total or proportion, so the chart
59
68
  * can say so rather than presenting a partial figure as whole (FR-013).
@@ -88,6 +97,8 @@ export interface GeometryInput {
88
97
  Record<'unknown' | 'notApplicable' | 'withheld', NonValueTexture>
89
98
  >;
90
99
  readonly heatmapRamp: readonly string[];
100
+ /** Corner radii from the theme, for bars and heatmap cells. */
101
+ readonly radius?: { readonly bar: number; readonly cell: number };
91
102
  readonly formatValue: (value: number) => string;
92
103
  readonly formatCategory: (raw: string) => string;
93
104
  readonly formatInstant: (at: number) => string;
package/src/index.ts CHANGED
@@ -62,6 +62,8 @@ export {
62
62
  resolveUnit,
63
63
  } from './contract/datasource';
64
64
 
65
+ export type { DistributionValues } from './contract/distribution';
66
+
65
67
  export type { MarkerKind, ProgressReading, ReferenceMarker } from './contract/marker';
66
68
  export { findTarget, isUsableTarget, readProgress } from './contract/marker';
67
69
 
@@ -149,7 +151,8 @@ export {
149
151
  TimeSeriesChart,
150
152
  } from './react/charts';
151
153
 
152
- export { ChartLegend, buildLegendEntries } from './react/ChartLegend';
154
+ export { ChartLegend, ColorScaleLegend, buildLegendEntries } from './react/ChartLegend';
155
+ export type { ColorScale } from './geometry/types';
153
156
  export { ChartThemeProvider, useChartTheme } from './react/ChartThemeProvider';
154
157
 
155
158
  // ---------------------------------------------------------------------------