@servicetitan/marketing-ui 7.3.0 → 7.4.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 (25) hide show
  1. package/dist/components/charts/common/color-tag.d.ts.map +1 -1
  2. package/dist/components/charts/common/color-tag.js +1 -0
  3. package/dist/components/charts/common/color-tag.js.map +1 -1
  4. package/dist/components/charts/line-chart/components/hover-popover.d.ts.map +1 -1
  5. package/dist/components/charts/line-chart/components/hover-popover.js +22 -13
  6. package/dist/components/charts/line-chart/components/hover-popover.js.map +1 -1
  7. package/dist/components/charts/line-chart/components/hover-popover.module.less +5 -0
  8. package/dist/components/charts/line-chart/components/hover-popover.module.less.d.ts +1 -0
  9. package/dist/components/charts/line-chart/components/svg-bars.d.ts.map +1 -1
  10. package/dist/components/charts/line-chart/components/svg-bars.js +16 -22
  11. package/dist/components/charts/line-chart/components/svg-bars.js.map +1 -1
  12. package/dist/components/stat/stat-card.d.ts.map +1 -1
  13. package/dist/components/stat/stat-card.js +53 -33
  14. package/dist/components/stat/stat-card.js.map +1 -1
  15. package/dist/components/stat/stat-card.module.less +17 -2
  16. package/dist/components/stat/stat-card.module.less.d.ts +3 -1
  17. package/package.json +2 -2
  18. package/src/components/charts/common/color-tag.tsx +5 -1
  19. package/src/components/charts/line-chart/components/hover-popover.module.less +5 -0
  20. package/src/components/charts/line-chart/components/hover-popover.module.less.d.ts +1 -0
  21. package/src/components/charts/line-chart/components/hover-popover.tsx +23 -12
  22. package/src/components/charts/line-chart/components/svg-bars.tsx +20 -37
  23. package/src/components/stat/stat-card.module.less +17 -2
  24. package/src/components/stat/stat-card.module.less.d.ts +3 -1
  25. package/src/components/stat/stat-card.tsx +44 -37
