@pallasoft/ps-mobile-components 1.13.0 → 1.13.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.
Files changed (34) hide show
  1. package/dist/components/ps-chart-bar/ps-chart-bar.js +11 -4
  2. package/dist/components/ps-donut-chart/ps-donut-chart.js +5 -4
  3. package/dist/components/ps-donut-reveal/index.d.ts +1 -0
  4. package/dist/components/ps-donut-reveal/index.js +1 -0
  5. package/dist/components/ps-donut-reveal/ps-donut-reveal-props.d.ts +6 -0
  6. package/dist/components/ps-donut-reveal/ps-donut-reveal-props.js +1 -0
  7. package/dist/components/ps-donut-reveal/ps-donut-reveal.d.ts +2 -0
  8. package/dist/components/ps-donut-reveal/ps-donut-reveal.js +14 -0
  9. package/dist/components/ps-donut-slice/ps-donut-slice-props.d.ts +0 -4
  10. package/dist/components/ps-donut-slice/ps-donut-slice.d.ts +1 -1
  11. package/dist/components/ps-donut-slice/ps-donut-slice.js +2 -14
  12. package/dist/components/ps-sortable-grid/ps-sortable-grid.js +3 -3
  13. package/dist/components/ps-sortable-grid/ps-sortable-item-props.d.ts +1 -1
  14. package/dist/components/ps-sortable-grid/ps-sortable-item.js +19 -5
  15. package/dist/constants/ps-chart-bar-corner.d.ts +6 -0
  16. package/dist/constants/ps-chart-bar-corner.js +5 -0
  17. package/dist/constants/ps-chart-config.d.ts +4 -1
  18. package/dist/constants/ps-chart-config.js +4 -1
  19. package/dist/constants/ps-sortable-grid-config.d.ts +2 -0
  20. package/dist/constants/ps-sortable-grid-config.js +2 -0
  21. package/dist/interfaces/ps-chart-arc-data.d.ts +9 -0
  22. package/dist/interfaces/ps-chart-arc-data.js +1 -0
  23. package/dist/interfaces/ps-chart-bar-rect.d.ts +2 -0
  24. package/dist/interfaces/ps-donut-slice-data.d.ts +1 -3
  25. package/dist/interfaces/ps-sortable-measure-data.d.ts +4 -0
  26. package/dist/interfaces/ps-sortable-measure-data.js +1 -0
  27. package/dist/services/ps-chart-arc-service.d.ts +14 -0
  28. package/dist/services/ps-chart-arc-service.js +83 -0
  29. package/dist/services/ps-chart-bar-service.d.ts +2 -0
  30. package/dist/services/ps-chart-bar-service.js +18 -3
  31. package/dist/services/ps-chart-donut-service.d.ts +2 -0
  32. package/dist/services/ps-chart-donut-service.js +42 -12
  33. package/dist/services/ps-waterfall-service.js +2 -0
  34. package/package.json +1 -1
@@ -1,12 +1,19 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Animated, { useAnimatedProps } from 'react-native-reanimated';
3
- import { Rect } from 'react-native-svg';
4
- import { PsChartConfig } from '../../constants/ps-chart-config';
3
+ import { G, Rect } from 'react-native-svg';
4
+ import { PsChartBarCorner } from '../../constants/ps-chart-bar-corner';
5
+ import { PsChartBarService } from '../../services/ps-chart-bar-service';
5
6
  const AnimatedRect = Animated.createAnimatedComponent(Rect);
6
7
  export const PsChartBar = ({ progress, rect }) => {
8
+ const radius = PsChartBarService.radius(rect);
9
+ const hasFlatBase = rect.corner === PsChartBarCorner.Value && radius > 0;
7
10
  const animatedProps = useAnimatedProps(() => ({
8
11
  height: rect.height * progress.value,
9
12
  y: rect.isNegative ? rect.y : rect.y + rect.height * (1 - progress.value),
10
13
  }));
11
- return (_jsx(AnimatedRect, { animatedProps: animatedProps, fill: rect.color, height: rect.height, rx: Math.min(PsChartConfig.barRadius, rect.width / 2), width: rect.width, x: rect.x, y: rect.y }));
14
+ const baseProps = useAnimatedProps(() => {
15
+ const height = Math.min(radius, rect.height * progress.value);
16
+ return { height, y: rect.isNegative ? rect.y : rect.y + rect.height - height };
17
+ });
18
+ return (_jsxs(G, { children: [_jsx(AnimatedRect, { animatedProps: animatedProps, fill: rect.color, height: rect.height, rx: radius, width: rect.width, x: rect.x, y: rect.y }), hasFlatBase ? (_jsx(AnimatedRect, { animatedProps: baseProps, fill: rect.color, height: Math.min(radius, rect.height), width: rect.width, x: rect.x, y: rect.isNegative ? rect.y : rect.y + rect.height - Math.min(radius, rect.height) })) : null] }));
12
19
  };
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useId, useState } from 'react';
3
3
  import { View } from 'react-native';
