@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/marketing-ui",
3
- "version": "5.11.0",
3
+ "version": "6.0.0",
4
4
  "description": "Marketing UI component and utils",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,7 @@
18
18
  "react-image-crop": "8.6.5"
19
19
  },
20
20
  "peerDependencies": {
21
+ "@servicetitan/anvil2": "^1.42.0",
21
22
  "@servicetitan/design-system": ">=13.2.1",
22
23
  "@servicetitan/react-ioc": ">=14.1.1",
23
24
  "@servicetitan/tokens": ">=12.2.1",
@@ -29,8 +30,9 @@
29
30
  "react": ">=17.0.2"
30
31
  },
31
32
  "devDependencies": {
33
+ "@servicetitan/anvil2": "^1.42.0",
32
34
  "@servicetitan/design-system": "~14.5.1",
33
- "@servicetitan/react-ioc": "^31.6.0",
35
+ "@servicetitan/react-ioc": "^32.2.0",
34
36
  "@servicetitan/tokens": ">=12.2.1",
35
37
  "@testing-library/react": "^14.2.1",
36
38
  "@types/accounting": "~0.4.2",
@@ -51,5 +53,5 @@
51
53
  "less": true,
52
54
  "webpack": false
53
55
  },
54
- "gitHead": "3dab3310534a39aaacb0069e1d377579fe0a7fab"
56
+ "gitHead": "39c8bee0e01ce314dbdd07049e1d0851a03225ab"
55
57
  }
@@ -0,0 +1,23 @@
1
+ @import (reference) '~@servicetitan/tokens/core/tokens.less';
2
+
3
+ .color-tag {
4
+ width: 15px;
5
+ height: 15px;
6
+ margin-right: @spacing-1;
7
+ border-radius: 4px;
8
+ }
9
+
10
+ .color-tag-outline {
11
+ border-style: solid;
12
+ border-width: 1px;
13
+ }
14
+
15
+ .color-tag-dashed {
16
+ height: 0;
17
+ border-top-style: dashed;
18
+ border-top-width: 3px;
19
+ }
20
+
21
+ .color-tag-small {
22
+ width: @spacing-2;
23
+ }
@@ -0,0 +1,6 @@
1
+ export const __esModule: true;
2
+ export const colorTag: string;
3
+ export const colorTagDashed: string;
4
+ export const colorTagOutline: string;
5
+ export const colorTagSmall: string;
6
+
@@ -0,0 +1,92 @@
1
+ import { FC, useId } from 'react';
2
+ import classNames from 'classnames';
3
+ import { BodyText, Stack } from '@servicetitan/design-system';
4
+ import * as Styles from './color-tag.module.less';
5
+
6
+ interface ColorTagProps {
7
+ label: string;
8
+ color: string;
9
+ className?: string;
10
+ colorTagClassName?: string;
11
+ small?: boolean;
12
+ dashed?: boolean;
13
+ strokeColor?: string;
14
+ outlineColor?: string;
15
+ pattern?: 'solid' | 'striped' | 'outline';
16
+ }
17
+
18
+ export const ColorTag: FC<ColorTagProps> = ({
19
+ label,
20
+ color,
21
+ className,
22
+ small,
23
+ dashed,
24
+ outlineColor,
25
+ pattern,
26
+ strokeColor,
27
+ colorTagClassName,
28
+ }) => {
29
+ const uid = useId();
30
+ const patternId = `pattern-${uid}`;
31
+
32
+ const width = small ? 50 : 22;
33
+ const height = small ? 10 : 25;
34
+ const radius = small ? 0 : 3;
35
+
36
+ return (
37
+ <Stack alignItems="center" className={className} gap={1}>
38
+ {pattern === 'striped' ? (
39
+ <svg
40
+ width={width}
41
+ height={height}
42
+ viewBox={`0 0 ${width} ${height}`}
43
+ className={classNames(Styles.colorTag, colorTagClassName)}
44
+ aria-hidden
45
+ >
46
+ <defs>
47
+ <pattern
48
+ id={patternId}
49
+ patternUnits="userSpaceOnUse"
50
+ width="8"
51
+ height="8"
52
+ patternTransform="rotate(45)"
53
+ >
54
+ <rect width="4" height="8" fill={color} opacity="0.08" />
55
+ <rect width="2" height="8" fill={color} />
56
+ </pattern>
57
+ </defs>
58
+ <rect
59
+ x="0"
60
+ y="0"
61
+ width={width}
62
+ height={height}
63
+ rx={radius}
64
+ ry={radius}
65
+ fill={`url(#${patternId})`}
66
+ stroke={strokeColor ?? color}
67
+ strokeWidth={1}
68
+ vectorEffect="non-scaling-stroke"
69
+ />
70
+ </svg>
71
+ ) : (
72
+ <div
73
+ className={classNames(
74
+ Styles.colorTag,
75
+ small && Styles.colorTagSmall,
76
+ dashed && Styles.colorTagDashed,
77
+ pattern === 'outline' && Styles.colorTagOutline,
78
+ colorTagClassName
79
+ )}
80
+ style={
81
+ dashed
82
+ ? { borderColor: strokeColor ?? color }
83
+ : pattern === 'outline'
84
+ ? { borderColor: outlineColor ?? color, borderRadius: radius }
85
+ : { backgroundColor: color, borderRadius: radius }
86
+ }
87
+ />
88
+ )}
89
+ <BodyText size={small ? 'xsmall' : 'small'}>{label}</BodyText>
90
+ </Stack>
91
+ );
92
+ };
@@ -0,0 +1 @@
1
+ export { ColorTag } from './color-tag';
@@ -8,31 +8,49 @@
8
8
  border-style: dashed;
