@bashem/rn-charts 0.0.2

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.
Files changed (97) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +35 -0
  3. package/lib/module/index.js +14 -0
  4. package/lib/module/index.js.map +1 -0
  5. package/lib/module/package.json +1 -0
  6. package/lib/module/skia/AreaChart/AreaChart.js +122 -0
  7. package/lib/module/skia/AreaChart/AreaChart.js.map +1 -0
  8. package/lib/module/skia/AreaChart/useAreaChart.js +141 -0
  9. package/lib/module/skia/AreaChart/useAreaChart.js.map +1 -0
  10. package/lib/module/skia/BarChart/BarChart.js +127 -0
  11. package/lib/module/skia/BarChart/BarChart.js.map +1 -0
  12. package/lib/module/skia/BarChart/useBarChart.js +172 -0
  13. package/lib/module/skia/BarChart/useBarChart.js.map +1 -0
  14. package/lib/module/skia/Common/VerticalLabel.js +73 -0
  15. package/lib/module/skia/Common/VerticalLabel.js.map +1 -0
  16. package/lib/module/skia/HeatMap/HeatMap.js +76 -0
  17. package/lib/module/skia/HeatMap/HeatMap.js.map +1 -0
  18. package/lib/module/skia/HeatMap/useHeatMap.js +139 -0
  19. package/lib/module/skia/HeatMap/useHeatMap.js.map +1 -0
  20. package/lib/module/skia/PieChart/PieChart.js +96 -0
  21. package/lib/module/skia/PieChart/PieChart.js.map +1 -0
  22. package/lib/module/skia/PieChart/usePieChart.js +103 -0
  23. package/lib/module/skia/PieChart/usePieChart.js.map +1 -0
  24. package/lib/module/skia/Popup.js +58 -0
  25. package/lib/module/skia/Popup.js.map +1 -0
  26. package/lib/module/skia/Progress/LinearProgress.js +69 -0
  27. package/lib/module/skia/Progress/LinearProgress.js.map +1 -0
  28. package/lib/module/skia/Progress/SemiCircleProgress.js +70 -0
  29. package/lib/module/skia/Progress/SemiCircleProgress.js.map +1 -0
  30. package/lib/module/skia/RadarChart/RadarChart.js +98 -0
  31. package/lib/module/skia/RadarChart/RadarChart.js.map +1 -0
  32. package/lib/module/skia/RadarChart/useRadarChart.js +164 -0
  33. package/lib/module/skia/RadarChart/useRadarChart.js.map +1 -0
  34. package/lib/module/skia/common.js +65 -0
  35. package/lib/module/skia/common.js.map +1 -0
  36. package/lib/module/util/colors.js +182 -0
  37. package/lib/module/util/colors.js.map +1 -0
  38. package/lib/module/util/util.js +71 -0
  39. package/lib/module/util/util.js.map +1 -0
  40. package/lib/typescript/index.d.ts +2 -0
  41. package/lib/typescript/index.d.ts.map +1 -0
  42. package/lib/typescript/package.json +1 -0
  43. package/lib/typescript/src/index.d.ts +10 -0
  44. package/lib/typescript/src/index.d.ts.map +1 -0
  45. package/lib/typescript/src/skia/AreaChart/AreaChart.d.ts +25 -0
  46. package/lib/typescript/src/skia/AreaChart/AreaChart.d.ts.map +1 -0
  47. package/lib/typescript/src/skia/AreaChart/useAreaChart.d.ts +47 -0
  48. package/lib/typescript/src/skia/AreaChart/useAreaChart.d.ts.map +1 -0
  49. package/lib/typescript/src/skia/BarChart/BarChart.d.ts +30 -0
  50. package/lib/typescript/src/skia/BarChart/BarChart.d.ts.map +1 -0
  51. package/lib/typescript/src/skia/BarChart/useBarChart.d.ts +41 -0
  52. package/lib/typescript/src/skia/BarChart/useBarChart.d.ts.map +1 -0
  53. package/lib/typescript/src/skia/Common/VerticalLabel.d.ts +17 -0
  54. package/lib/typescript/src/skia/Common/VerticalLabel.d.ts.map +1 -0
  55. package/lib/typescript/src/skia/HeatMap/HeatMap.d.ts +33 -0
  56. package/lib/typescript/src/skia/HeatMap/HeatMap.d.ts.map +1 -0
  57. package/lib/typescript/src/skia/HeatMap/useHeatMap.d.ts +25 -0
  58. package/lib/typescript/src/skia/HeatMap/useHeatMap.d.ts.map +1 -0
  59. package/lib/typescript/src/skia/PieChart/PieChart.d.ts +27 -0
  60. package/lib/typescript/src/skia/PieChart/PieChart.d.ts.map +1 -0
  61. package/lib/typescript/src/skia/PieChart/usePieChart.d.ts +13 -0
  62. package/lib/typescript/src/skia/PieChart/usePieChart.d.ts.map +1 -0
  63. package/lib/typescript/src/skia/Popup.d.ts +26 -0
  64. package/lib/typescript/src/skia/Popup.d.ts.map +1 -0
  65. package/lib/typescript/src/skia/Progress/LinearProgress.d.ts +18 -0
  66. package/lib/typescript/src/skia/Progress/LinearProgress.d.ts.map +1 -0
  67. package/lib/typescript/src/skia/Progress/SemiCircleProgress.d.ts +18 -0
  68. package/lib/typescript/src/skia/Progress/SemiCircleProgress.d.ts.map +1 -0
  69. package/lib/typescript/src/skia/RadarChart/RadarChart.d.ts +27 -0
  70. package/lib/typescript/src/skia/RadarChart/RadarChart.d.ts.map +1 -0
  71. package/lib/typescript/src/skia/RadarChart/useRadarChart.d.ts +41 -0
  72. package/lib/typescript/src/skia/RadarChart/useRadarChart.d.ts.map +1 -0
  73. package/lib/typescript/src/skia/common.d.ts +31 -0
  74. package/lib/typescript/src/skia/common.d.ts.map +1 -0
  75. package/lib/typescript/src/util/colors.d.ts +4 -0
  76. package/lib/typescript/src/util/colors.d.ts.map +1 -0
  77. package/lib/typescript/src/util/util.d.ts +33 -0
  78. package/lib/typescript/src/util/util.d.ts.map +1 -0
  79. package/package.json +172 -0
  80. package/src/index.tsx +12 -0
  81. package/src/skia/AreaChart/AreaChart.tsx +140 -0
  82. package/src/skia/AreaChart/useAreaChart.ts +180 -0
  83. package/src/skia/BarChart/BarChart.tsx +190 -0
  84. package/src/skia/BarChart/useBarChart.ts +210 -0
  85. package/src/skia/Common/VerticalLabel.tsx +91 -0
  86. package/src/skia/HeatMap/HeatMap.tsx +106 -0
  87. package/src/skia/HeatMap/useHeatMap.ts +175 -0
  88. package/src/skia/PieChart/PieChart.tsx +114 -0
  89. package/src/skia/PieChart/usePieChart.ts +156 -0
  90. package/src/skia/Popup.tsx +125 -0
  91. package/src/skia/Progress/LinearProgress.tsx +84 -0
  92. package/src/skia/Progress/SemiCircleProgress.tsx +82 -0
  93. package/src/skia/RadarChart/RadarChart.tsx +159 -0
  94. package/src/skia/RadarChart/useRadarChart.ts +208 -0
  95. package/src/skia/common.ts +82 -0
  96. package/src/util/colors.ts +186 -0
  97. package/src/util/util.ts +89 -0