4
- import Svg from 'react-native-svg';
4
+ import Svg, { G } from 'react-native-svg';
5
5
  import { PsChartConfig } from '../../constants/ps-chart-config';
6
6
  import { PsChartSkeletonVariant } from '../../constants/ps-chart-skeleton-variant';
7
7
  import { usePsChartReveal } from '../../hooks/use-ps-chart-reveal';
@@ -12,6 +12,7 @@ import { PsChartDonutService } from '../../services/ps-chart-donut-service';
12
12
  import { PsChartEmptyState } from '../ps-chart-empty-state/ps-chart-empty-state';
13
13
  import { PsChartLegend } from '../ps-chart-legend/ps-chart-legend';
14
14
  import { PsChartSkeleton } from '../ps-chart-skeleton/ps-chart-skeleton';
15
+ import { PsDonutReveal } from '../ps-donut-reveal/ps-donut-reveal';
15
16
  import { PsDonutSlice } from '../ps-donut-slice/ps-donut-slice';
16
17
  import { PsText } from '../ps-text/ps-text';
17
18
  export const PsDonutChart = ({ centerLabel, centerValue, data, emptyLabel, formatShare, height = PsChartConfig.donutHeight, isLoading = false, revealKey, }) => {
@@ -19,6 +20,7 @@ export const PsDonutChart = ({ centerLabel, centerValue, data, emptyLabel, forma
19
20
  const { onLayout, width } = usePsChartWidth();
20
21
  const fill = usePsWidgetFill();
21
22
  const [selected, setSelected] = useState(null);
23
+ const maskId = `donut${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
22
24
  const progress = usePsChartReveal(!isLoading && width > 0, revealKey);
23
25
  if (isLoading)
24
26
  return _jsx(PsChartSkeleton, { height: height, variant: PsChartSkeletonVariant.Donut });
@@ -27,7 +29,6 @@ export const PsDonutChart = ({ centerLabel, centerValue, data, emptyLabel, forma
27
29
  const slices = PsChartDonutService.slices(data, theme.chart, size);
28
30
  if (slices.length === 0)
29
31
  return _jsx(PsChartEmptyState, { height: height, label: emptyLabel });
30
- const circumference = PsChartDonutService.circumference(size);
31
32
  const chosen = selected === null ? null : data[selected];
32
33
  const readValue = chosen === null
33
34
  ? centerValue
@@ -47,7 +48,7 @@ export const PsDonutChart = ({ centerLabel, centerValue, data, emptyLabel, forma
47
48
  justifyContent: 'center',
48
49
  minHeight: height + fill,
49
50
  rowGap: PsChartConfig.donutGap,
50
- }, testID: 'ps-donut-chart', children: [_jsxs(View, { style: { height: size, width: size }, children: [_jsx(Svg, { height: size, width: size, children: slices.map((slice, index) => (_jsx(PsDonutSlice, { circumference: circumference, isDimmed: selected !== null && selected !== index, onPress: () => setSelected(selected === index ? null : index), progress: progress, size: size, slice: slice }, slice.id))) }), _jsxs(View, { pointerEvents: 'none', style: {
51
+ }, testID: 'ps-donut-chart', children: [_jsxs(View, { style: { height: size, width: size }, children: [_jsxs(Svg, { height: size, width: size, children: [_jsx(PsDonutReveal, { id: maskId, progress: progress, size: size }), _jsx(G, { mask: `url(#${maskId})`, children: slices.map((slice, index) => slice.path === '' ? null : (_jsx(PsDonutSlice, { isDimmed: selected !== null && selected !== index, onPress: () => setSelected(selected === index ? null : index), slice: slice }, slice.id))) })] }), _jsxs(View, { pointerEvents: 'none', style: {
51
52
  alignItems: 'center',
52
53
  bottom: 0,
53
54
  justifyContent: 'center',
@@ -0,0 +1 @@
1
+ export * from './ps-donut-reveal';
@@ -0,0 +1 @@
1
+ export * from './ps-donut-reveal';
@@ -0,0 +1,6 @@
1
+ import type { SharedValue } from 'react-native-reanimated';
2
+ export interface PsDonutRevealProps {
3
+ id: string;
4
+ progress: SharedValue<number>;
5
+ size: number;
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { PsDonutRevealProps } from './ps-donut-reveal-props';
2
+ export declare const PsDonutReveal: ({ id, progress, size }: PsDonutRevealProps) => import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import Animated, { useAnimatedProps } from 'react-native-reanimated';
3
+ import { Circle, Defs, Mask } from 'react-native-svg';
4
+ import { PsChartConfig } from '../../constants/ps-chart-config';
5
+ import { PsChartDonutService } from '../../services/ps-chart-donut-service';
6
+ const AnimatedCircle = Animated.createAnimatedComponent(Circle);
7
+ export const PsDonutReveal = ({ id, progress, size }) => {
8
+ const centre = size / 2;
9
+ const circumference = PsChartDonutService.circumference(size);
10
+ const animatedProps = useAnimatedProps(() => ({
11
+ strokeDasharray: [progress.value * circumference, circumference],
12
+ }));
13
+ return (_jsx(Defs, { children: _jsx(Mask, { height: size, id: id, maskUnits: 'userSpaceOnUse', width: size, x: 0, y: 0, children: _jsx(AnimatedCircle, { animatedProps: animatedProps, cx: centre, cy: centre, fill: 'none', origin: `${centre}, ${centre}`, r: PsChartDonutService.radius(size), rotation: -90, stroke: 'white', strokeDasharray: [0, circumference], strokeWidth: PsChartConfig.donutThickness + PsChartConfig.donutRevealOverlap }) }) }));
14
+ };
@@ -1,10 +1,6 @@
1
- import type { SharedValue } from 'react-native-reanimated';
2
1
  import type { PsDonutSliceData } from '../../interfaces/ps-donut-slice-data';
3
2
  export interface PsDonutSliceProps {
4
- circumference: number;
5
3
  isDimmed: boolean;
6
4
  onPress: () => void;
7
- progress: SharedValue<number>;
8
- size: number;
9
5
  slice: PsDonutSliceData;
10
6
  }
@@ -1,2 +1,2 @@
1
1
  import type { PsDonutSliceProps } from './ps-donut-slice-props';
2
- export declare const PsDonutSlice: ({ circumference, isDimmed, onPress, progress, size, slice, }: PsDonutSliceProps) => import("react").JSX.Element;
2
+ export declare const PsDonutSlice: ({ isDimmed, onPress, slice }: PsDonutSliceProps) => import("react").JSX.Element;
@@ -1,16 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import Animated, { useAnimatedProps } from 'react-native-reanimated';
3
- import { Circle } from 'react-native-svg';
2
+ import { Path } from 'react-native-svg';
4
3
  import { PsChartConfig } from '../../constants/ps-chart-config';
5
- import { PsChartDonutService } from '../../services/ps-chart-donut-service';
6
- const AnimatedCircle = Animated.createAnimatedComponent(Circle);
7
- export const PsDonutSlice = ({ circumference, isDimmed, onPress, progress, size, slice, }) => {
8
- const centre = size / 2;
9
- const animatedProps = useAnimatedProps(() => ({
10
- strokeDasharray: [
11
- Math.max(0, Math.min(slice.dashLength, progress.value * circumference - slice.start)),
12
- circumference,
13
- ],
14
- }));
15
- return (_jsx(AnimatedCircle, { animatedProps: animatedProps, cx: centre, cy: centre, fill: 'none', onPress: onPress, opacity: isDimmed ? 0.4 : 1, origin: `${centre}, ${centre}`, r: PsChartDonutService.radius(size), rotation: -90, stroke: slice.color, strokeDasharray: [slice.dashLength, circumference], strokeDashoffset: slice.dashOffset, strokeWidth: PsChartConfig.donutThickness }));
16
- };
4
+ export const PsDonutSlice = ({ isDimmed, onPress, slice }) => (_jsx(Path, { d: slice.path, fill: slice.color, fillRule: 'evenodd', onPress: onPress, opacity: isDimmed ? PsChartConfig.donutDimmedOpacity : 1 }));
@@ -135,12 +135,12 @@ export const PsSortableGrid = ({ getItemSpan, isDisabled = false, items, labels,
135
135
  notify(next);
136
136
  PsHapticService.play('selection', hasHaptics);
137
137
  }, [hasHaptics]);
138
- const measure = useCallback((id, total, renderedExtra) => {
139
- const natural = PsSortableGridService.natural(total, renderedExtra);
138
+ const measure = useCallback((id, total, fill) => {
139
+ const natural = PsSortableGridService.natural(total, fill);
140
140
  setMeasured((current) => current[id] !== undefined &&
141
141
  Math.abs(current[id] - natural) < PsSortableGridConfig.measureTolerance
142
142
  ? current
143
143
  : { ...current, [id]: natural });
144
144
  }, []);
145
- return (_jsx(View, { onLayout: (event) => setWidth(event.nativeEvent.layout.width), style: { height: PsSortableGridService.contentHeight(rects) }, children: order.map((item, index) => (_jsx(PsSortableItem, { canMoveDown: index < order.length - 1, canMoveUp: index > 0, dragX: dragX, dragY: dragY, extra: extras[index], id: item.id, isActive: item.id === activeId, isDisabled: isDisabled, isReady: isReady, label: item.label, labels: labels, onEnd: end, onMeasure: measure, onMove: moveBy, onStart: start, onUpdate: update, rect: rects[index], children: renderItem({ isActive: item.id === activeId, item }) }, item.id))) }));
145
+ return (_jsx(View, { onLayout: (event) => setWidth(event.nativeEvent.layout.width), style: { height: PsSortableGridService.contentHeight(rects) }, testID: PsSortableGridConfig.testId, children: order.map((item, index) => (_jsx(PsSortableItem, { canMoveDown: index < order.length - 1, canMoveUp: index > 0, dragX: dragX, dragY: dragY, extra: extras[index], id: item.id, isActive: item.id === activeId, isDisabled: isDisabled, isReady: isReady, label: item.label, labels: labels, onEnd: end, onMeasure: measure, onMove: moveBy, onStart: start, onUpdate: update, rect: rects[index], children: renderItem({ isActive: item.id === activeId, item }) }, item.id))) }));
146
146
  };
@@ -16,7 +16,7 @@ export interface PsSortableItemProps {
16
16
  label: string;
17
17
  labels: PsSortableGridLabels;
18
18
  onEnd: () => void;
19
- onMeasure: (id: string, height: number, renderedExtra: number) => void;
19
+ onMeasure: (id: string, height: number, fill: number) => void;
20
20
  onMove: (id: string, delta: number) => void;
21
21
  onStart: (id: string, absoluteY: number) => void;
22
22
  onUpdate: (translationX: number, translationY: number, absoluteY: number) => void;
@@ -1,5 +1,6 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+ import { View } from 'react-native';
3
4
  import { GestureDetector, usePanGesture } from 'react-native-gesture-handler';
4
5
  import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
5
6
  import { PsSortableGridConfig } from '../../constants/ps-sortable-grid-config';
@@ -11,6 +12,13 @@ export const PsSortableItem = ({ canMoveDown, canMoveUp, children, dragX, dragY,
11
12
  const top = useSharedValue(rect.y);
12
13
  const scale = useSharedValue(1);
13
14
  const wasActive = useRef(false);
15
+ const laidOut = useRef({ fill: 0, total: null });
16
+ const fill = isReady ? extra : 0;
17
+ const report = useCallback(() => {
18
+ const { fill: laidOutFill, total } = laidOut.current;
19
+ if (total !== null)
20
+ onMeasure(id, total, laidOutFill);
21
+ }, [id, onMeasure]);
14
22
  useEffect(() => {
15
23
  if (isActive) {
16
24
  wasActive.current = true;
@@ -49,12 +57,15 @@ export const PsSortableItem = ({ canMoveDown, canMoveUp, children, dragX, dragY,
49
57
  top: isActive ? dragY.value : top.value,
50
58
  transform: [{ scale: scale.value }],
51
59
  }));
52
- return (_jsx(GestureDetector, { gesture: pan, children: _jsx(Animated.View, { accessibilityActions: isDisabled
60
+ return (_jsx(GestureDetector, { gesture: pan, children: _jsxs(Animated.View, { accessibilityActions: isDisabled
53
61
  ? undefined
54
62
  : [
55
63
  ...(canMoveUp ? [{ label: labels.moveUp, name: 'moveUp' }] : []),
56
64
  ...(canMoveDown ? [{ label: labels.moveDown, name: 'moveDown' }] : []),
57
- ], accessibilityLabel: label, accessible: !isDisabled, collapsable: false, onAccessibilityAction: (event) => onMove(id, event.nativeEvent.actionName === 'moveUp' ? -1 : 1), onLayout: (event) => onMeasure(id, event.nativeEvent.layout.height, isReady ? extra : 0), style: [
65
+ ], accessibilityLabel: label, accessible: !isDisabled, collapsable: false, onAccessibilityAction: (event) => onMove(id, event.nativeEvent.actionName === 'moveUp' ? -1 : 1), onLayout: (event) => {
66
+ laidOut.current.total = event.nativeEvent.layout.height;
67
+ report();
68
+ }, style: [
58
69
  {
59
70
  borderRadius: PsSortableGridConfig.radius,
60
71
  opacity: isReady ? 1 : 0,
@@ -71,5 +82,8 @@ export const PsSortableItem = ({ canMoveDown, canMoveUp, children, dragX, dragY,
71
82
  }
72
83
  : undefined,
73
84
  animatedStyle,
74
- ], children: _jsx(PsWidgetFillContext.Provider, { value: isReady ? extra : 0, children: children }) }) }));
85
+ ], children: [_jsx(View, { onLayout: (event) => {
86
+ laidOut.current.fill = event.nativeEvent.layout.height;
87
+ report();
88
+ }, pointerEvents: 'none', style: { height: fill, left: 0, position: 'absolute', top: 0, width: 0 }, testID: PsSortableGridConfig.fillTestId }), _jsx(PsWidgetFillContext.Provider, { value: fill, children: children })] }) }));
75
89
  };
@@ -0,0 +1,6 @@
1
+ export declare const PsChartBarCorner: {
2
+ readonly Both: "both";
3
+ readonly None: "none";
4
+ readonly Value: "value";
5
+ };
6
+ export type PsChartBarCorner = (typeof PsChartBarCorner)[keyof typeof PsChartBarCorner];
@@ -0,0 +1,5 @@
1
+ export const PsChartBarCorner = {
2
+ Both: 'both',
3
+ None: 'none',
4
+ Value: 'value',
5
+ };
@@ -6,6 +6,7 @@ export declare const PsChartConfig: {
6
6
  readonly axisTickGap: 6;
7
7
  readonly barGapRatio: 0.3;
8
8
  readonly barRadius: 4;
9
+ readonly barRadiusWidthRatio: number;
9
10
  readonly charWidthRatio: 0.62;
10
11
  readonly compactBarGapRatio: 0.15;
11
12
  readonly compactSlot: 24;
@@ -18,9 +19,11 @@ export declare const PsChartConfig: {
18
19
  readonly donutCenterFontMax: 18;
19
20
  readonly donutCenterFontMin: 11;
20
21
  readonly donutCenterPadding: 8;
22
+ readonly donutDimmedOpacity: 0.4;
21
23
  readonly donutGap: 16;
22
24
  readonly donutMinLegendWidth: 120;
23
- readonly donutSliceGap: 2;
25
+ readonly donutRevealOverlap: 2;
26
+ readonly donutSliceGap: 3;
24
27
  readonly drawMs: 700;
25
28
  readonly ellipsis: "…";
26
29
  readonly gridLineCount: 3;
@@ -6,6 +6,7 @@ export const PsChartConfig = {
6
6
  axisTickGap: 6,
7
7
  barGapRatio: 0.3,
8
8
  barRadius: 4,
9
+ barRadiusWidthRatio: 1 / 3,
9
10
  charWidthRatio: 0.62,
10
11
  compactBarGapRatio: 0.15,
11
12
  compactSlot: 24,
@@ -18,9 +19,11 @@ export const PsChartConfig = {
18
19
  donutCenterFontMax: 18,
19
20
  donutCenterFontMin: 11,
20
21
  donutCenterPadding: 8,
22
+ donutDimmedOpacity: 0.4,
21
23
  donutGap: 16,
22
24
  donutMinLegendWidth: 120,
23
- donutSliceGap: 2,
25
+ donutRevealOverlap: 2,
26
+ donutSliceGap: 3,
24
27
  drawMs: 700,
25
28
  ellipsis: '…',
26
29
  gridLineCount: 3,
@@ -4,6 +4,7 @@ export declare const PsSortableGridConfig: {
4
4
  readonly columnGap: 12;
5
5
  readonly compactUnits: 2;
6
6
  readonly edgeRatio: 0.14;
7
+ readonly fillTestId: "ps-sortable-fill";
7
8
  readonly fullUnits: 2;
8
9
  readonly holdTolerance: 8;
9
10
  readonly longPressMs: 400;
@@ -17,4 +18,5 @@ export declare const PsSortableGridConfig: {
17
18
  readonly scaleMs: 140;
18
19
  readonly scrollRamp: 0.25;
19
20
  readonly scrollTickMs: 16;
21
+ readonly testId: "ps-sortable-grid";
20
22
  };
@@ -6,6 +6,7 @@ export const PsSortableGridConfig = {
6
6
  columnGap: PsChartConfig.kpiGap,
7
7
  compactUnits: 2,
8
8
  edgeRatio: 0.14,
9
+ fillTestId: 'ps-sortable-fill',
9
10
  fullUnits: 2,
10
11
  holdTolerance: 8,
11
12
  longPressMs: 400,
@@ -19,4 +20,5 @@ export const PsSortableGridConfig = {
19
20
  scaleMs: 140,
20
21
  scrollRamp: 0.25,
21
22
  scrollTickMs: 16,
23
+ testId: 'ps-sortable-grid',
22
24
  };
@@ -0,0 +1,9 @@
1
+ export interface PsChartArcData {
2
+ centre: number;
3
+ end: number;
4
+ gap: number;
5
+ inner: number;
6
+ outer: number;
7
+ radius: number;
8
+ start: number;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,7 @@
1
+ import type { PsChartBarCorner } from '../constants/ps-chart-bar-corner';
1
2
  export interface PsChartBarRect {
2
3
  color: string;
4
+ corner: PsChartBarCorner;
3
5
  height: number;
4
6
  id: string;
5
7
  isNegative: boolean;
@@ -1,7 +1,5 @@
1
1
  export interface PsDonutSliceData {
2
2
  color: string;
3
- dashLength: number;
4
- dashOffset: number;
5
3
  id: string;
6
- start: number;
4
+ path: string;
7
5
  }
@@ -0,0 +1,4 @@
1
+ export interface PsSortableMeasureData {
2
+ fill: number;
3
+ total: number | null;
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { PsChartArcData } from '../interfaces/ps-chart-arc-data';
2
+ export declare class PsChartArcService {
3
+ static sector(arc: PsChartArcData): string;
4
+ static ring(centre: number, inner: number, outer: number): string;
5
+ static minimumAngle(gap: number, radius: number, inner: number): number;
6
+ private static largeFlag;
7
+ private static angleOf;
8
+ private static cornerRadius;
9
+ private static corner;
10
+ private static edge;
11
+ private static at;
12
+ private static scale;
13
+ private static text;
14
+ }
@@ -0,0 +1,83 @@
1
+ export class PsChartArcService {
2
+ static sector(arc) {
3
+ const half = arc.gap / 2;
4
+ const radius = PsChartArcService.cornerRadius(arc);
5
+ const outerStart = PsChartArcService.corner(arc, arc.start, 1, arc.outer - radius, radius, half);
6
+ const outerEnd = PsChartArcService.corner(arc, arc.end, -1, arc.outer - radius, radius, half);
7
+ const innerEnd = PsChartArcService.corner(arc, arc.end, -1, arc.inner + radius, radius, half);
8
+ const innerStart = PsChartArcService.corner(arc, arc.start, 1, arc.inner + radius, radius, half);
9
+ const outerStartRim = PsChartArcService.scale(arc, outerStart, arc.outer / (arc.outer - radius));
10
+ const outerEndRim = PsChartArcService.scale(arc, outerEnd, arc.outer / (arc.outer - radius));
11
+ const innerEndRim = PsChartArcService.scale(arc, innerEnd, arc.inner / (arc.inner + radius));
12
+ const innerStartRim = PsChartArcService.scale(arc, innerStart, arc.inner / (arc.inner + radius));
13
+ const startOuterEdge = PsChartArcService.edge(arc, arc.start, 1, outerStart, half);
14
+ const endOuterEdge = PsChartArcService.edge(arc, arc.end, -1, outerEnd, half);
15
+ const endInnerEdge = PsChartArcService.edge(arc, arc.end, -1, innerEnd, half);
16
+ const startInnerEdge = PsChartArcService.edge(arc, arc.start, 1, innerStart, half);
17
+ return [
18
+ `M${PsChartArcService.text(startOuterEdge)}`,
19
+ `A${radius} ${radius} 0 0 1 ${PsChartArcService.text(outerStartRim)}`,
20
+ `A${arc.outer} ${arc.outer} 0 ${PsChartArcService.largeFlag(arc, outerStartRim, outerEndRim)} 1 ${PsChartArcService.text(outerEndRim)}`,
21
+ `A${radius} ${radius} 0 0 1 ${PsChartArcService.text(endOuterEdge)}`,
22
+ `L${PsChartArcService.text(endInnerEdge)}`,
23
+ `A${radius} ${radius} 0 0 1 ${PsChartArcService.text(innerEndRim)}`,
24
+ `A${arc.inner} ${arc.inner} 0 ${PsChartArcService.largeFlag(arc, innerStartRim, innerEndRim)} 0 ${PsChartArcService.text(innerStartRim)}`,
25
+ `A${radius} ${radius} 0 0 1 ${PsChartArcService.text(startInnerEdge)}`,
26
+ 'Z',
27
+ ].join(' ');
28
+ }
29
+ static ring(centre, inner, outer) {
30
+ return [
31
+ `M${centre} ${centre - outer}`,
32
+ `A${outer} ${outer} 0 1 1 ${centre} ${centre + outer}`,
33
+ `A${outer} ${outer} 0 1 1 ${centre} ${centre - outer}`,
34
+ `M${centre} ${centre - inner}`,
35
+ `A${inner} ${inner} 0 1 0 ${centre} ${centre + inner}`,
36
+ `A${inner} ${inner} 0 1 0 ${centre} ${centre - inner}`,
37
+ 'Z',
38
+ ].join(' ');
39
+ }
40
+ static minimumAngle(gap, radius, inner) {
41
+ return inner > 0 ? (gap + 2 * radius + 1) / inner : 0;
42
+ }
43
+ static largeFlag(arc, from, to) {
44
+ const turn = 2 * Math.PI;
45
+ const span = (((PsChartArcService.angleOf(arc, to) - PsChartArcService.angleOf(arc, from)) % turn) +
46
+ turn) %
47
+ turn;
48
+ return span > Math.PI ? 1 : 0;
49
+ }
50
+ static angleOf(arc, point) {
51
+ return Math.atan2(point.x - arc.centre, arc.centre - point.y);
52
+ }
53
+ static cornerRadius(arc) {
54
+ const room = ((arc.end - arc.start) * arc.inner - arc.gap) / 2;
55
+ return Math.max(0, Math.min(arc.radius, (arc.outer - arc.inner) / 2, room));
56
+ }
57
+ static corner(arc, angle, side, distance, radius, half) {
58
+ const offset = half + radius;
59
+ const along = Math.sqrt(Math.max(distance * distance - offset * offset, 0));
60
+ return PsChartArcService.at(arc, angle, side, along, offset);
61
+ }
62
+ static edge(arc, angle, side, centre, half) {
63
+ const dx = centre.x - arc.centre;
64
+ const dy = centre.y - arc.centre;
65
+ const along = dx * Math.sin(angle) - dy * Math.cos(angle);
66
+ return PsChartArcService.at(arc, angle, side, along, half);
67
+ }
68
+ static at(arc, angle, side, along, across) {
69
+ return {
70
+ x: arc.centre + along * Math.sin(angle) + side * across * Math.cos(angle),
71
+ y: arc.centre - along * Math.cos(angle) + side * across * Math.sin(angle),
72
+ };
73
+ }
74
+ static scale(arc, point, ratio) {
75
+ return {
76
+ x: arc.centre + (point.x - arc.centre) * ratio,
77
+ y: arc.centre + (point.y - arc.centre) * ratio,
78
+ };
79
+ }
80
+ static text(point) {
81
+ return `${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
82
+ }
83
+ }
@@ -9,5 +9,7 @@ export declare class PsChartBarService {
9
9
  static stacked(data: PsAreaChartDatum[], series: PsCartesianChartSeries[], colors: string[], domain: PsChartDomain, plot: PsChartPlot): PsChartBarRect[];
10
10
  static slot(count: number, plot: PsChartPlot): number;
11
11
  static group(slot: number, size: number): PsChartBarGroup;
12
+ static lastIndex(values: number[], sign: number): number;
13
+ static radius(rect: PsChartBarRect): number;
12
14
  private static rect;
13
15
  }
@@ -1,3 +1,4 @@
1
+ import { PsChartBarCorner } from '../constants/ps-chart-bar-corner';
1
2
  import { PsChartConfig } from '../constants/ps-chart-config';
2
3
  import { PsChartScaleService } from './ps-chart-scale-service';
3
4
  import { PsChartSeriesService } from './ps-chart-series-service';
@@ -10,7 +11,7 @@ export class PsChartBarService {
10
11
  const value = PsChartSeriesService.value(datum, entry.dataKey);
11
12
  const top = PsChartScaleService.y(value, domain, plot);
12
13
  const left = plot.left + row * slot + offset + column * width;
13
- return PsChartBarService.rect(`${row}:${entry.dataKey}`, colors[column], left, width, zero, top, value < 0);
14
+ return PsChartBarService.rect(`${row}:${entry.dataKey}`, colors[column], left, width, zero, top, value < 0, PsChartBarCorner.Value);
14
15
  }));
15
16
  }
16
17
  static stacked(data, series, colors, domain, plot) {
@@ -19,6 +20,9 @@ export class PsChartBarService {
19
20
  return data.flatMap((datum, row) => {
20
21
  let positive = 0;
21
22
  let negative = 0;
23
+ const values = series.map((entry) => PsChartSeriesService.value(datum, entry.dataKey));
24
+ const outerPositive = PsChartBarService.lastIndex(values, 1);
25
+ const outerNegative = PsChartBarService.lastIndex(values, -1);
22
26
  return series.map((entry, column) => {
23
27
  const value = PsChartSeriesService.value(datum, entry.dataKey);
24
28
  const from = value < 0 ? negative : positive;
@@ -27,7 +31,9 @@ export class PsChartBarService {
27
31
  negative = to;
28
32
  else
29
33
  positive = to;
30
- return PsChartBarService.rect(`${row}:${entry.dataKey}`, colors[column], plot.left + row * slot + offset, width, PsChartScaleService.y(from, domain, plot), PsChartScaleService.y(to, domain, plot), value < 0);
34
+ return PsChartBarService.rect(`${row}:${entry.dataKey}`, colors[column], plot.left + row * slot + offset, width, PsChartScaleService.y(from, domain, plot), PsChartScaleService.y(to, domain, plot), value < 0, column === outerPositive || column === outerNegative
35
+ ? PsChartBarCorner.Value
36
+ : PsChartBarCorner.None);
31
37
  });
32
38
  });
33
39
  }
@@ -41,9 +47,18 @@ export class PsChartBarService {
41
47
  const total = Math.max(slot * (1 - gap), Math.min(PsChartConfig.minBarWidth * size, slot));
42
48
  return { offset: (slot - total) / 2, width: total / size };
43
49
  }
44
- static rect(id, color, x, width, from, to, isNegative) {
50
+ static lastIndex(values, sign) {
51
+ return values.reduce((last, value, index) => (Math.sign(value) === sign ? index : last), -1);
52
+ }
53
+ static radius(rect) {
54
+ if (rect.corner === PsChartBarCorner.None)
55
+ return 0;
56
+ return Math.min(PsChartConfig.barRadius, rect.width * PsChartConfig.barRadiusWidthRatio);
57
+ }
58
+ static rect(id, color, x, width, from, to, isNegative, corner) {
45
59
  return {
46
60
  color,
61
+ corner,
47
62
  height: Math.abs(to - from),
48
63
  id,
49
64
  isNegative,
@@ -6,5 +6,7 @@ export declare class PsChartDonutService {
6
6
  static centerFontSize(text: string, size: number): number;
7
7
  static radius(size: number): number;
8
8
  static circumference(size: number): number;
9
+ static inner(size: number): number;
9
10
  static slices(data: PsDonutChartDatum[], colors: string[], size: number): PsDonutSliceData[];
11
+ static angles(shares: number[], minimum: number): number[];
10
12
  }
@@ -1,4 +1,5 @@
1
1
  import { PsChartConfig } from '../constants/ps-chart-config';
2
+ import { PsChartArcService } from './ps-chart-arc-service';
2
3
  import { PsChartTextService } from './ps-chart-text-service';
3
4
  export class PsChartDonutService {
4
5
  static layout(width, height) {
@@ -17,23 +18,52 @@ export class PsChartDonutService {
17
18
  static circumference(size) {
18
19
  return 2 * Math.PI * PsChartDonutService.radius(size);
19
20
  }
21
+ static inner(size) {
22
+ return Math.max(size / 2 - PsChartConfig.donutThickness, 0);
23
+ }
20
24
  static slices(data, colors, size) {
21
- const circumference = PsChartDonutService.circumference(size);
22
- const total = data.reduce((sum, datum) => sum + datum.share, 0);
25
+ const shares = data.map((datum) => Math.max(datum.share, 0));
26
+ const total = shares.reduce((sum, share) => sum + share, 0);
23
27
  if (total <= 0)
24
28
  return [];
25
- let start = 0;
26
- return data.map((datum, index) => {
27
- const length = (datum.share / total) * circumference;
28
- const slice = {
29
+ const centre = size / 2;
30
+ const outer = centre;
31
+ const inner = PsChartDonutService.inner(size);
32
+ const shown = shares.filter((share) => share > 0).length;
33
+ if (shown === 1)
34
+ return data.map((datum, index) => ({
29
35
  color: colors[index % colors.length],
30
- dashLength: Math.max(length - PsChartConfig.donutSliceGap, 0),
31
- dashOffset: -start,
32
36
  id: datum.id,
33
- start,
34
- };
35
- start += length;
36
- return slice;
37
+ path: shares[index] > 0 ? PsChartArcService.ring(centre, inner, outer) : '',
38
+ }));
39
+ const angles = PsChartDonutService.angles(shares, PsChartArcService.minimumAngle(PsChartConfig.donutSliceGap, PsChartConfig.barRadius, inner));
40
+ let start = 0;
41
+ return data.map((datum, index) => {
42
+ const end = start + angles[index];
43
+ const path = angles[index] > 0
44
+ ? PsChartArcService.sector({
45
+ centre,
46
+ end,
47
+ gap: PsChartConfig.donutSliceGap,
48
+ inner,
49
+ outer,
50
+ radius: PsChartConfig.barRadius,
51
+ start,
52
+ })
53
+ : '';
54
+ start = end;
55
+ return { color: colors[index % colors.length], id: datum.id, path };
37
56
  });
38
57
  }
58
+ static angles(shares, minimum) {
59
+ const turn = 2 * Math.PI;
60
+ const shown = shares.filter((share) => share > 0).length;
61
+ const floor = Math.min(minimum, turn / Math.max(shown, 1));
62
+ const total = shares.reduce((sum, share) => sum + share, 0);
63
+ const isSmall = shares.map((share) => share > 0 && (share / total) * turn < floor);
64
+ const smallCount = isSmall.filter(Boolean).length;
65
+ const largeTotal = shares.reduce((sum, share, index) => (isSmall[index] ? sum : sum + share), 0);
66
+ const left = turn - smallCount * floor;
67
+ return shares.map((share, index) => share <= 0 ? 0 : isSmall[index] ? floor : largeTotal > 0 ? (share / largeTotal) * left : 0);
68
+ }
39
69
  }
@@ -1,3 +1,4 @@
1
+ import { PsChartBarCorner } from '../constants/ps-chart-bar-corner';
1
2
  import { PsChartBarService } from './ps-chart-bar-service';
2
3
  import { PsChartScaleService } from './ps-chart-scale-service';
3
4
  export class PsWaterfallService {
@@ -25,6 +26,7 @@ export class PsWaterfallService {
25
26
  const to = PsChartScaleService.y(step.end, domain, plot);
26
27
  return {
27
28
  color: step.isTotal ? colors[2] : step.value < 0 ? colors[1] : colors[0],
29
+ corner: step.isTotal ? PsChartBarCorner.Value : PsChartBarCorner.Both,
28
30
  height: Math.abs(to - from),
29
31
  id: `${row}:${step.label}`,
30
32
  isNegative: step.end < step.start,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-mobile-components",
3
- "version": "1.13.0",
3
+ "version": "1.13.1",
4
4
  "private": false,
5
5
  "description": "Native iOS (SwiftUI) and Android (Jetpack Compose) Ps components for Pallasoft mobile apps",
6
6
  "main": "./dist/index.js",