9
9
  }
10
10
 
11
+ .percent-text-wrapper {
12
+ width: 32px;
13
+ height: 32px;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ border-radius: @border-radius-circular;
18
+ background: rgba(255, 255, 255, 0.8);
19
+
20
+ :global(.BodyText) {
21
+ font-size: @typescale-0;
22
+ }
23
+ }
24
+
11
25
  .stat-title {
12
26
  white-space: nowrap;
13
27
  }
14
28
 
15
29
  .pyramid-wrapper {
16
30
  position: absolute;
17
- inset: @spacing-3;
18
-
19
- .pyramid-line {
20
- stroke: @color-neutral-60;
21
- stroke-width: 2;
22
- }
31
+ inset: @spacing-3; // your existing chart padding
23
32
  }
24
33
 
25
- .pyramid-box-left,
26
34
  .pyramid-box-right {
27
35
  position: absolute;
28
36
  inset: @spacing-3;
37
+ z-index: 5;
29
38
  }
30
39
 
31
40
  .pyramid-box-left {
41
+ position: absolute;
42
+ top: @spacing-3;
43
+ bottom: @spacing-3;
44
+ left: @spacing-3;
32
45
  z-index: 4;
33
- width: 100%;
46
+ pointer-events: auto;
34
47
  }
35
48
 
