exodeui-react-native 1.0.5 → 1.0.7
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/package.json +1 -1
- package/src/ExodeUIView.tsx +15 -5
- package/src/engine.ts +161 -76
- package/src/useExodeUI.ts +11 -1
package/package.json
CHANGED
package/src/ExodeUIView.tsx
CHANGED
|
@@ -11,18 +11,20 @@ export interface ExodeUIViewProps {
|
|
|
11
11
|
autoPlay?: boolean;
|
|
12
12
|
fit?: Fit;
|
|
13
13
|
alignment?: Alignment;
|
|
14
|
-
onReady?: (engine: any) => void;
|
|
14
|
+
onReady?: (engine: any) => void;
|
|
15
15
|
onTrigger?: (triggerName: string, animationName: string) => void;
|
|
16
16
|
onInputUpdate?: (nameOrId: string, value: any) => void;
|
|
17
|
-
onComponentChange?: (event: ComponentEvent) => void;
|
|
18
|
-
onToggle?: (name: string, checked: boolean) => void;
|
|
19
|
-
onInputChange?: (name: string, text: string) => void;
|
|
17
|
+
onComponentChange?: (event: ComponentEvent) => void;
|
|
18
|
+
onToggle?: (name: string, checked: boolean) => void;
|
|
19
|
+
onInputChange?: (name: string, text: string) => void;
|
|
20
20
|
onInputFocus?: (name: string) => void;
|
|
21
21
|
onInputBlur?: (name: string) => void;
|
|
22
|
+
onButtonClick?: (name: string, objectId: string) => void;
|
|
23
|
+
onGraphPointClick?: (event: { objectId: string; componentName: string; datasetIndex: number; pointIndex: number; value: number; label: string }) => void;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export const ExodeUIView = forwardRef<any, ExodeUIViewProps>(
|
|
25
|
-
({ artboard, src, style, autoPlay = true, fit = 'Contain', alignment = 'Center', onReady, onTrigger, onInputUpdate, onInputFocus, onInputBlur, onToggle, onInputChange, onComponentChange }, ref) => {
|
|
27
|
+
({ artboard, src, style, autoPlay = true, fit = 'Contain', alignment = 'Center', onReady, onTrigger, onInputUpdate, onInputFocus, onInputBlur, onToggle, onInputChange, onComponentChange, onButtonClick, onGraphPointClick }, ref) => {
|
|
26
28
|
|
|
27
29
|
const engineRef = useRef<ExodeUIEngine>(new ExodeUIEngine());
|
|
28
30
|
const [picture, setPicture] = useState<SkPicture | null>(null);
|
|
@@ -72,6 +74,14 @@ export const ExodeUIView = forwardRef<any, ExodeUIViewProps>(
|
|
|
72
74
|
if (onComponentChange) engineRef.current.setComponentChangeCallback(onComponentChange);
|
|
73
75
|
}, [onComponentChange]);
|
|
74
76
|
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (onButtonClick) engineRef.current.setButtonClickCallback(onButtonClick);
|
|
79
|
+
}, [onButtonClick]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (onGraphPointClick) engineRef.current.setGraphPointClickCallback(onGraphPointClick);
|
|
83
|
+
}, [onGraphPointClick]);
|
|
84
|
+
|
|
75
85
|
// Load Artboard
|
|
76
86
|
useEffect(() => {
|
|
77
87
|
const loadArtboard = async () => {
|
package/src/engine.ts
CHANGED
|
@@ -35,6 +35,8 @@ export class ExodeUIEngine {
|
|
|
35
35
|
private onInputChange?: (name: string, text: string) => void;
|
|
36
36
|
private onInputFocus?: (name: string) => void;
|
|
37
37
|
private onInputBlur?: (name: string) => void;
|
|
38
|
+
private onButtonClick?: (name: string, objectId: string) => void;
|
|
39
|
+
private onGraphPointClick?: (event: { objectId: string; componentName: string; datasetIndex: number; pointIndex: number; value: number; label: string }) => void;
|
|
38
40
|
|
|
39
41
|
// Track triggers that were just fired in the current frame
|
|
40
42
|
private justFiredTriggers: Set<string> = new Set();
|
|
@@ -78,6 +80,12 @@ export class ExodeUIEngine {
|
|
|
78
80
|
setInputBlurCallback(cb: (name: string) => void) {
|
|
79
81
|
this.onInputBlur = cb;
|
|
80
82
|
}
|
|
83
|
+
setButtonClickCallback(cb: (name: string, objectId: string) => void) {
|
|
84
|
+
this.onButtonClick = cb;
|
|
85
|
+
}
|
|
86
|
+
setGraphPointClickCallback(cb: (event: { objectId: string; componentName: string; datasetIndex: number; pointIndex: number; value: number; label: string }) => void) {
|
|
87
|
+
this.onGraphPointClick = cb;
|
|
88
|
+
}
|
|
81
89
|
|
|
82
90
|
constructor() {}
|
|
83
91
|
|
|
@@ -964,6 +972,15 @@ export class ExodeUIEngine {
|
|
|
964
972
|
}
|
|
965
973
|
}
|
|
966
974
|
|
|
975
|
+
if (targetObj.type === 'Component' && (targetObj as any).variant === 'button') {
|
|
976
|
+
if (type === 'click' || type === 'PointerDown') {
|
|
977
|
+
if (this.onButtonClick) this.onButtonClick(targetObj.name, targetObj.id);
|
|
978
|
+
if (this.onComponentChange) {
|
|
979
|
+
this.onComponentChange({ objectId: targetObj.id, componentName: targetObj.name, variant: 'button', property: 'click', value: true });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
967
984
|
if (targetObj.type === 'Component' && (targetObj as any).variant === 'slider') {
|
|
968
985
|
if (type === 'PointerDown') {
|
|
969
986
|
this.draggingSliderId = targetObj.id;
|
|
@@ -1021,6 +1038,43 @@ export class ExodeUIEngine {
|
|
|
1021
1038
|
}
|
|
1022
1039
|
}
|
|
1023
1040
|
}
|
|
1041
|
+
|
|
1042
|
+
if (targetObj.type === 'LineGraph') {
|
|
1043
|
+
if (type === 'click' || type === 'PointerDown') {
|
|
1044
|
+
const geom = targetObj.geometry as any;
|
|
1045
|
+
const datasets = geom?.datasets || [];
|
|
1046
|
+
const state = this.objectStates.get(targetObj.id);
|
|
1047
|
+
const objW = state?.width || geom?.width || 200;
|
|
1048
|
+
const objH = state?.height || geom?.height || 150;
|
|
1049
|
+
const world = this.getWorldTransform(targetObj.id);
|
|
1050
|
+
const axisMarginL = 30;
|
|
1051
|
+
const axisMarginB = 20;
|
|
1052
|
+
const plotW = objW - axisMarginL;
|
|
1053
|
+
const plotH = objH - axisMarginB;
|
|
1054
|
+
let globalMax = 1;
|
|
1055
|
+
datasets.forEach((ds: any) => { const m = Math.max(...(ds.data || [0])); if (m > globalMax) globalMax = m; });
|
|
1056
|
+
|
|
1057
|
+
let closestDist = Infinity;
|
|
1058
|
+
let closestResult: any = null;
|
|
1059
|
+
datasets.forEach((ds: any, dIdx: number) => {
|
|
1060
|
+
const data = ds.data || [];
|
|
1061
|
+
const stepX = plotW / Math.max(data.length - 1, 1);
|
|
1062
|
+
data.forEach((val: number, pIdx: number) => {
|
|
1063
|
+
const px = world.x - objW/2 + axisMarginL + pIdx * stepX;
|
|
1064
|
+
const py = world.y + objH/2 - axisMarginB - (val / globalMax) * plotH;
|
|
1065
|
+
const dist = Math.sqrt((x - px) ** 2 + (y - py) ** 2);
|
|
1066
|
+
if (dist < closestDist) { closestDist = dist; closestResult = { dIdx, pIdx, val, label: ds.label || `Dataset ${dIdx + 1}` }; }
|
|
1067
|
+
});
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
if (closestResult && closestDist < 20 && this.onGraphPointClick) {
|
|
1071
|
+
this.onGraphPointClick({ objectId: targetObj.id, componentName: targetObj.name, datasetIndex: closestResult.dIdx, pointIndex: closestResult.pIdx, value: closestResult.val, label: closestResult.label });
|
|
1072
|
+
}
|
|
1073
|
+
if (this.onComponentChange) {
|
|
1074
|
+
this.onComponentChange({ objectId: targetObj.id, componentName: targetObj.name, variant: 'linegraph', property: 'click', value: closestResult?.val ?? null });
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1024
1078
|
}
|
|
1025
1079
|
}
|
|
1026
1080
|
|
|
@@ -1630,13 +1684,13 @@ export class ExodeUIEngine {
|
|
|
1630
1684
|
const paint = Skia.Paint();
|
|
1631
1685
|
|
|
1632
1686
|
const isPressed = this.draggingSliderId === obj.id;
|
|
1633
|
-
//
|
|
1634
|
-
const bgNormal = opts.backgroundColor || opts.background || '#3b82f6';
|
|
1687
|
+
// The .exode file uses 'buttonBgColor', fall back to 'backgroundColor'
|
|
1688
|
+
const bgNormal = opts.buttonBgColor || opts.backgroundColor || opts.background || '#3b82f6';
|
|
1635
1689
|
const bgActive = opts.activeBackgroundColor || opts.activeBackground || '#2563eb';
|
|
1636
|
-
const bgStr = typeof bgNormal === 'string' ? bgNormal : '#3b82f6';
|
|
1637
|
-
const bgActiveStr = typeof bgActive === 'string' ? bgActive : '#2563eb';
|
|
1690
|
+
const bgStr = (typeof bgNormal === 'string' ? bgNormal : '#3b82f6').replace(/\s+/g, '');
|
|
1691
|
+
const bgActiveStr = (typeof bgActive === 'string' ? bgActive : '#2563eb').replace(/\s+/g, '');
|
|
1638
1692
|
try {
|
|
1639
|
-
paint.setColor(Skia.Color(
|
|
1693
|
+
paint.setColor(Skia.Color(isPressed ? bgActiveStr : bgStr));
|
|
1640
1694
|
} catch {
|
|
1641
1695
|
paint.setColor(Skia.Color('#3b82f6'));
|
|
1642
1696
|
}
|
|
@@ -1644,7 +1698,8 @@ export class ExodeUIEngine {
|
|
|
1644
1698
|
const r = opts.cornerRadius ?? 8;
|
|
1645
1699
|
canvas.drawRRect(Skia.RRectXY({ x: -w/2, y: -h/2, width: w, height: h }, r, r), paint);
|
|
1646
1700
|
|
|
1647
|
-
|
|
1701
|
+
// label is an object {text, fontSize, color, ...}
|
|
1702
|
+
const labelObj = typeof opts.label === 'object' && opts.label !== null ? opts.label : {};
|
|
1648
1703
|
const labelText = labelObj.text || opts.text || opts.title || 'Button';
|
|
1649
1704
|
const fontSize = labelObj.fontSize || opts.fontSize || 14;
|
|
1650
1705
|
const labelColor = labelObj.color || opts.color || '#ffffff';
|
|
@@ -1658,8 +1713,7 @@ export class ExodeUIEngine {
|
|
|
1658
1713
|
}
|
|
1659
1714
|
if (isPressed) textPaint.setAlphaf(0.85);
|
|
1660
1715
|
|
|
1661
|
-
|
|
1662
|
-
const textWidth = font.getTextWidth ? font.getTextWidth(labelText) : 0;
|
|
1716
|
+
const textWidth = font.getTextWidth ? font.getTextWidth(String(labelText)) : 0;
|
|
1663
1717
|
canvas.save();
|
|
1664
1718
|
canvas.translate(-textWidth / 2, fontSize / 3);
|
|
1665
1719
|
canvas.drawText(String(labelText), 0, 0, textPaint, font);
|
|
@@ -1675,26 +1729,30 @@ export class ExodeUIEngine {
|
|
|
1675
1729
|
const trackColor = checked ? (opts.activeColor || '#3b82f6') : (opts.inactiveColor || '#374151');
|
|
1676
1730
|
try { paint.setColor(Skia.Color(trackColor.replace(/\s+/g, ''))); } catch { paint.setColor(Skia.Color('#374151')); }
|
|
1677
1731
|
|
|
1678
|
-
const r = h / 2;
|
|
1732
|
+
const r = opts.cornerRadius ?? h / 2;
|
|
1679
1733
|
canvas.drawRRect(Skia.RRectXY({ x: -w/2, y: -h/2, width: w, height: h }, r, r), paint);
|
|
1680
1734
|
|
|
1681
1735
|
const thumbPaint = Skia.Paint();
|
|
1682
|
-
|
|
1736
|
+
const thumbCol = opts.knobColor || opts.thumbColor || '#ffffff';
|
|
1737
|
+
try { thumbPaint.setColor(Skia.Color(thumbCol.replace(/\s+/g, ''))); } catch { thumbPaint.setColor(Skia.Color('#ffffff')); }
|
|
1683
1738
|
const thumbRadius = (h - 8) / 2;
|
|
1684
1739
|
const thumbX = checked ? (w / 2 - thumbRadius - 4) : (-w / 2 + thumbRadius + 4);
|
|
1685
1740
|
canvas.drawCircle(thumbX, 0, thumbRadius, thumbPaint);
|
|
1686
1741
|
|
|
1687
|
-
//
|
|
1688
|
-
const
|
|
1689
|
-
if (
|
|
1690
|
-
const fontSize =
|
|
1742
|
+
// label is an object: {text, position, fontSize, color, gap}
|
|
1743
|
+
const labelObj = typeof opts.label === 'object' && opts.label !== null ? opts.label : null;
|
|
1744
|
+
if (labelObj && labelObj.text) {
|
|
1745
|
+
const fontSize = labelObj.fontSize || 12;
|
|
1691
1746
|
const font = this.getFont(fontSize, 'Helvetica Neue');
|
|
1692
1747
|
const labelPaint = Skia.Paint();
|
|
1693
|
-
const labelColor =
|
|
1748
|
+
const labelColor = labelObj.color || '#ffffff';
|
|
1694
1749
|
try { labelPaint.setColor(Skia.Color((typeof labelColor === 'string' ? labelColor : '#ffffff').replace(/\s+/g, ''))); } catch { labelPaint.setColor(Skia.Color('#ffffff')); }
|
|
1750
|
+
const gap = labelObj.gap || 8;
|
|
1751
|
+
const position = labelObj.position || 'right';
|
|
1752
|
+
const offsetX = position === 'left' ? -(w/2 + gap + font.getTextWidth(String(labelObj.text))) : w/2 + gap;
|
|
1695
1753
|
canvas.save();
|
|
1696
|
-
canvas.translate(
|
|
1697
|
-
canvas.drawText(String(
|
|
1754
|
+
canvas.translate(offsetX, fontSize / 3);
|
|
1755
|
+
canvas.drawText(String(labelObj.text), 0, 0, labelPaint, font);
|
|
1698
1756
|
canvas.restore();
|
|
1699
1757
|
}
|
|
1700
1758
|
}
|
|
@@ -1714,7 +1772,7 @@ export class ExodeUIEngine {
|
|
|
1714
1772
|
canvas.drawRRect(Skia.RRectXY({ x: -w/2, y: -trackHeight/2, width: w, height: trackHeight }, trackHeight/2, trackHeight/2), trackPaint);
|
|
1715
1773
|
|
|
1716
1774
|
const activePaint = Skia.Paint();
|
|
1717
|
-
const activeColor = opts.activeColor ||
|
|
1775
|
+
const activeColor = opts.activeColor || '#3b82f6';
|
|
1718
1776
|
try { activePaint.setColor(Skia.Color((typeof activeColor === 'string' ? activeColor : '#3b82f6').replace(/\s+/g, ''))); } catch { activePaint.setColor(Skia.Color('#3b82f6')); }
|
|
1719
1777
|
const thumbWidth = opts.thumbWidth ?? 16;
|
|
1720
1778
|
const travelW = w - thumbWidth;
|
|
@@ -1727,16 +1785,39 @@ export class ExodeUIEngine {
|
|
|
1727
1785
|
try { thumbPaint.setColor(Skia.Color((typeof thumbCol === 'string' ? thumbCol : '#ffffff').replace(/\s+/g, ''))); } catch { thumbPaint.setColor(Skia.Color('#ffffff')); }
|
|
1728
1786
|
canvas.drawCircle(thumbX, 0, thumbWidth / 2, thumbPaint);
|
|
1729
1787
|
|
|
1730
|
-
//
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
const
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
const
|
|
1788
|
+
// Label: opts.label is an object {text, visible, fontSize, color}
|
|
1789
|
+
const labelObj = typeof opts.label === 'object' && opts.label !== null ? opts.label : null;
|
|
1790
|
+
if (labelObj && labelObj.text && labelObj.visible !== false) {
|
|
1791
|
+
const lFontSize = labelObj.fontSize || 12;
|
|
1792
|
+
const lFont = this.getFont(lFontSize, 'Helvetica Neue');
|
|
1793
|
+
const lPaint = Skia.Paint();
|
|
1794
|
+
const lColor = labelObj.color || '#ffffff';
|
|
1795
|
+
try { lPaint.setColor(Skia.Color((typeof lColor === 'string' ? lColor : '#9ca3af').replace(/\s+/g, ''))); } catch { lPaint.setColor(Skia.Color('#9ca3af')); }
|
|
1737
1796
|
canvas.save();
|
|
1738
|
-
canvas.translate(
|
|
1739
|
-
canvas.drawText(
|
|
1797
|
+
canvas.translate(-w/2, -h/2 - lFontSize - 2);
|
|
1798
|
+
canvas.drawText(String(labelObj.text), 0, 0, lPaint, lFont);
|
|
1799
|
+
canvas.restore();
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// Value tooltip: opts.showValueTooltip + tooltipOffsetY
|
|
1803
|
+
if (opts.showValueTooltip) {
|
|
1804
|
+
const tFontSize = 11;
|
|
1805
|
+
const tFont = this.getFont(tFontSize, 'Helvetica Neue');
|
|
1806
|
+
const tooltipBg = opts.tooltipBgColor || '#333333';
|
|
1807
|
+
const tooltipText = opts.tooltipColor || '#ffffff';
|
|
1808
|
+
const offsetY = opts.tooltipOffsetY ?? -25;
|
|
1809
|
+
const valStr = String(Math.round(value));
|
|
1810
|
+
const tWidth = tFont.getTextWidth ? tFont.getTextWidth(valStr) + 8 : 24;
|
|
1811
|
+
|
|
1812
|
+
const bgP = Skia.Paint();
|
|
1813
|
+
try { bgP.setColor(Skia.Color((typeof tooltipBg === 'string' ? tooltipBg : '#333333').replace(/\s+/g, ''))); } catch { bgP.setColor(Skia.Color('#333333')); }
|
|
1814
|
+
canvas.drawRRect(Skia.RRectXY({ x: thumbX - tWidth/2, y: offsetY - tFontSize - 2, width: tWidth, height: tFontSize + 6 }, 3, 3), bgP);
|
|
1815
|
+
|
|
1816
|
+
const tPaint = Skia.Paint();
|
|
1817
|
+
try { tPaint.setColor(Skia.Color((typeof tooltipText === 'string' ? tooltipText : '#ffffff').replace(/\s+/g, ''))); } catch { tPaint.setColor(Skia.Color('#ffffff')); }
|
|
1818
|
+
canvas.save();
|
|
1819
|
+
canvas.translate(thumbX - tWidth/2 + 4, offsetY - 4);
|
|
1820
|
+
canvas.drawText(valStr, 0, 0, tPaint, tFont);
|
|
1740
1821
|
canvas.restore();
|
|
1741
1822
|
}
|
|
1742
1823
|
}
|
|
@@ -2007,41 +2088,42 @@ export class ExodeUIEngine {
|
|
|
2007
2088
|
const data = ds.data || [];
|
|
2008
2089
|
if (data.length < 2) return;
|
|
2009
2090
|
|
|
2010
|
-
const lineColorStr =
|
|
2091
|
+
const lineColorStr = typeof ds.lineColor === 'string' ? ds.lineColor : '#3b82f6';
|
|
2092
|
+
// hsl/hsla colors need to be converted; Skia may not accept them directly
|
|
2093
|
+
// We'll try Skia.Color, and if it throws, fall back to a safe default
|
|
2011
2094
|
let lineColor;
|
|
2012
|
-
try { lineColor = Skia.Color(lineColorStr); } catch { lineColor = Skia.Color('#3b82f6'); }
|
|
2095
|
+
try { lineColor = Skia.Color(lineColorStr.replace(/\s+/g, '')); } catch { lineColor = Skia.Color('#3b82f6'); }
|
|
2013
2096
|
|
|
2014
|
-
const stepX = plotW / (data.length - 1);
|
|
2097
|
+
const stepX = plotW / (Math.max(data.length - 1, 1));
|
|
2015
2098
|
const getX = (i: number) => -w/2 + axisMarginL + i * stepX;
|
|
2016
2099
|
const getY = (val: number) => h/2 - axisMarginB - (val / globalMax) * plotH;
|
|
2017
2100
|
|
|
2018
|
-
|
|
2101
|
+
// Per-dataset smoothing from the .exode file
|
|
2102
|
+
const smooth = ds.smoothing === true;
|
|
2019
2103
|
|
|
2020
|
-
//
|
|
2021
|
-
if (ds.
|
|
2022
|
-
const
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
const
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
fillPath.lineTo(x, y);
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
try { fillPaint.setColor(lineColor); } catch { fillPaint.setColor(Skia.Color('#3b82f6')); }
|
|
2044
|
-
canvas.drawPath(fillPath, fillPaint);
|
|
2104
|
+
// Per-dataset area fill from .exode field 'showArea'
|
|
2105
|
+
if (ds.showArea === true && ds.areaColor) {
|
|
2106
|
+
const areaColorStr = typeof ds.areaColor === 'string' ? ds.areaColor : null;
|
|
2107
|
+
let areaColor;
|
|
2108
|
+
if (areaColorStr) {
|
|
2109
|
+
try { areaColor = Skia.Color(areaColorStr.replace(/\s+/g, '')); } catch { areaColor = null; }
|
|
2110
|
+
}
|
|
2111
|
+
if (areaColor) {
|
|
2112
|
+
const fillPath = Skia.Path.Make();
|
|
2113
|
+
fillPath.moveTo(getX(0), h/2 - axisMarginB);
|
|
2114
|
+
data.forEach((val: number, i: number) => {
|
|
2115
|
+
const x = getX(i); const y = getY(val);
|
|
2116
|
+
if (i === 0) fillPath.lineTo(x, y);
|
|
2117
|
+
else if (smooth) { const px = getX(i-1); const py = getY(data[i-1]); const cpX = (px+x)/2; fillPath.cubicTo(cpX, py, cpX, y, x, y); }
|
|
2118
|
+
else fillPath.lineTo(x, y);
|
|
2119
|
+
});
|
|
2120
|
+
fillPath.lineTo(getX(data.length - 1), h/2 - axisMarginB);
|
|
2121
|
+
fillPath.close();
|
|
2122
|
+
const fillPaint = Skia.Paint();
|
|
2123
|
+
fillPaint.setStyle(PaintStyle.Fill);
|
|
2124
|
+
fillPaint.setColor(areaColor);
|
|
2125
|
+
canvas.drawPath(fillPath, fillPaint);
|
|
2126
|
+
}
|
|
2045
2127
|
}
|
|
2046
2128
|
|
|
2047
2129
|
// Line
|
|
@@ -2052,26 +2134,29 @@ export class ExodeUIEngine {
|
|
|
2052
2134
|
|
|
2053
2135
|
const linePath = Skia.Path.Make();
|
|
2054
2136
|
data.forEach((val: number, i: number) => {
|
|
2055
|
-
const x = getX(i);
|
|
2056
|
-
const y = getY(val);
|
|
2137
|
+
const x = getX(i); const y = getY(val);
|
|
2057
2138
|
if (i === 0) linePath.moveTo(x, y);
|
|
2058
|
-
else if (smooth
|
|
2059
|
-
|
|
2060
|
-
const prevY = getY(data[i - 1]);
|
|
2061
|
-
const cpX = (prevX + x) / 2;
|
|
2062
|
-
linePath.cubicTo(cpX, prevY, cpX, y, x, y);
|
|
2063
|
-
} else {
|
|
2064
|
-
linePath.lineTo(x, y);
|
|
2065
|
-
}
|
|
2139
|
+
else if (smooth) { const px = getX(i-1); const py = getY(data[i-1]); const cpX = (px+x)/2; linePath.cubicTo(cpX, py, cpX, y, x, y); }
|
|
2140
|
+
else linePath.lineTo(x, y);
|
|
2066
2141
|
});
|
|
2067
2142
|
canvas.drawPath(linePath, linePaint);
|
|
2068
2143
|
|
|
2069
|
-
// Data points
|
|
2070
|
-
if (ds.showPoints !== false && data.length <=
|
|
2144
|
+
// Data points with custom point styling
|
|
2145
|
+
if (ds.showPoints !== false && data.length <= 30 && ds.pointSize) {
|
|
2146
|
+
const pr = ds.pointSize / 2;
|
|
2071
2147
|
data.forEach((val: number, i: number) => {
|
|
2148
|
+
// Outer stroke
|
|
2149
|
+
if (ds.pointStrokeColor) {
|
|
2150
|
+
const strokeP = Skia.Paint();
|
|
2151
|
+
strokeP.setStyle(PaintStyle.Stroke);
|
|
2152
|
+
strokeP.setStrokeWidth(ds.pointStrokeWidth || 2);
|
|
2153
|
+
try { strokeP.setColor(Skia.Color((ds.pointStrokeColor || '#3b82f6').replace(/\s+/g, ''))); } catch { strokeP.setColor(lineColor); }
|
|
2154
|
+
canvas.drawCircle(getX(i), getY(val), pr, strokeP);
|
|
2155
|
+
}
|
|
2156
|
+
// Fill
|
|
2072
2157
|
const dotPaint = Skia.Paint();
|
|
2073
|
-
try { dotPaint.setColor(
|
|
2074
|
-
canvas.drawCircle(getX(i), getY(val),
|
|
2158
|
+
try { dotPaint.setColor(Skia.Color((ds.pointFill || '#ffffff').replace(/\s+/g, ''))); } catch { dotPaint.setColor(Skia.Color('#ffffff')); }
|
|
2159
|
+
canvas.drawCircle(getX(i), getY(val), pr - (ds.pointStrokeWidth || 2)/2, dotPaint);
|
|
2075
2160
|
});
|
|
2076
2161
|
}
|
|
2077
2162
|
});
|
|
@@ -2107,15 +2192,15 @@ export class ExodeUIEngine {
|
|
|
2107
2192
|
});
|
|
2108
2193
|
}
|
|
2109
2194
|
|
|
2110
|
-
// Legend
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2195
|
+
// Legend — only if datasets have labels
|
|
2196
|
+
const datasetsWithLabels = datasets.filter((ds: any) => ds.label);
|
|
2197
|
+
if (datasetsWithLabels.length > 0) {
|
|
2198
|
+
datasetsWithLabels.forEach((ds: any, i: number) => {
|
|
2114
2199
|
const legX = -w/2 + axisMarginL + i * 80;
|
|
2115
2200
|
const legY = -h/2 + 10;
|
|
2116
2201
|
const legPaint = Skia.Paint();
|
|
2117
|
-
const lcStr =
|
|
2118
|
-
try { legPaint.setColor(Skia.Color(lcStr)); } catch { legPaint.setColor(Skia.Color('#3b82f6')); }
|
|
2202
|
+
const lcStr = typeof ds.lineColor === 'string' ? ds.lineColor : '#3b82f6';
|
|
2203
|
+
try { legPaint.setColor(Skia.Color(lcStr.replace(/\s+/g, ''))); } catch { legPaint.setColor(Skia.Color('#3b82f6')); }
|
|
2119
2204
|
canvas.drawRect({ x: legX, y: legY - 5, width: 16, height: 3 }, legPaint);
|
|
2120
2205
|
canvas.save();
|
|
2121
2206
|
canvas.translate(legX + 20, legY);
|
package/src/useExodeUI.ts
CHANGED
|
@@ -40,6 +40,14 @@ export function useExodeUI() {
|
|
|
40
40
|
engine?.updateObjectOptions(id, newOptions);
|
|
41
41
|
}, [engine]);
|
|
42
42
|
|
|
43
|
+
const onButtonClick = useCallback((cb: (name: string, objectId: string) => void) => {
|
|
44
|
+
engine?.setButtonClickCallback(cb);
|
|
45
|
+
}, [engine]);
|
|
46
|
+
|
|
47
|
+
const onGraphPointClick = useCallback((cb: (event: { objectId: string; componentName: string; datasetIndex: number; pointIndex: number; value: number; label: string }) => void) => {
|
|
48
|
+
engine?.setGraphPointClickCallback(cb);
|
|
49
|
+
}, [engine]);
|
|
50
|
+
|
|
43
51
|
return {
|
|
44
52
|
setEngine,
|
|
45
53
|
engine,
|
|
@@ -51,6 +59,8 @@ export function useExodeUI() {
|
|
|
51
59
|
fireTrigger,
|
|
52
60
|
updateConstraint,
|
|
53
61
|
updateGraphData,
|
|
54
|
-
updateObjectOptions
|
|
62
|
+
updateObjectOptions,
|
|
63
|
+
onButtonClick,
|
|
64
|
+
onGraphPointClick,
|
|
55
65
|
};
|
|
56
66
|
}
|