@@ -1 +1 @@
1
- {"version":3,"file":"color-tag.d.ts","sourceRoot":"","sources":["../../../../src/components/charts/common/color-tag.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAS,MAAM,OAAO,CAAC;AAKlC,UAAU,aAAa;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;CAC7C;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CA0EtC,CAAC"}
1
+ {"version":3,"file":"color-tag.d.ts","sourceRoot":"","sources":["../../../../src/components/charts/common/color-tag.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAS,MAAM,OAAO,CAAC;AAKlC,UAAU,aAAa;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;CAC7C;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CA8EtC,CAAC"}
@@ -62,6 +62,7 @@ export const ColorTag = ({ label, color, className, small, dashed, outlineColor,
62
62
  borderColor: strokeColor !== null && strokeColor !== void 0 ? strokeColor : color
63
63
  } : pattern === 'outline' ? {
64
64
  borderColor: outlineColor !== null && outlineColor !== void 0 ? outlineColor : color,
65
+ backgroundColor: color,
65
66
  borderRadius: radius
66
67
  } : {
67
68
  backgroundColor: color,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/components/charts/common/color-tag.tsx"],"sourcesContent":["import { FC, useId } from 'react';\nimport classNames from 'classnames';\nimport { BodyText, Stack } from '@servicetitan/design-system';\nimport * as Styles from './color-tag.module.less';\n\ninterface ColorTagProps {\n label: string;\n color: string;\n className?: string;\n colorTagClassName?: string;\n small?: boolean;\n dashed?: boolean;\n strokeColor?: string;\n outlineColor?: string;\n pattern?: 'solid' | 'striped' | 'outline';\n}\n\nexport const ColorTag: FC<ColorTagProps> = ({\n label,\n color,\n className,\n small,\n dashed,\n outlineColor,\n pattern,\n strokeColor,\n colorTagClassName,\n}) => {\n const uid = useId();\n const patternId = `pattern-${uid}`;\n\n const width = small ? 50 : 22;\n const height = small ? 10 : 25;\n const radius = small ? 0 : 3;\n\n return (\n <Stack alignItems=\"center\" className={className} gap={1}>\n {pattern === 'striped' ? (\n <svg\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n className={classNames(Styles.colorTag, colorTagClassName)}\n aria-hidden\n >\n <defs>\n <pattern\n id={patternId}\n patternUnits=\"userSpaceOnUse\"\n width=\"8\"\n height=\"8\"\n patternTransform=\"rotate(45)\"\n >\n <rect width=\"4\" height=\"8\" fill={color} opacity=\"0.08\" />\n <rect width=\"2\" height=\"8\" fill={color} />\n </pattern>\n </defs>\n <rect\n x=\"0\"\n y=\"0\"\n width={width}\n height={height}\n rx={radius}\n ry={radius}\n fill={`url(#${patternId})`}\n stroke={strokeColor ?? color}\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n />\n </svg>\n ) : (\n <div\n className={classNames(\n Styles.colorTag,\n small && Styles.colorTagSmall,\n dashed && Styles.colorTagDashed,\n pattern === 'outline' && Styles.colorTagOutline,\n colorTagClassName\n )}\n style={\n dashed\n ? { borderColor: strokeColor ?? color }\n : pattern === 'outline'\n ? { borderColor: outlineColor ?? color, borderRadius: radius }\n : { backgroundColor: color, borderRadius: radius }\n }\n />\n )}\n <BodyText size={small ? 'xsmall' : 'small'}>{label}</BodyText>\n </Stack>\n );\n};\n"],"names":["useId","classNames","BodyText","Stack","Styles","ColorTag","label","color","className","small","dashed","outlineColor","pattern","strokeColor","colorTagClassName","uid","patternId","width","height","radius","alignItems","gap","svg","viewBox","colorTag","aria-hidden","defs","id","patternUnits","patternTransform","rect","fill","opacity","x","y","rx","ry","stroke","strokeWidth","vectorEffect","div","colorTagSmall","colorTagDashed","colorTagOutline","style","borderColor","borderRadius","backgroundColor","size"],"mappings":";AAAA,SAAaA,KAAK,QAAQ,QAAQ;AAClC,OAAOC,gBAAgB,aAAa;AACpC,SAASC,QAAQ,EAAEC,KAAK,QAAQ,8BAA8B;AAC9D,YAAYC,YAAY,0BAA0B;AAclD,OAAO,MAAMC,WAA8B,CAAC,EACxCC,KAAK,EACLC,KAAK,EACLC,SAAS,EACTC,KAAK,EACLC,MAAM,EACNC,YAAY,EACZC,OAAO,EACPC,WAAW,EACXC,iBAAiB,EACpB;IACG,MAAMC,MAAMf;IACZ,MAAMgB,YAAY,CAAC,QAAQ,EAAED,KAAK;IAElC,MAAME,QAAQR,QAAQ,KAAK;IAC3B,MAAMS,SAAST,QAAQ,KAAK;IAC5B,MAAMU,SAASV,QAAQ,IAAI;IAE3B,qBACI,MAACN;QAAMiB,YAAW;QAASZ,WAAWA;QAAWa,KAAK;;YACjDT,YAAY,0BACT,MAACU;gBACGL,OAAOA;gBACPC,QAAQA;gBACRK,SAAS,CAAC,IAAI,EAAEN,MAAM,CAAC,EAAEC,QAAQ;gBACjCV,WAAWP,WAAWG,OAAOoB,QAAQ,EAAEV;gBACvCW,aAAW;;kCAEX,KAACC;kCACG,cAAA,MAACd;4BACGe,IAAIX;4BACJY,cAAa;4BACbX,OAAM;4BACNC,QAAO;4BACPW,kBAAiB;;8CAEjB,KAACC;oCAAKb,OAAM;oCAAIC,QAAO;oCAAIa,MAAMxB;oCAAOyB,SAAQ;;8CAChD,KAACF;oCAAKb,OAAM;oCAAIC,QAAO;oCAAIa,MAAMxB;;;;;kCAGzC,KAACuB;wBACGG,GAAE;wBACFC,GAAE;wBACFjB,OAAOA;wBACPC,QAAQA;wBACRiB,IAAIhB;wBACJiB,IAAIjB;wBACJY,MAAM,CAAC,KAAK,EAAEf,UAAU,CAAC,CAAC;wBAC1BqB,MAAM,EAAExB,wBAAAA,yBAAAA,cAAeN;wBACvB+B,aAAa;wBACbC,cAAa;;;+BAIrB,KAACC;gBACGhC,WAAWP,WACPG,OAAOoB,QAAQ,EACff,SAASL,OAAOqC,aAAa,EAC7B/B,UAAUN,OAAOsC,cAAc,EAC/B9B,YAAY,aAAaR,OAAOuC,eAAe,EAC/C7B;gBAEJ8B,OACIlC,SACM;oBAAEmC,WAAW,EAAEhC,wBAAAA,yBAAAA,cAAeN;gBAAM,IACpCK,YAAY,YACV;oBAAEiC,WAAW,EAAElC,yBAAAA,0BAAAA,eAAgBJ;oBAAOuC,cAAc3B;gBAAO,IAC3D;oBAAE4B,iBAAiBxC;oBAAOuC,cAAc3B;gBAAO;;0BAInE,KAACjB;gBAAS8C,MAAMvC,QAAQ,WAAW;0BAAUH;;;;AAGzD,EAAE"}
1
+ {"version":3,"sources":["../../../../src/components/charts/common/color-tag.tsx"],"sourcesContent":["import { FC, useId } from 'react';\nimport classNames from 'classnames';\nimport { BodyText, Stack } from '@servicetitan/design-system';\nimport * as Styles from './color-tag.module.less';\n\ninterface ColorTagProps {\n label: string;\n color: string;\n className?: string;\n colorTagClassName?: string;\n small?: boolean;\n dashed?: boolean;\n strokeColor?: string;\n outlineColor?: string;\n pattern?: 'solid' | 'striped' | 'outline';\n}\n\nexport const ColorTag: FC<ColorTagProps> = ({\n label,\n color,\n className,\n small,\n dashed,\n outlineColor,\n pattern,\n strokeColor,\n colorTagClassName,\n}) => {\n const uid = useId();\n const patternId = `pattern-${uid}`;\n\n const width = small ? 50 : 22;\n const height = small ? 10 : 25;\n const radius = small ? 0 : 3;\n\n return (\n <Stack alignItems=\"center\" className={className} gap={1}>\n {pattern === 'striped' ? (\n <svg\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n className={classNames(Styles.colorTag, colorTagClassName)}\n aria-hidden\n >\n <defs>\n <pattern\n id={patternId}\n patternUnits=\"userSpaceOnUse\"\n width=\"8\"\n height=\"8\"\n patternTransform=\"rotate(45)\"\n >\n <rect width=\"4\" height=\"8\" fill={color} opacity=\"0.08\" />\n <rect width=\"2\" height=\"8\" fill={color} />\n </pattern>\n </defs>\n <rect\n x=\"0\"\n y=\"0\"\n width={width}\n height={height}\n rx={radius}\n ry={radius}\n fill={`url(#${patternId})`}\n stroke={strokeColor ?? color}\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n />\n </svg>\n ) : (\n <div\n className={classNames(\n Styles.colorTag,\n small && Styles.colorTagSmall,\n dashed && Styles.colorTagDashed,\n pattern === 'outline' && Styles.colorTagOutline,\n colorTagClassName\n )}\n style={\n dashed\n ? { borderColor: strokeColor ?? color }\n : pattern === 'outline'\n ? {\n borderColor: outlineColor ?? color,\n backgroundColor: color,\n borderRadius: radius,\n }\n : { backgroundColor: color, borderRadius: radius }\n }\n />\n )}\n <BodyText size={small ? 'xsmall' : 'small'}>{label}</BodyText>\n </Stack>\n );\n};\n"],"names":["useId","classNames","BodyText","Stack","Styles","ColorTag","label","color","className","small","dashed","outlineColor","pattern","strokeColor","colorTagClassName","uid","patternId","width","height","radius","alignItems","gap","svg","viewBox","colorTag","aria-hidden","defs","id","patternUnits","patternTransform","rect","fill","opacity","x","y","rx","ry","stroke","strokeWidth","vectorEffect","div","colorTagSmall","colorTagDashed","colorTagOutline","style","borderColor","backgroundColor","borderRadius","size"],"mappings":";AAAA,SAAaA,KAAK,QAAQ,QAAQ;AAClC,OAAOC,gBAAgB,aAAa;AACpC,SAASC,QAAQ,EAAEC,KAAK,QAAQ,8BAA8B;AAC9D,YAAYC,YAAY,0BAA0B;AAclD,OAAO,MAAMC,WAA8B,CAAC,EACxCC,KAAK,EACLC,KAAK,EACLC,SAAS,EACTC,KAAK,EACLC,MAAM,EACNC,YAAY,EACZC,OAAO,EACPC,WAAW,EACXC,iBAAiB,EACpB;IACG,MAAMC,MAAMf;IACZ,MAAMgB,YAAY,CAAC,QAAQ,EAAED,KAAK;IAElC,MAAME,QAAQR,QAAQ,KAAK;IAC3B,MAAMS,SAAST,QAAQ,KAAK;IAC5B,MAAMU,SAASV,QAAQ,IAAI;IAE3B,qBACI,MAACN;QAAMiB,YAAW;QAASZ,WAAWA;QAAWa,KAAK;;YACjDT,YAAY,0BACT,MAACU;gBACGL,OAAOA;gBACPC,QAAQA;gBACRK,SAAS,CAAC,IAAI,EAAEN,MAAM,CAAC,EAAEC,QAAQ;gBACjCV,WAAWP,WAAWG,OAAOoB,QAAQ,EAAEV;gBACvCW,aAAW;;kCAEX,KAACC;kCACG,cAAA,MAACd;4BACGe,IAAIX;4BACJY,cAAa;4BACbX,OAAM;4BACNC,QAAO;4BACPW,kBAAiB;;8CAEjB,KAACC;oCAAKb,OAAM;oCAAIC,QAAO;oCAAIa,MAAMxB;oCAAOyB,SAAQ;;8CAChD,KAACF;oCAAKb,OAAM;oCAAIC,QAAO;oCAAIa,MAAMxB;;;;;kCAGzC,KAACuB;wBACGG,GAAE;wBACFC,GAAE;wBACFjB,OAAOA;wBACPC,QAAQA;wBACRiB,IAAIhB;wBACJiB,IAAIjB;wBACJY,MAAM,CAAC,KAAK,EAAEf,UAAU,CAAC,CAAC;wBAC1BqB,MAAM,EAAExB,wBAAAA,yBAAAA,cAAeN;wBACvB+B,aAAa;wBACbC,cAAa;;;+BAIrB,KAACC;gBACGhC,WAAWP,WACPG,OAAOoB,QAAQ,EACff,SAASL,OAAOqC,aAAa,EAC7B/B,UAAUN,OAAOsC,cAAc,EAC/B9B,YAAY,aAAaR,OAAOuC,eAAe,EAC/C7B;gBAEJ8B,OACIlC,SACM;oBAAEmC,WAAW,EAAEhC,wBAAAA,yBAAAA,cAAeN;gBAAM,IACpCK,YAAY,YACV;oBACIiC,WAAW,EAAElC,yBAAAA,0BAAAA,eAAgBJ;oBAC7BuC,iBAAiBvC;oBACjBwC,cAAc5B;gBAClB,IACA;oBAAE2B,iBAAiBvC;oBAAOwC,cAAc5B;gBAAO;;0BAInE,KAACjB;gBAAS8C,MAAMvC,QAAQ,WAAW;0BAAUH;;;;AAGzD,EAAE"}
@@ -1 +1 @@
1
- {"version":3,"file":"hover-popover.d.ts","sourceRoot":"","sources":["../../../../../src/components/charts/line-chart/components/hover-popover.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2D,EAAE,EAAiB,MAAM,OAAO,CAAC;AAgBnG,eAAO,MAAM,YAAY,EAAE,EAuHzB,CAAC"}
1
+ {"version":3,"file":"hover-popover.d.ts","sourceRoot":"","sources":["../../../../../src/components/charts/line-chart/components/hover-popover.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2D,EAAE,EAAE,MAAM,OAAO,CAAC;AAgBpF,eAAO,MAAM,YAAY,EAAE,EAkIzB,CAAC"}
@@ -38,24 +38,30 @@ export const HoverPopover = observer(()=>{
38
38
  display,
39
39
  popH
40
40
  ]);
41
- const popoverStyle = useMemo(()=>{
41
+ const { popoverStyle, arrowPosition } = useMemo(()=>{
42
42
  const posX = svgStore.periodX(hoveredIndex);
43
43
  const yHeights = metrics.filter((m)=>m.values[hoveredIndex] !== undefined).map((m)=>svgStore.periodY(m, hoveredIndex));
44
44
  const barHeight = yHeights.length ? stackedTotals ? yHeights.reduce((a, b)=>a + b, 0) : Math.max(...yHeights) : 0;
45
45
  const barHeightPercentRaw = svgStore.fpy(Math.max(0, Math.min(100, barHeight)));
46
46
  const barHeightPercent = Math.max(0, Math.min(100, Number(barHeightPercentRaw) || 0));
47
- const barTopPositionPx = barHeightPercent / 100 * CHART_HEIGHT_PX;
48
- const availableSpaceBelow = Math.max(0, CHART_HEIGHT_PX - barTopPositionPx - popH);
49
- const popoverOffsetPx = Math.min(OFFSET_PX, availableSpaceBelow);
47
+ const barTopPx = barHeightPercent / 100 * CHART_HEIGHT_PX;
48
+ const idealTopPx = barTopPx + OFFSET_PX;
49
+ const maxTopPx = Math.max(0, CHART_HEIGHT_PX - popH);
50
+ const popoverRepositioned = idealTopPx > maxTopPx;
51
+ const clampedTopPx = Math.min(idealTopPx, maxTopPx);
52
+ const topPercent = clampedTopPx / CHART_HEIGHT_PX * 100;
50
53
  return {
51
- top: `${barHeightPercent}%`,
52
- transform: `translateY(${popoverOffsetPx}px)`,
53
- position: 'absolute',
54
- ...isChartLeftSide ? {
55
- left: `${svgStore.fpx(posX + 2)}%`
56
- } : {
57
- right: `${svgStore.fpx(102 - posX)}%`
58
- }
54
+ popoverStyle: {
55
+ top: `${topPercent.toFixed(1)}%`,
56
+ transform: 'translateY(0)',
57
+ position: 'absolute',
58
+ ...isChartLeftSide ? {
59
+ left: `${svgStore.fpx(posX + 2)}%`
60
+ } : {
61
+ right: `${svgStore.fpx(102 - posX)}%`
62
+ }
63
+ },
64
+ arrowPosition: popoverRepositioned ? 'bottom' : 'top'
59
65
  };
60
66
  }, [
61
67
  svgStore,
@@ -68,6 +74,9 @@ export const HoverPopover = observer(()=>{
68
74
  if (hoveredIndex < 0 || hoveredIndex >= periods.length) {
69
75
  return null;
70
76
  }
77
+ if ((stackedTotals === null || stackedTotals === void 0 ? void 0 : stackedTotals[hoveredIndex]) === 0) {
78
+ return null;
79
+ }
71
80
  const period = periods[hoveredIndex];
72
81
  const partialWeek = !!period.partial;
73
82
  return /*#__PURE__*/ _jsxs("div", {
@@ -76,7 +85,7 @@ export const HoverPopover = observer(()=>{
76
85
  style: popoverStyle,
77
86
  children: [
78
87
  /*#__PURE__*/ _jsx("div", {
79
- className: classNames(Styles.arrow, isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight)
88
+ className: classNames(Styles.arrow, isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight, arrowPosition === 'bottom' && Styles.arrowBottom)
80
89
  }),
81
90
  /*#__PURE__*/ _jsx(Text, {
82
91
  size: "small",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/components/charts/line-chart/components/hover-popover.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC, CSSProperties } from 'react';\nimport classNames from 'classnames';\nimport { observer } from 'mobx-react';\nimport { useDependencies } from '@servicetitan/react-ioc';\nimport { BodyText } from '@servicetitan/design-system';\nimport { LineChartStore } from '../stores/line-chart.store';\nimport { SvgStore } from '../stores/svg.store';\nimport { getFormatter } from '../utils/formatters';\nimport { periodDateTitleFormatter } from '../utils/labels';\nimport * as Styles from './hover-popover.module.less';\nimport { ColorTag } from '../../common';\nimport { Text } from '@servicetitan/anvil2';\n\nconst CHART_HEIGHT_PX = 400;\nconst OFFSET_PX = 16;\n\nexport const HoverPopover: FC = observer(() => {\n const [\n {\n periods,\n resolution,\n hoveredIndex,\n metrics,\n display,\n formattedTotalAt,\n totalLabel,\n stackedTotals,\n },\n svgStore,\n ] = useDependencies(LineChartStore, SvgStore);\n const isChartLeftSide = hoveredIndex < periods.length / 2;\n\n const formatDateTitle = useMemo(() => periodDateTitleFormatter[resolution], [resolution]);\n const formatValue = useCallback(\n (title: string, value: number, isRight: boolean) =>\n getFormatter(isRight ? display.metricsRightFormat : display.metricsLeftFormat)(value) +\n ' ' +\n title,\n [display]\n );\n\n const popRef = useRef<HTMLDivElement | null>(null);\n const [popH, setPopH] = useState(0);\n\n useLayoutEffect(() => {\n if (!popRef.current) {\n return;\n }\n const rect = popRef.current.getBoundingClientRect();\n if (rect.height && Math.abs(rect.height - popH) > 0.5) {\n setPopH(rect.height);\n }\n }, [hoveredIndex, metrics, display, popH]);\n\n const popoverStyle = useMemo<CSSProperties>(() => {\n const posX = svgStore.periodX(hoveredIndex);\n\n const yHeights = metrics\n .filter(m => m.values[hoveredIndex] !== undefined)\n .map(m => svgStore.periodY(m, hoveredIndex));\n\n const barHeight = yHeights.length\n ? stackedTotals\n ? yHeights.reduce((a, b) => a + b, 0)\n : Math.max(...yHeights)\n : 0;\n const barHeightPercentRaw = svgStore.fpy(Math.max(0, Math.min(100, barHeight)));\n const barHeightPercent = Math.max(0, Math.min(100, Number(barHeightPercentRaw) || 0));\n\n const barTopPositionPx = (barHeightPercent / 100) * CHART_HEIGHT_PX;\n const availableSpaceBelow = Math.max(0, CHART_HEIGHT_PX - barTopPositionPx - popH);\n const popoverOffsetPx = Math.min(OFFSET_PX, availableSpaceBelow);\n\n return {\n top: `${barHeightPercent}%`,\n transform: `translateY(${popoverOffsetPx}px)`,\n position: 'absolute',\n ...(isChartLeftSide\n ? { left: `${svgStore.fpx(posX + 2)}%` }\n : { right: `${svgStore.fpx(102 - posX)}%` }),\n };\n }, [svgStore, hoveredIndex, isChartLeftSide, metrics, stackedTotals, popH]);\n\n if (hoveredIndex < 0 || hoveredIndex >= periods.length) {\n return null;\n }\n\n const period = periods[hoveredIndex]!;\n const partialWeek = !!period.partial;\n\n return (\n <div\n ref={popRef}\n className={classNames(Styles.popover, 'border border-radius-1 p-1')}\n style={popoverStyle}\n >\n <div\n className={classNames(\n Styles.arrow,\n isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight\n )}\n />\n <Text size=\"small\" variant=\"headline\" el=\"h6\">\n {stackedTotals\n ? `${formattedTotalAt(hoveredIndex)} ${totalLabel} | ${formatDateTitle(period)}`\n : formatDateTitle(period)}\n </Text>\n {partialWeek && (\n <BodyText size=\"xsmall\" subdued>\n Partial week\n </BodyText>\n )}\n {metrics.map(\n m =>\n m.values[hoveredIndex] !== undefined && (\n <ColorTag\n small\n label={formatValue(m.title, m.values[hoveredIndex], m.isRight)}\n color={m.color}\n key={m.title}\n className=\"m-t-1\"\n dashed={m.opts?.dashed}\n pattern={m.opts?.pattern}\n outlineColor={m.opts?.outlineColor}\n strokeColor={m.opts?.strokeColor}\n colorTagClassName={\n m.opts?.pattern === 'outline'\n ? Styles.colorTagOutlined\n : Styles.colorTag\n }\n />\n )\n )}\n </div>\n );\n});\n"],"names":["useCallback","useMemo","useRef","useLayoutEffect","useState","classNames","observer","useDependencies","BodyText","LineChartStore","SvgStore","getFormatter","periodDateTitleFormatter","Styles","ColorTag","Text","CHART_HEIGHT_PX","OFFSET_PX","HoverPopover","periods","resolution","hoveredIndex","metrics","display","formattedTotalAt","totalLabel","stackedTotals","svgStore","isChartLeftSide","length","formatDateTitle","formatValue","title","value","isRight","metricsRightFormat","metricsLeftFormat","popRef","popH","setPopH","current","rect","getBoundingClientRect","height","Math","abs","popoverStyle","posX","periodX","yHeights","filter","m","values","undefined","map","periodY","barHeight","reduce","a","b","max","barHeightPercentRaw","fpy","min","barHeightPercent","Number","barTopPositionPx","availableSpaceBelow","popoverOffsetPx","top","transform","position","left","fpx","right","period","partialWeek","partial","div","ref","className","popover","style","arrow","arrowLeft","arrowRight","size","variant","el","subdued","small","label","color","dashed","opts","pattern","outlineColor","strokeColor","colorTagClassName","colorTagOutlined","colorTag"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../src/components/charts/line-chart/components/hover-popover.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC } from 'react';\nimport classNames from 'classnames';\nimport { observer } from 'mobx-react';\nimport { useDependencies } from '@servicetitan/react-ioc';\nimport { BodyText } from '@servicetitan/design-system';\nimport { LineChartStore } from '../stores/line-chart.store';\nimport { SvgStore } from '../stores/svg.store';\nimport { getFormatter } from '../utils/formatters';\nimport { periodDateTitleFormatter } from '../utils/labels';\nimport * as Styles from './hover-popover.module.less';\nimport { ColorTag } from '../../common';\nimport { Text } from '@servicetitan/anvil2';\n\nconst CHART_HEIGHT_PX = 400;\nconst OFFSET_PX = 16;\n\nexport const HoverPopover: FC = observer(() => {\n const [\n {\n periods,\n resolution,\n hoveredIndex,\n metrics,\n display,\n formattedTotalAt,\n totalLabel,\n stackedTotals,\n },\n svgStore,\n ] = useDependencies(LineChartStore, SvgStore);\n const isChartLeftSide = hoveredIndex < periods.length / 2;\n\n const formatDateTitle = useMemo(() => periodDateTitleFormatter[resolution], [resolution]);\n const formatValue = useCallback(\n (title: string, value: number, isRight: boolean) =>\n getFormatter(isRight ? display.metricsRightFormat : display.metricsLeftFormat)(value) +\n ' ' +\n title,\n [display]\n );\n\n const popRef = useRef<HTMLDivElement | null>(null);\n const [popH, setPopH] = useState(0);\n\n useLayoutEffect(() => {\n if (!popRef.current) {\n return;\n }\n const rect = popRef.current.getBoundingClientRect();\n if (rect.height && Math.abs(rect.height - popH) > 0.5) {\n setPopH(rect.height);\n }\n }, [hoveredIndex, metrics, display, popH]);\n\n const { popoverStyle, arrowPosition } = useMemo(() => {\n const posX = svgStore.periodX(hoveredIndex);\n\n const yHeights = metrics\n .filter(m => m.values[hoveredIndex] !== undefined)\n .map(m => svgStore.periodY(m, hoveredIndex));\n\n const barHeight = yHeights.length\n ? stackedTotals\n ? yHeights.reduce((a, b) => a + b, 0)\n : Math.max(...yHeights)\n : 0;\n const barHeightPercentRaw = svgStore.fpy(Math.max(0, Math.min(100, barHeight)));\n const barHeightPercent = Math.max(0, Math.min(100, Number(barHeightPercentRaw) || 0));\n\n const barTopPx = (barHeightPercent / 100) * CHART_HEIGHT_PX;\n const idealTopPx = barTopPx + OFFSET_PX;\n const maxTopPx = Math.max(0, CHART_HEIGHT_PX - popH);\n const popoverRepositioned = idealTopPx > maxTopPx;\n const clampedTopPx = Math.min(idealTopPx, maxTopPx);\n const topPercent = (clampedTopPx / CHART_HEIGHT_PX) * 100;\n\n return {\n popoverStyle: {\n top: `${topPercent.toFixed(1)}%`,\n transform: 'translateY(0)',\n position: 'absolute' as const,\n ...(isChartLeftSide\n ? { left: `${svgStore.fpx(posX + 2)}%` }\n : { right: `${svgStore.fpx(102 - posX)}%` }),\n },\n arrowPosition: popoverRepositioned ? 'bottom' : 'top',\n };\n }, [svgStore, hoveredIndex, isChartLeftSide, metrics, stackedTotals, popH]);\n\n if (hoveredIndex < 0 || hoveredIndex >= periods.length) {\n return null;\n }\n\n if (stackedTotals?.[hoveredIndex] === 0) {\n return null;\n }\n\n const period = periods[hoveredIndex]!;\n const partialWeek = !!period.partial;\n\n return (\n <div\n ref={popRef}\n className={classNames(Styles.popover, 'border border-radius-1 p-1')}\n style={popoverStyle}\n >\n <div\n className={classNames(\n Styles.arrow,\n isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight,\n arrowPosition === 'bottom' && Styles.arrowBottom\n )}\n />\n <Text size=\"small\" variant=\"headline\" el=\"h6\">\n {stackedTotals\n ? `${formattedTotalAt(hoveredIndex)} ${totalLabel} | ${formatDateTitle(period)}`\n : formatDateTitle(period)}\n </Text>\n {partialWeek && (\n <BodyText size=\"xsmall\" subdued>\n Partial week\n </BodyText>\n )}\n {metrics.map(\n m =>\n m.values[hoveredIndex] !== undefined && (\n <ColorTag\n small\n label={formatValue(m.title, m.values[hoveredIndex], m.isRight)}\n color={m.color}\n key={m.title}\n className=\"m-t-1\"\n dashed={m.opts?.dashed}\n pattern={m.opts?.pattern}\n outlineColor={m.opts?.outlineColor}\n strokeColor={m.opts?.strokeColor}\n colorTagClassName={\n m.opts?.pattern === 'outline'\n ? Styles.colorTagOutlined\n : Styles.colorTag\n }\n />\n )\n )}\n </div>\n );\n});\n"],"names":["useCallback","useMemo","useRef","useLayoutEffect","useState","classNames","observer","useDependencies","BodyText","LineChartStore","SvgStore","getFormatter","periodDateTitleFormatter","Styles","ColorTag","Text","CHART_HEIGHT_PX","OFFSET_PX","HoverPopover","periods","resolution","hoveredIndex","metrics","display","formattedTotalAt","totalLabel","stackedTotals","svgStore","isChartLeftSide","length","formatDateTitle","formatValue","title","value","isRight","metricsRightFormat","metricsLeftFormat","popRef","popH","setPopH","current","rect","getBoundingClientRect","height","Math","abs","popoverStyle","arrowPosition","posX","periodX","yHeights","filter","m","values","undefined","map","periodY","barHeight","reduce","a","b","max","barHeightPercentRaw","fpy","min","barHeightPercent","Number","barTopPx","idealTopPx","maxTopPx","popoverRepositioned","clampedTopPx","topPercent","top","toFixed","transform","position","left","fpx","right","period","partialWeek","partial","div","ref","className","popover","style","arrow","arrowLeft","arrowRight","arrowBottom","size","variant","el","subdued","small","label","color","dashed","opts","pattern","outlineColor","strokeColor","colorTagClassName","colorTagOutlined","colorTag"],"mappings":"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"}
@@ -44,6 +44,11 @@
44
44
  transform: rotate(135deg);
45
45
  }
46
46
 
47
+ .arrow-bottom {
48
+ top: auto;
49
+ bottom: 12px;
50
+ }
51
+
47
52
  .color-tag {
48
53
  width: 20px;
49
54
  height: 6px;
@@ -1,5 +1,6 @@
1
1
  export const __esModule: true;
2
2
  export const arrow: string;
3
+ export const arrowBottom: string;
3
4
  export const arrowLeft: string;
4
5
  export const arrowRight: string;
5
6
  export const colorTag: string;
@@ -1 +1 @@
1
- {"version":3,"file":"svg-bars.d.ts","sourceRoot":"","sources":["../../../../../src/components/charts/line-chart/components/svg-bars.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAG3B,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI3D,UAAU,YAAY;IAClB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC,YAAY,CAuMpC,CAAC;AAEF,UAAU,iBAAiB;IACvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAyB7C,CAAC"}
1
+ {"version":3,"file":"svg-bars.d.ts","sourceRoot":"","sources":["../../../../../src/components/charts/line-chart/components/svg-bars.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAG3B,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAI3D,UAAU,YAAY;IAClB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC,YAAY,CAsLpC,CAAC;AAEF,UAAU,iBAAiB;IACvB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAyB7C,CAAC"}
@@ -55,23 +55,13 @@ export const SvgBars = observer(({ metrics, isStackedBarChart, isGroupedBarChart
55
55
  };
56
56
  });
57
57
  if (isStackedBarChart) {
58
- // Use ORIGINAL calculations - keep all spacing/positioning unchanged
59
58
  const spacingBetweenSegments = 1;
60
- const totalSpacing = (values.length - 1) * spacingBetweenSegments;
61
- let stackedBarHeight = values.reduce((sum, curr)=>sum + curr.val, 0) + totalSpacing;
62
- // Find first/last non-zero indices for visual styling
59
+ const totalValue = values.reduce((sum, curr)=>sum + curr.value, 0);
60
+ const totalYValue = values.reduce((sum, curr)=>sum + curr.val, 0);
63
61
  const firstNonZeroIdx = values.findIndex((v)=>v.value > 0);
64
62
  const lastNonZeroIdx = values.reduce((last, v, idx)=>v.value > 0 ? idx : last, -1);
65
- // Count 0-value segments below first non-zero (for text position adjustment)
66
- const zeroSegmentsBelowFirst = firstNonZeroIdx >= 0 ? values.slice(firstNonZeroIdx + 1).filter((v)=>v.value <= 0).length : 0;
67
- const totalValue = values.reduce((sum, curr)=>sum + curr.value, 0);
68
63
  if (totalValue > 0) {
69
- /*
70
- * Adjust text position to maintain consistent gap with first rendered segment:
71
- * 1. Subtract spacing for skipped segments from fpy argument
72
- * 2. Add pixel offset for zeros below first non-zero
73
- */ const textStackedBarHeight = stackedBarHeight - (firstNonZeroIdx > 0 ? firstNonZeroIdx : 0) * spacingBetweenSegments;
74
- const yTop = +fpy(textStackedBarHeight) + zeroSegmentsBelowFirst * spacingBetweenSegments;
64
+ const yTop = +fpy(totalYValue) - 2;
75
65
  const scaleX = 0.3;
76
66
  const scaleY = 1;
77
67
  paths.push(/*#__PURE__*/ _jsx("g", {
@@ -93,21 +83,25 @@ export const SvgBars = observer(({ metrics, isStackedBarChart, isGroupedBarChart
93
83
  })
94
84
  }, `total-${i}`));
95
85
  }
86
+ let stackedBarHeight = totalYValue;
87
+ let isFirstRendered = true;
88
+ const nonZeroCount = values.filter((v)=>v.value > 0).length;
89
+ const bottomTrim = Math.max(0, nonZeroCount - 1) * spacingBetweenSegments;
96
90
  for(let j = 0; j < values.length; j++){
97
91
  var _value_strokeColor;
98
92
  const value = values[j];
99
- stackedBarHeight -= spacingBetweenSegments;
100
- const TOP_RADIUS = 1;
101
- const xLeft = +fpx(x - barWidth / 2);
102
- const width = +fpx(barWidth);
103
93
  if (value.value <= 0) {
104
- stackedBarHeight -= value.val;
105
94
  continue;
106
95
  }
107
- const zeroSegments = values.slice(j + 1).filter((v)=>v.value <= 0).length;
108
- // Adjust yTop: move down by the space 0-value segments would occupy
109
- const yTop = +fpy(stackedBarHeight) + (values.length - 2) + zeroSegments * spacingBetweenSegments;
110
- const height = j === lastNonZeroIdx ? +fpx(value.val - 2) : +fpx(value.val);
96
+ if (!isFirstRendered) {
97
+ stackedBarHeight -= spacingBetweenSegments;
98
+ }
99
+ isFirstRendered = false;
100
+ const TOP_RADIUS = 1;
101
+ const xLeft = +fpx(x - barWidth / 2);
102
+ const width = +fpx(barWidth);
103
+ const yTop = +fpy(stackedBarHeight);
104
+ const height = j === lastNonZeroIdx ? +fpx(value.val - bottomTrim) : +fpx(value.val);
111
105
  const r = j === firstNonZeroIdx ? TOP_RADIUS : 0;
112
106
  const d = [
113
107
  `M ${xLeft} ${yTop + height}`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/components/charts/line-chart/components/svg-bars.tsx"],"sourcesContent":["import { FC } from 'react';\nimport { observer } from 'mobx-react';\nimport { useDependencies } from '@servicetitan/react-ioc';\nimport { ChartMetric } from '../utils/internal-interfaces';\nimport { keyVal } from '../utils/key';\nimport { SvgStore } from '../stores/svg.store';\n\ninterface SvgBarsProps {\n metrics: ChartMetric[];\n isStackedBarChart?: boolean;\n isGroupedBarChart?: boolean;\n}\n\nexport const SvgBars: FC<SvgBarsProps> = observer(\n ({ metrics, isStackedBarChart, isGroupedBarChart }) => {\n const [store] = useDependencies(SvgStore);\n const { fpx, fpy, barWidth, length } = store;\n const barWidthHalf = barWidth / 2;\n const paths = [];\n\n const patternDefs = metrics\n .filter(m => m.opts?.pattern === 'striped')\n .map(m => {\n const rotation = 20;\n const tileW = 0.6;\n const tileH = 0.6;\n const stripeWidth = Math.max(0.1, Math.floor(tileW / 20));\n const tintOpacity = 0.06;\n\n return (\n <pattern\n key={`pattern-${m.id}`}\n id={`stripe-pattern-${m.id}`}\n patternUnits=\"userSpaceOnUse\"\n width={tileW}\n height={tileH}\n patternTransform={`rotate(${rotation})`}\n >\n <rect width={tileW} height={tileH} fill={m.color} opacity={tintOpacity} />\n <rect width={stripeWidth} height={tileH} fill={m.color} />\n </pattern>\n );\n });\n\n for (let i = 0; i < length; i++) {\n const x = store.periodX(i);\n const values = metrics.map(m => ({\n id: m.id,\n color: m.valuesOpts?.[i]?.color ?? m.color,\n opacity: m.opacity,\n val: store.periodY(m, i),\n pattern: m.opts?.pattern,\n strokeColor: m.opts?.strokeColor,\n outlineColor: m.opts?.outlineColor,\n value: m.values[i],\n }));\n\n if (isStackedBarChart) {\n // Use ORIGINAL calculations - keep all spacing/positioning unchanged\n const spacingBetweenSegments = 1;\n const totalSpacing = (values.length - 1) * spacingBetweenSegments;\n let stackedBarHeight =\n values.reduce((sum, curr) => sum + curr.val, 0) + totalSpacing;\n\n // Find first/last non-zero indices for visual styling\n const firstNonZeroIdx = values.findIndex(v => v.value > 0);\n const lastNonZeroIdx = values.reduce(\n (last, v, idx) => (v.value > 0 ? idx : last),\n -1\n );\n\n // Count 0-value segments below first non-zero (for text position adjustment)\n const zeroSegmentsBelowFirst =\n firstNonZeroIdx >= 0\n ? values.slice(firstNonZeroIdx + 1).filter(v => v.value <= 0).length\n : 0;\n\n const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);\n if (totalValue > 0) {\n /*\n * Adjust text position to maintain consistent gap with first rendered segment:\n * 1. Subtract spacing for skipped segments from fpy argument\n * 2. Add pixel offset for zeros below first non-zero\n */\n const textStackedBarHeight =\n stackedBarHeight -\n (firstNonZeroIdx > 0 ? firstNonZeroIdx : 0) * spacingBetweenSegments;\n const yTop =\n +fpy(textStackedBarHeight) +\n zeroSegmentsBelowFirst * spacingBetweenSegments;\n const scaleX = 0.3;\n const scaleY = 1;\n\n paths.push(\n <g\n key={`total-${i}`}\n transform={`translate(${x},${yTop}) scale(${scaleX},${scaleY})`}\n pointerEvents=\"none\"\n >\n <text\n x={0}\n y={0}\n textAnchor=\"middle\"\n dominantBaseline=\"alphabetic\"\n fontSize=\"2.5\"\n fontWeight={600}\n fill=\"#111827\"\n stroke=\"white\"\n strokeWidth={0.8}\n paintOrder=\"stroke\"\n fontFamily=\"Nunito Sans\"\n >\n {Math.round(totalValue)}\n </text>\n </g>\n );\n }\n\n for (let j = 0; j < values.length; j++) {\n const value = values[j];\n stackedBarHeight -= spacingBetweenSegments;\n\n const TOP_RADIUS = 1;\n const xLeft = +fpx(x - barWidth / 2);\n const width = +fpx(barWidth);\n\n if (value.value <= 0) {\n stackedBarHeight -= value.val;\n continue;\n }\n const zeroSegments = values.slice(j + 1).filter(v => v.value <= 0).length;\n\n // Adjust yTop: move down by the space 0-value segments would occupy\n const yTop =\n +fpy(stackedBarHeight) +\n (values.length - 2) +\n zeroSegments * spacingBetweenSegments;\n const height = j === lastNonZeroIdx ? +fpx(value.val - 2) : +fpx(value.val);\n const r = j === firstNonZeroIdx ? TOP_RADIUS : 0;\n\n const d = [\n `M ${xLeft} ${yTop + height}`, // bottom-left\n `L ${xLeft} ${yTop + r}`, // up left edge\n `Q ${xLeft} ${yTop} ${xLeft + r / 2} ${yTop}`, // top-left corner\n `L ${xLeft + width - r / 2} ${yTop}`, // across top\n `Q ${xLeft + width} ${yTop} ${xLeft + width} ${yTop + r}`, // top-right corner\n `L ${xLeft + width} ${yTop + height}`, // down right edge\n 'Z',\n ].join(' ');\n paths.push(\n <path\n key={keyVal(value.id, i)}\n d={d}\n fill={\n value.pattern === 'striped'\n ? `url(#stripe-pattern-${value.id})`\n : value.color\n }\n stroke={\n value.pattern === 'outline'\n ? value.outlineColor\n : (value.strokeColor ?? value.color)\n }\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n strokeLinejoin=\"round\"\n />\n );\n\n stackedBarHeight -= value.val;\n }\n } else if (isGroupedBarChart) {\n for (let j = 0; j < values.length; j++) {\n const groupedBarX = (j * barWidth) / values.length;\n const value = values[j];\n\n paths.push(\n <rect\n key={keyVal(value.id, i)}\n x={x + groupedBarX - barWidthHalf}\n y={fpy(value.val)}\n width={barWidth / values.length - 0.1}\n height={fpx(value.val)}\n fill={value.color}\n opacity={value.opacity}\n />\n );\n }\n } else {\n values.sort((a, b) => b.val - a.val);\n for (const value of values) {\n paths.push(\n <rect\n key={keyVal(value.id, i)}\n x={fpx(x - barWidthHalf)}\n y={fpy(value.val)}\n width={fpx(barWidth)}\n height={fpx(value.val)}\n fill={value.color}\n />\n );\n }\n }\n }\n\n return (\n <g>\n {patternDefs.length > 0 && <defs>{patternDefs}</defs>}\n {paths}\n </g>\n );\n }\n);\n\ninterface SvgBarsHoverProps {\n onHover(ind: number): void;\n onLeave(ind: number): void;\n}\n\nexport const SvgBarsHover: FC<SvgBarsHoverProps> = observer(({ onHover, onLeave }) => {\n const [store] = useDependencies(SvgStore);\n const { fpx, fpy, barWidth, length } = store;\n const barWidthHalf = barWidth / 2;\n const paths = [];\n\n for (let i = 0; i < length; i++) {\n const x = store.periodX(i);\n\n paths.push(\n <rect\n key={keyVal('_', i)}\n onMouseEnter={() => onHover(i)}\n onMouseLeave={() => onLeave(i)}\n x={fpx(x - barWidthHalf)}\n y={fpy(100)}\n width={fpx(barWidth)}\n height=\"100%\"\n fill=\"white\"\n fillOpacity=\"0\"\n />\n );\n }\n\n return <g>{paths}</g>;\n});\n"],"names":["observer","useDependencies","keyVal","SvgStore","SvgBars","metrics","isStackedBarChart","isGroupedBarChart","store","fpx","fpy","barWidth","length","barWidthHalf","paths","patternDefs","filter","m","opts","pattern","map","rotation","tileW","tileH","stripeWidth","Math","max","floor","tintOpacity","id","patternUnits","width","height","patternTransform","rect","fill","color","opacity","i","x","periodX","values","valuesOpts","val","periodY","strokeColor","outlineColor","value","spacingBetweenSegments","totalSpacing","stackedBarHeight","reduce","sum","curr","firstNonZeroIdx","findIndex","v","lastNonZeroIdx","last","idx","zeroSegmentsBelowFirst","slice","totalValue","textStackedBarHeight","yTop","scaleX","scaleY","push","g","transform","pointerEvents","text","y","textAnchor","dominantBaseline","fontSize","fontWeight","stroke","strokeWidth","paintOrder","fontFamily","round","j","TOP_RADIUS","xLeft","zeroSegments","r","d","join","path","vectorEffect","strokeLinejoin","groupedBarX","sort","a","b","defs","SvgBarsHover","onHover","onLeave","onMouseEnter","onMouseLeave","fillOpacity"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../src/components/charts/line-chart/components/svg-bars.tsx"],"sourcesContent":["import { FC } from 'react';\nimport { observer } from 'mobx-react';\nimport { useDependencies } from '@servicetitan/react-ioc';\nimport { ChartMetric } from '../utils/internal-interfaces';\nimport { keyVal } from '../utils/key';\nimport { SvgStore } from '../stores/svg.store';\n\ninterface SvgBarsProps {\n metrics: ChartMetric[];\n isStackedBarChart?: boolean;\n isGroupedBarChart?: boolean;\n}\n\nexport const SvgBars: FC<SvgBarsProps> = observer(\n ({ metrics, isStackedBarChart, isGroupedBarChart }) => {\n const [store] = useDependencies(SvgStore);\n const { fpx, fpy, barWidth, length } = store;\n const barWidthHalf = barWidth / 2;\n const paths = [];\n\n const patternDefs = metrics\n .filter(m => m.opts?.pattern === 'striped')\n .map(m => {\n const rotation = 20;\n const tileW = 0.6;\n const tileH = 0.6;\n const stripeWidth = Math.max(0.1, Math.floor(tileW / 20));\n const tintOpacity = 0.06;\n\n return (\n <pattern\n key={`pattern-${m.id}`}\n id={`stripe-pattern-${m.id}`}\n patternUnits=\"userSpaceOnUse\"\n width={tileW}\n height={tileH}\n patternTransform={`rotate(${rotation})`}\n >\n <rect width={tileW} height={tileH} fill={m.color} opacity={tintOpacity} />\n <rect width={stripeWidth} height={tileH} fill={m.color} />\n </pattern>\n );\n });\n\n for (let i = 0; i < length; i++) {\n const x = store.periodX(i);\n const values = metrics.map(m => ({\n id: m.id,\n color: m.valuesOpts?.[i]?.color ?? m.color,\n opacity: m.opacity,\n val: store.periodY(m, i),\n pattern: m.opts?.pattern,\n strokeColor: m.opts?.strokeColor,\n outlineColor: m.opts?.outlineColor,\n value: m.values[i],\n }));\n\n if (isStackedBarChart) {\n const spacingBetweenSegments = 1;\n const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);\n const totalYValue = values.reduce((sum, curr) => sum + curr.val, 0);\n\n const firstNonZeroIdx = values.findIndex(v => v.value > 0);\n const lastNonZeroIdx = values.reduce(\n (last, v, idx) => (v.value > 0 ? idx : last),\n -1\n );\n\n if (totalValue > 0) {\n const yTop = +fpy(totalYValue) - 2;\n const scaleX = 0.3;\n const scaleY = 1;\n\n paths.push(\n <g\n key={`total-${i}`}\n transform={`translate(${x},${yTop}) scale(${scaleX},${scaleY})`}\n pointerEvents=\"none\"\n >\n <text\n x={0}\n y={0}\n textAnchor=\"middle\"\n dominantBaseline=\"alphabetic\"\n fontSize=\"2.5\"\n fontWeight={600}\n fill=\"#111827\"\n stroke=\"white\"\n strokeWidth={0.8}\n paintOrder=\"stroke\"\n fontFamily=\"Nunito Sans\"\n >\n {Math.round(totalValue)}\n </text>\n </g>\n );\n }\n\n let stackedBarHeight = totalYValue;\n let isFirstRendered = true;\n const nonZeroCount = values.filter(v => v.value > 0).length;\n const bottomTrim = Math.max(0, nonZeroCount - 1) * spacingBetweenSegments;\n\n for (let j = 0; j < values.length; j++) {\n const value = values[j];\n\n if (value.value <= 0) {\n continue;\n }\n\n if (!isFirstRendered) {\n stackedBarHeight -= spacingBetweenSegments;\n }\n isFirstRendered = false;\n\n const TOP_RADIUS = 1;\n const xLeft = +fpx(x - barWidth / 2);\n const width = +fpx(barWidth);\n const yTop = +fpy(stackedBarHeight);\n const height =\n j === lastNonZeroIdx ? +fpx(value.val - bottomTrim) : +fpx(value.val);\n const r = j === firstNonZeroIdx ? TOP_RADIUS : 0;\n\n const d = [\n `M ${xLeft} ${yTop + height}`, // bottom-left\n `L ${xLeft} ${yTop + r}`, // up left edge\n `Q ${xLeft} ${yTop} ${xLeft + r / 2} ${yTop}`, // top-left corner\n `L ${xLeft + width - r / 2} ${yTop}`, // across top\n `Q ${xLeft + width} ${yTop} ${xLeft + width} ${yTop + r}`, // top-right corner\n `L ${xLeft + width} ${yTop + height}`, // down right edge\n 'Z',\n ].join(' ');\n paths.push(\n <path\n key={keyVal(value.id, i)}\n d={d}\n fill={\n value.pattern === 'striped'\n ? `url(#stripe-pattern-${value.id})`\n : value.color\n }\n stroke={\n value.pattern === 'outline'\n ? value.outlineColor\n : (value.strokeColor ?? value.color)\n }\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n strokeLinejoin=\"round\"\n />\n );\n\n stackedBarHeight -= value.val;\n }\n } else if (isGroupedBarChart) {\n for (let j = 0; j < values.length; j++) {\n const groupedBarX = (j * barWidth) / values.length;\n const value = values[j];\n\n paths.push(\n <rect\n key={keyVal(value.id, i)}\n x={x + groupedBarX - barWidthHalf}\n y={fpy(value.val)}\n width={barWidth / values.length - 0.1}\n height={fpx(value.val)}\n fill={value.color}\n opacity={value.opacity}\n />\n );\n }\n } else {\n values.sort((a, b) => b.val - a.val);\n for (const value of values) {\n paths.push(\n <rect\n key={keyVal(value.id, i)}\n x={fpx(x - barWidthHalf)}\n y={fpy(value.val)}\n width={fpx(barWidth)}\n height={fpx(value.val)}\n fill={value.color}\n />\n );\n }\n }\n }\n\n return (\n <g>\n {patternDefs.length > 0 && <defs>{patternDefs}</defs>}\n {paths}\n </g>\n );\n }\n);\n\ninterface SvgBarsHoverProps {\n onHover(ind: number): void;\n onLeave(ind: number): void;\n}\n\nexport const SvgBarsHover: FC<SvgBarsHoverProps> = observer(({ onHover, onLeave }) => {\n const [store] = useDependencies(SvgStore);\n const { fpx, fpy, barWidth, length } = store;\n const barWidthHalf = barWidth / 2;\n const paths = [];\n\n for (let i = 0; i < length; i++) {\n const x = store.periodX(i);\n\n paths.push(\n <rect\n key={keyVal('_', i)}\n onMouseEnter={() => onHover(i)}\n onMouseLeave={() => onLeave(i)}\n x={fpx(x - barWidthHalf)}\n y={fpy(100)}\n width={fpx(barWidth)}\n height=\"100%\"\n fill=\"white\"\n fillOpacity=\"0\"\n />\n );\n }\n\n return <g>{paths}</g>;\n});\n"],"names":["observer","useDependencies","keyVal","SvgStore","SvgBars","metrics","isStackedBarChart","isGroupedBarChart","store","fpx","fpy","barWidth","length","barWidthHalf","paths","patternDefs","filter","m","opts","pattern","map","rotation","tileW","tileH","stripeWidth","Math","max","floor","tintOpacity","id","patternUnits","width","height","patternTransform","rect","fill","color","opacity","i","x","periodX","values","valuesOpts","val","periodY","strokeColor","outlineColor","value","spacingBetweenSegments","totalValue","reduce","sum","curr","totalYValue","firstNonZeroIdx","findIndex","v","lastNonZeroIdx","last","idx","yTop","scaleX","scaleY","push","g","transform","pointerEvents","text","y","textAnchor","dominantBaseline","fontSize","fontWeight","stroke","strokeWidth","paintOrder","fontFamily","round","stackedBarHeight","isFirstRendered","nonZeroCount","bottomTrim","j","TOP_RADIUS","xLeft","r","d","join","path","vectorEffect","strokeLinejoin","groupedBarX","sort","a","b","defs","SvgBarsHover","onHover","onLeave","onMouseEnter","onMouseLeave","fillOpacity"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../../src/components/stat/stat-card.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,SAAS,EAAY,MAAM,OAAO,CAAC;AAEhD,OAAO,EAEH,mBAAmB,EAKtB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAe,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAmCtE,UAAU,aAAa;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CAyEtC,CAAC;AAEF,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CAyEtC,CAAC"}
1
+ {"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../../src/components/stat/stat-card.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,SAAS,EAAY,MAAM,OAAO,CAAC;AAEhD,OAAO,EAEH,mBAAmB,EAMtB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EAAe,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAqCtE,UAAU,aAAa;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CAyEtC,CAAC;AAEF,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CA6EtC,CAAC"}
@@ -1,12 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import classNames from 'classnames';
4
- import { BodyText, Eyebrow, Popover, Stack, Tooltip } from '@servicetitan/design-system';
4
+ import { BodyText, Eyebrow, Headline, Popover, Stack, Tooltip } from '@servicetitan/design-system';
5
5
  import * as Styles from './stat-card.module.less';
6
6
  import { formatValue } from '../../utils/formatters';
7
7
  import { Icon } from '@servicetitan/anvil2';
8
8
  import TrendingUpSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_up.svg';
9
9
  import TrendingDownSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_down.svg';
10
+ import InfoSVG from '@servicetitan/anvil2/assets/icons/material/round/info.svg';
11
+ import { tokens } from '@servicetitan/tokens/core/index';
10
12
  const calculateDiff = (value, prev, percents)=>{
11
13
  const diff = (value - prev) * (percents ? 100 : 1);
12
14
  const absDiff = Math.abs(diff);
@@ -82,46 +84,64 @@ export const StatCard = ({ title, description, popoverContent, value, percent, m
82
84
  const [popoverShown, setPopoverShown] = useState(false);
83
85
  const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';
84
86
  const val = value === undefined ? '\u00A0' : formatValue(value, format);
87
+ const hasInfo = !!description || !!popoverContent;
85
88
  const eyebrow = /*#__PURE__*/ _jsx(Eyebrow, {
86
- className: classNames(Styles.title, 'ta-center'),
87
89
  "data-cy": `marketing-stat-${title}-title`,
88
- onMouseEnter: ()=>{
89
- setPopoverShown(true);
90
- },
91
90
  children: title
92
91
  });
93
- return /*#__PURE__*/ _jsxs(Stack, {
92
+ const infoIcon = /*#__PURE__*/ _jsx(Icon, {
93
+ svg: InfoSVG,
94
+ color: tokens.colorNeutral100
95
+ });
96
+ const infoContent = popoverContent ? /*#__PURE__*/ _jsx(Popover, {
97
+ open: popoverShown,
98
+ trigger: infoIcon,
99
+ onMouseEnter: ()=>setPopoverShown(true),
100
+ children: popoverContent
101
+ }) : description ? /*#__PURE__*/ _jsx(Tooltip, {
102
+ text: description,
103
+ "data-cy": `marketing-stat-${title}-tooltip`,
104
+ children: infoIcon
105
+ }) : null;
106
+ return /*#__PURE__*/ _jsx(Stack, {
94
107
  direction: "column",
95
- alignItems: "center",
96
- className: classNames('p-y-3', {
97
- 'bg-white border-radius-2 border': !clean,
108
+ className: classNames('p-2', {
109
+ 'bg-white border': !clean,
110
+ [Styles.card]: !clean,
98
111
  'flex-grow-1 flex-basis-0': fill
99
112
  }, className),
100
113
  onMouseLeave: ()=>setPopoverShown(false),
101
- children: [
102
- popoverContent ? /*#__PURE__*/ _jsx(Popover, {
103
- open: popoverShown,
104
- trigger: eyebrow,
105
- children: popoverContent
106
- }) : description ? /*#__PURE__*/ _jsx(Tooltip, {
107
- text: description,
108
- "data-cy": `marketing-stat-${title}-tooltip`,
109
- children: eyebrow
110
- }) : eyebrow,
111
- /*#__PURE__*/ _jsx(BodyText, {
112
- className: "fs-6-i ff-display",
113
- "data-cy": `marketing-stat-${title}-value`,
114
- children: val
115
- }),
116
- !valueOnly && /*#__PURE__*/ _jsx(StatDiff, {
117
- value: value,
118
- prev: prev,
119
- format: format,
120
- inverted: inverted,
121
- neutral: neutral,
122
- diffPercentOnly: diffPercentOnly
123
- })
124
- ]
114
+ children: /*#__PURE__*/ _jsxs("div", {
115
+ className: "p-3",
116
+ children: [
117
+ /*#__PURE__*/ _jsxs("div", {
118
+ className: Styles.titleRow,
119
+ children: [
120
+ eyebrow,
121
+ hasInfo && infoContent
122
+ ]
123
+ }),
124
+ /*#__PURE__*/ _jsxs("div", {
125
+ className: Styles.valueRow,
126
+ children: [
127
+ /*#__PURE__*/ _jsx(Headline, {
128
+ size: "xlarge",
129
+ className: "m-b-0-i",
130
+ "data-cy": `marketing-stat-${title}-value`,
131
+ children: val
132
+ }),
133
+ !valueOnly && /*#__PURE__*/ _jsx(StatDiff, {
134
+ value: value,
135
+ prev: prev,
136
+ format: format,
137
+ inverted: inverted,
138
+ neutral: neutral,
139
+ diffPercentOnly: diffPercentOnly
140
+ })
141
+ ]
142
+ })
143
+ ]
144
+ })
125
145
  });
126
146
  };
127
147
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/stat/stat-card.tsx"],"sourcesContent":["import { FC, ReactNode, useState } from 'react';\nimport classNames from 'classnames';\nimport {\n BodyText,\n BodyTextPropsStrict,\n Eyebrow,\n Popover,\n Stack,\n Tooltip,\n} from '@servicetitan/design-system';\nimport * as Styles from './stat-card.module.less';\nimport { formatValue, NumberFormatter } from '../../utils/formatters';\nimport { Icon } from '@servicetitan/anvil2';\nimport TrendingUpSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_up.svg';\nimport TrendingDownSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_down.svg';\n\nconst calculateDiff = (\n value: number,\n prev: number,\n percents?: boolean\n): [number, number, boolean] => {\n const diff = (value - prev) * (percents ? 100 : 1);\n const absDiff = Math.abs(diff);\n let diffPercent = 0;\n\n if (percents) {\n diffPercent = diff;\n } else if (absDiff) {\n diffPercent = prev ? (100 * absDiff) / prev : 100;\n }\n\n return [absDiff, diffPercent, diff >= 0];\n};\n\nconst formatDifference = (value: number, isPlus: boolean, format: NumberFormatter): string => {\n return (isPlus ? '+' : '-') + formatValue(value, format);\n};\n\nconst formatDifferencePercentage = (value: number, isPlus: boolean): string => {\n if (!value) {\n return '';\n }\n\n return (isPlus ? '+' : '-') + formatValue(value, 'percent-100');\n};\n\ninterface StatDiffProps {\n value?: number;\n prev?: number;\n size?: BodyTextPropsStrict['size'];\n format: NumberFormatter;\n inverted?: boolean;\n neutral?: boolean;\n className?: string;\n diffPercentOnly?: boolean;\n}\n\nexport const StatDiff: FC<StatDiffProps> = ({\n value,\n prev,\n size,\n format,\n inverted,\n neutral,\n className,\n diffPercentOnly = false,\n}) => {\n const percents = format === 'percent';\n const [absDiff, diffPercent, isIncrease] = calculateDiff(value ?? 0, prev ?? 0, percents);\n const diff =\n absDiff === 0 ? (\n ''\n ) : (\n <Icon\n svg={isIncrease ? TrendingUpSVG : TrendingDownSVG}\n color={\n neutral\n ? 'neutral-200'\n : inverted\n ? isIncrease\n ? 'red'\n : 'green'\n : isIncrease\n ? 'green'\n : 'red'\n }\n />\n );\n let text = '';\n\n if (percents) {\n text += formatDifferencePercentage(absDiff, isIncrease);\n } else {\n const diffPercentage = formatDifferencePercentage(diffPercent, isIncrease);\n\n if (diffPercentOnly) {\n text += `${diffPercentage}`;\n } else {\n text += `${formatDifference(absDiff, isIncrease, format)}`;\n\n if (diffPercent !== 0) {\n text += ` (${diffPercentage})`;\n }\n }\n }\n\n return (\n <Stack\n className={classNames(Styles.statDiff, className)}\n justifyContent=\"center\"\n alignItems=\"center\"\n >\n <Stack.Item className=\"m-r-half m-t-half\">\n <span>{diff}</span>\n </Stack.Item>\n <Stack.Item>\n <BodyText\n size={size ?? 'small'}\n data-cy=\"stat-diff-value\"\n className={classNames({\n 'c-red-500': !neutral && (inverted ? isIncrease : !isIncrease),\n 'c-green-500': !neutral && (inverted ? !isIncrease : isIncrease),\n 'c-neutral-200': !!neutral,\n })}\n >\n {value === undefined ? '\\u00A0' : text}\n </BodyText>\n </Stack.Item>\n </Stack>\n );\n};\n\nexport interface StatCardProps {\n title: string;\n description?: string;\n popoverContent?: ReactNode;\n value?: number;\n prev?: number;\n percent?: boolean;\n money?: boolean;\n rate?: boolean;\n clean?: boolean;\n inverted?: boolean;\n neutral?: boolean;\n fill?: boolean;\n valueOnly?: boolean;\n className?: string;\n diffPercentOnly?: boolean;\n}\n\nexport const StatCard: FC<StatCardProps> = ({\n title,\n description,\n popoverContent,\n value,\n percent,\n money,\n rate,\n prev,\n clean,\n inverted,\n neutral,\n fill,\n valueOnly,\n className,\n diffPercentOnly = false,\n}) => {\n const [popoverShown, setPopoverShown] = useState(false);\n const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';\n const val = value === undefined ? '\\u00A0' : formatValue(value, format);\n\n const eyebrow = (\n <Eyebrow\n className={classNames(Styles.title, 'ta-center')}\n data-cy={`marketing-stat-${title}-title`}\n onMouseEnter={() => {\n setPopoverShown(true);\n }}\n >\n {title}\n </Eyebrow>\n );\n\n return (\n <Stack\n direction=\"column\"\n alignItems=\"center\"\n className={classNames(\n 'p-y-3',\n {\n 'bg-white border-radius-2 border': !clean,\n 'flex-grow-1 flex-basis-0': fill,\n },\n className\n )}\n onMouseLeave={() => setPopoverShown(false)}\n >\n {popoverContent ? (\n <Popover open={popoverShown} trigger={eyebrow}>\n {popoverContent}\n </Popover>\n ) : description ? (\n <Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>\n {eyebrow}\n </Tooltip>\n ) : (\n eyebrow\n )}\n <BodyText className=\"fs-6-i ff-display\" data-cy={`marketing-stat-${title}-value`}>\n {val}\n </BodyText>\n {!valueOnly && (\n <StatDiff\n value={value}\n prev={prev}\n format={format}\n inverted={inverted}\n neutral={neutral}\n diffPercentOnly={diffPercentOnly}\n />\n )}\n </Stack>\n );\n};\n"],"names":["useState","classNames","BodyText","Eyebrow","Popover","Stack","Tooltip","Styles","formatValue","Icon","TrendingUpSVG","TrendingDownSVG","calculateDiff","value","prev","percents","diff","absDiff","Math","abs","diffPercent","formatDifference","isPlus","format","formatDifferencePercentage","StatDiff","size","inverted","neutral","className","diffPercentOnly","isIncrease","svg","color","text","diffPercentage","statDiff","justifyContent","alignItems","Item","span","data-cy","undefined","StatCard","title","description","popoverContent","percent","money","rate","clean","fill","valueOnly","popoverShown","setPopoverShown","val","eyebrow","onMouseEnter","direction","onMouseLeave","open","trigger"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/components/stat/stat-card.tsx"],"sourcesContent":["import { FC, ReactNode, useState } from 'react';\nimport classNames from 'classnames';\nimport {\n BodyText,\n BodyTextPropsStrict,\n Eyebrow,\n Headline,\n Popover,\n Stack,\n Tooltip,\n} from '@servicetitan/design-system';\nimport * as Styles from './stat-card.module.less';\nimport { formatValue, NumberFormatter } from '../../utils/formatters';\nimport { Icon } from '@servicetitan/anvil2';\nimport TrendingUpSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_up.svg';\nimport TrendingDownSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_down.svg';\nimport InfoSVG from '@servicetitan/anvil2/assets/icons/material/round/info.svg';\nimport { tokens } from '@servicetitan/tokens/core/index';\n\nconst calculateDiff = (\n value: number,\n prev: number,\n percents?: boolean\n): [number, number, boolean] => {\n const diff = (value - prev) * (percents ? 100 : 1);\n const absDiff = Math.abs(diff);\n let diffPercent = 0;\n\n if (percents) {\n diffPercent = diff;\n } else if (absDiff) {\n diffPercent = prev ? (100 * absDiff) / prev : 100;\n }\n\n return [absDiff, diffPercent, diff >= 0];\n};\n\nconst formatDifference = (value: number, isPlus: boolean, format: NumberFormatter): string => {\n return (isPlus ? '+' : '-') + formatValue(value, format);\n};\n\nconst formatDifferencePercentage = (value: number, isPlus: boolean): string => {\n if (!value) {\n return '';\n }\n\n return (isPlus ? '+' : '-') + formatValue(value, 'percent-100');\n};\n\ninterface StatDiffProps {\n value?: number;\n prev?: number;\n size?: BodyTextPropsStrict['size'];\n format: NumberFormatter;\n inverted?: boolean;\n neutral?: boolean;\n className?: string;\n diffPercentOnly?: boolean;\n}\n\nexport const StatDiff: FC<StatDiffProps> = ({\n value,\n prev,\n size,\n format,\n inverted,\n neutral,\n className,\n diffPercentOnly = false,\n}) => {\n const percents = format === 'percent';\n const [absDiff, diffPercent, isIncrease] = calculateDiff(value ?? 0, prev ?? 0, percents);\n const diff =\n absDiff === 0 ? (\n ''\n ) : (\n <Icon\n svg={isIncrease ? TrendingUpSVG : TrendingDownSVG}\n color={\n neutral\n ? 'neutral-200'\n : inverted\n ? isIncrease\n ? 'red'\n : 'green'\n : isIncrease\n ? 'green'\n : 'red'\n }\n />\n );\n let text = '';\n\n if (percents) {\n text += formatDifferencePercentage(absDiff, isIncrease);\n } else {\n const diffPercentage = formatDifferencePercentage(diffPercent, isIncrease);\n\n if (diffPercentOnly) {\n text += `${diffPercentage}`;\n } else {\n text += `${formatDifference(absDiff, isIncrease, format)}`;\n\n if (diffPercent !== 0) {\n text += ` (${diffPercentage})`;\n }\n }\n }\n\n return (\n <Stack\n className={classNames(Styles.statDiff, className)}\n justifyContent=\"center\"\n alignItems=\"center\"\n >\n <Stack.Item className=\"m-r-half m-t-half\">\n <span>{diff}</span>\n </Stack.Item>\n <Stack.Item>\n <BodyText\n size={size ?? 'small'}\n data-cy=\"stat-diff-value\"\n className={classNames({\n 'c-red-500': !neutral && (inverted ? isIncrease : !isIncrease),\n 'c-green-500': !neutral && (inverted ? !isIncrease : isIncrease),\n 'c-neutral-200': !!neutral,\n })}\n >\n {value === undefined ? '\\u00A0' : text}\n </BodyText>\n </Stack.Item>\n </Stack>\n );\n};\n\nexport interface StatCardProps {\n title: string;\n description?: string;\n popoverContent?: ReactNode;\n value?: number;\n prev?: number;\n percent?: boolean;\n money?: boolean;\n rate?: boolean;\n clean?: boolean;\n inverted?: boolean;\n neutral?: boolean;\n fill?: boolean;\n valueOnly?: boolean;\n className?: string;\n diffPercentOnly?: boolean;\n}\n\nexport const StatCard: FC<StatCardProps> = ({\n title,\n description,\n popoverContent,\n value,\n percent,\n money,\n rate,\n prev,\n clean,\n inverted,\n neutral,\n fill,\n valueOnly,\n className,\n diffPercentOnly = false,\n}) => {\n const [popoverShown, setPopoverShown] = useState(false);\n const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';\n const val = value === undefined ? '\\u00A0' : formatValue(value, format);\n const hasInfo = !!description || !!popoverContent;\n\n const eyebrow = <Eyebrow data-cy={`marketing-stat-${title}-title`}>{title}</Eyebrow>;\n\n const infoIcon = <Icon svg={InfoSVG} color={tokens.colorNeutral100} />;\n\n const infoContent = popoverContent ? (\n <Popover open={popoverShown} trigger={infoIcon} onMouseEnter={() => setPopoverShown(true)}>\n {popoverContent}\n </Popover>\n ) : description ? (\n <Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>\n {infoIcon}\n </Tooltip>\n ) : null;\n\n return (\n <Stack\n direction=\"column\"\n className={classNames(\n 'p-2',\n {\n 'bg-white border': !clean,\n [Styles.card]: !clean,\n 'flex-grow-1 flex-basis-0': fill,\n },\n className\n )}\n onMouseLeave={() => setPopoverShown(false)}\n >\n <div className=\"p-3\">\n <div className={Styles.titleRow}>\n {eyebrow}\n {hasInfo && infoContent}\n </div>\n <div className={Styles.valueRow}>\n <Headline\n size=\"xlarge\"\n className=\"m-b-0-i\"\n data-cy={`marketing-stat-${title}-value`}\n >\n {val}\n </Headline>\n {!valueOnly && (\n <StatDiff\n value={value}\n prev={prev}\n format={format}\n inverted={inverted}\n neutral={neutral}\n diffPercentOnly={diffPercentOnly}\n />\n )}\n </div>\n </div>\n </Stack>\n );\n};\n"],"names":["useState","classNames","BodyText","Eyebrow","Headline","Popover","Stack","Tooltip","Styles","formatValue","Icon","TrendingUpSVG","TrendingDownSVG","InfoSVG","tokens","calculateDiff","value","prev","percents","diff","absDiff","Math","abs","diffPercent","formatDifference","isPlus","format","formatDifferencePercentage","StatDiff","size","inverted","neutral","className","diffPercentOnly","isIncrease","svg","color","text","diffPercentage","statDiff","justifyContent","alignItems","Item","span","data-cy","undefined","StatCard","title","description","popoverContent","percent","money","rate","clean","fill","valueOnly","popoverShown","setPopoverShown","val","hasInfo","eyebrow","infoIcon","colorNeutral100","infoContent","open","trigger","onMouseEnter","direction","card","onMouseLeave","div","titleRow","valueRow"],"mappings":";AAAA,SAAwBA,QAAQ,QAAQ,QAAQ;AAChD,OAAOC,gBAAgB,aAAa;AACpC,SACIC,QAAQ,EAERC,OAAO,EACPC,QAAQ,EACRC,OAAO,EACPC,KAAK,EACLC,OAAO,QACJ,8BAA8B;AACrC,YAAYC,YAAY,0BAA0B;AAClD,SAASC,WAAW,QAAyB,yBAAyB;AACtE,SAASC,IAAI,QAAQ,uBAAuB;AAC5C,OAAOC,mBAAmB,mEAAmE;AAC7F,OAAOC,qBAAqB,qEAAqE;AACjG,OAAOC,aAAa,4DAA4D;AAChF,SAASC,MAAM,QAAQ,kCAAkC;AAEzD,MAAMC,gBAAgB,CAClBC,OACAC,MACAC;IAEA,MAAMC,OAAO,AAACH,CAAAA,QAAQC,IAAG,IAAMC,CAAAA,WAAW,MAAM,CAAA;IAChD,MAAME,UAAUC,KAAKC,GAAG,CAACH;IACzB,IAAII,cAAc;IAElB,IAAIL,UAAU;QACVK,cAAcJ;IAClB,OAAO,IAAIC,SAAS;QAChBG,cAAcN,OAAO,AAAC,MAAMG,UAAWH,OAAO;IAClD;IAEA,OAAO;QAACG;QAASG;QAAaJ,QAAQ;KAAE;AAC5C;AAEA,MAAMK,mBAAmB,CAACR,OAAeS,QAAiBC;IACtD,OAAO,AAACD,CAAAA,SAAS,MAAM,GAAE,IAAKhB,YAAYO,OAAOU;AACrD;AAEA,MAAMC,6BAA6B,CAACX,OAAeS;IAC/C,IAAI,CAACT,OAAO;QACR,OAAO;IACX;IAEA,OAAO,AAACS,CAAAA,SAAS,MAAM,GAAE,IAAKhB,YAAYO,OAAO;AACrD;AAaA,OAAO,MAAMY,WAA8B,CAAC,EACxCZ,KAAK,EACLC,IAAI,EACJY,IAAI,EACJH,MAAM,EACNI,QAAQ,EACRC,OAAO,EACPC,SAAS,EACTC,kBAAkB,KAAK,EAC1B;IACG,MAAMf,WAAWQ,WAAW;IAC5B,MAAM,CAACN,SAASG,aAAaW,WAAW,GAAGnB,cAAcC,kBAAAA,mBAAAA,QAAS,GAAGC,iBAAAA,kBAAAA,OAAQ,GAAGC;IAChF,MAAMC,OACFC,YAAY,IACR,mBAEA,KAACV;QACGyB,KAAKD,aAAavB,gBAAgBC;QAClCwB,OACIL,UACM,gBACAD,WACEI,aACI,QACA,UACJA,aACE,UACA;;IAI1B,IAAIG,OAAO;IAEX,IAAInB,UAAU;QACVmB,QAAQV,2BAA2BP,SAASc;IAChD,OAAO;QACH,MAAMI,iBAAiBX,2BAA2BJ,aAAaW;QAE/D,IAAID,iBAAiB;YACjBI,QAAQ,GAAGC,gBAAgB;QAC/B,OAAO;YACHD,QAAQ,GAAGb,iBAAiBJ,SAASc,YAAYR,SAAS;YAE1D,IAAIH,gBAAgB,GAAG;gBACnBc,QAAQ,CAAC,EAAE,EAAEC,eAAe,CAAC,CAAC;YAClC;QACJ;IACJ;IAEA,qBACI,MAAChC;QACG0B,WAAW/B,WAAWO,OAAO+B,QAAQ,EAAEP;QACvCQ,gBAAe;QACfC,YAAW;;0BAEX,KAACnC,MAAMoC,IAAI;gBAACV,WAAU;0BAClB,cAAA,KAACW;8BAAMxB;;;0BAEX,KAACb,MAAMoC,IAAI;0BACP,cAAA,KAACxC;oBACG2B,IAAI,EAAEA,iBAAAA,kBAAAA,OAAQ;oBACde,WAAQ;oBACRZ,WAAW/B,WAAW;wBAClB,aAAa,CAAC8B,WAAYD,CAAAA,WAAWI,aAAa,CAACA,UAAS;wBAC5D,eAAe,CAACH,WAAYD,CAAAA,WAAW,CAACI,aAAaA,UAAS;wBAC9D,iBAAiB,CAAC,CAACH;oBACvB;8BAECf,UAAU6B,YAAY,WAAWR;;;;;AAKtD,EAAE;AAoBF,OAAO,MAAMS,WAA8B,CAAC,EACxCC,KAAK,EACLC,WAAW,EACXC,cAAc,EACdjC,KAAK,EACLkC,OAAO,EACPC,KAAK,EACLC,IAAI,EACJnC,IAAI,EACJoC,KAAK,EACLvB,QAAQ,EACRC,OAAO,EACPuB,IAAI,EACJC,SAAS,EACTvB,SAAS,EACTC,kBAAkB,KAAK,EAC1B;IACG,MAAM,CAACuB,cAAcC,gBAAgB,GAAGzD,SAAS;IACjD,MAAM0B,SAASyB,QAAQ,UAAUD,UAAU,YAAYE,OAAO,SAAS;IACvE,MAAMM,MAAM1C,UAAU6B,YAAY,WAAWpC,YAAYO,OAAOU;IAChE,MAAMiC,UAAU,CAAC,CAACX,eAAe,CAAC,CAACC;IAEnC,MAAMW,wBAAU,KAACzD;QAAQyC,WAAS,CAAC,eAAe,EAAEG,MAAM,MAAM,CAAC;kBAAGA;;IAEpE,MAAMc,yBAAW,KAACnD;QAAKyB,KAAKtB;QAASuB,OAAOtB,OAAOgD,eAAe;;IAElE,MAAMC,cAAcd,+BAChB,KAAC5C;QAAQ2D,MAAMR;QAAcS,SAASJ;QAAUK,cAAc,IAAMT,gBAAgB;kBAC/ER;SAELD,4BACA,KAACzC;QAAQ8B,MAAMW;QAAaJ,WAAS,CAAC,eAAe,EAAEG,MAAM,QAAQ,CAAC;kBACjEc;SAEL;IAEJ,qBACI,KAACvD;QACG6D,WAAU;QACVnC,WAAW/B,WACP,OACA;YACI,mBAAmB,CAACoD;YACpB,CAAC7C,OAAO4D,IAAI,CAAC,EAAE,CAACf;YAChB,4BAA4BC;QAChC,GACAtB;QAEJqC,cAAc,IAAMZ,gBAAgB;kBAEpC,cAAA,MAACa;YAAItC,WAAU;;8BACX,MAACsC;oBAAItC,WAAWxB,OAAO+D,QAAQ;;wBAC1BX;wBACAD,WAAWI;;;8BAEhB,MAACO;oBAAItC,WAAWxB,OAAOgE,QAAQ;;sCAC3B,KAACpE;4BACGyB,MAAK;4BACLG,WAAU;4BACVY,WAAS,CAAC,eAAe,EAAEG,MAAM,MAAM,CAAC;sCAEvCW;;wBAEJ,CAACH,2BACE,KAAC3B;4BACGZ,OAAOA;4BACPC,MAAMA;4BACNS,QAAQA;4BACRI,UAAUA;4BACVC,SAASA;4BACTE,iBAAiBA;;;;;;;AAO7C,EAAE"}
@@ -11,6 +11,21 @@
11
11
  margin-bottom: 2px;
12
12
  }
13
13
 
14
- .title {
15
- border-bottom: 1px dashed @color-neutral-90;
14
+ .title-row {
15
+ display: flex;
16
+ align-items: flex-start;
17
+ gap: 8px;
18
+ }
19
+
20
+ .value-row {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ align-items: flex-end;
24
+ gap: 8px;
25
+ margin-top: @spacing-1;
26
+ }
27
+
28
+ .card {
29
+ border-radius: 12px;
30
+ min-width: min-content;
16
31
  }
@@ -1,5 +1,7 @@
1
1
  export const __esModule: true;
2
+ export const card: string;
2
3
  export const statDiff: string;
3
4
  export const statExtendedDiff: string;
4
- export const title: string;
5
+ export const titleRow: string;
6
+ export const valueRow: string;
5
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/marketing-ui",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "Marketing UI component and utils",
5
5
  "repository": {
6
6
  "type": "git",
@@ -53,5 +53,5 @@
53
53
  "less": true,
54
54
  "webpack": false
55
55
  },
56
- "gitHead": "b013c9f39c05c44f888a5bca5ff6aadb89cd7a6f"
56
+ "gitHead": "7e2b147613dc2f96854d2e2c4458a4891dc6aadc"
57
57
  }
@@ -81,7 +81,11 @@ export const ColorTag: FC<ColorTagProps> = ({
81
81
  dashed
82
82
  ? { borderColor: strokeColor ?? color }
83
83
  : pattern === 'outline'
84
- ? { borderColor: outlineColor ?? color, borderRadius: radius }
84
+ ? {
85
+ borderColor: outlineColor ?? color,
86
+ backgroundColor: color,
87
+ borderRadius: radius,
88
+ }
85
89
  : { backgroundColor: color, borderRadius: radius }
86
90
  }
87
91
  />
@@ -44,6 +44,11 @@
44
44
  transform: rotate(135deg);
45
45
  }
46
46
 
47
+ .arrow-bottom {
48
+ top: auto;
49
+ bottom: 12px;
50
+ }
51
+
47
52
  .color-tag {
48
53
  width: 20px;
49
54
  height: 6px;
@@ -1,5 +1,6 @@
1
1
  export const __esModule: true;
2
2
  export const arrow: string;
3
+ export const arrowBottom: string;
3
4
  export const arrowLeft: string;
4
5
  export const arrowRight: string;
5
6
  export const colorTag: string;
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC, CSSProperties } from 'react';
1
+ import { useCallback, useMemo, useRef, useLayoutEffect, useState, FC } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { observer } from 'mobx-react';
4
4
  import { useDependencies } from '@servicetitan/react-ioc';
@@ -52,7 +52,7 @@ export const HoverPopover: FC = observer(() => {
52
52
  }
53
53
  }, [hoveredIndex, metrics, display, popH]);
54
54
 
55
- const popoverStyle = useMemo<CSSProperties>(() => {
55
+ const { popoverStyle, arrowPosition } = useMemo(() => {
56
56
  const posX = svgStore.periodX(hoveredIndex);
57
57
 
58
58
  const yHeights = metrics
@@ -67,17 +67,23 @@ export const HoverPopover: FC = observer(() => {
67
67
  const barHeightPercentRaw = svgStore.fpy(Math.max(0, Math.min(100, barHeight)));
68
68
  const barHeightPercent = Math.max(0, Math.min(100, Number(barHeightPercentRaw) || 0));
69
69
 
70
- const barTopPositionPx = (barHeightPercent / 100) * CHART_HEIGHT_PX;
71
- const availableSpaceBelow = Math.max(0, CHART_HEIGHT_PX - barTopPositionPx - popH);
72
- const popoverOffsetPx = Math.min(OFFSET_PX, availableSpaceBelow);
70
+ const barTopPx = (barHeightPercent / 100) * CHART_HEIGHT_PX;
71
+ const idealTopPx = barTopPx + OFFSET_PX;
72
+ const maxTopPx = Math.max(0, CHART_HEIGHT_PX - popH);
73
+ const popoverRepositioned = idealTopPx > maxTopPx;
74
+ const clampedTopPx = Math.min(idealTopPx, maxTopPx);
75
+ const topPercent = (clampedTopPx / CHART_HEIGHT_PX) * 100;
73
76
 
74
77
  return {
75
- top: `${barHeightPercent}%`,
76
- transform: `translateY(${popoverOffsetPx}px)`,
77
- position: 'absolute',
78
- ...(isChartLeftSide
79
- ? { left: `${svgStore.fpx(posX + 2)}%` }
80
- : { right: `${svgStore.fpx(102 - posX)}%` }),
78
+ popoverStyle: {
79
+ top: `${topPercent.toFixed(1)}%`,
80
+ transform: 'translateY(0)',
81
+ position: 'absolute' as const,
82
+ ...(isChartLeftSide
83
+ ? { left: `${svgStore.fpx(posX + 2)}%` }
84
+ : { right: `${svgStore.fpx(102 - posX)}%` }),
85
+ },
86
+ arrowPosition: popoverRepositioned ? 'bottom' : 'top',
81
87
  };
82
88
  }, [svgStore, hoveredIndex, isChartLeftSide, metrics, stackedTotals, popH]);
83
89
 
@@ -85,6 +91,10 @@ export const HoverPopover: FC = observer(() => {
85
91
  return null;
86
92
  }
87
93
 
94
+ if (stackedTotals?.[hoveredIndex] === 0) {
95
+ return null;
96
+ }
97
+
88
98
  const period = periods[hoveredIndex]!;
89
99
  const partialWeek = !!period.partial;
90
100
 
@@ -97,7 +107,8 @@ export const HoverPopover: FC = observer(() => {
97
107
  <div
98
108
  className={classNames(
99
109
  Styles.arrow,
100
- isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight
110
+ isChartLeftSide ? Styles.arrowLeft : Styles.arrowRight,
111
+ arrowPosition === 'bottom' && Styles.arrowBottom
101
112
  )}
102
113
  />
103
114
  <Text size="small" variant="headline" el="h6">
@@ -56,38 +56,18 @@ export const SvgBars: FC<SvgBarsProps> = observer(
56
56
  }));
57
57
 
58
58
  if (isStackedBarChart) {
59
- // Use ORIGINAL calculations - keep all spacing/positioning unchanged
60
59
  const spacingBetweenSegments = 1;
61
- const totalSpacing = (values.length - 1) * spacingBetweenSegments;
62
- let stackedBarHeight =
63
- values.reduce((sum, curr) => sum + curr.val, 0) + totalSpacing;
60
+ const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);
61
+ const totalYValue = values.reduce((sum, curr) => sum + curr.val, 0);
64
62
 
65
- // Find first/last non-zero indices for visual styling
66
63
  const firstNonZeroIdx = values.findIndex(v => v.value > 0);
67
64
  const lastNonZeroIdx = values.reduce(
68
65
  (last, v, idx) => (v.value > 0 ? idx : last),
69
66
  -1
70
67
  );
71
68
 
72
- // Count 0-value segments below first non-zero (for text position adjustment)
73
- const zeroSegmentsBelowFirst =
74
- firstNonZeroIdx >= 0
75
- ? values.slice(firstNonZeroIdx + 1).filter(v => v.value <= 0).length
76
- : 0;
77
-
78
- const totalValue = values.reduce((sum, curr) => sum + curr.value, 0);
79
69
  if (totalValue > 0) {
80
- /*
81
- * Adjust text position to maintain consistent gap with first rendered segment:
82
- * 1. Subtract spacing for skipped segments from fpy argument
83
- * 2. Add pixel offset for zeros below first non-zero
84
- */
85
- const textStackedBarHeight =
86
- stackedBarHeight -
87
- (firstNonZeroIdx > 0 ? firstNonZeroIdx : 0) * spacingBetweenSegments;
88
- const yTop =
89
- +fpy(textStackedBarHeight) +
90
- zeroSegmentsBelowFirst * spacingBetweenSegments;
70
+ const yTop = +fpy(totalYValue) - 2;
91
71
  const scaleX = 0.3;
92
72
  const scaleY = 1;
93
73
 
@@ -116,26 +96,29 @@ export const SvgBars: FC<SvgBarsProps> = observer(
116
96
  );
117
97
  }
118
98
 
99
+ let stackedBarHeight = totalYValue;
100
+ let isFirstRendered = true;
101
+ const nonZeroCount = values.filter(v => v.value > 0).length;
102
+ const bottomTrim = Math.max(0, nonZeroCount - 1) * spacingBetweenSegments;
103
+
119
104
  for (let j = 0; j < values.length; j++) {
120
105
  const value = values[j];
121
- stackedBarHeight -= spacingBetweenSegments;
122
-
123
- const TOP_RADIUS = 1;
124
- const xLeft = +fpx(x - barWidth / 2);
125
- const width = +fpx(barWidth);
126
106
 
127
107
  if (value.value <= 0) {
128
- stackedBarHeight -= value.val;
129
108
  continue;
130
109
  }
131
- const zeroSegments = values.slice(j + 1).filter(v => v.value <= 0).length;
132
-
133
- // Adjust yTop: move down by the space 0-value segments would occupy
134
- const yTop =
135
- +fpy(stackedBarHeight) +
136
- (values.length - 2) +
137
- zeroSegments * spacingBetweenSegments;
138
- const height = j === lastNonZeroIdx ? +fpx(value.val - 2) : +fpx(value.val);
110
+
111
+ if (!isFirstRendered) {
112
+ stackedBarHeight -= spacingBetweenSegments;
113
+ }
114
+ isFirstRendered = false;
115
+
116
+ const TOP_RADIUS = 1;
117
+ const xLeft = +fpx(x - barWidth / 2);
118
+ const width = +fpx(barWidth);
119
+ const yTop = +fpy(stackedBarHeight);
120
+ const height =
121
+ j === lastNonZeroIdx ? +fpx(value.val - bottomTrim) : +fpx(value.val);
139
122
  const r = j === firstNonZeroIdx ? TOP_RADIUS : 0;
140
123
 
141
124
  const d = [
@@ -11,6 +11,21 @@
11
11
  margin-bottom: 2px;
12
12
  }
13
13
 
14
- .title {
15
- border-bottom: 1px dashed @color-neutral-90;
14
+ .title-row {
15
+ display: flex;
16
+ align-items: flex-start;
17
+ gap: 8px;
18
+ }
19
+
20
+ .value-row {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ align-items: flex-end;
24
+ gap: 8px;
25
+ margin-top: @spacing-1;
26
+ }
27
+
28
+ .card {
29
+ border-radius: 12px;
30
+ min-width: min-content;
16
31
  }
@@ -1,5 +1,7 @@
1
1
  export const __esModule: true;
2
+ export const card: string;
2
3
  export const statDiff: string;
3
4
  export const statExtendedDiff: string;
4
- export const title: string;
5
+ export const titleRow: string;
6
+ export const valueRow: string;
5
7
 
@@ -4,6 +4,7 @@ import {
4
4
  BodyText,
5
5
  BodyTextPropsStrict,
6
6
  Eyebrow,
7
+ Headline,
7
8
  Popover,
8
9
  Stack,
9
10
  Tooltip,
@@ -13,6 +14,8 @@ import { formatValue, NumberFormatter } from '../../utils/formatters';
13
14
  import { Icon } from '@servicetitan/anvil2';
14
15
  import TrendingUpSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_up.svg';
15
16
  import TrendingDownSVG from '@servicetitan/anvil2/assets/icons/material/round/trending_down.svg';
17
+ import InfoSVG from '@servicetitan/anvil2/assets/icons/material/round/info.svg';
18
+ import { tokens } from '@servicetitan/tokens/core/index';
16
19
 
17
20
  const calculateDiff = (
18
21
  value: number,
@@ -168,57 +171,61 @@ export const StatCard: FC<StatCardProps> = ({
168
171
  const [popoverShown, setPopoverShown] = useState(false);
169
172
  const format = money ? 'money' : percent ? 'percent' : rate ? 'rate' : 'number';
170
173
  const val = value === undefined ? '\u00A0' : formatValue(value, format);
174
+ const hasInfo = !!description || !!popoverContent;
171
175
 
172
- const eyebrow = (
173
- <Eyebrow
174
- className={classNames(Styles.title, 'ta-center')}
175
- data-cy={`marketing-stat-${title}-title`}
176
- onMouseEnter={() => {
177
- setPopoverShown(true);
178
- }}
179
- >
180
- {title}
181
- </Eyebrow>
182
- );
176
+ const eyebrow = <Eyebrow data-cy={`marketing-stat-${title}-title`}>{title}</Eyebrow>;
177
+
178
+ const infoIcon = <Icon svg={InfoSVG} color={tokens.colorNeutral100} />;
179
+
180
+ const infoContent = popoverContent ? (
181
+ <Popover open={popoverShown} trigger={infoIcon} onMouseEnter={() => setPopoverShown(true)}>
182
+ {popoverContent}
183
+ </Popover>
184
+ ) : description ? (
185
+ <Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>
186
+ {infoIcon}
187
+ </Tooltip>
188
+ ) : null;
183
189
 
184
190
  return (
185
191
  <Stack
186
192
  direction="column"
187
- alignItems="center"
188
193
  className={classNames(
189
- 'p-y-3',
194
+ 'p-2',
190
195
  {
191
- 'bg-white border-radius-2 border': !clean,
196
+ 'bg-white border': !clean,
197
+ [Styles.card]: !clean,
192
198
  'flex-grow-1 flex-basis-0': fill,
193
199
  },
194
200
  className
195
201
  )}
196
202
  onMouseLeave={() => setPopoverShown(false)}
197
203
  >
198
- {popoverContent ? (
199
- <Popover open={popoverShown} trigger={eyebrow}>
200
- {popoverContent}
201
- </Popover>
202
- ) : description ? (
203
- <Tooltip text={description} data-cy={`marketing-stat-${title}-tooltip`}>
204
+ <div className="p-3">
205
+ <div className={Styles.titleRow}>
204
206
  {eyebrow}
205
- </Tooltip>
206
- ) : (
207
- eyebrow
208
- )}
209
- <BodyText className="fs-6-i ff-display" data-cy={`marketing-stat-${title}-value`}>
210
- {val}
211
- </BodyText>
212
- {!valueOnly && (
213
- <StatDiff
214
- value={value}
215
- prev={prev}
216
- format={format}
217
- inverted={inverted}
218
- neutral={neutral}
219
- diffPercentOnly={diffPercentOnly}
220
- />
221
- )}
207
+ {hasInfo && infoContent}
208
+ </div>
209
+ <div className={Styles.valueRow}>
210
+ <Headline
211
+ size="xlarge"
212
+ className="m-b-0-i"
213
+ data-cy={`marketing-stat-${title}-value`}
214
+ >
215
+ {val}
216
+ </Headline>
217
+ {!valueOnly && (
218
+ <StatDiff
219
+ value={value}
220
+ prev={prev}
221
+ format={format}
222
+ inverted={inverted}
223
+ neutral={neutral}
224
+ diffPercentOnly={diffPercentOnly}
225
+ />
226
+ )}
227
+ </div>
228
+ </div>
222
229
  </Stack>
223
230
  );
224
231
  };