@@ -0,0 +1,106 @@
1
+ import React, { type Ref } from 'react';
2
+ import { View, Modal } from 'react-native';
3
+ import { Canvas, Group, Rect } from '@shopify/react-native-skia';
4
+ import useHeatMap from './useHeatMap';
5
+ import type { CommonStyle } from '../common';
6
+ import Popup, { type PopupStyle } from '../Popup';
7
+
8
+ export type DayData = {
9
+ date: string;
10
+ value: number;
11
+ dayOfWeek: number;
12
+ week: number;
13
+ x: number;
14
+ y: number;
15
+ };
16
+
17
+ export interface HandleOutSideTouch {
18
+ touchedOutside: () => void;
19
+ }
20
+
21
+ export interface HeatMapStyle extends CommonStyle {
22
+ cellSize?: number;
23
+ cellGap?: number;
24
+ cellMaxColor?: string;
25
+ cellMinColor?: string;
26
+ }
27
+
28
+ export interface HeatMapProps {
29
+ startDate: string;
30
+ endDate: string;
31
+ data?: Record<string, number>;
32
+ style?: HeatMapStyle;
33
+ minValue?: number;
34
+ maxValue?: number;
35
+ ref?: Ref<HandleOutSideTouch | undefined>;
36
+ popupStyle?: PopupStyle<DayData>;
37
+ }
38
+
39
+ function HeatMap(props: HeatMapProps) {
40
+ const {
41
+ daysInRange,
42
+ totalWidth,
43
+ totalHeight,
44
+ popupData,
45
+ popupRef,
46
+ popupDimension,
47
+ touchHandler,
48
+ getColor,
49
+ cellSize,
50
+ onTouchOutside,
51
+ } = useHeatMap(props);
52
+
53
+ const [viewOffset, setViewOffset] = React.useState({ x: 0, y: 0 });
54
+
55
+ return (
56
+ <View
57
+ style={{ backgroundColor: props.style?.backgroundColor }}
58
+ ref={(view) => {
59
+ view?.measureInWindow((fx, fy) => {
60
+ setViewOffset((prev) => {
61
+ if (prev.x === fx && prev.y === fy) {
62
+ return prev;
63
+ }
64
+ return { x: fx, y: fy };
65
+ });
66
+ });
67
+ }}
68
+ >
69
+ <Canvas
70
+ style={{ width: totalWidth, height: totalHeight }}
71
+ onTouchStart={(event) =>
72
+ touchHandler(event.nativeEvent.locationX, event.nativeEvent.locationY)
73
+ }
74
+ >
75
+ <Group>
76
+ {daysInRange.map((day) => {
77
+ return (
78
+ <Rect
79
+ key={day.date}
80
+ x={day.x}
81
+ y={day.y}
82
+ width={cellSize}
83
+ height={cellSize}
84
+ color={getColor(day.value)}
85
+ />
86
+ );
87
+ })}
88
+ </Group>
89
+ </Canvas>
90
+
91
+ {popupData && props.popupStyle && (
92
+ <Popup
93
+ popupData={{ x: popupData.x, y: popupData.y, data: popupData.day }}
94
+ totalWidth={totalWidth}
95
+ totalHeight={totalHeight}
96
+ touchHandler={touchHandler}
97
+ onTouchOutside={onTouchOutside}
98
+ popupStyle={props.popupStyle}
99
+ viewOffset={viewOffset}
100
+ />
101
+ )}
102
+ </View>
103
+ );
104
+ }
105
+
106
+ export default HeatMap;
@@ -0,0 +1,175 @@
1
+ import type { View } from "react-native-reanimated/lib/typescript/Animated";
2
+ import type { DayData, HeatMapProps } from "./HeatMap";
3
+ import { useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+
5
+ function useHeatMap({
6
+ startDate,
7
+ endDate,
8
+ data,
9
+ style,
10
+ minValue,
11
+ maxValue,
12
+ ref,
13
+ popupStyle
14
+ }: HeatMapProps) {
15
+
16
+ const cellSize = style?.cellSize ?? 24;
17
+ const cellGap = style?.cellGap ?? 4;
18
+ const cellMaxColor = style?.cellMaxColor ?? '#50f555ff';
19
+ const cellMinColor = style?.cellMinColor ?? '#ffffffff';
20
+
21
+ const numberOfDaysInWeek = 7;
22
+ const numberOfMsInDay = 1000 * 60 * 60 * 24;
23
+
24
+ const [popupData, setPopupData] = useState<
25
+ { x: number; y: number; day: DayData; } | undefined
26
+ >(undefined);
27
+
28
+ const [popupDimension, setPopupDimension] = useState({
29
+ width: 0,
30
+ height: 0,
31
+ });
32
+
33
+ const popupRef = useRef<View>(null);
34
+
35
+ const formatDate = (date: Date) =>
36
+ `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
37
+ 2,
38
+ '0'
39
+ )}-${String(date.getDate()).padStart(2, '0')}`;
40
+
41
+ const { daysInRange, computedMin, computedMax } = useMemo(() => {
42
+ const start = new Date(startDate);
43
+ const end = new Date(endDate);
44
+
45
+ const output: DayData[] = [];
46
+ let computedMax = Number.MIN_VALUE;
47
+ let computedMin = Number.MAX_VALUE;
48
+
49
+ const startDayOfWeek = start.getDay();
50
+
51
+ for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
52
+ const dateStr = formatDate(d);
53
+ const value = data?.[dateStr] ?? 0;
54
+
55
+ const dayOfWeek = d.getDay();
56
+ const daysFromStart = Math.floor(
57
+ (d.getTime() - start.getTime()) / numberOfMsInDay
58
+ );
59
+
60
+ const week = Math.floor(
61
+ (startDayOfWeek + daysFromStart) / numberOfDaysInWeek
62
+ );
63
+
64
+ computedMax = Math.max(computedMax, value);
65
+ computedMin = Math.min(computedMin, value);
66
+
67
+ output.push({
68
+ date: dateStr,
69
+ value,
70
+ dayOfWeek,
71
+ week,
72
+ x: week * (cellSize + cellGap),
73
+ y: dayOfWeek * (cellSize + cellGap),
74
+ });
75
+ }
76
+
77
+ return {
78
+ daysInRange: output,
79
+ computedMin: minValue !== undefined ? minValue : computedMin,
80
+ computedMax: maxValue !== undefined ? maxValue : computedMax,
81
+ };
82
+ }, [startDate, endDate, data, minValue, maxValue, cellSize, cellGap]);
83
+
84
+ // --- COLOR LOGIC ---
85
+ const getColor = (value: number) => {
86
+ if (value <= 0) return cellMinColor;
87
+
88
+ const intensity = Math.min(
89
+ 1,
90
+ Math.max(0, (value - computedMin) / (computedMax - computedMin || 1))
91
+ );
92
+
93
+ const bigint = parseInt(cellMaxColor.replace('#', ''), 16);
94
+ const r = (bigint >> 16) & 255;
95
+ const g = (bigint >> 8) & 255;
96
+ const b = bigint & 255;
97
+
98
+ const mix = (base: number) => Math.round(255 - (255 - base) * intensity);
99
+
100
+ return `rgb(${mix(r)}, ${mix(g)}, ${mix(b)})`;
101
+ };
102
+
103
+ // Heatmap size
104
+ const numWeeks = Math.ceil(daysInRange.length / 7 + 1);
105
+ const totalWidth = numWeeks * (cellSize + cellGap);
106
+ const totalHeight = 7 * (cellSize + cellGap);
107
+
108
+ // --- POPUP MEASUREMENT ---
109
+ useLayoutEffect(() => {
110
+ if (popupRef.current) {
111
+ popupRef.current.measure((x, y, width, height) => {
112
+ setPopupDimension({ width, height });
113
+ });
114
+ }
115
+ }, [popupData]);
116
+
117
+ // --- TOUCH HANDLER ---
118
+ const touchHandler = (x: number, y: number) => {
119
+ if (!popupStyle?.renderPopup || (x < 0 || y < 0 || x >= totalWidth || y >= totalHeight)) {
120
+ setPopupData(undefined);
121
+ return;
122
+ }
123
+
124
+ const col = Math.floor(x / (cellSize + cellGap));
125
+ const row = Math.floor(y / (cellSize + cellGap));
126
+
127
+ const start = new Date(startDate);
128
+ const startDayOfWeek = start.getDay();
129
+
130
+ const index = col * numberOfDaysInWeek + row;
131
+
132
+ if (
133
+ index >= startDayOfWeek &&
134
+ index - startDayOfWeek < daysInRange.length
135
+ ) {
136
+ const day = daysInRange[index - startDayOfWeek];
137
+ if (day) {
138
+ setPopupData({
139
+ x: col * (cellSize + cellGap),
140
+ y: row * (cellSize + cellGap),
141
+ day,
142
+ });
143
+ return;
144
+ }
145
+ }
146
+
147
+ setPopupData(undefined);
148
+ };
149
+ const onTouchOutside = () => {
150
+ setPopupData(undefined);
151
+ };
152
+
153
+ useImperativeHandle(ref, () => ({
154
+ touchedOutside: () => {
155
+ setPopupData(undefined);
156
+ }
157
+ }), [ref]);
158
+
159
+ return {
160
+ daysInRange,
161
+ computedMin,
162
+ computedMax,
163
+ totalWidth,
164
+ totalHeight,
165
+ popupData,
166
+ popupRef,
167
+ popupDimension,
168
+ touchHandler,
169
+ getColor,
170
+ cellSize,
171
+ onTouchOutside
172
+ };
173
+ }
174
+
175
+ export default useHeatMap;
@@ -0,0 +1,114 @@
1
+ import { Canvas, Path } from '@shopify/react-native-skia';
2
+ import { type CommonStyle } from '../common';
3
+ import { Text, View } from 'react-native';
4
+ import { usePieChart } from './usePieChart';
5
+ import Popup, { type PopupStyle } from '../Popup';
6
+ import { useState } from 'react';
7
+
8
+ export interface PopupData {
9
+ centerX: number;
10
+ centerY: number;
11
+ data: PieSlice;
12
+ }
13
+
14
+ export type PieSlice = {
15
+ value: number;
16
+ color?: string;
17
+ label?: string;
18
+ };
19
+
20
+ export type PieChartProps = {
21
+ slices: PieSlice[];
22
+ style: PieChartStyles;
23
+ centerView?: React.ReactNode;
24
+ onSliceTouch?: (slice: PieSlice | undefined) => void;
25
+ popupStyle?: PopupStyle<PieSlice>;
26
+ };
27
+
28
+ export interface PieChartStyles extends CommonStyle {
29
+ radius?: number;
30
+ innerRadius?: number;
31
+ innerColor?: string;
32
+ }
33
+
34
+ function PieChart(props: PieChartProps) {
35
+ const { radius, innerRadius, paths, popupData, touchHandler } =
36
+ usePieChart(props);
37
+ const { style, centerView, popupStyle } = props;
38
+
39
+ const paddingTop = style.paddingTop ?? style.padding ?? 0;
40
+ const paddingBottom = style.paddingBottom ?? style.padding ?? 0;
41
+ const paddingLeft = style.paddingLeft ?? style.padding ?? 0;
42
+ const paddingRight = style.paddingRight ?? style.padding ?? 0;
43
+ const [viewOffset, setViewOffset] = useState({ x: 0, y: 0 });
44
+
45
+ return (
46
+ <View
47
+ style={{
48
+ paddingTop: paddingTop,
49
+ paddingBottom: paddingBottom,
50
+ paddingRight: paddingRight,
51
+ paddingLeft: paddingLeft,
52
+ backgroundColor: style.backgroundColor ?? 'transparent',
53
+ }}
54
+ ref={(view) => {
55
+ view?.measureInWindow((fx, fy) => {
56
+ setViewOffset((prev) => {
57
+ if (prev.x === fx && prev.y === fy) {
58
+ return prev;
59
+ }
60
+ return { x: fx, y: fy };
61
+ });
62
+ });
63
+ }}
64
+ >
65
+ {centerView && (
66
+ <View
67
+ style={{
68
+ position: 'absolute',
69
+ top: paddingTop + radius - innerRadius,
70
+ left: paddingLeft + radius - innerRadius,
71
+ width: innerRadius * 2,
72
+ height: innerRadius * 2,
73
+ borderRadius: innerRadius,
74
+ justifyContent: 'center',
75
+ alignItems: 'center',
76
+ backgroundColor: style.innerColor ?? 'black',
77
+ }}
78
+ >
79
+ {centerView}
80
+ </View>
81
+ )}
82
+ <Canvas
83
+ style={{
84
+ width: radius * 2,
85
+ height: radius * 2,
86
+ backgroundColor: style.backgroundColor ?? 'transparent',
87
+ }}
88
+ onTouchStart={(event) =>
89
+ touchHandler(event.nativeEvent.locationX, event.nativeEvent.locationY)
90
+ }
91
+ >
92
+ {paths.map(({ path, color }, index) => (
93
+ <Path key={index} path={path} color={color} stroke={{ width: 5 }} />
94
+ ))}
95
+ </Canvas>
96
+ {popupData && (
97
+ <Popup
98
+ popupData={{
99
+ x: popupData.centerX,
100
+ y: popupData.centerY,
101
+ data: popupData.data,
102
+ }}
103
+ totalWidth={radius * 2}
104
+ totalHeight={radius * 2}
105
+ touchHandler={(x, y) => touchHandler(x - paddingLeft, y - paddingTop)}
106
+ viewOffset={viewOffset}
107
+ popupStyle={popupStyle}
108
+ />
109
+ )}
110
+ </View>
111
+ );
112
+ }
113
+
114
+ export default PieChart;
@@ -0,0 +1,156 @@
1
+ import { useState } from "react";
2
+ import type { PieChartProps, PieChartStyles, PieSlice, PopupData } from "./PieChart";
3
+ import { rect, Skia } from "@shopify/react-native-skia";
4
+ import { getRandomRGBColor } from "../common";
5
+
6
+ function deegreesToRadians(degrees: number): number {
7
+ return (degrees * Math.PI) / 180;
8
+ }
9
+
10
+ function ypoint(angle: number, radius: number, cy: number): number {
11
+ return cy - radius * Math.sin(deegreesToRadians(angle));
12
+ }
13
+
14
+ function xpoint(angle: number, radius: number, cx: number): number {
15
+ return cx - radius * Math.cos(deegreesToRadians(angle));
16
+ }
17
+
18
+ function getCircularPoints(
19
+ startAngle: number,
20
+ radius: number,
21
+ angle: number,
22
+ cx: number,
23
+ cy: number
24
+ ): [number, number, number, number] {
25
+ let x1 = xpoint(startAngle, radius, cx);
26
+ let y1 = ypoint(startAngle, radius, cy);
27
+ let x2 = xpoint(startAngle + angle, radius, cx);
28
+ let y2 = ypoint(startAngle + angle, radius, cy);
29
+
30
+ return [x1, y1, x2, y2];
31
+ }
32
+
33
+ export function usePieChart({
34
+ slices,
35
+ style,
36
+ onSliceTouch
37
+ }: PieChartProps
38
+ ) {
39
+ const [popupData, setPopupData] = useState<PopupData | undefined>(undefined);
40
+
41
+ const radius = style.radius ?? 150;
42
+ const diameter = radius * 2;
43
+ const innerRadius = style.innerRadius ?? 100;
44
+ const cx = radius;
45
+ const cy = radius;
46
+
47
+ const total = slices.reduce((sum, slice) => sum + slice.value, 0);
48
+
49
+ let startAngle = 0;
50
+
51
+ const paths = slices.map(({ value, color }, index) => {
52
+ const sweepAngle = (value / total) * 360;
53
+
54
+ let [x1, y1, x2, y2] = getCircularPoints(
55
+ startAngle,
56
+ radius,
57
+ sweepAngle,
58
+ cx,
59
+ cy
60
+ );
61
+ let [cx1, cy1, cx2, cy2] = getCircularPoints(
62
+ startAngle,
63
+ innerRadius,
64
+ sweepAngle,
65
+ cx,
66
+ cy
67
+ );
68
+
69
+ const path = Skia.Path.Make();
70
+ path.moveTo(cx1, cy1);
71
+ path.lineTo(x1, y1);
72
+ path.addArc(
73
+ rect(
74
+ cx - radius,
75
+ cy - radius,
76
+ radius * 2,
77
+ radius * 2
78
+ ),
79
+ startAngle + 180,
80
+ sweepAngle
81
+ );
82
+ path.lineTo(cx2, cy2);
83
+
84
+ path.addArc(
85
+ rect(
86
+ cx - innerRadius,
87
+ cy - innerRadius,
88
+ innerRadius * 2,
89
+ innerRadius * 2
90
+ ),
91
+ startAngle + 180 + sweepAngle,
92
+ -sweepAngle
93
+ );
94
+ path.lineTo(x1, y1);
95
+ path.close();
96
+
97
+ startAngle += sweepAngle;
98
+
99
+ return { path, color: color ?? getRandomRGBColor() };
100
+ });
101
+
102
+ const touchHandler = (locationX: number, locationY: number) => {
103
+ if (!onSliceTouch || locationX < 0 || locationY < 0 || locationX >= diameter || locationY >= diameter) {
104
+ setPopupData(undefined);
105
+ return;
106
+ }
107
+
108
+ let foundPath = false;
109
+ let angles = 0;
110
+
111
+ paths.forEach(({ path }, index) => {
112
+ let slice = slices[index];
113
+ if (!slice) return;
114
+
115
+ let lastAngle = (slice.value / total) * 360;
116
+ if (path.contains(locationX, locationY)) {
117
+ const label = slice.label || 'Slice';
118
+
119
+ const outerX = xpoint(angles + lastAngle / 2, radius, cx);
120
+ const innerX = xpoint(angles + lastAngle / 2, innerRadius, cx);
121
+
122
+ const outerY = ypoint(angles + lastAngle / 2, radius, cy);
123
+ const innerY = ypoint(angles + lastAngle / 2, innerRadius, cy);
124
+
125
+ const centerY = (outerY + innerY) / 2;
126
+ const centerX = (outerX + innerX) / 2;
127
+
128
+ onSliceTouch?.(slice);
129
+ setPopupData({
130
+ centerX: centerX,
131
+ centerY: centerY,
132
+ data: slice,
133
+ });
134
+
135
+ foundPath = true;
136
+ return;
137
+ }
138
+
139
+ angles += lastAngle;
140
+ });
141
+
142
+ if (!foundPath) {
143
+ console.log('No slice found at touch location');
144
+ onSliceTouch?.(undefined);
145
+ setPopupData(undefined);
146
+ }
147
+ };
148
+ return {
149
+ paths,
150
+ diameter,
151
+ innerRadius,
152
+ radius,
153
+ popupData,
154
+ touchHandler,
155
+ };
156
+ }
@@ -0,0 +1,125 @@
1
+ import React from 'react';
2
+ import { Modal, View } from 'react-native';
3
+
4
+ export interface PopupStyle<T> {
5
+ width?: number;
6
+ height?: number;
7
+ renderPopup?: (data: T) => React.ReactNode;
8
+ }
9
+
10
+ interface PopupData<T> {
11
+ x: number;
12
+ y: number;
13
+ data: T;
14
+ }
15
+
16
+ interface PopupProps<T> {
17
+ popupData?: PopupData<T> | PopupData<T>[];
18
+ totalWidth: number;
19
+ totalHeight: number;
20
+ touchHandler?: (x: number, y: number) => void;
21
+ onTouchOutside?: () => void;
22
+ popupStyle?: PopupStyle<T>;
23
+ viewOffset: {
24
+ x: number;
25
+ y: number;
26
+ };
27
+ }
28
+
29
+ export default function Popup<T>({
30
+ popupData,
31
+ totalWidth,
32
+ totalHeight,
33
+ touchHandler,
34
+ onTouchOutside,
35
+ popupStyle,
36
+ viewOffset,
37
+ }: PopupProps<T>) {
38
+ return (
39
+ <>
40
+ {popupData && popupStyle?.renderPopup && (
41
+ <Modal
42
+ animationType="fade"
43
+ transparent={true}
44
+ visible={true}
45
+ onRequestClose={() => {
46
+ onTouchOutside?.();
47
+ }}
48
+ onTouchStart={(e) => {
49
+ console.log('modal touched ');
50
+ }}
51
+ >
52
+ <View
53
+ style={{
54
+ flex: 1,
55
+ width: '100%',
56
+ height: '100%',
57
+ }}
58
+ onTouchStart={(e) => {
59
+ const x = e.nativeEvent.pageX;
60
+ const y = e.nativeEvent.pageY;
61
+ touchHandler?.(x - viewOffset.x, y - viewOffset.y);
62
+ }}
63
+ >
64
+ {popupData && !Array.isArray(popupData) && (
65
+ <View
66
+ style={[
67
+ {
68
+ position: 'absolute',
69
+ left: Math.max(
70
+ 0,
71
+ Math.min(popupData.x, totalWidth - (popupStyle?.width ?? 0)) +
72
+ viewOffset.x
73
+ ),
74
+ top: Math.max(
75
+ 0,
76
+ Math.min(
77
+ popupData.y,
78
+ totalHeight - (popupStyle?.height ?? 0)
79
+ ) + viewOffset.y
80
+ ),
81
+ },
82
+ ]}
83
+ onTouchStart={(e) => e.stopPropagation()}
84
+ >
85
+ {popupStyle?.renderPopup(popupData.data)}
86
+ </View>
87
+ )}
88
+
89
+ {popupData &&
90
+ Array.isArray(popupData) &&
91
+ popupData.map((popupItem, index) =>
92
+ (
93
+ <View
94
+ key={index}
95
+ style={[
96
+ {
97
+ position: 'absolute',
98
+ left: Math.max(
99
+ 0,
100
+ Math.min(
101
+ popupItem.x,
102
+ totalWidth - (popupStyle?.width ?? 0)
103
+ ) + viewOffset.x
104
+ ),
105
+ top: Math.max(
106
+ 0,
107
+ Math.min(
108
+ popupItem.y,
109
+ totalHeight - (popupStyle?.height ?? 0)
110
+ ) + viewOffset.y
111
+ ),
112
+ },
113
+ ]}
114
+ onTouchStart={(e) => e.stopPropagation()}
115
+ >
116
+ {popupStyle?.renderPopup?.(popupItem.data)}
117
+ </View>
118
+ )
119
+ )}
120
+ </View>
121
+ </Modal>
122
+ )}
123
+ </>
124
+ );
125
+ }