@servicetitan/marketing-ui 5.11.0 → 6.0.0

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 (123) hide show
  1. package/dist/components/charts/common/color-tag.d.ts +15 -0
  2. package/dist/components/charts/common/color-tag.d.ts.map +1 -0
  3. package/dist/components/charts/common/color-tag.js +79 -0
  4. package/dist/components/charts/common/color-tag.js.map +1 -0
  5. package/dist/components/charts/common/color-tag.module.less +23 -0
  6. package/dist/components/charts/common/color-tag.module.less.d.ts +6 -0
  7. package/dist/components/charts/common/index.d.ts +2 -0
  8. package/dist/components/charts/common/index.d.ts.map +1 -0
  9. package/dist/components/charts/common/index.js +3 -0
  10. package/dist/components/charts/common/index.js.map +1 -0
  11. package/dist/components/charts/funnel-chart/components/funnel-chart.d.ts.map +1 -1
  12. package/dist/components/charts/funnel-chart/components/funnel-chart.js +115 -70
  13. package/dist/components/charts/funnel-chart/components/funnel-chart.js.map +1 -1
  14. package/dist/components/charts/funnel-chart/components/funnel-chart.module.less +28 -10
  15. package/dist/components/charts/funnel-chart/components/funnel-chart.module.less.d.ts +3 -1
  16. package/dist/components/charts/funnel-chart/components/funnel-svg.d.ts +2 -0
  17. package/dist/components/charts/funnel-chart/components/funnel-svg.d.ts.map +1 -1
  18. package/dist/components/charts/funnel-chart/components/funnel-svg.js +72 -31
  19. package/dist/components/charts/funnel-chart/components/funnel-svg.js.map +1 -1
  20. package/dist/components/charts/funnel-chart/funnel-chart.stories.d.ts.map +1 -1
  21. package/dist/components/charts/funnel-chart/index.d.ts +1 -1
  22. package/dist/components/charts/funnel-chart/index.d.ts.map +1 -1
  23. package/dist/components/charts/funnel-chart/index.js +0 -1
  24. package/dist/components/charts/funnel-chart/index.js.map +1 -1
  25. package/dist/components/charts/funnel-chart/utils/const.d.ts +1 -1
  26. package/dist/components/charts/funnel-chart/utils/const.js +1 -1
  27. package/dist/components/charts/funnel-chart/utils/const.js.map +1 -1
  28. package/dist/components/charts/funnel-chart/utils/interface.d.ts +1 -0
  29. package/dist/components/charts/funnel-chart/utils/interface.d.ts.map +1 -1
  30. package/dist/components/charts/funnel-chart/utils/interface.js.map +1 -1
  31. package/dist/components/charts/funnel-chart/utils/svg-rounded-path.d.ts +2 -0
  32. package/dist/components/charts/funnel-chart/utils/svg-rounded-path.d.ts.map +1 -0
  33. package/dist/components/charts/funnel-chart/utils/svg-rounded-path.js +47 -0
  34. package/dist/components/charts/funnel-chart/utils/svg-rounded-path.js.map +1 -0
  35. package/dist/components/charts/line-chart/components/hover-popover.d.ts.map +1 -1
  36. package/dist/components/charts/line-chart/components/hover-popover.js +13 -7
  37. package/dist/components/charts/line-chart/components/hover-popover.js.map +1 -1
  38. package/dist/components/charts/line-chart/components/hover-popover.module.less +10 -0
  39. package/dist/components/charts/line-chart/components/hover-popover.module.less.d.ts +2 -0
  40. package/dist/components/charts/line-chart/components/stuff.d.ts +0 -8
  41. package/dist/components/charts/line-chart/components/stuff.d.ts.map +1 -1
  42. package/dist/components/charts/line-chart/components/stuff.js +6 -20
  43. package/dist/components/charts/line-chart/components/stuff.js.map +1 -1
  44. package/dist/components/charts/line-chart/components/stuff.module.less +0 -16
  45. package/dist/components/charts/line-chart/components/stuff.module.less.d.ts +0 -3
  46. package/dist/components/charts/line-chart/components/svg-bars.d.ts.map +1 -1
  47. package/dist/components/charts/line-chart/components/svg-bars.js +97 -15
  48. package/dist/components/charts/line-chart/components/svg-bars.js.map +1 -1
  49. package/dist/components/charts/line-chart/index.d.ts +1 -1
  50. package/dist/components/charts/line-chart/index.d.ts.map +1 -1
  51. package/dist/components/charts/line-chart/index.js +0 -1
  52. package/dist/components/charts/line-chart/index.js.map +1 -1
  53. package/dist/components/charts/line-chart/line-chart.stories.d.ts.map +1 -1
  54. package/dist/components/charts/line-chart/stores/line-chart.store.d.ts +7 -2
  55. package/dist/components/charts/line-chart/stores/line-chart.store.d.ts.map +1 -1
  56. package/dist/components/charts/line-chart/stores/line-chart.store.js +41 -3
  57. package/dist/components/charts/line-chart/stores/line-chart.store.js.map +1 -1
  58. package/dist/components/charts/line-chart/utils/interfaces.d.ts +4 -0
  59. package/dist/components/charts/line-chart/utils/interfaces.d.ts.map +1 -1
  60. package/dist/components/charts/line-chart/utils/interfaces.js.map +1 -1
  61. package/dist/components/charts/line-chart/utils/labels.js +1 -1
  62. package/dist/components/charts/line-chart/utils/labels.js.map +1 -1
  63. package/dist/components/charts/pie-chart/components/pie-chart.d.ts.map +1 -1
  64. package/dist/components/charts/pie-chart/components/pie-chart.js +24 -13
  65. package/dist/components/charts/pie-chart/components/pie-chart.js.map +1 -1
  66. package/dist/components/charts/pie-chart/components/pie-chart.module.less +15 -0
  67. package/dist/components/charts/pie-chart/components/pie-chart.module.less.d.ts +1 -0
  68. package/dist/components/charts/pie-chart/components/pie.d.ts.map +1 -1
  69. package/dist/components/charts/pie-chart/components/pie.js +105 -28
  70. package/dist/components/charts/pie-chart/components/pie.js.map +1 -1
  71. package/dist/components/charts/pie-chart/index.d.ts +1 -1
  72. package/dist/components/charts/pie-chart/index.d.ts.map +1 -1
  73. package/dist/components/charts/pie-chart/index.js +0 -1
  74. package/dist/components/charts/pie-chart/index.js.map +1 -1
  75. package/dist/components/charts/pie-chart/pie-chart.stories.d.ts.map +1 -1
  76. package/dist/components/charts/pie-chart/utils/const.js +1 -1
  77. package/dist/components/charts/pie-chart/utils/const.js.map +1 -1
  78. package/dist/components/image-cropper/image-cropper.d.ts.map +1 -1
  79. package/dist/components/image-cropper/image-cropper.js +1 -1
  80. package/dist/components/image-cropper/image-cropper.js.map +1 -1
  81. package/dist/components/stat/stat-card.d.ts.map +1 -1
  82. package/dist/components/stat/stat-card.js +28 -12
  83. package/dist/components/stat/stat-card.js.map +1 -1
  84. package/dist/utils/date/date-range-picker-state.d.ts +3 -2
  85. package/dist/utils/date/date-range-picker-state.d.ts.map +1 -1
  86. package/dist/utils/date/date-range-picker-state.js +0 -1
  87. package/dist/utils/date/date-range-picker-state.js.map +1 -1
  88. package/package.json +5 -3
  89. package/src/components/charts/common/color-tag.module.less +23 -0
  90. package/src/components/charts/common/color-tag.module.less.d.ts +6 -0
  91. package/src/components/charts/common/color-tag.tsx +92 -0
  92. package/src/components/charts/common/index.ts +1 -0
  93. package/src/components/charts/funnel-chart/components/funnel-chart.module.less +28 -10
  94. package/src/components/charts/funnel-chart/components/funnel-chart.module.less.d.ts +3 -1
  95. package/src/components/charts/funnel-chart/components/funnel-chart.tsx +107 -78
  96. package/src/components/charts/funnel-chart/components/funnel-svg.tsx +91 -23
  97. package/src/components/charts/funnel-chart/funnel-chart.stories.tsx +3 -1
  98. package/src/components/charts/funnel-chart/index.ts +1 -1
  99. package/src/components/charts/funnel-chart/utils/const.ts +1 -1
  100. package/src/components/charts/funnel-chart/utils/interface.ts +1 -0
  101. package/src/components/charts/funnel-chart/utils/svg-rounded-path.ts +86 -0
  102. package/src/components/charts/line-chart/components/hover-popover.module.less +10 -0
  103. package/src/components/charts/line-chart/components/hover-popover.module.less.d.ts +2 -0
  104. package/src/components/charts/line-chart/components/hover-popover.tsx +29 -9
  105. package/src/components/charts/line-chart/components/stuff.module.less +0 -16
  106. package/src/components/charts/line-chart/components/stuff.module.less.d.ts +0 -3
  107. package/src/components/charts/line-chart/components/stuff.tsx +4 -30
  108. package/src/components/charts/line-chart/components/svg-bars.tsx +106 -11
  109. package/src/components/charts/line-chart/index.ts +1 -1
  110. package/src/components/charts/line-chart/line-chart.stories.tsx +13 -8
  111. package/src/components/charts/line-chart/stores/line-chart.store.ts +41 -3
  112. package/src/components/charts/line-chart/utils/interfaces.ts +4 -0
  113. package/src/components/charts/line-chart/utils/labels.ts +1 -1
  114. package/src/components/charts/pie-chart/components/pie-chart.module.less +15 -0
  115. package/src/components/charts/pie-chart/components/pie-chart.module.less.d.ts +1 -0
  116. package/src/components/charts/pie-chart/components/pie-chart.tsx +23 -13
  117. package/src/components/charts/pie-chart/components/pie.tsx +106 -40
  118. package/src/components/charts/pie-chart/index.ts +1 -1
  119. package/src/components/charts/pie-chart/pie-chart.stories.tsx +3 -4
  120. package/src/components/charts/pie-chart/utils/const.ts +1 -1
  121. package/src/components/image-cropper/image-cropper.tsx +2 -1
  122. package/src/components/stat/stat-card.tsx +34 -16
  123. package/src/utils/date/date-range-picker-state.ts +3 -2
