@coinbase/cds-mobile-visualization 3.4.0-beta.23 → 3.4.0-beta.24

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,177 @@
1
+ import React, { memo, useCallback, useMemo } from 'react';
2
+ import { Pressable, StyleSheet, View } from 'react-native';
3
+ import { useScreenReaderStatus } from '@coinbase/cds-mobile/hooks/useScreenReaderStatus';
4
+ import { useCartesianChartContext } from '../ChartProvider';
5
+ import { useScrubberContext } from '../utils';
6
+ import { getPointOnSerializableScale } from '../utils/point';
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ const normalizeScrubberAccessibilityStep = function (step, defaultStep) {
9
+ if (defaultStep === void 0) {
10
+ defaultStep = 1;
11
+ }
12
+ const resolvedDefaultStep = Number.isFinite(defaultStep) ? Math.max(1, Math.floor(defaultStep)) : 1;
13
+ if (step === undefined || !Number.isFinite(step)) {
14
+ return resolvedDefaultStep;
15
+ }
16
+ return Math.max(1, Math.floor(step));
17
+ };
18
+ const getScrubberSampledIndices = (dataLength, step) => {
19
+ if (dataLength <= 0) return [];
20
+ const lastIndex = dataLength - 1;
21
+ if (lastIndex === 0) return [0];
22
+ const normalizedStep = Math.max(1, Math.floor(step));
23
+ const sampledIndices = [0];
24
+ for (let dataIndex = normalizedStep; dataIndex < lastIndex; dataIndex += normalizedStep) {
25
+ sampledIndices.push(dataIndex);
26
+ }
27
+ sampledIndices.push(lastIndex);
28
+ return sampledIndices;
29
+ };
30
+ const getCategoryValueForIndex = (index, scale, axis) => {
31
+ if (scale.type === 'band') {
32
+ return index;
33
+ }
34
+ const axisData = axis == null ? void 0 : axis.data;
35
+ if (axisData && Array.isArray(axisData) && typeof axisData[0] === 'number') {
36
+ var _numericData$index;
37
+ const numericData = axisData;
38
+ return (_numericData$index = numericData[index]) != null ? _numericData$index : index;
39
+ }
40
+ return index;
41
+ };
42
+ const getScrubberSegmentWeights = function (sampledIndices, dataLength, categoryScale, categoryAxis, drawingArea, orientation) {
43
+ if (orientation === void 0) {
44
+ orientation = 'horizontal';
45
+ }
46
+ const dimensionSize = orientation === 'horizontal' ? drawingArea.width : drawingArea.height;
47
+ const dimensionStart = orientation === 'horizontal' ? drawingArea.x : drawingArea.y;
48
+ const dimensionEnd = dimensionStart + dimensionSize;
49
+ if (sampledIndices.length === 0 || !categoryScale || !categoryAxis || dimensionSize <= 0) {
50
+ const segmentWeights = sampledIndices.map((index, position) => {
51
+ var _sampledIndices;
52
+ const nextIndex = (_sampledIndices = sampledIndices[position + 1]) != null ? _sampledIndices : dataLength;
53
+ return Math.max(1, nextIndex - index);
54
+ });
55
+ return {
56
+ leading: 0,
57
+ segmentWeights,
58
+ trailing: 0
59
+ };
60
+ }
61
+ if (categoryScale.type === 'band') {
62
+ const bandScale = categoryScale;
63
+ const segmentWeights = [];
64
+ let leading = 0;
65
+ let trailing = 0;
66
+ for (let i = 0; i < sampledIndices.length; i++) {
67
+ const categoryValue = getCategoryValueForIndex(sampledIndices[i], categoryScale, categoryAxis);
68
+ const posStart = getPointOnSerializableScale(categoryValue, bandScale, 'stepStart');
69
+ const posEnd = getPointOnSerializableScale(categoryValue, bandScale, 'stepEnd');
70
+ segmentWeights.push(Math.max(1, Math.abs(posEnd - posStart)));
71
+ if (i === 0) {
72
+ leading = Math.max(0, Math.min(posStart, posEnd) - dimensionStart);
73
+ }
74
+ if (i === sampledIndices.length - 1) {
75
+ trailing = Math.max(0, dimensionEnd - Math.max(posStart, posEnd));
76
+ }
77
+ }
78
+ return {
79
+ leading,
80
+ segmentWeights,
81
+ trailing
82
+ };
83
+ }
84
+ const segmentWeights = sampledIndices.map((index, position) => {
85
+ const prevIndex = position > 0 ? sampledIndices[position - 1] : -1;
86
+ const categoryValue = getCategoryValueForIndex(index, categoryScale, categoryAxis);
87
+ const posEnd = getPointOnSerializableScale(categoryValue, categoryScale);
88
+ const posStart = prevIndex < 0 ? dimensionStart : getPointOnSerializableScale(getCategoryValueForIndex(prevIndex, categoryScale, categoryAxis), categoryScale);
89
+ return Math.max(1, Math.abs(posEnd - posStart));
90
+ });
91
+ return {
92
+ leading: 0,
93
+ segmentWeights,
94
+ trailing: 0
95
+ };
96
+ };
97
+ const styles = StyleSheet.create({
98
+ container: {
99
+ position: 'absolute'
100
+ },
101
+ segments: {
102
+ flex: 1
103
+ }
104
+ });
105
+ export const ScrubberAccessibilityView = /*#__PURE__*/memo(_ref => {
106
+ let {
107
+ accessibilityLabel,
108
+ accessibilityStep
109
+ } = _ref;
110
+ const isScreenReaderEnabled = useScreenReaderStatus();
111
+ const {
112
+ dataLength,
113
+ drawingArea,
114
+ layout,
115
+ getXAxis,
116
+ getYAxis,
117
+ getXSerializableScale,
118
+ getYSerializableScale
119
+ } = useCartesianChartContext();
120
+ const {
121
+ enableScrubbing
122
+ } = useScrubberContext();
123
+ const isHorizontalLayout = layout === 'horizontal';
124
+ const categoryAxis = useMemo(() => isHorizontalLayout ? getYAxis() : getXAxis(), [isHorizontalLayout, getXAxis, getYAxis]);
125
+ const categoryScale = useMemo(() => isHorizontalLayout ? getYSerializableScale() : getXSerializableScale(), [isHorizontalLayout, getXSerializableScale, getYSerializableScale]);
126
+ const resolvedStep = useMemo(() => normalizeScrubberAccessibilityStep(accessibilityStep), [accessibilityStep]);
127
+ const sampledIndices = useMemo(() => getScrubberSampledIndices(dataLength, resolvedStep), [dataLength, resolvedStep]);
128
+ const segmentOrientation = isHorizontalLayout ? 'vertical' : 'horizontal';
129
+ const {
130
+ leading,
131
+ segmentWeights,
132
+ trailing
133
+ } = useMemo(() => getScrubberSegmentWeights(sampledIndices, dataLength, categoryScale, categoryAxis, drawingArea, segmentOrientation), [sampledIndices, dataLength, categoryScale, categoryAxis, drawingArea, segmentOrientation]);
134
+ const sampledSegments = useMemo(() => {
135
+ if (accessibilityLabel === undefined) return [];
136
+ return sampledIndices.map((index, position) => {
137
+ var _segmentWeights$posit;
138
+ const weight = (_segmentWeights$posit = segmentWeights[position]) != null ? _segmentWeights$posit : 1;
139
+ const pointLabel = accessibilityLabel(index);
140
+ return {
141
+ index,
142
+ weight,
143
+ accessibilityLabel: pointLabel || "Data point " + (index + 1)
144
+ };
145
+ });
146
+ }, [accessibilityLabel, sampledIndices, segmentWeights]);
147
+ const getSegmentStyle = useCallback(weight => ({
148
+ flex: weight
149
+ }), []);
150
+ const overlayStyle = useMemo(() => ({
151
+ left: drawingArea.x,
152
+ top: drawingArea.y,
153
+ width: drawingArea.width,
154
+ height: drawingArea.height
155
+ }), [drawingArea.x, drawingArea.y, drawingArea.width, drawingArea.height]);
156
+ const shouldHide = useMemo(() => !isScreenReaderEnabled || !enableScrubbing || !accessibilityLabel || dataLength <= 0 || drawingArea.width <= 0 || drawingArea.height <= 0 || sampledSegments.length === 0, [isScreenReaderEnabled, enableScrubbing, accessibilityLabel, dataLength, drawingArea.width, drawingArea.height, sampledSegments.length]);
157
+ if (shouldHide) return;
158
+ const segmentsFlexDirection = isHorizontalLayout ? 'column' : 'row';
159
+ return /*#__PURE__*/_jsx(View, {
160
+ pointerEvents: "box-none",
161
+ style: [styles.container, overlayStyle],
162
+ children: /*#__PURE__*/_jsxs(View, {
163
+ style: [styles.segments, {
164
+ flexDirection: segmentsFlexDirection
165
+ }],
166
+ children: [leading > 0 && /*#__PURE__*/_jsx(View, {
167
+ style: getSegmentStyle(leading)
168
+ }), sampledSegments.map(segment => /*#__PURE__*/_jsx(Pressable, {
169
+ accessible: true,
170
+ accessibilityLabel: segment.accessibilityLabel,
171
+ style: getSegmentStyle(segment.weight)
172
+ }, segment.index)), trailing > 0 && /*#__PURE__*/_jsx(View, {
173
+ style: getSegmentStyle(trailing)
174
+ })]
175
+ })
176
+ });
177
+ });
@@ -18,11 +18,15 @@ import { getLineData, unwrapAnimatedValue, useScrubberContext } from '../../util
18
18
  import { DefaultScrubberBeacon, DefaultScrubberBeaconLabel, DefaultScrubberLabel, Scrubber } from '..';
