@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.
- package/dist/components/charts/common/color-tag.d.ts +15 -0
- package/dist/components/charts/common/color-tag.d.ts.map +1 -0
- package/dist/components/charts/common/color-tag.js +79 -0
- package/dist/components/charts/common/color-tag.js.map +1 -0
- package/dist/components/charts/common/color-tag.module.less +23 -0
- package/dist/components/charts/common/color-tag.module.less.d.ts +6 -0
- package/dist/components/charts/common/index.d.ts +2 -0
- package/dist/components/charts/common/index.d.ts.map +1 -0
- package/dist/components/charts/common/index.js +3 -0
- package/dist/components/charts/common/index.js.map +1 -0
- package/dist/components/charts/funnel-chart/components/funnel-chart.d.ts.map +1 -1
- package/dist/components/charts/funnel-chart/components/funnel-chart.js +115 -70
- package/dist/components/charts/funnel-chart/components/funnel-chart.js.map +1 -1
- package/dist/components/charts/funnel-chart/components/funnel-chart.module.less +28 -10
- package/dist/components/charts/funnel-chart/components/funnel-chart.module.less.d.ts +3 -1
- package/dist/components/charts/funnel-chart/components/funnel-svg.d.ts +2 -0
- package/dist/components/charts/funnel-chart/components/funnel-svg.d.ts.map +1 -1
- package/dist/components/charts/funnel-chart/components/funnel-svg.js +72 -31
- package/dist/components/charts/funnel-chart/components/funnel-svg.js.map +1 -1
- package/dist/components/charts/funnel-chart/funnel-chart.stories.d.ts.map +1 -1
- package/dist/components/charts/funnel-chart/index.d.ts +1 -1
- package/dist/components/charts/funnel-chart/index.d.ts.map +1 -1
- package/dist/components/charts/funnel-chart/index.js +0 -1
- package/dist/components/charts/funnel-chart/index.js.map +1 -1
- package/dist/components/charts/funnel-chart/utils/const.d.ts +1 -1
- package/dist/components/charts/funnel-chart/utils/const.js +1 -1
- package/dist/components/charts/funnel-chart/utils/const.js.map +1 -1
- package/dist/components/charts/funnel-chart/utils/interface.d.ts +1 -0
- package/dist/components/charts/funnel-chart/utils/interface.d.ts.map +1 -1
- package/dist/components/charts/funnel-chart/utils/interface.js.map +1 -1
- package/dist/components/charts/funnel-chart/utils/svg-rounded-path.d.ts +2 -0
- package/dist/components/charts/funnel-chart/utils/svg-rounded-path.d.ts.map +1 -0
- package/dist/components/charts/funnel-chart/utils/svg-rounded-path.js +47 -0
- package/dist/components/charts/funnel-chart/utils/svg-rounded-path.js.map +1 -0
- package/dist/components/charts/line-chart/components/hover-popover.d.ts.map +1 -1
- package/dist/components/charts/line-chart/components/hover-popover.js +13 -7
- package/dist/components/charts/line-chart/components/hover-popover.js.map +1 -1
- package/dist/components/charts/line-chart/components/hover-popover.module.less +10 -0
- package/dist/components/charts/line-chart/components/hover-popover.module.less.d.ts +2 -0
- package/dist/components/charts/line-chart/components/stuff.d.ts +0 -8
- package/dist/components/charts/line-chart/components/stuff.d.ts.map +1 -1
- package/dist/components/charts/line-chart/components/stuff.js +6 -20
- package/dist/components/charts/line-chart/components/stuff.js.map +1 -1
- package/dist/components/charts/line-chart/components/stuff.module.less +0 -16
- package/dist/components/charts/line-chart/components/stuff.module.less.d.ts +0 -3
- package/dist/components/charts/line-chart/components/svg-bars.d.ts.map +1 -1
- package/dist/components/charts/line-chart/components/svg-bars.js +97 -15
- package/dist/components/charts/line-chart/components/svg-bars.js.map +1 -1
- package/dist/components/charts/line-chart/index.d.ts +1 -1
- package/dist/components/charts/line-chart/index.d.ts.map +1 -1
- package/dist/components/charts/line-chart/index.js +0 -1
- package/dist/components/charts/line-chart/index.js.map +1 -1
- package/dist/components/charts/line-chart/line-chart.stories.d.ts.map +1 -1
- package/dist/components/charts/line-chart/stores/line-chart.store.d.ts +7 -2
- package/dist/components/charts/line-chart/stores/line-chart.store.d.ts.map +1 -1
- package/dist/components/charts/line-chart/stores/line-chart.store.js +41 -3
- package/dist/components/charts/line-chart/stores/line-chart.store.js.map +1 -1
- package/dist/components/charts/line-chart/utils/interfaces.d.ts +4 -0
- package/dist/components/charts/line-chart/utils/interfaces.d.ts.map +1 -1
- package/dist/components/charts/line-chart/utils/interfaces.js.map +1 -1
- package/dist/components/charts/line-chart/utils/labels.js +1 -1
- package/dist/components/charts/line-chart/utils/labels.js.map +1 -1
- package/dist/components/charts/pie-chart/components/pie-chart.d.ts.map +1 -1
- package/dist/components/charts/pie-chart/components/pie-chart.js +24 -13
- package/dist/components/charts/pie-chart/components/pie-chart.js.map +1 -1
- package/dist/components/charts/pie-chart/components/pie-chart.module.less +15 -0
- package/dist/components/charts/pie-chart/components/pie-chart.module.less.d.ts +1 -0
- package/dist/components/charts/pie-chart/components/pie.d.ts.map +1 -1
- package/dist/components/charts/pie-chart/components/pie.js +105 -28
- package/dist/components/charts/pie-chart/components/pie.js.map +1 -1
- package/dist/components/charts/pie-chart/index.d.ts +1 -1
- package/dist/components/charts/pie-chart/index.d.ts.map +1 -1
- package/dist/components/charts/pie-chart/index.js +0 -1
- package/dist/components/charts/pie-chart/index.js.map +1 -1
- package/dist/components/charts/pie-chart/pie-chart.stories.d.ts.map +1 -1
- package/dist/components/charts/pie-chart/utils/const.js +1 -1
- package/dist/components/charts/pie-chart/utils/const.js.map +1 -1
- package/dist/components/image-cropper/image-cropper.d.ts.map +1 -1
- package/dist/components/image-cropper/image-cropper.js +1 -1
- package/dist/components/image-cropper/image-cropper.js.map +1 -1
- package/dist/components/stat/stat-card.d.ts.map +1 -1
- package/dist/components/stat/stat-card.js +28 -12
- package/dist/components/stat/stat-card.js.map +1 -1
- package/dist/utils/date/date-range-picker-state.d.ts +3 -2
- package/dist/utils/date/date-range-picker-state.d.ts.map +1 -1
- package/dist/utils/date/date-range-picker-state.js +0 -1
- package/dist/utils/date/date-range-picker-state.js.map +1 -1
- package/package.json +5 -3
- package/src/components/charts/common/color-tag.module.less +23 -0
- package/src/components/charts/common/color-tag.module.less.d.ts +6 -0
- package/src/components/charts/common/color-tag.tsx +92 -0
- package/src/components/charts/common/index.ts +1 -0
- package/src/components/charts/funnel-chart/components/funnel-chart.module.less +28 -10
- package/src/components/charts/funnel-chart/components/funnel-chart.module.less.d.ts +3 -1
- package/src/components/charts/funnel-chart/components/funnel-chart.tsx +107 -78
- package/src/components/charts/funnel-chart/components/funnel-svg.tsx +91 -23
- package/src/components/charts/funnel-chart/funnel-chart.stories.tsx +3 -1
- package/src/components/charts/funnel-chart/index.ts +1 -1
- package/src/components/charts/funnel-chart/utils/const.ts +1 -1
- package/src/components/charts/funnel-chart/utils/interface.ts +1 -0
- package/src/components/charts/funnel-chart/utils/svg-rounded-path.ts +86 -0
- package/src/components/charts/line-chart/components/hover-popover.module.less +10 -0
- package/src/components/charts/line-chart/components/hover-popover.module.less.d.ts +2 -0
- package/src/components/charts/line-chart/components/hover-popover.tsx +29 -9
- package/src/components/charts/line-chart/components/stuff.module.less +0 -16
- package/src/components/charts/line-chart/components/stuff.module.less.d.ts +0 -3
- package/src/components/charts/line-chart/components/stuff.tsx +4 -30
- package/src/components/charts/line-chart/components/svg-bars.tsx +106 -11
- package/src/components/charts/line-chart/index.ts +1 -1
- package/src/components/charts/line-chart/line-chart.stories.tsx +13 -8
- package/src/components/charts/line-chart/stores/line-chart.store.ts +41 -3
- package/src/components/charts/line-chart/utils/interfaces.ts +4 -0
- package/src/components/charts/line-chart/utils/labels.ts +1 -1
- package/src/components/charts/pie-chart/components/pie-chart.module.less +15 -0
- package/src/components/charts/pie-chart/components/pie-chart.module.less.d.ts +1 -0
- package/src/components/charts/pie-chart/components/pie-chart.tsx +23 -13
- package/src/components/charts/pie-chart/components/pie.tsx +106 -40
- package/src/components/charts/pie-chart/index.ts +1 -1
- package/src/components/charts/pie-chart/pie-chart.stories.tsx +3 -4
- package/src/components/charts/pie-chart/utils/const.ts +1 -1
- package/src/components/image-cropper/image-cropper.tsx +2 -1
- package/src/components/stat/stat-card.tsx +34 -16
- 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": "
|
|
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": "^
|
|
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": "
|
|
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,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
|
-
|
|
46
|
+
pointer-events: auto;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
.
|
|
37
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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={
|
|
60
|
+
style={rightStyles}
|
|
62
61
|
>
|
|
63
|
-
{sections.map(({ id, title, value, color, prev,
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
<
|
|
117
|
-
{sections.map(s =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
className=
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
onRowAnchors?.(sections.map(s => (s.yt + s.yb) / 2));
|
|
48
|
+
}, [onRowAnchors, sections]);
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
{
|
|
82
|
-
|
|
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((
|
|
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(
|
|
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: '#
|
|
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 =
|
|
1
|
+
export const defaultTopSideLength = 70;
|
|
2
2
|
export const defaultBottomSideLength = 30;
|