@chipmobilesdk/rn-chart 0.2.0 → 0.2.1
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.
- package/CHANGELOG.md +126 -115
- package/README.md +381 -381
- package/package.json +46 -49
- package/skills/sdk-rn-chart/SKILL.md +68 -69
- package/src/a11y/summary.ts +170 -177
- package/src/contract/datasource.ts +126 -136
- package/src/contract/errors.ts +129 -133
- package/src/contract/format.ts +162 -168
- package/src/contract/interaction.ts +70 -71
- package/src/contract/marker.ts +75 -75
- package/src/contract/props.ts +57 -60
- package/src/contract/state.ts +88 -94
- package/src/contract/theme.ts +169 -177
- package/src/contract/value.ts +121 -125
- package/src/geometry/bar.ts +271 -273
- package/src/geometry/distribution.ts +197 -203
- package/src/geometry/heatmap.ts +189 -195
- package/src/geometry/hitTest.ts +117 -120
- package/src/geometry/layout.ts +162 -173
- package/src/geometry/markers.ts +55 -57
- package/src/geometry/path.ts +193 -199
- package/src/geometry/progress.ts +223 -230
- package/src/geometry/scale.ts +140 -146
- package/src/geometry/ticks.ts +145 -153
- package/src/geometry/timeSeries.ts +168 -168
- package/src/geometry/types.ts +94 -94
- package/src/index.ts +158 -176
- package/src/react/ChartBoundary.tsx +96 -96
- package/src/react/ChartFrame.tsx +583 -622
- package/src/react/ChartLegend.tsx +211 -215
- package/src/react/ChartThemeProvider.tsx +44 -48
- package/src/react/charts.tsx +154 -161
- package/src/react/useChartLayout.ts +124 -124
- package/src/react/useChartSelection.ts +152 -170
- package/src/render/Axis.tsx +120 -122
- package/src/render/Marks.tsx +173 -205
- package/src/validation/budgets.ts +103 -110
- package/src/validation/validateDatasource.ts +174 -174
package/src/geometry/bar.ts
CHANGED
|
@@ -1,273 +1,271 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bar geometry: one batched path per series, grouped or stacked.
|
|
3
|
-
*
|
|
4
|
-
* Bars measure length from zero, so the domain always includes it — otherwise
|
|
5
|
-
* a bar's length means nothing. Negative values place the baseline correctly
|
|
6
|
-
* rather than clamping to the plot floor.
|
|
7
|
-
*
|
|
8
|
-
* PURE. No React, no SVG.
|
|
9
|
-
*/
|
|
10
|
-
import type { ChartDatasource, Position, Series } from '../contract/datasource';
|
|
11
|
-
import { isKnown, isWithheld } from '../contract/value';
|
|
12
|
-
import type { HitRegion } from './hitTest';
|
|
13
|
-
import { insetForAxes, plotBottom, plotRight } from './layout';
|
|
14
|
-
import { horizontalMarkers, markerDomainValues } from './markers';
|
|
15
|
-
import { rectBatchPath, type RectSpec } from './path';
|
|
16
|
-
import { bandScale, domainFrom, linearScale, widenDegenerateDomain } from './scale';
|
|
17
|
-
import { thinTicks, type Tick } from './ticks';
|
|
18
|
-
import type { GeometryInput, MarkPath, ResolvedGeometry } from './types';
|
|
19
|
-
|
|
20
|
-
export type BarGrouping = 'grouped' | 'stacked';
|
|
21
|
-
|
|
22
|
-
export interface BarOptions {
|
|
23
|
-
readonly grouping?: BarGrouping;
|
|
24
|
-
readonly radius?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Bars accept both category and instant positions — that dual acceptance is
|
|
29
|
-
* what makes swapping a time-series chart for a bar chart free. Both collapse
|
|
30
|
-
* to a string key for the band scale.
|
|
31
|
-
*/
|
|
32
|
-
function categoryKey(position: Position): string {
|
|
33
|
-
switch (position.kind) {
|
|
34
|
-
case 'category':
|
|
35
|
-
return position.category;
|
|
36
|
-
case 'instant':
|
|
37
|
-
return String(position.at);
|
|
38
|
-
default:
|
|
39
|
-
return '';
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function countExcluded<M>(datasource: ChartDatasource<M>): number {
|
|
44
|
-
let excluded = 0;
|
|
45
|
-
for (const series of datasource.series) {
|
|
46
|
-
for (const point of series.points) {
|
|
47
|
-
if (!isKnown(point.value)) {
|
|
48
|
-
excluded += 1;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return excluded;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function groupedDomainValues<M>(datasource: ChartDatasource<M>): number[] {
|
|
56
|
-
const values: number[] = [];
|
|
57
|
-
for (const series of datasource.series) {
|
|
58
|
-
for (const point of series.points) {
|
|
59
|
-
if (isKnown(point.value)) {
|
|
60
|
-
values.push(point.value.value);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return values;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* A stacked axis must fit the stack totals, not the individual values.
|
|
69
|
-
* Positive and negative parts accumulate separately so a stack straddling zero
|
|
70
|
-
* still fits.
|
|
71
|
-
*/
|
|
72
|
-
function stackedDomainValues<M>(
|
|
73
|
-
categories: readonly string[],
|
|
74
|
-
visible: readonly Series<M>[],
|
|
75
|
-
): number[] {
|
|
76
|
-
const values: number[] = [];
|
|
77
|
-
|
|
78
|
-
for (const category of categories) {
|
|
79
|
-
let positive = 0;
|
|
80
|
-
let negative = 0;
|
|
81
|
-
|
|
82
|
-
for (const series of visible) {
|
|
83
|
-
const point = series.points.find(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
seriesCount
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
project
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
x: barX
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
};
|
|
273
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Bar geometry: one batched path per series, grouped or stacked.
|
|
3
|
+
*
|
|
4
|
+
* Bars measure length from zero, so the domain always includes it — otherwise
|
|
5
|
+
* a bar's length means nothing. Negative values place the baseline correctly
|
|
6
|
+
* rather than clamping to the plot floor.
|
|
7
|
+
*
|
|
8
|
+
* PURE. No React, no SVG.
|
|
9
|
+
*/
|
|
10
|
+
import type { ChartDatasource, Position, Series } from '../contract/datasource';
|
|
11
|
+
import { isKnown, isWithheld } from '../contract/value';
|
|
12
|
+
import type { HitRegion } from './hitTest';
|
|
13
|
+
import { insetForAxes, plotBottom, plotRight } from './layout';
|
|
14
|
+
import { horizontalMarkers, markerDomainValues } from './markers';
|
|
15
|
+
import { rectBatchPath, type RectSpec } from './path';
|
|
16
|
+
import { bandScale, domainFrom, linearScale, widenDegenerateDomain } from './scale';
|
|
17
|
+
import { thinTicks, type Tick } from './ticks';
|
|
18
|
+
import type { GeometryInput, MarkPath, ResolvedGeometry } from './types';
|
|
19
|
+
|
|
20
|
+
export type BarGrouping = 'grouped' | 'stacked';
|
|
21
|
+
|
|
22
|
+
export interface BarOptions {
|
|
23
|
+
readonly grouping?: BarGrouping;
|
|
24
|
+
readonly radius?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bars accept both category and instant positions — that dual acceptance is
|
|
29
|
+
* what makes swapping a time-series chart for a bar chart free. Both collapse
|
|
30
|
+
* to a string key for the band scale.
|
|
31
|
+
*/
|
|
32
|
+
function categoryKey(position: Position): string {
|
|
33
|
+
switch (position.kind) {
|
|
34
|
+
case 'category':
|
|
35
|
+
return position.category;
|
|
36
|
+
case 'instant':
|
|
37
|
+
return String(position.at);
|
|
38
|
+
default:
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function countExcluded<M>(datasource: ChartDatasource<M>): number {
|
|
44
|
+
let excluded = 0;
|
|
45
|
+
for (const series of datasource.series) {
|
|
46
|
+
for (const point of series.points) {
|
|
47
|
+
if (!isKnown(point.value)) {
|
|
48
|
+
excluded += 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return excluded;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function groupedDomainValues<M>(datasource: ChartDatasource<M>): number[] {
|
|
56
|
+
const values: number[] = [];
|
|
57
|
+
for (const series of datasource.series) {
|
|
58
|
+
for (const point of series.points) {
|
|
59
|
+
if (isKnown(point.value)) {
|
|
60
|
+
values.push(point.value.value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return values;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A stacked axis must fit the stack totals, not the individual values.
|
|
69
|
+
* Positive and negative parts accumulate separately so a stack straddling zero
|
|
70
|
+
* still fits.
|
|
71
|
+
*/
|
|
72
|
+
function stackedDomainValues<M>(
|
|
73
|
+
categories: readonly string[],
|
|
74
|
+
visible: readonly Series<M>[],
|
|
75
|
+
): number[] {
|
|
76
|
+
const values: number[] = [];
|
|
77
|
+
|
|
78
|
+
for (const category of categories) {
|
|
79
|
+
let positive = 0;
|
|
80
|
+
let negative = 0;
|
|
81
|
+
|
|
82
|
+
for (const series of visible) {
|
|
83
|
+
const point = series.points.find(p => categoryKey(p.position) === category);
|
|
84
|
+
if (point && isKnown(point.value)) {
|
|
85
|
+
if (point.value.value >= 0) {
|
|
86
|
+
positive += point.value.value;
|
|
87
|
+
} else {
|
|
88
|
+
negative += point.value.value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
values.push(positive, negative);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return values;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function groupedBarX(
|
|
99
|
+
bandStart: number,
|
|
100
|
+
bandWidth: number,
|
|
101
|
+
seriesIndex: number,
|
|
102
|
+
seriesCount: number,
|
|
103
|
+
): { barX: number; barWidth: number } {
|
|
104
|
+
const barWidth = bandWidth / Math.max(seriesCount, 1);
|
|
105
|
+
return { barX: bandStart + seriesIndex * barWidth, barWidth };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Grows from the zero baseline, upward or downward as the sign requires. */
|
|
109
|
+
function groupedBarY(
|
|
110
|
+
value: number,
|
|
111
|
+
zeroY: number,
|
|
112
|
+
project: (v: number) => number,
|
|
113
|
+
): { top: number; height: number } {
|
|
114
|
+
const valueY = project(value);
|
|
115
|
+
return { top: Math.min(zeroY, valueY), height: Math.abs(valueY - zeroY) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Grows from wherever the running stack for this sign currently ends. */
|
|
119
|
+
function stackedBarY(
|
|
120
|
+
value: number,
|
|
121
|
+
category: string,
|
|
122
|
+
stack: Map<string, number>,
|
|
123
|
+
project: (v: number) => number,
|
|
124
|
+
): { top: number; height: number } {
|
|
125
|
+
const base = stack.get(category) ?? 0;
|
|
126
|
+
const from = project(base);
|
|
127
|
+
const to = project(base + value);
|
|
128
|
+
stack.set(category, base + value);
|
|
129
|
+
return { top: Math.min(from, to), height: Math.abs(to - from) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildBarGeometry<M>(
|
|
133
|
+
datasource: ChartDatasource<M>,
|
|
134
|
+
input: GeometryInput,
|
|
135
|
+
options: BarOptions = {},
|
|
136
|
+
): ResolvedGeometry {
|
|
137
|
+
const grouping: BarGrouping = options.grouping ?? 'grouped';
|
|
138
|
+
|
|
139
|
+
// Preserve first-seen order rather than sorting: the app chose this order.
|
|
140
|
+
const categories: string[] = [];
|
|
141
|
+
const seen = new Set<string>();
|
|
142
|
+
for (const series of datasource.series) {
|
|
143
|
+
for (const point of series.points) {
|
|
144
|
+
const key = categoryKey(point.position);
|
|
145
|
+
if (key && !seen.has(key)) {
|
|
146
|
+
seen.add(key);
|
|
147
|
+
categories.push(key);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const visible = datasource.series.filter(s => !s.hidden);
|
|
153
|
+
const excluded = countExcluded(datasource);
|
|
154
|
+
const values = [
|
|
155
|
+
...(grouping === 'stacked'
|
|
156
|
+
? stackedDomainValues(categories, visible)
|
|
157
|
+
: groupedDomainValues(datasource)),
|
|
158
|
+
// A threshold above every bar must still be on the chart.
|
|
159
|
+
...markerDomainValues(datasource.markers),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
// Labels depend only on the domain, so they are known before any pixel is,
|
|
163
|
+
// and the plot can make room for them first.
|
|
164
|
+
// Bars grow from zero, so padding past zero on a side no bar reaches only
|
|
165
|
+
// adds a meaningless tick (-5.5 under all-positive data) crowding the 0.
|
|
166
|
+
const padded = domainFrom(values, { includeZero: true, padFraction: 0.05 });
|
|
167
|
+
const domain = widenDegenerateDomain({
|
|
168
|
+
min: values.some(v => v < 0) ? padded.min : Math.max(padded.min, 0),
|
|
169
|
+
max: values.some(v => v > 0) ? padded.max : Math.min(padded.max, 0),
|
|
170
|
+
});
|
|
171
|
+
const yTickValues = [domain.min, 0, domain.max].filter((v, i, arr) => arr.indexOf(v) === i);
|
|
172
|
+
const xLabel = (c: string) =>
|
|
173
|
+
datasource.positionKind === 'instant'
|
|
174
|
+
? input.formatInstant(Number(c))
|
|
175
|
+
: input.formatCategory(c);
|
|
176
|
+
const plot = insetForAxes(input.plot, {
|
|
177
|
+
xLabels: categories.map(xLabel),
|
|
178
|
+
yLabels: yTickValues.map(input.formatValue),
|
|
179
|
+
fontSize: input.fontSize,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const xBand = bandScale(categories, { min: plot.x, max: plotRight(plot) }, 0.25);
|
|
183
|
+
const y = linearScale(domain, { min: plotBottom(plot), max: plot.y });
|
|
184
|
+
const zeroY = y.project(0);
|
|
185
|
+
|
|
186
|
+
const paths: MarkPath[] = [];
|
|
187
|
+
const hitRegions: HitRegion[] = [];
|
|
188
|
+
|
|
189
|
+
// Running stack offsets, tracked separately per sign so negative bars grow
|
|
190
|
+
// downward from zero while positive bars grow upward.
|
|
191
|
+
const stackUp = new Map<string, number>();
|
|
192
|
+
const stackDown = new Map<string, number>();
|
|
193
|
+
|
|
194
|
+
visible.forEach((series, seriesIndex) => {
|
|
195
|
+
const rects: RectSpec[] = [];
|
|
196
|
+
|
|
197
|
+
for (const point of series.points) {
|
|
198
|
+
const category = categoryKey(point.position);
|
|
199
|
+
const bandStart = xBand.project(category);
|
|
200
|
+
if (bandStart === undefined || !isKnown(point.value)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const value = point.value.value;
|
|
205
|
+
|
|
206
|
+
const { barX, barWidth } =
|
|
207
|
+
grouping === 'grouped'
|
|
208
|
+
? groupedBarX(bandStart, xBand.bandWidth, seriesIndex, visible.length)
|
|
209
|
+
: { barX: bandStart, barWidth: xBand.bandWidth };
|
|
210
|
+
|
|
211
|
+
const stack = value >= 0 ? stackUp : stackDown;
|
|
212
|
+
const { top, height } =
|
|
213
|
+
grouping === 'stacked'
|
|
214
|
+
? stackedBarY(value, category, stack, y.project)
|
|
215
|
+
: groupedBarY(value, zeroY, y.project);
|
|
216
|
+
|
|
217
|
+
rects.push({
|
|
218
|
+
x: barX,
|
|
219
|
+
y: top,
|
|
220
|
+
width: barWidth,
|
|
221
|
+
height,
|
|
222
|
+
radius: options.radius,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
hitRegions.push({
|
|
226
|
+
seriesId: series.id,
|
|
227
|
+
pointId: point.id,
|
|
228
|
+
x: barX + barWidth / 2,
|
|
229
|
+
y: top + height / 2,
|
|
230
|
+
bounds: { x: barX, y: top, width: barWidth, height },
|
|
231
|
+
selectable: !isWithheld(point.value),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (rects.length > 0) {
|
|
236
|
+
paths.push({
|
|
237
|
+
key: `bars-${series.id}`,
|
|
238
|
+
seriesId: series.id,
|
|
239
|
+
d: rectBatchPath(rects),
|
|
240
|
+
color: input.seriesColors[series.id],
|
|
241
|
+
pattern: series.pattern ?? 'solid',
|
|
242
|
+
filled: true,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const xTicks: Tick[] = categories.map(c => ({
|
|
248
|
+
value: 0,
|
|
249
|
+
position: xBand.centerOf(c) ?? plot.x,
|
|
250
|
+
label: xLabel(c),
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
const yTicks: Tick[] = yTickValues.map(v => ({
|
|
254
|
+
value: v,
|
|
255
|
+
position: y.project(v),
|
|
256
|
+
label: input.formatValue(v),
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
const markers = horizontalMarkers(datasource.markers, plot, y.project);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
plot,
|
|
263
|
+
paths,
|
|
264
|
+
markers: markers.lines,
|
|
265
|
+
labels: markers.labels,
|
|
266
|
+
xTicks: thinTicks(xTicks, input.fontSize, plot.width),
|
|
267
|
+
yTicks,
|
|
268
|
+
hitRegions,
|
|
269
|
+
incomplete: excluded > 0,
|
|
270
|
+
};
|
|
271
|
+
}
|