@@ -8,6 +8,7 @@ export interface FunnelChartSection<T> {
8
8
  value: number;
9
9
  prev?: number;
10
10
  color: string;
11
+ outlineColor?: string;
11
12
  textClass?: string;
12
13
  data?: T;
13
14
  }
@@ -0,0 +1,86 @@
1
+ const cornerRadius = (
2
+ xTopLeft: number,
3
+ xTopRight: number,
4
+ xBottomRight: number,
5
+ xBottomLeft: number,
6
+ yTop: number,
7
+ yBottom: number,
8
+ radius: number
9
+ ) => {
10
+ const topWidth = Math.max(0, xTopRight - xTopLeft);
11
+ const bottomWidth = Math.max(0, xBottomRight - xBottomLeft);
12
+ const height = Math.max(0, yBottom - yTop);
13
+ const maxAllowed = Math.min(topWidth / 2, bottomWidth / 2, height / 2);
14
+ return Math.max(0, Math.min(radius, maxAllowed));
15
+ };
16
+
17
+ export const roundedPath = (
18
+ xTopLeft: number,
19
+ xTopRight: number,
20
+ xBottomRight: number,
21
+ xBottomLeft: number,
22
+ yTop: number,
23
+ yBottom: number,
24
+ roundTop: boolean,
25
+ roundBottom: boolean,
26
+ radius: number
27
+ ) => {
28
+ const effectiveRadius = cornerRadius(
29
+ xTopLeft,
30
+ xTopRight,
31
+ xBottomRight,
32
+ xBottomLeft,
33
+ yTop,
34
+ yBottom,
35
+ roundBottom ? radius / 2 : radius
36
+ );
37
+
38
+ const insetTop = roundTop ? effectiveRadius : 0;
39
+ const insetBottom = roundBottom ? effectiveRadius : 0;
40
+
41
+ const fmt = (n: number) => n.toFixed(2);
42
+ const M = (x: number, y: number) => `M${fmt(x)},${fmt(y)}`;
43
+ const L = (x: number, y: number) => `L${fmt(x)},${fmt(y)}`;
44
+ const Q = (cx: number, cy: number, x: number, y: number) =>
45
+ `Q${fmt(cx)},${fmt(cy)} ${fmt(x)},${fmt(y)}`;
46
+
47
+ const path: string[] = [];
48
+
49
+ // top-left
50
+ path.push(M(xTopLeft + insetTop, yTop));
51
+
52
+ // Top edge → to top-right
53
+ path.push(L(xTopRight - insetTop, yTop));
54
+
55
+ // Top-right corner
56
+ if (roundTop && effectiveRadius > 0) {
57
+ path.push(Q(xTopRight, yTop, xTopRight, yTop + effectiveRadius));
58
+ }
59
+
60
+ // Right edge → down to bottom
61
+ path.push(L(xBottomRight, yBottom - insetBottom));
62
+
63
+ // Bottom-right corner
64
+ if (roundBottom && effectiveRadius > 0) {
65
+ path.push(Q(xBottomRight, yBottom, xBottomRight - effectiveRadius, yBottom));
66
+ }
67
+
68
+ // Bottom edge → to bottom-left
69
+ path.push(L(xBottomLeft + insetBottom, yBottom));
70
+
71
+ // Bottom-left corner
72
+ if (roundBottom && effectiveRadius > 0) {
73
+ path.push(Q(xBottomLeft, yBottom, xBottomLeft, yBottom - effectiveRadius));
74
+ }
75
+
76
+ // Left edge → up to top
77
+ path.push(L(xTopLeft, yTop + insetTop));
78
+
79
+ // Top-left corner
80
+ if (roundTop && effectiveRadius > 0) {
81
+ path.push(Q(xTopLeft, yTop, xTopLeft + effectiveRadius, yTop));
82
+ }
83
+
84
+ path.push('Z');
85
+ return path.join(' ');
86
+ };
@@ -18,3 +18,13 @@
18
18
  width: 200px;