19
19
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
20
  const sampleData = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
21
+ const chartAccessibilityLabel = "Price chart with " + sampleData.length + " data points. Swipe to navigate.";
22
+ const getScrubberAccessibilityLabel = index => "Point " + (index + 1) + ": " + sampleData[index];
21
23
  const BasicScrubber = () => {
22
24
  return /*#__PURE__*/_jsx(LineChart, {
23
25
  enableScrubbing: true,
24
26
  showArea: true,
25
27
  showYAxis: true,
28
+ accessibilityLabel: chartAccessibilityLabel,
29
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
26
30
  height: 150,
27
31
  series: [{
28
32
  id: 'prices',
@@ -48,26 +52,35 @@ const BasicScrubber = () => {
48
52
  })
49
53
  });
50
54
  };
55
+ const seriesFilterData = {
56
+ top: [15, 28, 32, 44, 46, 36, 40, 45, 48, 38],
57
+ upperMiddle: [12, 23, 21, 29, 34, 28, 31, 38, 42, 35],
58
+ lowerMiddle: [8, 15, 14, 25, 20, 18, 22, 28, 24, 30],
59
+ bottom: [4, 8, 11, 15, 16, 14, 16, 10, 12, 14]
60
+ };
51
61
  const SeriesFilter = () => {
62
+ const getScrubberAccessibilityLabel = useCallback(index => "Point " + (index + 1) + ": top " + seriesFilterData.top[index] + ", lowerMiddle " + seriesFilterData.lowerMiddle[index], []);
52
63
  return /*#__PURE__*/_jsx(LineChart, {
53
64
  enableScrubbing: true,
65
+ accessibilityLabel: "Chart with multiple series. Swipe to navigate.",
66
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
54
67
  height: 150,
55
68
  series: [{
56
69
  id: 'top',
57
- data: [15, 28, 32, 44, 46, 36, 40, 45, 48, 38]
70
+ data: seriesFilterData.top
58
71
  }, {
59
72
  id: 'upperMiddle',
60
- data: [12, 23, 21, 29, 34, 28, 31, 38, 42, 35],
73
+ data: seriesFilterData.upperMiddle,
61
74
  color: '#ef4444',
62
75
  type: 'dotted'
63
76
  }, {
64
77
  id: 'lowerMiddle',
65
- data: [8, 15, 14, 25, 20, 18, 22, 28, 24, 30],
78
+ data: seriesFilterData.lowerMiddle,
66
79
  color: '#f59e0b',
67
80
  curve: 'natural'
68
81
  }, {
69
82
  id: 'bottom',
70
- data: [4, 8, 11, 15, 16, 14, 16, 10, 12, 14],
83
+ data: seriesFilterData.bottom,
71
84
  color: '#800080',
72
85
  curve: 'step',
73
86
  showArea: true
@@ -81,6 +94,8 @@ const WithLabels = () => {
81
94
  return /*#__PURE__*/_jsx(LineChart, {
82
95
  enableScrubbing: true,
83
96
  showArea: true,
97
+ accessibilityLabel: chartAccessibilityLabel,
98
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
84
99
  height: 150,
85
100
  series: [{
86
101
  id: 'prices',
@@ -97,6 +112,8 @@ const IdlePulse = () => {
97
112
  return /*#__PURE__*/_jsx(LineChart, {
98
113
  enableScrubbing: true,
99
114
  showArea: true,
115
+ accessibilityLabel: chartAccessibilityLabel,
116
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
100
117
  height: 150,
101
118
  series: [{
102
119
  id: 'prices',
@@ -115,6 +132,8 @@ const ImperativePulse = () => {
115
132
  children: [/*#__PURE__*/_jsx(LineChart, {
116
133
  enableScrubbing: true,
117
134
  showArea: true,
135
+ accessibilityLabel: chartAccessibilityLabel,
136
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
118
137
  height: 150,
119
138
  series: [{
120
139
  id: 'prices',
@@ -145,6 +164,8 @@ const BeaconStroke = () => {
145
164
  children: /*#__PURE__*/_jsx(LineChart, {
146
165
  enableScrubbing: true,
147
166
  showArea: true,
167
+ accessibilityLabel: chartAccessibilityLabel,
168
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
148
169
  height: 150,
149
170
  series: [{
150
171
  id: 'prices',
@@ -172,6 +193,8 @@ const CustomBeacon = () => {
172
193
  enableScrubbing: true,
173
194
  showArea: true,
174
195
  showYAxis: true,
196
+ accessibilityLabel: chartAccessibilityLabel,
197
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
175
198
  height: 150,
176
199
  series: [{
177
200
  id: 'prices',
@@ -249,7 +272,9 @@ const CustomBeaconLabel = () => {
249
272
  enableScrubbing: true,
250
273
  showArea: true,
251
274
  showYAxis: true,
275
+ accessibilityLabel: "Temperature chart with 6 data points. Swipe to navigate.",
252
276
  areaType: "dotted",
277
+ getScrubberAccessibilityLabel: index => "Point " + (index + 1) + ": " + [25, 30, 35, 45, 60, 100][index] + "\xB0F",
253
278
  height: 150,
254
279
  series: [{
255
280
  id: 'Boston',
@@ -372,7 +397,9 @@ const PercentageBeaconLabels = () => {
372
397
  children: /*#__PURE__*/_jsx(LineChart, {
373
398
  enableScrubbing: true,
374
399
  showArea: true,
400
+ accessibilityLabel: "NYC vs ATL comparison chart. Swipe to navigate.",
375
401
  areaType: "dotted",
402
+ getScrubberAccessibilityLabel: index => "Point " + (index + 1),
376
403
  height: 150,
377
404
  inset: {
378
405
  bottom: 8,
@@ -410,7 +437,9 @@ const PercentageBeaconLabels = () => {
410
437
  children: /*#__PURE__*/_jsx(LineChart, {
411
438
  enableScrubbing: true,
412
439
  showArea: true,
440
+ accessibilityLabel: "NYC vs ATL comparison chart. Swipe to navigate.",
413
441
  areaType: "dotted",
442
+ getScrubberAccessibilityLabel: index => "Point " + (index + 1),
414
443
  height: 150,
415
444
  inset: {
416
445
  bottom: 8,
@@ -449,6 +478,8 @@ const HideBeaconLabels = () => {
449
478
  enableScrubbing: true,
450
479
  legend: true,
451
480
  showArea: true,
481
+ accessibilityLabel: "Website visitors across 7 pages. Swipe to navigate.",
482
+ getScrubberAccessibilityLabel: index => "Page " + (index + 1) + ": " + [2400, 1398, 9800, 3908, 4800, 3800, 4300][index] + " views",
452
483
  height: 200,
453
484
  inset: {
454
485
  top: 60
@@ -475,6 +506,8 @@ const LabelElevated = () => {
475
506
  return /*#__PURE__*/_jsx(LineChart, {
476
507
  enableScrubbing: true,
477
508
  showArea: true,
509
+ accessibilityLabel: chartAccessibilityLabel,
510
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
478
511
  height: 200,
479
512
  inset: {
480
513
  top: 60
@@ -508,6 +541,8 @@ const CustomLabelComponent = () => {
508
541
  return /*#__PURE__*/_jsx(LineChart, {
509
542
  enableScrubbing: true,
510
543
  showArea: true,
544
+ accessibilityLabel: chartAccessibilityLabel,
545
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
511
546
  height: 200,
512
547
  inset: {
513
548
  top: 16,
@@ -523,11 +558,15 @@ const CustomLabelComponent = () => {
523
558
  })
524
559
  });
525
560
  };
561
+ const ethData = [5, 15, 18, 30, 65, 30, 15, 35, 15, 2, 45, 12, 15, 40];
526
562
  const LabelFonts = () => {
563
+ const getScrubberAccessibilityLabel = useCallback(index => "Day " + (index + 1) + ": BTC " + sampleData[index] + ", ETH " + ethData[index], []);
527
564
  return /*#__PURE__*/_jsx(LineChart, {
528
565
  enableScrubbing: true,
529
566
  showArea: true,
530
567
  showYAxis: true,
568
+ accessibilityLabel: "BTC and ETH comparison chart. Swipe to navigate.",
569
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
531
570
  height: 150,
532
571
  series: [{
533
572
  id: 'btc',
@@ -536,7 +575,7 @@ const LabelFonts = () => {
536
575
  color: assets.btc.color
537
576
  }, {
538
577
  id: 'eth',
539
- data: [5, 15, 18, 30, 65, 30, 15, 35, 15, 2, 45, 12, 15, 40],
578
+ data: ethData,
540
579
  label: 'ETH',
541
580
  color: assets.eth.color
542
581
  }],
@@ -556,6 +595,8 @@ const LabelBoundsInset = () => {
556
595
  children: [/*#__PURE__*/_jsx(LineChart, {
557
596
  enableScrubbing: true,
558
597
  showArea: true,
598
+ accessibilityLabel: chartAccessibilityLabel,
599
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
559
600
  height: 150,
560
601
  inset: {
561
602
  left: 0,
@@ -572,6 +613,8 @@ const LabelBoundsInset = () => {
572
613
  }), /*#__PURE__*/_jsx(LineChart, {
573
614
  enableScrubbing: true,
574
615
  showArea: true,
616
+ accessibilityLabel: chartAccessibilityLabel,
617
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
575
618
  height: 150,
576
619
  inset: {
577
620
  left: 0,
@@ -597,6 +640,8 @@ const CustomLine = () => {
597
640
  return /*#__PURE__*/_jsx(LineChart, {
598
641
  enableScrubbing: true,
599
642
  showArea: true,
643
+ accessibilityLabel: chartAccessibilityLabel,
644
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
600
645
  height: 150,
601
646
  series: [{
602
647
  id: 'prices',
@@ -629,6 +674,8 @@ const HiddenScrubberWhenIdle = () => {
629
674
  return /*#__PURE__*/_jsx(LineChart, {
630
675
  enableScrubbing: true,
631
676
  showArea: true,
677
+ accessibilityLabel: chartAccessibilityLabel,
678
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
632
679
  height: 150,
633
680
  series: [{
634
681
  id: 'prices',
@@ -645,6 +692,8 @@ const HideOverlay = () => {
645
692
  return /*#__PURE__*/_jsx(LineChart, {
646
693
  enableScrubbing: true,
647
694
  showArea: true,
695
+ accessibilityLabel: chartAccessibilityLabel,
696
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
648
697
  height: 150,
649
698
  series: [{
650
699
  id: 'prices',
@@ -737,10 +786,13 @@ const MatchupBeaconLabels = () => {
737
786
  seriesId: seriesId
738
787
  }));
739
788
  });
789
+ const getScrubberAccessibilityLabel = useCallback(index => "Point " + (index + 1) + ": BLUE " + matchupBlueData[index] + "%, RED " + matchupRedData[index] + "%", []);
740
790
  return /*#__PURE__*/_jsx(LineChart, {
741
791
  enableScrubbing: true,
742
792
  showArea: true,
793
+ accessibilityLabel: "BLUE vs RED matchup chart. Swipe to navigate.",
743
794
  areaType: "dotted",
795
+ getScrubberAccessibilityLabel: getScrubberAccessibilityLabel,
744
796
  height: 300,
745
797
  inset: {
746
798
  bottom: 8,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/cds-mobile-visualization",
3
- "version": "3.4.0-beta.23",
3
+ "version": "3.4.0-beta.24",
4
4
  "description": "Coinbase Design System - Mobile Visualization Native",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,9 +36,9 @@
36
36
  "CHANGELOG"
37
37
  ],
38
38
  "peerDependencies": {
39
- "@coinbase/cds-common": "^8.52.0",
39
+ "@coinbase/cds-common": "^8.52.2",
40
40
  "@coinbase/cds-lottie-files": "^3.3.4",
41
- "@coinbase/cds-mobile": "^8.52.0",
41
+ "@coinbase/cds-mobile": "^8.52.2",
42
42
  "@coinbase/cds-utils": "^2.3.5",
43
43
  "@shopify/react-native-skia": "^1.12.4 || ^2.0.0",
44
44
  "react": "^18.3.1",
@@ -57,9 +57,9 @@
57
57
  "@babel/preset-env": "^7.28.0",
58
58
  "@babel/preset-react": "^7.27.1",
59
59
  "@babel/preset-typescript": "^7.27.1",
60
- "@coinbase/cds-common": "^8.52.0",
60
+ "@coinbase/cds-common": "^8.52.2",
61
61
  "@coinbase/cds-lottie-files": "^3.3.4",
62
- "@coinbase/cds-mobile": "^8.52.0",
62
+ "@coinbase/cds-mobile": "^8.52.2",
63
63
  "@coinbase/cds-utils": "^2.3.5",
64
64
  "@shopify/react-native-skia": "1.12.4",
65
65
  "@types/react": "^18.3.12",