36
- .pyramid-box-right {
37
- z-index: 5;
49
+ .leftTitle,
50
+ .leftDiff {
51
+ position: absolute;
52
+ left: 0;
53
+ display: flex;
54
+ justify-content: flex-end;
55
+ align-items: end;
38
56
  }
@@ -1,8 +1,10 @@
1
1
  export const __esModule: true;
2
2
  export const flex1: string;
3
+ export const leftDiff: string;
4
+ export const leftTitle: string;
5
+ export const percentTextWrapper: string;
3
6
  export const pyramidBoxLeft: string;
4
7
  export const pyramidBoxRight: string;
5
- export const pyramidLine: string;
6
8
  export const pyramidWrapper: string;
7
9
  export const statTitle: string;
8
10
  export const title: string;
@@ -1,16 +1,6 @@
1
- import { useMemo, useState, FC } from 'react';
1
+ import { useMemo, useState, FC, Fragment } from 'react';
2
2
  import classNames from 'classnames';
3
- import {
4
- BodyText,
5
- Eyebrow,
6
- Headline,
7
- Icon,
8
- Mask,
9
- Popover,
10
- Stack,
11
- StatusLight,
12
- Tooltip,
13
- } from '@servicetitan/design-system';
3
+ import { BodyText, Eyebrow, Icon, Mask, Stack, Tooltip } from '@servicetitan/design-system';
14
4
  import { tokens } from '@servicetitan/tokens/core';
15
5
  import { formatValue } from '../../../../utils/formatters';
16
6
  import { StatDiff } from '../../../stat/stat-card';
@@ -18,6 +8,8 @@ import { FunnelChartProps } from '../utils/interface';
18
8
  import { defaultBottomSideLength, defaultTopSideLength } from '../utils/const';
19
9
  import { FunnelPyramidSvg } from './funnel-svg';
20
10
  import * as Styles from './funnel-chart.module.less';
11
+ import { Popover } from '@servicetitan/anvil2';
12
+ import { ColorTag } from '../../common';
21
13
 
22
14
  export const FunnelChart: FC<FunnelChartProps> = ({
23
15
  sections,
@@ -29,11 +21,16 @@ export const FunnelChart: FC<FunnelChartProps> = ({
29
21
  className,
30
22
  }) => {
31
23
  const [popoverShown, setPopoverShown] = useState<number | undefined>(undefined);
32
-
24
+ const [rowYs, setRowYs] = useState<number[]>([]);
33
25
  const colors = useMemo(() => sections.map(s => s.color), [sections]);
26
+ const outlineColors = useMemo(() => sections.map(s => s.outlineColor), [sections]);
34
27
  const hidePopover = () => setPopoverShown(undefined);
35
- const pyramidTextsStyles = useMemo(
36
- () => ({ left: `${100 - topSideLength}%` }),
28
+
29
+ const rightStyles = useMemo(
30
+ () => ({
31
+ left: `${100 - topSideLength}%`,
32
+ width: `${topSideLength}%`,
33
+ }),
37
34
  [topSideLength]
38
35
  );
39
36
 
@@ -48,8 +45,10 @@ export const FunnelChart: FC<FunnelChartProps> = ({
48
45
  <div className={Styles.pyramidWrapper}>
49
46
  <FunnelPyramidSvg
50
47
  colors={colors}
48
+ outlineColors={outlineColors}
51
49
  topSideLength={topSideLength}
52
50
  bottomSideLength={bottomSideLength}
51
+ onRowAnchors={setRowYs}
53
52
  />
54
53
  </div>
55
54
 
@@ -58,9 +57,9 @@ export const FunnelChart: FC<FunnelChartProps> = ({
58
57
  'd-f flex-column justify-content-around',
59
58
  Styles.pyramidBoxRight
60
59
  )}
61
- style={pyramidTextsStyles}
60
+ style={rightStyles}
62
61
  >
63
- {sections.map(({ id, title, value, color, prev, textClass, data }, ind) => (
62
+ {sections.map(({ id, title, value, color, prev, data, outlineColor }) => (
64
63
  <Stack
65
64
  key={title}
66
65
  className={Styles.flex1}
@@ -69,75 +68,105 @@ export const FunnelChart: FC<FunnelChartProps> = ({
69
68
  onMouseEnter={() => setPopoverShown(id)}
70
69
  onMouseLeave={hidePopover}
71
70
  >
72
- <Popover
73
- className="of-hidden"
74
- trigger={
75
- <Headline
76
- size="small"
77
- className={classNames('m-x-2 m-b-0-i', textClass)}
78
- data-cy={`marketing-funnel-section-${id}-value`}
71
+ <Popover open={popoverShown === id} openOnHover placement="right">
72
+ <Popover.Trigger>
73
+ {props => (
74
+ <span {...props}>
75
+ <div className={Styles.percentTextWrapper}>
76
+ <BodyText
77
+ className={classNames('m-x-2 m-b-0-i')}
78
+ data-cy={`marketing-funnel-section-${id}-value`}
79
+ >
80
+ {formatValue(value, format)}
81
+ </BodyText>
82
+ </div>
83
+ </span>
84
+ )}
85
+ </Popover.Trigger>
86
+ <Popover.Content>
87
+ <Stack
88
+ alignItems="flex-start"
89
+ justifyContent="flex-start"
90
+ direction="column"
91
+ data-cy={`marketing-funnel-popover-${id}-content`}
79
92
  >
80
- {formatValue(value, format)}
81
- </Headline>
82
- }
83
- open={popoverShown === id}
84
- direction={ind === 0 ? 'lt' : 'lb'}
85
- padding="s"
86
- width="auto"
87
- portal
88
- >
89
- <Stack
90
- alignItems="center"
91
- justifyContent="flex-start"
92
- data-cy={`marketing-funnel-popover-${id}-content`}
93
- >
94
- <StatusLight color={color} />
95
- <BodyText bold className="m-r-half">
96
- {formatValue(value, format)}
97
- </BodyText>
98
- <BodyText bold className="m-r-1">
99
- {title}
100
- </BodyText>
101
- <StatDiff value={value} prev={prev} size="xsmall" format={format} />
102
- </Stack>
93
+ <Stack>
94
+ <ColorTag
95
+ label=""
96
+ color={color}
97
+ outlineColor={outlineColor}
98
+ pattern={outlineColor ? 'outline' : 'solid'}
99
+ />
100
+ <BodyText bold className="m-r-half">
101
+ {title} {formatValue(value, format)}
102
+ </BodyText>
103
+ </Stack>
104
+ <Stack.Item>
105
+ <StatDiff
106
+ value={value}
107
+ prev={prev}
108
+ size="xsmall"
109
+ format={format}
110
+ />
111
+ </Stack.Item>
112
+ </Stack>
103
113
 
104
- {!!PopoverContent && (
105
- <PopoverContent
106
- id={id}
107
- value={value}
108
- text={formatValue(value, format)}
109
- data={data}
110
- />
111
- )}
114
+ {!!PopoverContent && (
115
+ <PopoverContent
116
+ id={id}
117
+ value={value}
118
+ text={formatValue(value, format)}
119
+ data={data}
120
+ />
121
+ )}
122
+ </Popover.Content>
112
123
  </Popover>
113
124
  </Stack>
114
125
  ))}
115
126
  </div>
116
- <Stack className={Styles.pyramidBoxLeft} direction="column">
117
- {sections.map(s => (
118
- <Stack
119
- key={s.id}
120
- className={Styles.flex1}
121
- justifyContent="flex-start"
122
- alignItems="flex-end"
123
- >
124
- <Eyebrow size="small" className={classNames(Styles.statTitle, 'm-r-half')}>
125
- {s.title}
126
- </Eyebrow>
127
+ <div className={Styles.pyramidBoxLeft} style={{ width: `${100 - topSideLength}%` }}>
128
+ {sections.map((s, i) => {
129
+ const y = rowYs[i] ?? 0;
130
+ const TITLE_UP = 24;
131
+ const DIFF_DOWN = 4;
127
132
 
128
- <Tooltip direction="t" portal text={s.description}>
129
- <Icon
130
- name="info"
131
- className="m-r-1"
132
- size="14px"
133
- color={tokens.colorNeutral90}
134
- />
135
- </Tooltip>
133
+ return (
134
+ <Fragment key={s.id}>
135
+ <div
136
+ className={Styles.leftTitle}
137
+ style={{ top: `calc(${y}% - ${TITLE_UP}px)` }}
138
+ >
139
+ <Eyebrow
140
+ size="small"
141
+ className={classNames(Styles.statTitle, 'm-r-half')}
142
+ >
143
+ {s.title}
144
+ </Eyebrow>
145
+ <Tooltip direction="t" portal text={s.description}>
146
+ <Icon
147
+ name="info"
148
+ className="m-r-1"
149
+ size="14px"
150
+ color={tokens.colorNeutral90}
151
+ />
152
+ </Tooltip>
153
+ </div>
136
154
 
137
- <StatDiff value={s.value} prev={s.prev} size="xsmall" format={format} />
138
- </Stack>
139
- ))}
140
- </Stack>
155
+ <div
156
+ className={Styles.leftDiff}
157
+ style={{ top: `calc(${y}% + ${DIFF_DOWN}px)` }}
158
+ >
159
+ <StatDiff
160
+ value={s.value}
161
+ prev={s.prev}
162
+ size="small"
163
+ format={format}
164
+ />
165
+ </div>
166
+ </Fragment>
167
+ );
168
+ })}
169
+ </div>
141
170
  </Mask>
142
171
  );
143
172
  };
@@ -1,6 +1,7 @@
1
- import { useMemo, FC } from 'react';
1
+ import { useMemo, FC, useEffect } from 'react';
2
2
  import { tokens } from '@servicetitan/tokens/core';
3
3
  import { defaultBottomSideLength, defaultTopSideLength } from '../utils/const';
4
+ import { roundedPath } from '../utils/svg-rounded-path';
4
5
 
5
6
  const st = (v: number) => v.toFixed(2);
6
7
 
@@ -8,12 +9,16 @@ export interface FunnelPyramidSvgProps {
8
9
  colors: string[];
9
10
  topSideLength?: number;
10
11
  bottomSideLength?: number;
12
+ outlineColors?: (string | undefined)[];
13
+ onRowAnchors?: (ysPct: number[]) => void;
11
14
  }
12
15
 
13
16
  export const FunnelPyramidSvg: FC<FunnelPyramidSvgProps> = ({
14
17
  colors,
15
18
  topSideLength = defaultTopSideLength,
16
19
  bottomSideLength = defaultBottomSideLength,
20
+ outlineColors,
21
+ onRowAnchors,
17
22
  }) => {
18
23
  const sections = useMemo(() => {
19
24
  if (!colors.length) {
@@ -38,29 +43,39 @@ export const FunnelPyramidSvg: FC<FunnelPyramidSvgProps> = ({
38
43
  });
39
44
  }, [colors, topSideLength, bottomSideLength]);
40
45
 
41
- const paths = useMemo(() => {
42
- if (!sections.length) {
43
- return [];
44
- }
46
+ useEffect(() => {
47
+ onRowAnchors?.(sections.map(s => (s.yt + s.yb) / 2));
48
+ }, [onRowAnchors, sections]);
45
49
 
46
- return sections.map((s, ind) => {
47
- let path = '';
50
+ const pointAlong = (start: number, end: number, t: number) => (1 - t) * start + t * end;
51
+ const pxToViewBoxUnits = (px: number) => (px / 200) * 100;
48
52
 
49
- path += `M${st(s.xtl)},${st(s.yt)} `;
50
- path += `L${st(s.xtr)},${st(s.yt)} `;
51
- path += `L${st(s.xbr)},${st(s.yb)} `;
52
- path += `L${st(s.xbl)},${st(s.yb)} `;
53
- path += 'Z';
53
+ const GAP_PX = 4;
54
+ const SEAM_PX = 1;
54
55
 
55
- return {
56
- key: ind.toString(),
57
- path,
58
- color: s.c,
59
- };
56
+ const gapVU = pxToViewBoxUnits(GAP_PX);
57
+ const seamVU = pxToViewBoxUnits(SEAM_PX);
58
+
59
+ const lines = useMemo(() => {
60
+ return sections.map((section, i) => {
61
+ const y = (section.yt + section.yb) / 2;
62
+
63
+ const height = section.yb - section.yt;
64
+ const t = (y - section.yt) / height;
65
+ const xLeftAtMid = pointAlong(section.xtl, section.xbl, t);
66
+ const x2 = Math.max(0, xLeftAtMid - gapVU);
67
+
68
+ return { id: i, y, x2 };
60
69
  });
61
- }, [sections]);
70
+ }, [sections, gapVU]);
62
71
 
63
- const lines = useMemo(() => sections.slice(1).map((s, ind) => [ind, s.xtr, s.yt]), [sections]);
72
+ const seams = sections.slice(0, -1).map((s, i) => ({
73
+ key: i,
74
+ x1: s.xbl,
75
+ x2: s.xbr,
76
+ yTopEdge: s.yb - gapVU / 2 + seamVU / 2,
77
+ color: outlineColors?.[i],
78
+ }));
64
79
 
65
80
  return (
66
81
  <svg
@@ -78,9 +93,62 @@ export const FunnelPyramidSvg: FC<FunnelPyramidSvgProps> = ({
78
93
  preserveAspectRatio="none"
79
94
  xmlns="http://www.w3.org/2000/svg"
80
95
  >
81
- {paths.map(({ key, path, color }) => (
82
- <path key={key} d={path} fill={color} />
96
+ {sections.map((section, i) => {
97
+ const isTop = i === 0;
98
+ const isBottom = i === sections.length - 1;
99
+
100
+ const d = roundedPath(
101
+ section.xtl,
102
+ section.xtr,
103
+ section.xbr,
104
+ section.xbl,
105
+ section.yt,
106
+ section.yb,
107
+ isTop,
108
+ isBottom,
109
+ 2.5
110
+ );
111
+
112
+ return (
113
+ <path
114
+ key={`section-${section.xbl}-${section.yb}-${section.xbr}-${section.yb}`}
115
+ d={d}
116
+ fill={colors[i]}
117
+ stroke={outlineColors?.[i]}
118
+ strokeWidth={1}
119
+ vectorEffect="non-scaling-stroke"
120
+ strokeLinejoin="round"
121
+ />
122
+ );
123
+ })}
124
+ {sections.slice(0, -1).map(section => (
125
+ <line
126
+ key={`gap-${section.xbl}-${section.yb}-${section.xbr}-${section.yb}`}
127
+ x1={st(section.xbl)}
128
+ y1={st(section.yb)}
129
+ x2={st(section.xbr)}
130
+ y2={st(section.yb)}
131
+ stroke="#fff"
132
+ strokeWidth={GAP_PX}
133
+ vectorEffect="non-scaling-stroke"
134
+ strokeLinecap="round"
135
+ />
83
136
  ))}
137
+ {seams.map(({ key, x1, x2, yTopEdge, color }) =>
138
+ color ? (
139
+ <line
140
+ key={`seam-${key}`}
141
+ x1={st(x1)}
142
+ y1={st(yTopEdge)}
143
+ x2={st(x2)}
144
+ y2={st(yTopEdge)}
145
+ stroke={color}
146
+ strokeWidth={SEAM_PX}
147
+ vectorEffect="non-scaling-stroke"
148
+ strokeLinecap="round"
149
+ />
150
+ ) : null
151
+ )}
84
152
  </svg>
85
153
  <svg
86
154
  width="100%"
@@ -91,12 +159,12 @@ export const FunnelPyramidSvg: FC<FunnelPyramidSvgProps> = ({
91
159
  preserveAspectRatio="none"
92
160
  xmlns="http://www.w3.org/2000/svg"
93
161
  >
94
- {lines.map(([id, x, y]) => (
162
+ {lines.map(({ id, x2, y }) => (
95
163
  <line
96
164
  key={id}
97
165
  x1="0"
98
166
  y1={st(y)}
99
- x2={st(x)}
167
+ x2={st(x2)}
100
168
  y2={st(y)}
101
169
  stroke={tokens.colorNeutral60}
102
170
  strokeWidth={0.5}
@@ -14,12 +14,13 @@ const w = (cb: () => ReactElement) => () => (
14
14
  const sections3 = (): FunnelChartSection<{ info: string[] }>[] => [
15
15
  {
16
16
  id: 1,
17
- color: '#A9D1FF',
17
+ color: '#F2F9FF',
18
18
  title: 'Revenue',
19
19
  description: 'rate 1 description',
20
20
  value: 0.33,
21
21
  prev: 0.38,
22
22
  data: { info: ['random text 1'] },
23
+ outlineColor: '#3892F3',
23
24
  },
24
25
  {
25
26
  id: 2,
@@ -65,6 +66,7 @@ export const funnelChart4Sections = w(() => (
65
66
  description: 'rate 1 description',
66
67
  value: 0.33,
67
68
  prev: 0.38,
69
+ outlineColor: '#D0D8DD',
68
70
  },
69
71
  {
70
72
  id: 2,
@@ -1,2 +1,2 @@
1
1
  export { FunnelChart } from './components/funnel-chart';
2
- export * from './utils/interface';
2
+ export type * from './utils/interface';
@@ -1,2 +1,2 @@
1
- export const defaultTopSideLength = 66;
1
+ export const defaultTopSideLength = 70;
2
2
  export const defaultBottomSideLength = 30;