19
19
  top: 20%;
20
20
  }
21
+
22
+ .color-tag {
23
+ width: 20px;
24
+ height: 6px;
25
+ }
26
+
27
+ .color-tag-outlined {
28
+ width: 18px;
29
+ height: 4px;
30
+ }
@@ -1,4 +1,6 @@
1
1
  export const __esModule: true;
2
+ export const colorTag: string;
3
+ export const colorTagOutlined: string;
2
4
  export const line: string;
3
5
  export const popover: string;
4
6
  export const trigger: string;
@@ -8,13 +8,23 @@ import { SvgStore } from '../stores/svg.store';
8
8
  import { getFormatter } from '../utils/formatters';
9
9
  import { periodDateTitleFormatter } from '../utils/labels';
10
10
  import * as Styles from './hover-popover.module.less';
11
- import { ColorTag } from './stuff';
11
+ import { ColorTag } from '../../common';
12
+ import { Text } from '@servicetitan/anvil2';
12
13
 
13
14
  export const HoverPopover: FC = observer(() => {
14
- const [{ periods, resolution, hoveredIndex, metrics, display }, svgStore] = useDependencies(
15
- LineChartStore,
16
- SvgStore
17
- );
15
+ const [
16
+ {
17
+ periods,
18
+ resolution,
19
+ hoveredIndex,
20
+ metrics,
21
+ display,
22
+ formattedTotalAt,
23
+ totalLabel,
24
+ stackedTotals,
25
+ },
26
+ svgStore,
27
+ ] = useDependencies(LineChartStore, SvgStore);
18
28
 
19
29
  const formatDateTitle = useMemo(() => periodDateTitleFormatter[resolution], [resolution]);
20
30
  const formatValue = useCallback(
@@ -51,9 +61,11 @@ export const HoverPopover: FC = observer(() => {
51
61
  className={classNames(Styles.popover, 'bg-white border border-radius-1 p-1')}
52
62
  style={popoverStyle}
53
63
  >
54
- <BodyText size="small" bold>
55
- {formatDateTitle(period)}
56
- </BodyText>
64
+ <Text size="small" variant="headline" el="h6">
65
+ {stackedTotals
66
+ ? `${formattedTotalAt(hoveredIndex)} ${totalLabel} | ${formatDateTitle(period)}`
67
+ : formatDateTitle(period)}
68
+ </Text>
57
69
  {partialWeek && (
58
70
  <BodyText size="xsmall" subdued>
59
71
  Partial week
@@ -63,12 +75,20 @@ export const HoverPopover: FC = observer(() => {
63
75
  m =>
64
76
  m.values[hoveredIndex] !== undefined && (
65
77
  <ColorTag
78
+ small
66
79
  label={formatValue(m.title, m.values[hoveredIndex], m.isRight)}
67
80
  color={m.color}
68
81
  key={m.title}
69
82
  className="m-t-1"
70
83
  dashed={m.opts?.dashed}
71
- small
84
+ pattern={m.opts?.pattern}
85
+ outlineColor={m.opts?.outlineColor}
86
+ strokeColor={m.opts?.strokeColor}
87
+ colorTagClassName={
88
+ m.opts?.pattern === 'outline'
89
+ ? Styles.colorTagOutlined
90
+ : Styles.colorTag
91
+ }
72
92
  />
73
93
  )
74
94
  )}
@@ -1,21 +1,5 @@
1
1
  @import (reference) '~@servicetitan/tokens/core/tokens.less';
2
2
 
3
- .color-tag {
4
- width: @spacing-3;
5
- margin-right: @spacing-1;
6
- height: 3px;
7
- }
8
-
9
- .color-tag-dashed {
10
- height: 0;
11
- border-top-style: dashed;
12
- border-top-width: 3px;
13
- }
14
-
15
- .color-tag-small {
16
- width: @spacing-2;
17
- }
18
-
19
3
  .x-axis-label {
20
4
  font-size: @typescale-0;
21
5
  color: @color-neutral-90;
@@ -1,6 +1,3 @@
1
1
  export const __esModule: true;
2
- export const colorTag: string;
3
- export const colorTagDashed: string;
4
- export const colorTagSmall: string;
5
2
  export const xAxisLabel: string;
6
3
 
@@ -4,36 +4,7 @@ import { BodyText, Stack } from '@servicetitan/design-system';
4
4
  import * as Styles from './stuff.module.less';
5
5
  import { LineChartMetric } from '../utils/interfaces';
6
6
  import { ChartXLabels } from '../utils/internal-interfaces';
7
-
8
- interface ColorTagProps {
9
- label: string;
10
- color: string;
11
- className?: string;
12
- small?: boolean;
13
- dashed?: boolean;
14
- }
15
-
16
- export const ColorTag: FC<ColorTagProps> = ({ label, color, className, small, dashed }) => (
17
- <Stack alignItems="center" className={className}>
18
- <div
19
- style={
20
- dashed
21
- ? {
22
- borderColor: color,
23
- }
24
- : {
25
- backgroundColor: color,
26
- }
27
- }
28
- className={classNames(
29
- Styles.colorTag,
30
- dashed && Styles.colorTagDashed,
31
- small && Styles.colorTagSmall
32
- )}
33
- />
34
- <BodyText size={small ? 'xsmall' : 'small'}>{label}</BodyText>
35
- </Stack>
36
- );
7
+ import { ColorTag } from '../../common';
37
8
 
38
9
  interface MetricsTitleProps {
39
10
  metrics: LineChartMetric[];
@@ -47,6 +18,9 @@ export const MetricsTitle: FC<MetricsTitleProps> = ({ metrics }) => (
47
18
  label={m.title}
48
19
  color={m.color}
49
20
  dashed={m.opts?.dashed}
21
+ pattern={m.opts?.pattern}
22
+ outlineColor={m.opts?.outlineColor}
23
+ strokeColor={m.opts?.strokeColor}
50
24
  className="m-r-4"
51
25
  />
52
26
  ))}
@@ -13,12 +13,35 @@ interface SvgBarsProps {
13
13
 
14
14
  export const SvgBars: FC<SvgBarsProps> = observer(
15
15
  ({ metrics, isStackedBarChart, isGroupedBarChart }) => {
16
- // eslint-disable-next-line react-hooks/rules-of-hooks
17
16
  const [store] = useDependencies(SvgStore);
18
17
  const { fpx, fpy, barWidth, length } = store;
19
18
  const barWidthHalf = barWidth / 2;
20
19
  const paths = [];
21
20
 
21
+ const patternDefs = metrics
22
+ .filter(m => m.opts?.pattern === 'striped')
23
+ .map(m => {
24
+ const rotation = 20;
25
+ const tileW = 0.6;
26
+ const tileH = 0.6;
27
+ const stripeWidth = Math.max(0.1, Math.floor(tileW / 20));
28
+ const tintOpacity = 0.06;
29
+
30
+ return (
31
+ <pattern
32
+ key={`pattern-${m.id}`}
33
+ id={`stripe-pattern-${m.id}`}
34
+ patternUnits="userSpaceOnUse"
35
+ width={tileW}
36
+ height={tileH}
37
+ patternTransform={`rotate(${rotation})`}
38
+ >
39
+ <rect width={tileW} height={tileH} fill={m.color} opacity={tintOpacity} />
40
+ <rect width={stripeWidth} height={tileH} fill={m.color} />
41
+ </pattern>
42
+ );
43
+ });
44
+
22
45
  for (let i = 0; i < length; i++) {
23
46
  const x = store.periodX(i);
24
47
  const values = metrics.map(m => ({
@@ -26,22 +49,90 @@ export const SvgBars: FC<SvgBarsProps> = observer(
26
49
  color: m.valuesOpts?.[i]?.color ?? m.color,
27
50
  opacity: m.opacity,
28
51
  val: store.periodY(m, i),
52
+ pattern: m.opts?.pattern,
53
+ strokeColor: m.opts?.strokeColor,
54
+ outlineColor: m.opts?.outlineColor,
55
+ value: m.values[i],
29
56
  }));
30
57
 
31
58
  if (isStackedBarChart) {
32
- let stackedBarHeight = values.reduce((sum, curr) => sum + curr.val, 0);
59
+ const spacingBetweenSegments = 1; // Approximately 4px in SVG coordinates
60
+ const totalSpacing = (values.length - 1) * spacingBetweenSegments;
61
+ let stackedBarHeight =
62
+ values.reduce((sum, curr) => sum + curr.val, 0) + totalSpacing;
63
+
64
+ const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);
65
+ if (totalValue > 0) {
66
+ const yTop = +fpy(stackedBarHeight);
67
+ const scaleX = 0.3;
68
+ const scaleY = 1;
33
69
 
34
- for (const value of values) {
35
70
  paths.push(
36
- <rect
71
+ <g
72
+ key={`total-${i}`}
73
+ transform={`translate(${x},${yTop}) scale(${scaleX},${scaleY})`}
74
+ pointerEvents="none"
75
+ >
76
+ <text
77
+ x={0}
78
+ y={0}
79
+ textAnchor="middle"
80
+ dominantBaseline="alphabetic"
81
+ fontSize="2.5"
82
+ fontWeight={600}
83
+ fill="#111827"
84
+ stroke="white"
85
+ strokeWidth={0.8}
86
+ paintOrder="stroke"
87
+ fontFamily="Nunito Sans"
88
+ >
89
+ {Math.round(totalValue)}
90
+ </text>
91
+ </g>
92
+ );
93
+ }
94
+
95
+ for (let j = 0; j < values.length; j++) {
96
+ const value = values[j];
97
+ stackedBarHeight -= spacingBetweenSegments;
98
+
99
+ const TOP_RADIUS = 1;
100
+ const xLeft = +fpx(x - barWidth / 2);
101
+ const yTop = +fpy(stackedBarHeight) + (values.length - 2);
102
+ const height = +fpx(value.val);
103
+ const width = +fpx(barWidth);
104
+
105
+ const r = j === 0 ? TOP_RADIUS : 0; // radius must be numeric
106
+
107
+ const d = [
108
+ `M ${xLeft} ${yTop + height}`, // bottom-left
109
+ `L ${xLeft} ${yTop + r}`, // up left edge
110
+ `Q ${xLeft} ${yTop} ${xLeft + r / 2} ${yTop}`, // top-left corner
111
+ `L ${xLeft + width - r / 2} ${yTop}`, // across top
112
+ `Q ${xLeft + width} ${yTop} ${xLeft + width} ${yTop + r}`, // top-right corner
113
+ `L ${xLeft + width} ${yTop + height}`, // down right edge
114
+ 'Z',
115
+ ].join(' ');
116
+ paths.push(
117
+ <path
37
118
  key={keyVal(value.id, i)}
38
- x={fpx(x - barWidthHalf)}
39
- y={fpy(stackedBarHeight)}
40
- width={fpx(barWidth)}
41
- height={fpx(value.val)}
42
- fill={value.color}
119
+ d={d}
120
+ fill={
121
+ value.pattern === 'striped'
122
+ ? `url(#stripe-pattern-${value.id})`
123
+ : value.color
124
+ }
125
+ stroke={
126
+ value.pattern === 'outline'
127
+ ? value.outlineColor
128
+ : (value.strokeColor ?? value.color)
129
+ }
130
+ strokeWidth={1}
131
+ vectorEffect="non-scaling-stroke"
132
+ strokeLinejoin="round"
43
133
  />
44
134
  );
135
+
45
136
  stackedBarHeight -= value.val;
46
137
  }
47
138
  } else if (isGroupedBarChart) {
@@ -78,7 +169,12 @@ export const SvgBars: FC<SvgBarsProps> = observer(
78
169
  }
79
170
  }
80
171
 
81
- return <g>{paths}</g>;
172
+ return (
173
+ <g>
174
+ {patternDefs.length > 0 && <defs>{patternDefs}</defs>}
175
+ {paths}
176
+ </g>
177
+ );
82
178
  }
83
179
  );
84
180
 
@@ -88,7 +184,6 @@ interface SvgBarsHoverProps {
88
184
  }
89
185
 
90
186
  export const SvgBarsHover: FC<SvgBarsHoverProps> = observer(({ onHover, onLeave }) => {
91
- // eslint-disable-next-line react-hooks/rules-of-hooks
92
187
  const [store] = useDependencies(SvgStore);
93
188
  const { fpx, fpy, barWidth, length } = store;
94
189
  const barWidthHalf = barWidth / 2;
@@ -1,2 +1,2 @@
1
- export * from './utils/interfaces';
1
+ export type * from './utils/interfaces';
2
2
  export * from './components/container';
@@ -1,6 +1,7 @@
1
1
  import { FC, useState } from 'react';
2
2
  import { Form } from '@servicetitan/design-system';
3
3
  import { LineChart, LineChartPeriod } from './index';
4
+ import { core } from '@servicetitan/anvil2/token';
4
5
 
5
6
  export default {
6
7
  title: 'Marketing UI/charts/LineChart',
@@ -122,24 +123,28 @@ export const stackedBarChartDailyBottomTitles = () => {
122
123
  return (
123
124
  <LineChart
124
125
  resolution="day"
126
+ totalMetricName="Leads"
125
127
  metrics={[
126
128
  {
127
129
  id: 2,
128
130
  title: 'Lead Calls',
129
131
  type: 'stacked-bar',
130
- color: '#D0D8DD',
131
- },
132
- {
133
- id: 3,
134
- title: 'Online Bookings',
135
- type: 'stacked-bar',
136
- color: '#08BFDF',
132
+ color: '#3892F3',
133
+ opts: {
134
+ pattern: 'striped',
135
+ strokeColor: core.semantic.ChartsMonochrome3Stroke.value,
136
+ },
137
137
  },
138
+ { id: 3, title: 'Online Bookings', type: 'stacked-bar', color: '#2B84E0' },
138
139
  {
139
140
  id: 4,
140
141
  title: 'Manual Calls',
141
142
  type: 'stacked-bar',
142
- color: '#1FBC70',
143
+ color: '#F2F9FF',
144
+ opts: {
145
+ pattern: 'outline',
146
+ outlineColor: core.semantic.ChartsMonochrome3Stroke.value,
147
+ },
143
148
  },
144
149
  ]}
145
150
  metricValues={stackedBarChartValues.values}
@@ -1,6 +1,6 @@
1
1
  import { injectable } from '@servicetitan/react-ioc';
2
- import { action, observable, makeObservable } from 'mobx';
3
- import {
2
+ import { action, observable, makeObservable, computed } from 'mobx';
3
+ import type {
4
4
  LineChartDisplay,
5
5
  LineChartDisplayValueFormat,
6
6
  LineChartMetricValues,
@@ -9,7 +9,7 @@ import {
9
9
  LineChartResolution,
10
10
  } from '../utils/interfaces';
11
11
  import { defaultDisplay } from '../utils/const';
12
- import { ChartMetric, SideMetricsSettings } from '../utils/internal-interfaces';
12
+ import type { ChartMetric, SideMetricsSettings } from '../utils/internal-interfaces';
13
13
  import { getFormatter } from '../utils/formatters';
14
14
 
15
15
  const getSideMetricsSettings = (
@@ -86,12 +86,39 @@ export class LineChartStore {
86
86
  @observable metrics: ChartMetric[] = [];
87
87
  @observable periods: LineChartPeriod[] = [];
88
88
  @observable resolution: LineChartResolution = 'day';
89
+ @observable totalMetricName?: string;
89
90
 
90
91
  @observable left?: SideMetricsSettings;
91
92
  @observable right?: SideMetricsSettings;
92
93
 
93
94
  @observable hoveredIndex = -1;
94
95
 
96
+ @computed get stackedTotals(): number[] | undefined {
97
+ const stackedSeries = this.metrics.filter(m => m.type === 'stacked-bar');
98
+ if (stackedSeries.length === 0) {
99
+ return undefined;
100
+ }
101
+ const length = Math.max(this.periods.length, ...stackedSeries.map(s => s.values.length));
102
+ if (length === 0) {
103
+ return [];
104
+ }
105
+
106
+ const totals = Array.from({ length }, () => 0);
107
+
108
+ for (let i = 0; i < length; i++) {
109
+ for (const s of stackedSeries) {
110
+ const v = s.values[i];
111
+ totals[i] += !Number.isNaN(v) ? v : 0;
112
+ }
113
+ }
114
+
115
+ return totals;
116
+ }
117
+
118
+ get totalLabel(): string {
119
+ return this.totalMetricName ?? 'Total';
120
+ }
121
+
95
122
  constructor() {
96
123
  makeObservable(this);
97
124
  }
@@ -131,6 +158,7 @@ export class LineChartStore {
131
158
  );
132
159
 
133
160
  this.resolution = props.resolution;
161
+ this.totalMetricName = props.totalMetricName;
134
162
  this.periods = props.periods || [];
135
163
  this.hoveredIndex = -1;
136
164
  this.isInit = true;
@@ -143,4 +171,14 @@ export class LineChartStore {
143
171
  this.hoveredIndex = -1;
144
172
  }
145
173
  };
174
+
175
+ formattedTotalAt = (index: number): string => {
176
+ const formatter = getFormatter(this.display.yLeftFormat);
177
+ return formatter(this.totalAt(index));
178
+ };
179
+
180
+ private totalAt = (index: number): number =>
181
+ this.stackedTotals && index >= 0 && index < this.stackedTotals.length
182
+ ? this.stackedTotals[index]
183
+ : 0;
146
184
  }
@@ -6,6 +6,9 @@ export type LineChartMetricsTitlePosition = 'top' | 'top-right' | 'bottom';
6
6
 
7
7
  export interface LineChartMetricOpts {
8
8
  dashed?: boolean;
9
+ strokeColor?: string;
10
+ outlineColor?: string;
11
+ pattern?: 'solid' | 'striped' | 'outline';
9
12
  }
10
13
 
11
14
  export interface LineChartMetric {
@@ -40,6 +43,7 @@ export interface LineChartData {
40
43
  periods: LineChartPeriod[];
41
44
  metricValues: LineChartMetricValues[];
42
45
  resolution: LineChartResolution;
46
+ totalMetricName?: string;
43
47
  }
44
48
 
45
49
  export type LineChartDisplayValueFormatterName = 'number' | 'money' | 'moneyShort';
@@ -104,7 +104,7 @@ export const periodDateTitleFormatter: Record<
104
104
  moment(period.from).format('HH:mm') +
105
105
  ' - ' +
106
106
  moment(period.to).format('HH:mm'),
107
- day: period => moment(period.from).format('MMMM DD, YYYY'),
107
+ day: period => moment(period.from).format('MMM DD, YYYY'),
108
108
  month: period => moment(period.from).format('MMMM YYYY'),
109
109
  week: period =>
110
110
  moment(period.from).format('MM/DD/YYYY') + '-' + moment(period.to).format('MM/DD/YYYY'),
@@ -1,3 +1,18 @@
1
+ @import (reference) '@servicetitan/tokens/core/tokens.less';
2
+
1
3
  .title-wrapper {
2
4
  max-width: 120px;
3
5
  }
6
+
7
+ .percent-text-wrapper {
8
+ width: 32px;
9
+ height: 32px;
10
+ border-radius: @border-radius-circular;
11
+ background: @color-neutral-0;
12
+ border: 1px solid rgba(0, 0, 0, 0.08);
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ margin: @spacing-0 auto;
17
+ box-shadow: 0 0 0 1px @color-neutral-0 inset;
18
+ }
@@ -1,3 +1,4 @@
1
1
  export const __esModule: true;
2
+ export const percentTextWrapper: string;
2
3
  export const titleWrapper: string;
3
4
 
@@ -1,13 +1,15 @@
1
1
  import { useMemo, FC } from 'react';
2
- import classNames from 'classnames';
3
2
  import { BodyText, Stack, StatusLight } from '@servicetitan/design-system';
4
3
  import { PieChartProps, PiePiece } from '../utils/interface';
5
4
  import { convertSessionsToPieces, radiusRelativeDefault } from '../utils/const';
6
5
  import { Pie } from './pie';
6
+ import { Flex } from '@servicetitan/anvil2';
7
+ import classNames from 'classnames';
7
8
  import * as Styles from './pie-chart.module.less';
9
+ import { ColorTag } from '../../common';
8
10
 
9
11
  const PieTitles: FC<{ title: string; pieces: PiePiece[] }> = ({ title, pieces }) => {
10
- return (
12
+ return pieces.length > 2 ? (
11
13
  <div className={classNames(Styles.titleWrapper, 'of-y-auto p-t-2')}>
12
14
  <div>
13
15
  {!!pieces.length && (
@@ -25,6 +27,12 @@ const PieTitles: FC<{ title: string; pieces: PiePiece[] }> = ({ title, pieces })
25
27
  ))}
26
28
  </div>
27
29
  </div>
30
+ ) : (
31
+ <Flex direction="row" gap={8}>
32
+ {pieces.map(piece => (
33
+ <ColorTag key={piece.title} label={piece.title} color={piece.color} />
34
+ ))}
35
+ </Flex>
28
36
  );
29
37
  };
30
38
 
@@ -46,17 +54,19 @@ export const PieChart: FC<PieChartProps> = ({
46
54
  const style = useMemo(() => ({ height, width }), [height, width]);
47
55
 
48
56
  return (
49
- <div className="d-f flex-row" style={style}>
50
- <Pie
51
- title={title}
52
- pieces={pieces}
53
- content={content}
54
- popoverContent={popoverContent}
55
- popoverDirection={popoverDirection}
56
- radiusRelative={radiusRelative}
57
- hideTitles={hideTitles}
58
- />
57
+ <Stack direction={pieces.length > 2 ? 'row' : 'column'}>
58
+ <div style={style}>
59
+ <Pie
60
+ title={title}
61
+ pieces={pieces}
62
+ content={content}
63
+ popoverContent={popoverContent}
64
+ popoverDirection={popoverDirection}
65
+ radiusRelative={radiusRelative}
66
+ hideTitles={hideTitles}
67
+ />
68
+ </div>
59
69
  {!hideTitles && <PieTitles title={title} pieces={pieces} />}
60
- </div>
70
+ </Stack>
61
71
  );
62
72
  };