@servicetitan/marketing-ui 5.11.1 → 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 (99) 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/utils/const.d.ts +1 -1
  22. package/dist/components/charts/funnel-chart/utils/const.js +1 -1
  23. package/dist/components/charts/funnel-chart/utils/const.js.map +1 -1
  24. package/dist/components/charts/funnel-chart/utils/interface.d.ts +1 -0
  25. package/dist/components/charts/funnel-chart/utils/interface.d.ts.map +1 -1
  26. package/dist/components/charts/funnel-chart/utils/interface.js.map +1 -1
  27. package/dist/components/charts/funnel-chart/utils/svg-rounded-path.d.ts +2 -0
  28. package/dist/components/charts/funnel-chart/utils/svg-rounded-path.d.ts.map +1 -0
  29. package/dist/components/charts/funnel-chart/utils/svg-rounded-path.js +47 -0
  30. package/dist/components/charts/funnel-chart/utils/svg-rounded-path.js.map +1 -0
  31. package/dist/components/charts/line-chart/components/hover-popover.d.ts.map +1 -1
  32. package/dist/components/charts/line-chart/components/hover-popover.js +13 -7
  33. package/dist/components/charts/line-chart/components/hover-popover.js.map +1 -1
  34. package/dist/components/charts/line-chart/components/hover-popover.module.less +10 -0
  35. package/dist/components/charts/line-chart/components/hover-popover.module.less.d.ts +2 -0
  36. package/dist/components/charts/line-chart/components/stuff.d.ts +0 -8
  37. package/dist/components/charts/line-chart/components/stuff.d.ts.map +1 -1
  38. package/dist/components/charts/line-chart/components/stuff.js +6 -20
  39. package/dist/components/charts/line-chart/components/stuff.js.map +1 -1
  40. package/dist/components/charts/line-chart/components/stuff.module.less +0 -16
  41. package/dist/components/charts/line-chart/components/stuff.module.less.d.ts +0 -3
  42. package/dist/components/charts/line-chart/components/svg-bars.d.ts.map +1 -1
  43. package/dist/components/charts/line-chart/components/svg-bars.js +97 -13
  44. package/dist/components/charts/line-chart/components/svg-bars.js.map +1 -1
  45. package/dist/components/charts/line-chart/line-chart.stories.d.ts.map +1 -1
  46. package/dist/components/charts/line-chart/stores/line-chart.store.d.ts +5 -0
  47. package/dist/components/charts/line-chart/stores/line-chart.store.d.ts.map +1 -1
  48. package/dist/components/charts/line-chart/stores/line-chart.store.js +41 -1
  49. package/dist/components/charts/line-chart/stores/line-chart.store.js.map +1 -1
  50. package/dist/components/charts/line-chart/utils/interfaces.d.ts +4 -0
  51. package/dist/components/charts/line-chart/utils/interfaces.d.ts.map +1 -1
  52. package/dist/components/charts/line-chart/utils/interfaces.js.map +1 -1
  53. package/dist/components/charts/line-chart/utils/labels.js +1 -1
  54. package/dist/components/charts/line-chart/utils/labels.js.map +1 -1
  55. package/dist/components/charts/pie-chart/components/pie-chart.d.ts.map +1 -1
  56. package/dist/components/charts/pie-chart/components/pie-chart.js +24 -13
  57. package/dist/components/charts/pie-chart/components/pie-chart.js.map +1 -1
  58. package/dist/components/charts/pie-chart/components/pie-chart.module.less +15 -0
  59. package/dist/components/charts/pie-chart/components/pie-chart.module.less.d.ts +1 -0
  60. package/dist/components/charts/pie-chart/components/pie.d.ts.map +1 -1
  61. package/dist/components/charts/pie-chart/components/pie.js +105 -28
  62. package/dist/components/charts/pie-chart/components/pie.js.map +1 -1
  63. package/dist/components/charts/pie-chart/pie-chart.stories.d.ts.map +1 -1
  64. package/dist/components/charts/pie-chart/utils/const.js +1 -1
  65. package/dist/components/charts/pie-chart/utils/const.js.map +1 -1
  66. package/dist/components/stat/stat-card.d.ts.map +1 -1
  67. package/dist/components/stat/stat-card.js +28 -12
  68. package/dist/components/stat/stat-card.js.map +1 -1
  69. package/package.json +5 -3
  70. package/src/components/charts/common/color-tag.module.less +23 -0
  71. package/src/components/charts/common/color-tag.module.less.d.ts +6 -0
  72. package/src/components/charts/common/color-tag.tsx +92 -0
  73. package/src/components/charts/common/index.ts +1 -0
  74. package/src/components/charts/funnel-chart/components/funnel-chart.module.less +28 -10
  75. package/src/components/charts/funnel-chart/components/funnel-chart.module.less.d.ts +3 -1
  76. package/src/components/charts/funnel-chart/components/funnel-chart.tsx +107 -78
  77. package/src/components/charts/funnel-chart/components/funnel-svg.tsx +91 -23
  78. package/src/components/charts/funnel-chart/funnel-chart.stories.tsx +3 -1
  79. package/src/components/charts/funnel-chart/utils/const.ts +1 -1
  80. package/src/components/charts/funnel-chart/utils/interface.ts +1 -0
  81. package/src/components/charts/funnel-chart/utils/svg-rounded-path.ts +86 -0
  82. package/src/components/charts/line-chart/components/hover-popover.module.less +10 -0
  83. package/src/components/charts/line-chart/components/hover-popover.module.less.d.ts +2 -0
  84. package/src/components/charts/line-chart/components/hover-popover.tsx +29 -9
  85. package/src/components/charts/line-chart/components/stuff.module.less +0 -16
  86. package/src/components/charts/line-chart/components/stuff.module.less.d.ts +0 -3
  87. package/src/components/charts/line-chart/components/stuff.tsx +4 -30
  88. package/src/components/charts/line-chart/components/svg-bars.tsx +106 -9
  89. package/src/components/charts/line-chart/line-chart.stories.tsx +13 -8
  90. package/src/components/charts/line-chart/stores/line-chart.store.ts +39 -1
  91. package/src/components/charts/line-chart/utils/interfaces.ts +4 -0
  92. package/src/components/charts/line-chart/utils/labels.ts +1 -1
  93. package/src/components/charts/pie-chart/components/pie-chart.module.less +15 -0
  94. package/src/components/charts/pie-chart/components/pie-chart.module.less.d.ts +1 -0
  95. package/src/components/charts/pie-chart/components/pie-chart.tsx +23 -13
  96. package/src/components/charts/pie-chart/components/pie.tsx +106 -40
  97. package/src/components/charts/pie-chart/pie-chart.stories.tsx +3 -4
  98. package/src/components/charts/pie-chart/utils/const.ts +1 -1
  99. package/src/components/stat/stat-card.tsx +34 -16
@@ -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
- export const defaultTopSideLength = 66;
1
+ export const defaultTopSideLength = 70;
2
2
  export const defaultBottomSideLength = 30;
@@ -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;