@verifiedinc-public/shared-ui-elements 12.4.0 → 12.4.1-beta.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/chart/AreaChart/index.d.ts +13 -1
- package/dist/components/chart/AreaChart/index.mjs +1 -1
- package/dist/components/chart/ConversionOverTimeChart/ConversionOverTimeChart.tooltip.d.ts +38 -0
- package/dist/components/chart/ConversionOverTimeChart/ConversionOverTimeChart.tooltip.mjs +1 -0
- package/dist/components/chart/ConversionOverTimeChart/index.d.ts +6 -0
- package/dist/components/chart/ConversionOverTimeChart/index.mjs +1 -1
- package/dist/components/chart/SynchronizedMetricsChart/SynchronizedMetricsChart.map.d.ts +10 -0
- package/dist/components/chart/SynchronizedMetricsChart/SynchronizedMetricsChart.map.mjs +1 -1
- package/dist/components/chart/SynchronizedMetricsChart/SynchronizedMetricsChart.mjs +1 -1
- package/dist/components/chart/SynchronizedMetricsChart/SynchronizedMetricsChart.tooltip.d.ts +38 -0
- package/dist/components/chart/SynchronizedMetricsChart/SynchronizedMetricsChart.tooltip.mjs +1 -0
- package/dist/components/chart/SynchronizedMetricsChart/SynchronizedMetricsChart.types.d.ts +16 -0
- package/dist/components/chart/trend.d.ts +65 -0
- package/dist/components/chart/trend.mjs +1 -0
- package/package.json +1 -1
|
@@ -7,6 +7,16 @@ export interface AreaSeriesChartData {
|
|
|
7
7
|
dataKey: string;
|
|
8
8
|
color?: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* A straight best-fit line drawn over the areas. Recharts' `AreaChart` accepts
|
|
12
|
+
* only `Area` children, which is why this renders a `ComposedChart`.
|
|
13
|
+
*/
|
|
14
|
+
export interface AreaTrendLine {
|
|
15
|
+
/** Column holding the fitted values. Must not collide with a series dataKey. */
|
|
16
|
+
dataKey: string;
|
|
17
|
+
color?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
10
20
|
interface AreaChartProps {
|
|
11
21
|
/**
|
|
12
22
|
* Chart data points. Each object should include the x-axis key and one key per series.
|
|
@@ -28,6 +38,8 @@ interface AreaChartProps {
|
|
|
28
38
|
tooltip?: ComponentProps<typeof Tooltip>;
|
|
29
39
|
/** Override props applied to every Area element in the chart. */
|
|
30
40
|
area?: ComponentProps<typeof Area>;
|
|
41
|
+
/** Straight best-fit overlays, drawn above the areas. */
|
|
42
|
+
trendLines?: AreaTrendLine[];
|
|
31
43
|
/** Reference lines to highlight specific values or thresholds. */
|
|
32
44
|
referenceLines?: Array<ComponentProps<typeof ReferenceLine>>;
|
|
33
45
|
/** Shaded reference areas to highlight specific ranges. */
|
|
@@ -40,5 +52,5 @@ interface AreaChartProps {
|
|
|
40
52
|
/** Stack mode for areas. 'none' renders independent areas; 'stack' stacks by count; 'expand' normalizes to 100%. @default 'none' */
|
|
41
53
|
stackMode?: 'none' | 'stack' | 'expand';
|
|
42
54
|
}
|
|
43
|
-
export declare function AreaChart({ data, series, color, xAxis, yAxis, tooltip, area, referenceLines, referenceAreas, sx, isAnimationActive, areaType, stackMode, }: AreaChartProps): ReactElement;
|
|
55
|
+
export declare function AreaChart({ data, series, color, xAxis, yAxis, tooltip, area, trendLines, referenceLines, referenceAreas, sx, isAnimationActive, areaType, stackMode, }: Readonly<AreaChartProps>): ReactElement;
|
|
44
56
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{useTheme as
|
|
1
|
+
"use strict";import{useTheme as D,Box as K}from"@mui/material";import{ResponsiveContainer as g,ComposedChart as O,CartesianGrid as T,XAxis as $,YAxis as L,Tooltip as P,ReferenceLine as R,ReferenceArea as W,Area as b,Line as j}from"recharts";import{chartDefaultProps as G,xAxisDefaultProps as w,yAxisDefaultProps as B}from"../shared.mjs";import{jsx as a,jsxs as y}from"react/jsx-runtime";function I({data:f,series:n,color:x,xAxis:h,yAxis:A,tooltip:k,area:u,trendLines:l,referenceLines:d,referenceAreas:p,sx:v,isAnimationActive:c=!1,areaType:C="monotone",stackMode:s="none"}){const m=D(),i=x??m.palette.primary.main;return a(K,{sx:{width:"100%",height:"100%",...v},children:a(g,{children:y(O,{data:f,stackOffset:s==="expand"?"expand":void 0,...G,children:[a("defs",{children:n.map((e,r)=>{const t=`gradient-${r}`,o=e.color??i;return y("linearGradient",{id:t,x1:"0",y1:"0",x2:"0",y2:"1",children:[a("stop",{offset:"5%",stopColor:o,stopOpacity:.2}),a("stop",{offset:"95%",stopColor:o,stopOpacity:0})]},t)})}),a(T,{strokeDasharray:"3 3",vertical:!1}),a($,{...w,...h}),a(L,{...B,...A}),a(P,{cursor:{stroke:m.palette.neutral.main,strokeWidth:1},...k}),d?.map((e,r)=>a(R,{...e},e.label??r)),p?.map((e,r)=>a(W,{...e},e.label??r)),n.map((e,r)=>{const t=`gradient-${r}`,o=e.color??i;return a(b,{name:e.key,dataKey:e.dataKey,fill:`url(#${t})`,fillOpacity:s!=="none"?1:.6,stroke:o,strokeWidth:2,type:C,isAnimationActive:c,stackId:s!=="none"?"stack":void 0,...u},e.key)}),l?.map(e=>a(j,{dataKey:e.dataKey,name:e.name??e.dataKey,stroke:e.color??i,strokeWidth:2,strokeDasharray:"7 5",type:"linear",dot:!1,activeDot:!1,isAnimationActive:c,legendType:"none"},`trend-${e.dataKey}`))]})})})}export{I as AreaChart};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { MetricsIntervalType } from '../../../constants/metrics';
|
|
3
|
+
export interface TrendReadout {
|
|
4
|
+
/** Series label the fit belongs to. */
|
|
5
|
+
name: string;
|
|
6
|
+
color?: string;
|
|
7
|
+
/** Change per interval bucket, already in display units. */
|
|
8
|
+
slope: number;
|
|
9
|
+
stepMs: number;
|
|
10
|
+
}
|
|
11
|
+
interface PayloadEntry {
|
|
12
|
+
dataKey?: string | number;
|
|
13
|
+
name?: string | number;
|
|
14
|
+
value?: number | string;
|
|
15
|
+
color?: string;
|
|
16
|
+
payload?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
interface ConversionChartTooltipProps {
|
|
19
|
+
/** Keys of the fitted columns, excluded from the value rows. */
|
|
20
|
+
trendDataKeys: string[];
|
|
21
|
+
/** One entry per fitted series; empty when the trend is off. */
|
|
22
|
+
trends: TrendReadout[];
|
|
23
|
+
interval?: MetricsIntervalType;
|
|
24
|
+
unit: 'count' | 'percent';
|
|
25
|
+
sortByValueDesc: boolean;
|
|
26
|
+
valueFormatter: (value: number | string, name: string | number, entry: PayloadEntry) => string;
|
|
27
|
+
labelFormatter: (label: number) => string;
|
|
28
|
+
/** Injected by Recharts. */
|
|
29
|
+
active?: boolean;
|
|
30
|
+
payload?: PayloadEntry[];
|
|
31
|
+
label?: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Tooltip for the funnel charts. Mirrors the stock Recharts layout so nothing
|
|
35
|
+
* shifts visually, then adds one slope row per fitted stage underneath.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ConversionChartTooltip({ trendDataKeys, trends, interval, unit, sortByValueDesc, valueFormatter, labelFormatter, active, payload, label, }: Readonly<ConversionChartTooltipProps>): React.ReactNode;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";import{Box as s,Typography as e,Stack as v}from"@mui/material";import{formatSlope as y}from"../trend.mjs";import{jsxs as i,jsx as t}from"react/jsx-runtime";const o={fontSize:"0.875rem",lineHeight:1.5};function S({trendDataKeys:d,trends:a,interval:p,unit:m,sortByValueDesc:c,valueFormatter:u,labelFormatter:h,active:b,payload:n,label:x}){if(!b||!(n!=null&&n.length))return null;const l=n.filter(r=>r.dataKey!=null&&!d.includes(String(r.dataKey))),g=c?[...l].sort((r,f)=>Number(f.value??0)-Number(r.value??0)):l;return i(s,{sx:{bgcolor:"background.paper",border:"1px solid",borderColor:"divider",borderRadius:1,boxShadow:3,px:1.5,py:1.25,maxWidth:360},children:[t(e,{sx:{...o,fontWeight:700,mb:.5},children:h(Number(x))}),g.map(r=>i(e,{sx:{...o,color:r.color??"inherit"},children:[String(r.name??r.dataKey)," :"," ",u(r.value??0,r.name??"",r)]},String(r.dataKey))),a.length>0&&t(s,{sx:{mt:.75,pt:.75,borderTop:"1px solid",borderColor:"divider"},children:a.map(r=>i(v,{direction:"row",spacing:2,sx:{justifyContent:"space-between"},children:[t(e,{sx:{...o,fontWeight:700},children:a.length>1?`Trend (${r.name})`:"Trend"}),t(e,{sx:{...o,fontWeight:700},children:y(r.slope,{unit:m,interval:p,stepMs:r.stepMs})})]},r.name))})]})}export{S as ConversionChartTooltip};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
import { SxProps } from '@mui/material';
|
|
3
3
|
import { AreaSeriesChartData } from '../AreaChart';
|
|
4
|
+
import { MetricsIntervalType } from '../../../constants/metrics';
|
|
4
5
|
import { BrandFilter } from '../../BrandFilterInput';
|
|
5
6
|
export type { AreaSeriesChartData } from '../AreaChart';
|
|
6
7
|
type ViewMode = 'absolute' | 'percent';
|
|
@@ -79,6 +80,11 @@ export interface ConversionOverTimeChartProps {
|
|
|
79
80
|
filter: {
|
|
80
81
|
timezone?: string;
|
|
81
82
|
brands?: BrandFilter[];
|
|
83
|
+
/**
|
|
84
|
+
* The selected bucket size. The trend readout reports its slope per this
|
|
85
|
+
* interval; when omitted it is named from the spacing in the data.
|
|
86
|
+
*/
|
|
87
|
+
interval?: MetricsIntervalType;
|
|
82
88
|
};
|
|
83
89
|
sx?: SxProps;
|
|
84
90
|
stackMode?: 'stack' | 'none';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{useState as
|
|
1
|
+
"use strict";import{useState as I}from"react";import{Stack as V,Box as de,ToggleButton as D,ToggleButtonGroup as ue}from"@mui/material";import{EmptyChartSection as O}from"../EmptyChartSection.mjs";import{LoadingChartSection as W}from"../LoadingChartSection.mjs";import{AreaChart as me}from"../AreaChart/index.mjs";import{ConversionChartTooltip as ce}from"./ConversionOverTimeChart.tooltip.mjs";import{trendSeries as pe}from"../trend.mjs";import{SeriesPercentageChartLegend as fe}from"../SeriesPercentageChartLegend/index.mjs";import{useStyle as he}from"../styles.mjs";import{formatExtendedDate as Y,formatDateMMYY as ye}from"../../../utils/date.mjs";import{DEFAULT_TIMEZONE as ve}from"../../form/TimezoneInput/timezones.mjs";import{jsx as s,jsxs as M}from"react/jsx-runtime";const F="__trend_";function G(h,d,u){return h.map(i=>{const m=u==="sum"?d.reduce((r,c)=>r+(Number(i[c.dataKey])||0),0):Math.max(0,...d.map(r=>Number(i[r.dataKey])||0)),o={date:i.date};for(const r of d)o[r.dataKey]=m===0?0:(Number(i[r.dataKey])||0)/m;return o})}function ge({brands:h,data:d,seriesConfig:u}){const i=new Map,m=h?new Set(h.map(o=>o._raw.brandUuid)):null;for(const o of d)if(!(m&&!m.has(o.brandUuid)))for(const r of o.interval??[]){const c=+new Date(r.date);let n=i.get(c);if(!n){n={date:c};for(const p of u)n[p.dataKey]=0;i.set(c,n)}for(const p of u)n[p.dataKey]=n[p.dataKey]+Number(r[p.dataKey]||0)}return{series:u,data:Array.from(i.values()).sort((o,r)=>o.date-r.date)}}function be({data:h,series:d,chartData:u,seriesConfig:i,stackMode:m="stack",isLoading:o,isFetching:r,isSuccess:c,filter:n,sx:p,legendBrand:f,showLegendUuid:H=!0,views:$,defaultViewKey:J,extraToggles:L,percentBasis:Q="max",sortTooltipByValueDesc:B=!1}){var T;const A=he(),C=n.timezone??ve,x=$??[{key:"absolute",label:"Numbers",mode:"absolute"},{key:"percent",label:"Percentages",mode:"percent"}],[z,R]=I(J??((T=x[0])==null?void 0:T.key)??"absolute"),[v,q]=I(!1),a=x.find(e=>e.key===z)??x[0],g=a?.mode??"absolute",N=a?.stackMode??m,X=a?.percentBasis??Q,E=a?.chartData??u,ee=a?.seriesConfig??i,ae=a?.data??h,te=a?.series??d,l=E!==void 0?o?{series:[],data:[]}:ge({brands:n.brands,data:E,seriesConfig:ee??[]}):{series:te??[],data:ae??[]},P=!l.data.length||!c,U=!l.data.length&&o;if(!$){if(U)return s(W,{});if(P)return s(O,{})}const re=N==="none"&&g==="percent"?G(l.data,l.series,X):null,_=N==="stack"?g==="absolute"?"stack":"expand":"none",oe=g==="absolute"?{tickFormatter:e=>Number(e).toLocaleString(),allowDecimals:!1}:{tickFormatter:e=>`${(e*100).toFixed(0)}%`,domain:[0,1]},Z=g==="absolute"?e=>Number(e).toLocaleString():N==="stack"?(e,t,y)=>{const K=l.series.reduce((w,le)=>w+(Number(y.payload[le.dataKey])||0),0);return K===0?"0.0%":`${(Number(e)/K*100).toFixed(1)}%`}:e=>`${(Number(e)*100).toFixed(1)}%`,ne=B?e=>-(Number(e?.value)||0):void 0,k=re??l.data,S=g==="percent",j=_==="none"&&k.length>1,b=j?l.series.map(e=>{const t=pe(k,e.dataKey,{clampTo:S?[0,1]:void 0,interval:n.interval});return t?{serie:e,fit:t}:null}).filter(e=>e!==null):[],se=b.map(({serie:e})=>`${F}${e.dataKey}`),ie=v&&b.length?k.map((e,t)=>{const y={...e};for(const{serie:K,fit:w}of b)y[`${F}${K.dataKey}`]=w.values[t];return y}):k;return M(V,{children:[M(de,{sx:{display:"flex",flexDirection:"column",gap:1,height:A.regularChartWrapper.height},children:[M(V,{direction:"row",spacing:1,justifyContent:"flex-end",children:[j&&s(D,{value:"trend",selected:v,onChange:()=>q(e=>!e),size:"small","aria-label":"Show trend line","aria-pressed":v,children:"Show Trend"}),L?.map(e=>s(D,{value:e.id,selected:e.selected,onChange:()=>e.onChange(!e.selected),size:"small","aria-label":e.ariaLabel??e.label,"aria-pressed":e.selected,children:e.label},e.id)),s(ue,{value:z,exclusive:!0,onChange:(e,t)=>{t!==null&&R(t)},size:"small",children:x.map(e=>s(D,{value:e.key,children:e.label},e.key))})]}),U?s(W,{}):P?s(O,{}):s(me,{data:ie,series:l.series,trendLines:v?b.map(({serie:e})=>({dataKey:`${F}${e.dataKey}`,color:e.color,name:`${e.key} trend`})):void 0,stackMode:_,isAnimationActive:!0,xAxis:{dataKey:"date",type:"number",domain:["dataMin","dataMax"],tickFormatter:e=>ye(e,{timeZone:C,hour12:!1,hour:"numeric"}),allowDuplicatedCategory:!1},yAxis:oe,tooltip:{formatter:Z,itemSorter:ne,labelFormatter:e=>Y(e,{timeZone:C,hour12:!1}),content:s(ce,{trendDataKeys:se,trends:v?b.map(({serie:e,fit:t})=>({name:e.key,color:e.color,slope:S?t.slopePerInterval*100:t.slopePerInterval,stepMs:t.stepMs})):[],interval:n.interval,unit:S?"percent":"count",sortByValueDesc:B,valueFormatter:(e,t,y)=>Z(e,t,y),labelFormatter:e=>Y(e,{timeZone:C,hour12:!1})})},sx:{...A.regularChartWrapper,height:"unset",flex:1,minHeight:0,opacity:r?.4:1,...p}})]}),f&&s(fe,{payload:[{uuid:f.uuid,value:f.value,color:f.color,dataKey:f.dataKey??f.uuid,brandName:f.brandName}],showUuid:H})]})}export{be as ConversionOverTimeChart,G as normalizePercentData};
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { BrandFilter } from '../../../components/BrandFilterInput';
|
|
2
2
|
import { BrandIntervalData } from '../ConversionOverTimeChart';
|
|
3
3
|
import { SubChartConfig, SynchronizedSubChartConfig } from './SynchronizedMetricsChart.types';
|
|
4
|
+
/**
|
|
5
|
+
* Overall rate per timestamp across the visible brands, pooling the raw counts
|
|
6
|
+
* before dividing. Exported for testing.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pooledPercentageByDate({ chartData, brands, numerator, denominator, }: {
|
|
9
|
+
chartData: BrandIntervalData[];
|
|
10
|
+
brands: BrandFilter[];
|
|
11
|
+
numerator: string;
|
|
12
|
+
denominator: string;
|
|
13
|
+
}): Record<number, number>;
|
|
4
14
|
export declare function mapSynchronizedSubCharts({ chartData, subChartConfig, brands, colorMap, isLoading, }: {
|
|
5
15
|
chartData: BrandIntervalData[];
|
|
6
16
|
subChartConfig: readonly [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{mapSeriesTimeSeriesData as
|
|
1
|
+
"use strict";import{mapSeriesTimeSeriesData as l}from"../SeriesChart/SeriesChart.map.mjs";function p({chartData:o,brands:i,numerator:m,denominator:d}){const u=new Set(i.map(e=>e._raw.brandUuid)),r=new Map;for(const e of o)if(u.has(e.brandUuid))for(const a of e.interval??[]){const s=+new Date(a.date),n=r.get(s)??{numerator:0,denominator:0};n.numerator+=Number(a[m])||0,n.denominator+=Number(a[d])||0,r.set(s,n)}const t={};for(const[e,a]of r)t[e]=a.denominator>0?Math.min(a.numerator/a.denominator*100,100):0;return t}function y({chartData:o,subChartConfig:i,brands:m,colorMap:d,isLoading:u}){if(u)return[{title:i[0].title,data:[]}];const r={brands:m,colorMap:d,data:o};return i.map(t=>{if(t.dataKey!=null)return{title:t.title,data:l({...r,keyValue:t.dataKey}),tooltipFormatter:t.tooltipFormatter,yAxisTickFormatter:t.yAxisTickFormatter,yAxisDomain:t.yAxisDomain,isPercentage:!1};const{numerator:e,denominator:a}=t.percentageOf,s=o.map(n=>({...n,interval:(n.interval??[]).map(c=>({...c,percentage:Number(c[a])>0?Math.min(Number(c[e])/Number(c[a])*100,100):0}))}));return{title:t.title,data:l({...r,data:s,keyValue:"percentage"}),tooltipFormatter:t.tooltipFormatter,yAxisTickFormatter:t.yAxisTickFormatter,yAxisDomain:t.yAxisDomain,isPercentage:!0,totalByDate:p({chartData:o,brands:m,numerator:e,denominator:a})}})}export{y as mapSynchronizedSubCharts,p as pooledPercentageByDate};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";import{useState as
|
|
1
|
+
"use strict";import{useState as _,useMemo as b}from"react";import{Stack as C,ToggleButton as P,useTheme as U,Typography as O}from"@mui/material";import{ResponsiveContainer as Z,LineChart as G,CartesianGrid as R,XAxis as X,YAxis as q,Tooltip as H,Line as I}from"recharts";import{formatDateMMYY as J}from"../../../utils/date.mjs";import{DEFAULT_TIMEZONE as Q}from"../../form/TimezoneInput/timezones.mjs";import{EmptyChartSection as V}from"../EmptyChartSection.mjs";import{LoadingChartSection as $}from"../LoadingChartSection.mjs";import{SeriesPercentageChartLegend as ee}from"../SeriesPercentageChartLegend/index.mjs";import{chartDefaultProps as te,xAxisDefaultProps as ae,yAxisDefaultProps as re}from"../shared.mjs";import{mapSynchronizedSubCharts as oe}from"./SynchronizedMetricsChart.map.mjs";import{SynchronizedChartTooltip as ne}from"./SynchronizedMetricsChart.tooltip.mjs";import{trendSeries as ie}from"../trend.mjs";import{jsx as t,jsxs as x}from"react/jsx-runtime";import se from"../../../node_modules/d3-scale/src/symlog.mjs";const le="synchronized-metrics",de=200,u="__total__",ce="Total",k="__trend__",me="Trend";function ue(o){return!o.isPercentage||o.totalByDate?u:o.data.length===1?o.data[0].uuid:null}function E(o){const a=new Map;for(const i of o)for(const n of i.chartData){let d=a.get(n.date);d||(d={date:n.date},a.set(n.date,d)),d[i.uuid]=n.value}return Array.from(a.values()).sort((i,n)=>i.date-n.date)}const pe=u;function W(o,a){return a.length?o.map(i=>({...i,[u]:a.reduce((n,d)=>n+(Number(i[d])||0),0)})):o}function he({title:o,mergedData:a,brands:i,timezone:n,syncId:d,isPercentage:c,showTotal:z,showTrend:p,trendTarget:m,interval:f,logScale:w,tooltipFormatter:T,yAxisTickFormatter:L,yAxisDomain:v}){const h=U(),S=w&&!c,F=z&&!c&&i.length>1,s=S?se():void 0,l=b(()=>m?ie(a,m,{clampTo:c?[0,100]:void 0,interval:f}):null,[a,m,c,f]),M=b(()=>{if(!p||!l)return a;const r=a.length-1;return a.map((D,g)=>({...D,...g===0||g===r?{[k]:l.values[g]}:{}}))},[a,p,l]);return x(C,{children:[t(O,{variant:"h5",sx:{mb:.5,fontSize:"1.15rem"},children:o}),t(Z,{width:"100%",height:de,children:x(G,{data:M,syncId:d,margin:te.margin,children:[t(R,{vertical:!1}),t(X,{dataKey:"date",tickFormatter:r=>J(r,{timeZone:n,hour12:!1,hour:"numeric"}),...ae}),t(q,{tickFormatter:L??(r=>Number(r).toLocaleString()),allowDecimals:!1,domain:v,scale:s,...re}),t(H,{cursor:{stroke:h.palette.neutral.main,strokeWidth:1},wrapperStyle:{zIndex:h.zIndex.tooltip},content:t(ne,{timezone:n,totalDataKey:u,trendDataKey:k,trendSlope:p?l?.slopePerInterval:void 0,trendStepMs:l?.stepMs,trendInterval:f,trendUnit:c?"percent":"count",valueFormatter:T??(r=>Number(r).toLocaleString())})}),i.map(r=>t(I,{dataKey:r.uuid,name:r.name,stroke:r.color,type:"monotone",strokeWidth:2,isAnimationActive:!1,dot:!1},r.uuid)),F&&t(I,{dataKey:u,name:ce,stroke:h.palette.text.primary,strokeWidth:2,strokeDasharray:"4 4",type:"monotone",isAnimationActive:!1,dot:!1},u),p&&l&&t(I,{dataKey:k,name:me,connectNulls:!0,stroke:h.palette.secondary.main,strokeWidth:2,strokeDasharray:"7 5",type:"linear",isAnimationActive:!1,dot:!1,activeDot:!1,legendType:"none"},k)]})})]})}function ge({subCharts:o,chartData:a,subChartConfig:i,colorMap:n,syncId:d=le,isLoading:c,isSuccess:z,isFetching:p,filter:m,sx:f}){const w=m.timezone??Q,[T,L]=_(!1),[v,h]=_(!1),[S,F]=_(!1),s=a?oe({chartData:a,subChartConfig:i,brands:m.brands,colorMap:n,isLoading:c}):o,l=s.every(e=>e.data.length===0),M=b(()=>s.map(e=>{const y=E(e.data);if(e.isPercentage){const A=e.totalByDate;return A?y.map(N=>({...N,[u]:A[N.date]??0})):y}const B=e.data.map(A=>A.uuid);return W(y,B)}),[s]),r=s.some(e=>!e.isPercentage),D=s.some(e=>!e.isPercentage&&e.data.length>1),g=s.map(e=>ue(e)),K=g.some(e=>e!==null),Y=b(()=>s[0].data.map(e=>({uuid:e.brandUuid??e.uuid,value:e.name,color:e.color,dataKey:e.uuid,brandName:e.brandName})),[s]);if(l&&c)return t($,{});if(l||!z)return t(V,{});const j=r||D||K;return x(C,{sx:{width:"100%",...f},children:[j&&x(C,{direction:"row",spacing:1,justifyContent:"flex-end",sx:{mb:1},children:[D&&t(P,{value:"total",selected:T,onChange:()=>L(e=>!e),size:"small","aria-label":"Show Total line","aria-pressed":T,children:"Show Total"}),K&&t(P,{value:"trend",selected:v,onChange:()=>h(e=>!e),size:"small","aria-label":"Show trend line","aria-pressed":v,children:"Show Trend"}),r&&t(P,{value:"log",selected:S,onChange:()=>F(e=>!e),size:"small","aria-label":"Toggle logarithmic scale","aria-pressed":S,children:"Log Scale"})]}),x(C,{sx:{opacity:p?.4:1,gap:2},children:[s.map((e,y)=>t(he,{title:e.title,mergedData:M[y],brands:e.data,timezone:w,syncId:d,isPercentage:e.isPercentage??!1,showTotal:T,showTrend:v,trendTarget:g[y],interval:m.interval,logScale:S,tooltipFormatter:e.tooltipFormatter,yAxisTickFormatter:e.yAxisTickFormatter,yAxisDomain:e.yAxisDomain},e.title)),t(ee,{payload:Y})]})]})}export{ge as SynchronizedMetricsChart,pe as TOTAL_DATA_KEY,W as enrichWithTotal,E as mergeChartData};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
import { MetricsIntervalType } from '../../../constants/metrics';
|
|
3
|
+
/**
|
|
4
|
+
* Brand rows rendered before the remainder is collapsed into a count. Sized so
|
|
5
|
+
* the tallest column stays within a sub-chart's height.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TOOLTIP_BRAND_CAP = 24;
|
|
8
|
+
interface PayloadEntry {
|
|
9
|
+
dataKey?: string | number;
|
|
10
|
+
name?: string | number;
|
|
11
|
+
value?: number | string;
|
|
12
|
+
color?: string;
|
|
13
|
+
}
|
|
14
|
+
interface SynchronizedChartTooltipProps {
|
|
15
|
+
timezone: string;
|
|
16
|
+
totalDataKey: string;
|
|
17
|
+
/** Excluded from the ranked rows; reported as a slope instead. */
|
|
18
|
+
trendDataKey: string;
|
|
19
|
+
/** Change per interval bucket. Undefined when the trend is off. */
|
|
20
|
+
trendSlope?: number;
|
|
21
|
+
/** Measured bucket spacing, used to name the interval if it isn't given. */
|
|
22
|
+
trendStepMs?: number;
|
|
23
|
+
trendInterval?: MetricsIntervalType;
|
|
24
|
+
trendUnit: 'count' | 'percent';
|
|
25
|
+
valueFormatter: (value: number | string) => string;
|
|
26
|
+
/** Injected by Recharts. */
|
|
27
|
+
active?: boolean;
|
|
28
|
+
payload?: PayloadEntry[];
|
|
29
|
+
label?: number | string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Tooltip for a synchronized sub-chart. Ranks brands by their value at the
|
|
33
|
+
* hovered point, lays them out in columns and caps the list, so a chart with
|
|
34
|
+
* every brand selected stays readable inside its own height instead of running
|
|
35
|
+
* over the charts below it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function SynchronizedChartTooltip({ timezone, totalDataKey, trendDataKey, trendSlope, trendStepMs, trendInterval, trendUnit, valueFormatter, active, payload, label, }: Readonly<SynchronizedChartTooltipProps>): React.ReactNode;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";import{Box as m,Typography as o,Stack as g}from"@mui/material";import{formatExtendedDate as W}from"../../../utils/date.mjs";import{formatSlope as K}from"../trend.mjs";import{jsxs as i,jsx as r}from"react/jsx-runtime";const b=24,z=8,j=16;function B(t){return t<=z?1:t<=j?2:3}const a={fontSize:"0.8125rem",lineHeight:1.4};function y({row:t,bold:n,valueFormatter:l}){return i(g,{direction:"row",spacing:1.5,sx:{minWidth:0,alignItems:"baseline",justifyContent:"space-between",color:t.color},children:[r(o,{component:"span",sx:{...a,fontWeight:n?700:400,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.name}),r(o,{component:"span",sx:{...a,fontWeight:700,flexShrink:0},children:l(t.value??0)})]})}function D({timezone:t,totalDataKey:n,trendDataKey:l,trendSlope:p,trendStepMs:v,trendInterval:S,trendUnit:w,valueFormatter:c,active:k,payload:d,label:C}){if(!k||!(d!=null&&d.length))return null;const h=d.filter(e=>e.dataKey!=null&&e.dataKey!==l).map(e=>({key:String(e.dataKey),name:String(e.name??e.dataKey),color:e.color??"inherit",value:e.value??0})).sort((e,T)=>Number(T.value)-Number(e.value)),u=h.find(e=>e.key===n),f=h.filter(e=>e.key!==n),s=f.slice(0,b),x=f.length-s.length;return i(m,{sx:{bgcolor:"background.paper",border:"1px solid",borderColor:"divider",borderRadius:1,boxShadow:3,px:1.25,py:1,maxWidth:560},children:[r(o,{sx:{fontSize:"0.8125rem",lineHeight:1.4,fontWeight:700,mb:.5},children:W(C??0,{timeZone:t,hour12:!1})}),u&&r(m,{sx:{pb:.5,mb:.5,borderBottom:"1px solid",borderColor:"divider"},children:r(y,{row:u,bold:!0,valueFormatter:c})}),r(m,{sx:{display:"grid",gridTemplateColumns:`repeat(${B(s.length)}, minmax(0, 1fr))`,columnGap:2},children:s.map(e=>r(y,{row:e,valueFormatter:c},e.key))}),x>0&&i(o,{sx:{fontSize:"0.75rem",lineHeight:1.4,color:"text.secondary",mt:.5},children:["+ ",x," more"]}),p!==void 0&&i(g,{direction:"row",spacing:2,sx:{mt:.75,pt:.75,borderTop:"1px solid",borderColor:"divider",justifyContent:"space-between"},children:[r(o,{sx:{...a,fontWeight:700},children:"Trend"}),r(o,{sx:{...a,fontWeight:700},children:K(p,{unit:w,interval:S,stepMs:v})})]})]})}export{D as SynchronizedChartTooltip,b as TOOLTIP_BRAND_CAP};
|
|
@@ -2,6 +2,7 @@ import { SxProps } from '@mui/material';
|
|
|
2
2
|
import { SeriesChartData } from '../SeriesChart';
|
|
3
3
|
import { BrandFilter } from '../../../components/BrandFilterInput';
|
|
4
4
|
import { BrandIntervalData } from '../ConversionOverTimeChart';
|
|
5
|
+
import { MetricsIntervalType } from '../../../constants/metrics';
|
|
5
6
|
export interface SubChartConfig {
|
|
6
7
|
title: string;
|
|
7
8
|
data: SeriesChartData[];
|
|
@@ -13,6 +14,16 @@ export interface SubChartConfig {
|
|
|
13
14
|
* opt out of the Total line and the Log Scale toggle.
|
|
14
15
|
*/
|
|
15
16
|
isPercentage?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Pooled value per timestamp for a percentage sub-chart:
|
|
19
|
+
* `sum(numerator) / sum(denominator)` across the visible brands, which is the
|
|
20
|
+
* overall rate. Averaging the per-brand rates instead would weight a brand
|
|
21
|
+
* with two events the same as one with two thousand.
|
|
22
|
+
*
|
|
23
|
+
* Not drawn - percentage sub-charts have no Total line. It exists so the
|
|
24
|
+
* trend has a series to fit when more than one brand is selected.
|
|
25
|
+
*/
|
|
26
|
+
totalByDate?: Record<number, number>;
|
|
16
27
|
}
|
|
17
28
|
export type SynchronizedSubChartConfig = {
|
|
18
29
|
title: string;
|
|
@@ -40,6 +51,11 @@ type SynchronizedMetricsChartBaseProps = {
|
|
|
40
51
|
filter: {
|
|
41
52
|
timezone?: string;
|
|
42
53
|
brands: BrandFilter[];
|
|
54
|
+
/**
|
|
55
|
+
* The selected bucket size. The trend readout reports its slope per this
|
|
56
|
+
* interval; when omitted it is named from the spacing in the data.
|
|
57
|
+
*/
|
|
58
|
+
interval?: MetricsIntervalType;
|
|
43
59
|
};
|
|
44
60
|
sx?: SxProps;
|
|
45
61
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { MetricsIntervalType } from '../../constants/metrics';
|
|
2
|
+
export interface TrendFit {
|
|
3
|
+
/** Change per data point. */
|
|
4
|
+
slope: number;
|
|
5
|
+
intercept: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Ordinary least squares of `values` against `xs`, so the slope is per unit of
|
|
9
|
+
* x. Pass timestamps for `xs` whenever the samples are not evenly spaced - the
|
|
10
|
+
* metrics API only returns buckets that contain data, so a series can jump four
|
|
11
|
+
* days between two "hourly" points. Fitting against the index there would
|
|
12
|
+
* measure change per *returned row* and report it as change per hour.
|
|
13
|
+
*
|
|
14
|
+
* `xs` defaults to the point index, which is only correct for evenly spaced
|
|
15
|
+
* input.
|
|
16
|
+
*/
|
|
17
|
+
export declare function olsFit(values: number[], xs?: number[]): TrendFit;
|
|
18
|
+
/**
|
|
19
|
+
* Median gap between consecutive timestamps, used to name the bucket when the
|
|
20
|
+
* caller doesn't tell us which interval is selected.
|
|
21
|
+
*/
|
|
22
|
+
export declare function medianStepMs(dates: number[]): number;
|
|
23
|
+
/** Average lengths - months and quarters vary, so these are used for matching only. */
|
|
24
|
+
export declare const INTERVAL_MS: Record<MetricsIntervalType, number>;
|
|
25
|
+
/**
|
|
26
|
+
* Names the bucket from the spacing actually present in the data. Matching is
|
|
27
|
+
* on the ratio rather than the difference, so a 31-day month doesn't get
|
|
28
|
+
* mistaken for a quarter.
|
|
29
|
+
*/
|
|
30
|
+
export declare function intervalFromStep(stepMs: number): MetricsIntervalType | null;
|
|
31
|
+
/** Compact label for a bucket that matches no named interval, e.g. `8h`. */
|
|
32
|
+
export declare function formatStepShort(stepMs: number): string;
|
|
33
|
+
export interface TrendSeries {
|
|
34
|
+
/** Fitted value per point, aligned with the input rows. */
|
|
35
|
+
values: number[];
|
|
36
|
+
/**
|
|
37
|
+
* Change per interval bucket, measured against elapsed time rather than row
|
|
38
|
+
* count so gaps in the data don't inflate it.
|
|
39
|
+
*/
|
|
40
|
+
slopePerInterval: number;
|
|
41
|
+
/** Measured spacing, so the caller can name the bucket. */
|
|
42
|
+
stepMs: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Fits `dataKey` across `rows` against their timestamps and returns the fitted
|
|
46
|
+
* column plus its slope per interval. Returns null when there is nothing
|
|
47
|
+
* meaningful to fit.
|
|
48
|
+
*/
|
|
49
|
+
export declare function trendSeries(rows: Array<Record<string, number>>, dataKey: string, options?: {
|
|
50
|
+
clampTo?: [number, number];
|
|
51
|
+
/** Bucket the slope is reported in. Defaults to the measured spacing. */
|
|
52
|
+
interval?: MetricsIntervalType;
|
|
53
|
+
}): TrendSeries | null;
|
|
54
|
+
export interface FormatSlopeOptions {
|
|
55
|
+
/** `percent` reports percentage points, since a change in a rate is not a rate. */
|
|
56
|
+
unit?: 'count' | 'percent';
|
|
57
|
+
/** The selected interval. Falls back to naming the measured spacing. */
|
|
58
|
+
interval?: MetricsIntervalType | null;
|
|
59
|
+
stepMs?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* `+4.2 / day`, `+18 / hour`, `+150 / month` - the slope is always reported per
|
|
63
|
+
* whichever interval the operator is looking at.
|
|
64
|
+
*/
|
|
65
|
+
export declare function formatSlope(value: number, { unit, interval, stepMs }?: FormatSlopeOptions): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";import{MetricsInterval as c}from"../../constants/metrics.mjs";function M(e,o){const n=e.length;if(n<2)return{slope:0,intercept:e[0]??0};const r=o??e.map((s,a)=>a),t=r.reduce((s,a)=>s+a,0)/n,l=e.reduce((s,a)=>s+a,0)/n;let m=0,i=0;for(let s=0;s<n;s+=1)m+=(r[s]-t)*(e[s]-l),i+=(r[s]-t)**2;const u=i===0?0:m/i;return{slope:u,intercept:l-u*t}}function N(e){if(e.length<2)return 0;const o=[];for(let r=1;r<e.length;r+=1)o.push(e[r]-e[r-1]);o.sort((r,t)=>r-t);const n=Math.floor(o.length/2);return o.length%2===0?(o[n-1]+o[n])/2:o[n]}const h=6e4,f=60*h,p=24*f,I={[c.MINUTE]:h,[c.HOUR]:f,[c.DAY]:p,[c.WEEK]:7*p,[c.MONTH]:30.44*p,[c.QUARTER]:91.31*p,[c.YEAR]:365.25*p};function d(e){if(e<=0)return null;let o=null,n=Number.POSITIVE_INFINITY;for(const[r,t]of Object.entries(I)){const l=e>t?e/t:t/e;l<n&&(n=l,o=r)}return n<=1.5?o:null}function S(e){return e<=0?"":e>=p?`${+(e/p).toFixed(1)}d`:e>=f?`${+(e/f).toFixed(1)}h`:`${Math.max(1,Math.round(e/h))}m`}function E(e,o,n={}){if(e.length<2)return null;const r=e.map(a=>Number(a[o])||0),t=e.map(a=>Number(a.date)),l=M(r,t),[m,i]=n.clampTo??[0,Number.POSITIVE_INFINITY],u=N(t),s=n.interval?I[n.interval]:u;return{values:t.map(a=>Math.min(i,Math.max(m,l.intercept+l.slope*a))),slopePerInterval:l.slope*s,stepMs:u}}function T(e,{unit:o="count",interval:n,stepMs:r=0}={}){const t=Math.abs(e),l=e>0?"+":e<0?"-":"",m=t>=10?0:t>=1?1:2,i=`${l}${t.toFixed(m)}${o==="percent"?" pp":""}`,u=n??d(r)??S(r);return u?`${i} / ${u}`:i}export{I as INTERVAL_MS,T as formatSlope,S as formatStepShort,d as intervalFromStep,N as medianStepMs,M as olsFit,E as trendSeries};
|
package/package.json
CHANGED