@sybilion/uilib 1.3.55 → 1.3.56

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.
@@ -5,16 +5,16 @@ import { getForecastColor, ChartLines } from '../../ChartAreaInteractive/ChartLi
5
5
  import { Skeleton } from '../../Skeleton/Skeleton.js';
6
6
  import { chartRenderQueue } from '../../../../utils/chartRenderQueue.js';
7
7
  import { Tooltip, LineChart, ComposedChart } from 'recharts';
8
- import { ChartContainer } from './ChartContainer.js';
9
- import { ChartTooltipContent } from './ChartTooltipContent.js';
10
- import { CustomChartLegend } from './CustomChartLegend/CustomChartLegend.js';
11
- import { QuantileBands } from './QuantileBands.js';
12
8
  import { resolveChartMargin, getPlotViewBox } from '../tools/chartPlotGeometry.js';
13
9
  import { formatDate } from '../tools/formatters.js';
14
10
  import S from './BaseChartWrapper.styl.js';
15
11
  import { ChartAxes } from './ChartAxes.js';
12
+ import { ChartContainer } from './ChartContainer.js';
16
13
  import { ChartGrid } from './ChartGrid.js';
14
+ import { ChartTooltipContent } from './ChartTooltipContent.js';
15
+ import { CustomChartLegend } from './CustomChartLegend/CustomChartLegend.js';
17
16
  import { LegendSvg } from './LegendSvg/LegendSvg.js';
17
+ import { QuantileBands } from './QuantileBands.js';
18
18
 
19
19
  function clampTooltipTranslate(args) {
20
20
  const { coordinate, viewBox, tooltipWidth: tw, tooltipHeight: th, offset, edgeMargin, } = args;
@@ -48,8 +48,9 @@ BaseChartWrapperLoading.displayName = 'BaseChartWrapperLoading';
48
48
  * Separated to maintain hook order consistency
49
49
  */
50
50
  const BaseChartWrapperContent = forwardRef((props, ref) => {
51
- const { chartConfig = {}, chartData, historicalLineColor, forecastData = [], loading, hasCombinedData, renderId, isDarkTheme, height, className, loadingComponentClassName, footerClassName, chartClassName, xAxisClassName, yAxisClassName, legendClassName, footerActions, quantileBands, quantileBandKey, xMin, xMax, yMin, yMax, autoScaleYAxis = true, formatNumber, formatDate: formatDateFn = formatDate, labelFormatter, onLegendClick, margin, chartType = 'composed', disableAnimation = false, showGrid = true, showAxes = true, showTooltip = true, showLegend = true, showChartAxesLegend = true, xAxisLabel, yAxisLabel, showActiveDots = true, overlayElements, hiddenSeries, excludeLegendIds, onAnalysisSelect, onFailedAnalysisClick, containerProps, error, loadingMessage, noDataMessage = 'No data available', onGridHeightChange, forecastLineStyle = 'dashed', disableHistoricalAnimation = false, onShowAll: _onShowAll, onShowOnly: _onShowOnly, maxVisibleItems, preventDeselection, legendVariant = 'default', legendWidth = 1000, legendMarginLeft = 0, } = props;
51
+ const { chartConfig = {}, chartData, historicalLineColor, forecastData = [], loading, hasCombinedData, renderId, isDarkTheme, height, className, loadingComponentClassName, footerClassName, chartClassName, xAxisClassName, yAxisClassName, legendClassName, footerActions, quantileBands, quantileBandKey, xMin, xMax, yMin, yMax, autoScaleYAxis = true, formatNumber, formatDate: formatDateFn = formatDate, labelFormatter, onLegendClick, margin, chartType = 'composed', disableAnimation = false, disableLineDrawAnimation = false, showGrid = true, showAxes = true, showTooltip = true, showLegend = true, showChartAxesLegend = true, xAxisLabel, yAxisLabel, showActiveDots = true, overlayElements, hiddenSeries, excludeLegendIds, onAnalysisSelect, onFailedAnalysisClick, containerProps, error, loadingMessage, noDataMessage = 'No data available', onGridHeightChange, forecastLineStyle = 'dashed', disableHistoricalAnimation = false, onShowAll: _onShowAll, onShowOnly: _onShowOnly, maxVisibleItems, preventDeselection, legendVariant = 'default', legendWidth = 1000, legendMarginLeft = 0, } = props;
52
52
  const [shouldAnimate, setShouldAnimate] = useState(false);
53
+ const lineDataInitializedRef = useRef(false);
53
54
  const rootRef = useRef(null);
54
55
  const setRefs = (node) => {
55
56
  rootRef.current = node;
@@ -238,13 +239,20 @@ const BaseChartWrapperContent = forwardRef((props, ref) => {
238
239
  }));
239
240
  }, [forecastData, excludeLegendIds, showLegend]);
240
241
  useEffect(() => {
241
- if (disableAnimation)
242
+ if (disableAnimation) {
243
+ setShouldAnimate(false);
242
244
  return;
245
+ }
246
+ if (disableLineDrawAnimation && !lineDataInitializedRef.current) {
247
+ lineDataInitializedRef.current = true;
248
+ setShouldAnimate(false);
249
+ return;
250
+ }
243
251
  // Enable animation briefly when line data changes (not quantile band data)
244
252
  setShouldAnimate(true);
245
253
  const timer = setTimeout(() => setShouldAnimate(false), 1000);
246
254
  return () => clearTimeout(timer);
247
- }, [lineDataForAnimation, disableAnimation]);
255
+ }, [lineDataForAnimation, disableAnimation, disableLineDrawAnimation]);
248
256
  const isLoaded = useMemo(() => !loading && chartData.length > 0, [loading, chartData.length]);
249
257
  // const resizePinContainer = useThrottleCallback(() => {
250
258
  // const grid = rootRef.current?.querySelector('.recharts-cartesian-grid');
@@ -307,7 +315,7 @@ const BaseChartWrapperContent = forwardRef((props, ref) => {
307
315
  }
308
316
  const ChartComponent = chartType === 'line' ? LineChart : ComposedChart;
309
317
  const defaultLabelFormatter = (v) => formatDateFn(v, true);
310
- return (jsxs("div", { className: cn(S.root, !showLegend && S.noLegend, !showChartAxesLegend && S.hideChartAxesLegend, isLoaded && S.loaded, className), ref: setRefs, children: [loading && (jsx("div", { className: S.loadingOverlay, children: jsx(Skeleton, {}) })), showGrid && (jsx(ChartContainer, { config: chartConfig, className: cn(S.gridLayer, chartClassName), style: height ? { height: `${height}px` } : undefined, children: jsxs(ChartComponent, { data: chartData, margin: margin, children: [jsx(ChartGrid, {}), showAxes && (jsx(ChartAxes, { formatDate: formatDateFn, formatNumber: formatNumber, xAxisClassName: xAxisClassName, yAxisClassName: yAxisClassName, xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax, autoScaleYAxis: effectiveAutoScale }))] }) })), jsx(ChartContainer, { config: chartConfig, className: cn(S.chartLayer, chartClassName), style: height ? { height: `${height}px` } : undefined, ...containerProps, children: jsxs(ChartComponent, { data: chartData, margin: margin, children: [showAxes && (jsx(ChartAxes, { formatDate: formatDateFn, formatNumber: formatNumber, xAxisClassName: cn(xAxisClassName), yAxisClassName: cn(yAxisClassName), xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax, autoScaleYAxis: effectiveAutoScale })), quantileBands?.[0] && (jsx(QuantileBands, { hiddenBands: hiddenSeries, quantileBandKey: quantileBandKey, animate: true, animationDuration: 150, animationBegin: 0, customBands: quantileBands, showLegend: showLegend })), jsx(ChartLines, { historicalLineColor: historicalLineColor, chartData: chartData, forecastData: forecastData, hiddenSeries: hiddenSeries, isDarkTheme: isDarkTheme, shouldAnimate: shouldAnimate, showLegend: showLegend, forecastLineStyle: forecastLineStyle }), showTooltip && (jsx("div", { children: jsx(Tooltip, { cursor: false, offset: TOOLTIP_OFFSET, allowEscapeViewBox: { x: false, y: false }, content: renderTooltipContent }) }))] }) }), overlayElements, jsxs("div", { className: cn(S.footer, footerClassName), children: [showLegend &&
318
+ return (jsxs("div", { className: cn(S.root, !showLegend && S.noLegend, !showChartAxesLegend && S.hideChartAxesLegend, disableLineDrawAnimation && S.noLineDrawAnimation, isLoaded && S.loaded, className), ref: setRefs, children: [loading && (jsx("div", { className: S.loadingOverlay, children: jsx(Skeleton, {}) })), showGrid && (jsx(ChartContainer, { config: chartConfig, className: cn(S.gridLayer, chartClassName), style: height ? { height: `${height}px` } : undefined, children: jsxs(ChartComponent, { data: chartData, margin: margin, isAnimationActive: !disableAnimation, children: [jsx(ChartGrid, {}), showAxes && (jsx(ChartAxes, { formatDate: formatDateFn, formatNumber: formatNumber, xAxisClassName: xAxisClassName, yAxisClassName: yAxisClassName, xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax, autoScaleYAxis: effectiveAutoScale }))] }) })), jsx(ChartContainer, { config: chartConfig, className: cn(S.chartLayer, chartClassName), style: height ? { height: `${height}px` } : undefined, ...containerProps, children: jsxs(ChartComponent, { data: chartData, margin: margin, isAnimationActive: !disableAnimation, children: [showAxes && (jsx(ChartAxes, { formatDate: formatDateFn, formatNumber: formatNumber, xAxisClassName: cn(xAxisClassName), yAxisClassName: cn(yAxisClassName), xAxisLabel: xAxisLabel, yAxisLabel: yAxisLabel, xMin: xMin, xMax: xMax, yMin: yMin, yMax: yMax, autoScaleYAxis: effectiveAutoScale })), quantileBands?.[0] && (jsx(QuantileBands, { hiddenBands: hiddenSeries, quantileBandKey: quantileBandKey, animate: true, animationDuration: 150, animationBegin: 0, customBands: quantileBands, showLegend: showLegend })), jsx(ChartLines, { historicalLineColor: historicalLineColor, chartData: chartData, forecastData: forecastData, hiddenSeries: hiddenSeries, isDarkTheme: isDarkTheme, shouldAnimate: shouldAnimate, disableAnimation: disableAnimation, disableHistoricalAnimation: disableHistoricalAnimation, showLegend: showLegend, forecastLineStyle: forecastLineStyle }), showTooltip && (jsx("div", { children: jsx(Tooltip, { cursor: false, offset: TOOLTIP_OFFSET, allowEscapeViewBox: { x: false, y: false }, content: renderTooltipContent }) }))] }) }), overlayElements, jsxs("div", { className: cn(S.footer, footerClassName), children: [showLegend &&
311
319
  (legendVariant === 'svg' ? (jsx(LegendSvg, { payload: legendPayload.map(p => ({
312
320
  value: p.value,
313
321
  color: p.color,
@@ -1,7 +1,7 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = ".BaseChartWrapper_chartContainer__J6CIc,.BaseChartWrapper_root__hhfho{min-height:100%}.BaseChartWrapper_root__hhfho{position:relative;width:100%}.BaseChartWrapper_gridLayer__cwJXA{pointer-events:none;position:absolute;z-index:1}.BaseChartWrapper_gridLayer__cwJXA .recharts-cartesian-axis{pointer-events:auto;z-index:1}.BaseChartWrapper_chartLayer__HmBaJ{inset:0;pointer-events:auto;position:relative;z-index:3}.BaseChartWrapper_chartLayer__HmBaJ .recharts-surface{background:transparent}.BaseChartWrapper_chartLayer__HmBaJ .recharts-layer{pointer-events:none}.BaseChartWrapper_chartLayer__HmBaJ .recharts-tooltip-wrapper{visibility:visible!important}.BaseChartWrapper_invisibleAxis__TQ-Zk .recharts-cartesian-axis-tick{visibility:hidden}.BaseChartWrapper_legend__gkSy1{position:absolute;top:100%;width:100%}.BaseChartWrapper_noLegend__jqaPa{display:block}.BaseChartWrapper_hideChartAxesLegend__dgVx- .recharts-cartesian-axis-tick-label{display:none!important}.BaseChartWrapper_chartLoadingContainer__AJP-0{align-items:center;display:flex;height:var(--chart-height);justify-content:center;width:100%}.BaseChartWrapper_loadingOverlay__GcOkA{height:100%;inset:0;opacity:.5;pointer-events:none;position:absolute;transition:opacity .1s ease-out;width:100%;z-index:10}.BaseChartWrapper_loadingOverlay__GcOkA [data-slot=skeleton]{border-radius:var(--p-4)!important}.BaseChartWrapper_loaded__Gj15C .BaseChartWrapper_loadingOverlay__GcOkA{opacity:0}.BaseChartWrapper_errorContainer__1bXy1{align-items:center;color:var(--destructive);display:flex;justify-content:center;min-height:250px}.BaseChartWrapper_errorMessage__veSmB{color:var(--destructive)}.BaseChartWrapper_footer__nP5u6{align-items:center;display:flex;gap:var(--p-2);justify-content:space-between}";
4
- var S = {"root":"BaseChartWrapper_root__hhfho","chartContainer":"BaseChartWrapper_chartContainer__J6CIc","gridLayer":"BaseChartWrapper_gridLayer__cwJXA","chartLayer":"BaseChartWrapper_chartLayer__HmBaJ","invisibleAxis":"BaseChartWrapper_invisibleAxis__TQ-Zk","legend":"BaseChartWrapper_legend__gkSy1","noLegend":"BaseChartWrapper_noLegend__jqaPa","hideChartAxesLegend":"BaseChartWrapper_hideChartAxesLegend__dgVx-","chartLoadingContainer":"BaseChartWrapper_chartLoadingContainer__AJP-0","loadingOverlay":"BaseChartWrapper_loadingOverlay__GcOkA","loaded":"BaseChartWrapper_loaded__Gj15C","errorContainer":"BaseChartWrapper_errorContainer__1bXy1","errorMessage":"BaseChartWrapper_errorMessage__veSmB","footer":"BaseChartWrapper_footer__nP5u6"};
3
+ var css_248z = ".BaseChartWrapper_chartContainer__J6CIc,.BaseChartWrapper_root__hhfho{min-height:100%}.BaseChartWrapper_root__hhfho{position:relative;width:100%}.BaseChartWrapper_gridLayer__cwJXA{pointer-events:none;position:absolute;z-index:1}.BaseChartWrapper_gridLayer__cwJXA .recharts-cartesian-axis{pointer-events:auto;z-index:1}.BaseChartWrapper_chartLayer__HmBaJ{inset:0;pointer-events:auto;position:relative;z-index:3}.BaseChartWrapper_chartLayer__HmBaJ .recharts-surface{background:transparent}.BaseChartWrapper_chartLayer__HmBaJ .recharts-layer{pointer-events:none}.BaseChartWrapper_chartLayer__HmBaJ .recharts-tooltip-wrapper{visibility:visible!important}.BaseChartWrapper_invisibleAxis__TQ-Zk .recharts-cartesian-axis-tick{visibility:hidden}.BaseChartWrapper_legend__gkSy1{position:absolute;top:100%;width:100%}.BaseChartWrapper_noLegend__jqaPa{display:block}.BaseChartWrapper_hideChartAxesLegend__dgVx- .recharts-cartesian-axis-tick-label{display:none!important}.BaseChartWrapper_chartLoadingContainer__AJP-0{align-items:center;display:flex;height:var(--chart-height);justify-content:center;width:100%}.BaseChartWrapper_loadingOverlay__GcOkA{height:100%;inset:0;opacity:.5;pointer-events:none;position:absolute;transition:opacity .1s ease-out;width:100%;z-index:10}.BaseChartWrapper_loadingOverlay__GcOkA [data-slot=skeleton]{border-radius:var(--p-4)!important}.BaseChartWrapper_loaded__Gj15C .BaseChartWrapper_loadingOverlay__GcOkA{opacity:0}.BaseChartWrapper_errorContainer__1bXy1{align-items:center;color:var(--destructive);display:flex;justify-content:center;min-height:250px}.BaseChartWrapper_errorMessage__veSmB{color:var(--destructive)}.BaseChartWrapper_noLineDrawAnimation__cyrzV .recharts-line-curve{stroke-dasharray:inherit!important}.BaseChartWrapper_footer__nP5u6{align-items:center;display:flex;gap:var(--p-2);justify-content:space-between}";
4
+ var S = {"root":"BaseChartWrapper_root__hhfho","chartContainer":"BaseChartWrapper_chartContainer__J6CIc","gridLayer":"BaseChartWrapper_gridLayer__cwJXA","chartLayer":"BaseChartWrapper_chartLayer__HmBaJ","invisibleAxis":"BaseChartWrapper_invisibleAxis__TQ-Zk","legend":"BaseChartWrapper_legend__gkSy1","noLegend":"BaseChartWrapper_noLegend__jqaPa","hideChartAxesLegend":"BaseChartWrapper_hideChartAxesLegend__dgVx-","chartLoadingContainer":"BaseChartWrapper_chartLoadingContainer__AJP-0","loadingOverlay":"BaseChartWrapper_loadingOverlay__GcOkA","loaded":"BaseChartWrapper_loaded__Gj15C","errorContainer":"BaseChartWrapper_errorContainer__1bXy1","errorMessage":"BaseChartWrapper_errorMessage__veSmB","noLineDrawAnimation":"BaseChartWrapper_noLineDrawAnimation__cyrzV","footer":"BaseChartWrapper_footer__nP5u6"};
5
5
  styleInject(css_248z);
6
6
 
7
7
  export { S as default };
@@ -23,7 +23,7 @@ function resolveQuantileBandFillForForecastLine(lineColor, forecastIndex) {
23
23
  return mapped ?? getForecastQuantileBandColor(forecastIndex);
24
24
  }
25
25
  // Memoized component for chart lines - only re-renders when data/analyses/hiddenSeries change
26
- const ChartLines = memo(({ chartData, forecastData, hiddenSeries, isDarkTheme, shouldAnimate, historicalLineColor = isDarkTheme ? '#ffffff' : '#000000', showLegend = true, forecastLineStyle = 'dashed', disableHistoricalAnimation = false, }) => {
26
+ const ChartLines = memo(({ chartData, forecastData, hiddenSeries, isDarkTheme, shouldAnimate, historicalLineColor = isDarkTheme ? '#ffffff' : '#000000', showLegend = true, forecastLineStyle = 'dashed', disableAnimation = false, disableHistoricalAnimation = false, }) => {
27
27
  const dotStroke = isDarkTheme ? '#000000' : '#FFFFFF';
28
28
  const activeDotHistorical = {
29
29
  r: 6,
@@ -75,10 +75,15 @@ const ChartLines = memo(({ chartData, forecastData, hiddenSeries, isDarkTheme, s
75
75
  };
76
76
  const renderHistoricalDot = (props) => renderDot(props, lastHistoricalIndex, 'historical', historicalLineColor);
77
77
  const renderForecastDot = (props, id, color) => renderDot(props, lastForecastIndices[id] ?? -1, `forecast_${id}`, color);
78
- return (jsxs(Fragment, { children: [jsx(Line, { type: "monotone", dataKey: "historical", stroke: historicalLineColor, strokeWidth: 1, dot: props => renderHistoricalDot(props), activeDot: activeDotHistorical, name: "Historical Data", legendType: showLegend === false ? 'none' : undefined, strokeDasharray: "0", hide: hiddenSeries?.has('historical'), isAnimationActive: shouldAnimate && !disableHistoricalAnimation, animationDuration: disableHistoricalAnimation ? 0 : ANIMATION_DURATION, animationBegin: 0, animationEasing: "ease-out" }, "historical"), forecastData?.map((data, index) => {
78
+ const animationActive = Boolean(shouldAnimate) && !disableAnimation;
79
+ const historicalAnimationActive = animationActive && !disableHistoricalAnimation;
80
+ const forecastAnimationActive = animationActive;
81
+ const animationDuration = disableAnimation ? 0 : ANIMATION_DURATION;
82
+ const valueAnimationEnabled = !disableAnimation;
83
+ return (jsxs(Fragment, { children: [jsx(Line, { type: "monotone", dataKey: "historical", stroke: historicalLineColor, strokeWidth: 1, dot: props => renderHistoricalDot(props), activeDot: activeDotHistorical, name: "Historical Data", legendType: showLegend === false ? 'none' : undefined, strokeDasharray: "0", hide: hiddenSeries?.has('historical'), isAnimationActive: historicalAnimationActive, animationDuration: animationDuration, animateNewValues: valueAnimationEnabled, animationBegin: 0, animationEasing: "ease-out" }, "historical"), forecastData?.map((data, index) => {
79
84
  const { id, name, color = FORECAST_LINE_COLORS[index % FORECAST_LINE_COLORS.length], } = data;
80
85
  const dataKey = `forecast_${id}`;
81
- return (jsx(Line, { type: "monotone", dataKey: dataKey, stroke: color, strokeWidth: 1, strokeDasharray: forecastLineStyle === 'dashed' ? '4 2' : '0', dot: props => renderForecastDot(props, id, color), activeDot: activeDotForecast(color), name: `${name || `${id}`}`, legendType: showLegend === false ? 'none' : undefined, hide: hiddenSeries?.has(dataKey), isAnimationActive: shouldAnimate, animationDuration: ANIMATION_DURATION, animationBegin: 0, animationEasing: "ease-out", className: "chart-line-blinking" }, `forecast-${id}`));
86
+ return (jsx(Line, { type: "monotone", dataKey: dataKey, stroke: color, strokeWidth: 1, strokeDasharray: forecastLineStyle === 'dashed' ? '4 2' : '0', dot: props => renderForecastDot(props, id, color), activeDot: activeDotForecast(color), name: `${name || `${id}`}`, legendType: showLegend === false ? 'none' : undefined, hide: hiddenSeries?.has(dataKey), isAnimationActive: forecastAnimationActive, animationDuration: animationDuration, animateNewValues: valueAnimationEnabled, animationBegin: 0, animationEasing: "ease-out", className: data.status === 'pending' ? 'chart-line-blinking' : undefined }, `forecast-${id}`));
82
87
  })] }));
83
88
  });
84
89
  ChartLines.displayName = 'ChartLines';
@@ -114,7 +114,8 @@ function DriversComparisonChart({ payload, datasetHistorical = [], loading = fal
114
114
  };
115
115
  });
116
116
  }, [originalDriversById, sortedDriversWithData, viewTab]);
117
- const seriesInitKeyResolved = `${seriesInitKey ?? `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`}:${viewTab}`;
117
+ const seriesInitKeyResolved = seriesInitKey ??
118
+ `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`;
118
119
  const backtestsSeriesInitKeyRef = useRef('');
119
120
  useEffect(() => {
120
121
  if (tableSeriesRows.length === 0)
@@ -136,7 +137,7 @@ function DriversComparisonChart({ payload, datasetHistorical = [], loading = fal
136
137
  }, [seriesInitKeyResolved, tableSeriesRows]);
137
138
  const driversComparisonChartData = useMemo(() => buildDriversComparisonChartData(mergedWithHistorical, datasetHistorical, chartForecastData.map(f => f.id)), [chartForecastData, datasetHistorical, mergedWithHistorical]);
138
139
  const showEmptyOverlay = (runAnalysisHint || Boolean(statusHint)) && !loading;
139
- return (jsxs("div", { className: cn(S.root, className), children: [jsxs("div", { className: cn(S.chartShell, loading && S.chartShellLoading), children: [jsx("div", { className: S.chartSlot, children: jsxs("div", { className: S.chartWithOverlay, children: [jsx("div", { className: cn(S.chartInteractiveLayer, showEmptyOverlay && S.chartInteractiveDimmed), children: jsx(ChartAreaInteractive, { chartRenderId: `drivers-comparison-${viewTab}`, disableHistoricalAnimation: true, disableTimeRangeSelector: true, enableTimeRangeBrush: true, chartContainerClassName: S.chartContainer, chartData: driversComparisonChartData, forecastData: chartForecastData, timeRange: timeRange, onTimeRangeChange: handleTimeRangeChange, pinMonth: undefined, onPinMonthChange: () => { }, isDarkTheme: isDarkTheme, loading: chartLoading, hasCombinedData: mergedWithHistorical.length > 0, forecastLineStyle: "solid", showLegend: false, hiddenSeries: hiddenSeries, toggleLegendSeries: toggleSeries, ensureAnalysisSeriesVisible: showSeries }, viewTab) }), showEmptyOverlay && (jsx("div", { className: S.chartEmptyOverlay, role: "status", "aria-live": "polite", children: jsx("div", { className: S.chartEmptyBlurb, children: jsx(ChartEmptyState, { variant: "inline", hint: runAnalysisHint
140
+ return (jsxs("div", { className: cn(S.root, className), children: [jsxs("div", { className: cn(S.chartShell, loading && S.chartShellLoading), children: [jsx("div", { className: S.chartSlot, children: jsxs("div", { className: S.chartWithOverlay, children: [jsx("div", { className: cn(S.chartInteractiveLayer, showEmptyOverlay && S.chartInteractiveDimmed), children: jsx(ChartAreaInteractive, { chartRenderId: "drivers-comparison", disableLineDrawAnimation: true, disableHistoricalAnimation: true, disableTimeRangeSelector: true, enableTimeRangeBrush: true, chartContainerClassName: S.chartContainer, chartData: driversComparisonChartData, forecastData: chartForecastData, timeRange: timeRange, onTimeRangeChange: handleTimeRangeChange, pinMonth: undefined, onPinMonthChange: () => { }, isDarkTheme: isDarkTheme, loading: chartLoading, hasCombinedData: mergedWithHistorical.length > 0, forecastLineStyle: "solid", showLegend: false, hiddenSeries: hiddenSeries, toggleLegendSeries: toggleSeries, ensureAnalysisSeriesVisible: showSeries }) }), showEmptyOverlay && (jsx("div", { className: S.chartEmptyOverlay, role: "status", "aria-live": "polite", children: jsx("div", { className: S.chartEmptyBlurb, children: jsx(ChartEmptyState, { variant: "inline", hint: runAnalysisHint
140
141
  ? 'Run a completed analysis to load the drivers comparison chart for an analysis.'
141
142
  : undefined, status: statusHint ?? undefined, statusTone: statusTone }) }) }))] }) }), loading && (jsx("div", { className: S.loadingLayer, "aria-busy": "true", "aria-live": "polite", children: jsx("div", { className: S.loadingMessage, children: jsx(TextShimmer, { as: "span", className: S.loadingText, children: "Loading drivers comparison\u2026" }) }) }))] }), jsx("div", { className: S.seriesSection, children: tableSeriesRows.length === 0 ? (jsx("div", { className: S.seriesEmptyWrap, children: jsx("div", { className: S.chartEmptyBlurb, children: jsx(ChartEmptyState, { variant: "inline", align: "center", status: "No series" }) }) })) : (jsx("div", { className: S.seriesTableWrapper, children: jsx(PageXScroll, { size: "md", fullWidth: true, innerClassName: S.seriesTableContainer, scrollbarClassName: S.seriesScrollbar, children: jsxs(Table, { withBackground: true, withPaddings: true, className: S.seriesTable, children: [jsx(TableHeader, { children: jsxs(TableRow, { children: [jsx(TableHead, { className: S.seriesColSeries, children: "Driver name" }), jsx(TableHead, { children: jsxs(TableCellValue, { children: ["Importance", jsxs(Tooltip, { children: [jsx(TooltipTrigger, { asChild: true, children: jsx("span", { children: jsx(InfoIcon, { size: 16, style: { cursor: 'help' } }) }) }), jsx(TooltipContent, { side: "top", maxWidth: 300, children: "How much this driver contributes to price movements in the forecast model. Higher = stronger influence. Reflects relative weight of each driver, scored from 0% to 100%." })] })] }) }), jsx(TableHead, { children: jsxs(TableCellValue, { children: ["Lag", jsxs(Tooltip, { children: [jsx(TooltipTrigger, { asChild: true, children: jsx("span", { children: jsx(InfoIcon, { size: 16, style: { cursor: 'help' } }) }) }), jsx(TooltipContent, { side: "top", maxWidth: 300, children: "Lag shows how far ahead this driver predicts price movement. A range (e.g. 9\u201312 months) means the predictive signal is strongest somewhere in that window \u2014 not at a single fixed point." })] })] }) })] }) }), jsx(TableBody, { children: tableSeriesRows.map(row => {
142
143
  const dataKey = toForecastDataKey(row.id);
@@ -15,8 +15,7 @@ function resolveDriverLagLabel(driver) {
15
15
  if (typeof driver.lag === 'string' && driver.lag.trim()) {
16
16
  return driver.lag;
17
17
  }
18
- const raw = driver;
19
- const overallLag = raw.overall_lag ?? raw.overallLag;
18
+ const overallLag = driver.overall_lag ?? driver.overallLag;
20
19
  if (typeof overallLag === 'string' && overallLag.trim()) {
21
20
  return overallLag;
22
21
  }
@@ -2,9 +2,9 @@ import { ComponentProps, ReactNode } from 'react';
2
2
  import type { QuantileBandConfig } from '#uilib/components/ui/Chart/chartForecastVisualization.types';
3
3
  import { ChartDataPoint } from '#uilib/components/ui/ChartAreaInteractive/ChartAreaInteractive.types';
4
4
  import { ForecastItemData } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
5
+ import { LegendPayload } from 'recharts/types/component/DefaultLegendContent';
5
6
  import type { ChartConfig } from '../Chart.types';
6
7
  import { ChartContainer } from './ChartContainer';
7
- import { LegendPayload } from 'recharts/types/component/DefaultLegendContent';
8
8
  export interface BaseChartWrapperProps {
9
9
  renderId?: string;
10
10
  chartConfig?: ChartConfig;
@@ -43,6 +43,11 @@ export interface BaseChartWrapperProps {
43
43
  children?: ReactNode;
44
44
  chartType?: 'composed' | 'line';
45
45
  disableAnimation?: boolean;
46
+ /**
47
+ * Skip left-to-right line path draw; still animate Y on data updates.
48
+ * Independent of `disableAnimation` (full off).
49
+ */
50
+ disableLineDrawAnimation?: boolean;
46
51
  showGrid?: boolean;
47
52
  showAxes?: boolean;
48
53
  showTooltip?: boolean;
@@ -34,7 +34,7 @@ export declare const getForecastColor: (index: number) => string;
34
34
  export declare const getForecastQuantileBandColor: (index: number) => any;
35
35
  /** Same tint logic as intervals/threshold overlays: band fill follows forecast line hex when known. */
36
36
  export declare function resolveQuantileBandFillForForecastLine(lineColor: string, forecastIndex: number): string;
37
- export declare const ChartLines: import("react").MemoExoticComponent<({ chartData, forecastData, hiddenSeries, isDarkTheme, shouldAnimate, historicalLineColor, showLegend, forecastLineStyle, disableHistoricalAnimation, }: {
37
+ export declare const ChartLines: import("react").MemoExoticComponent<({ chartData, forecastData, hiddenSeries, isDarkTheme, shouldAnimate, historicalLineColor, showLegend, forecastLineStyle, disableAnimation, disableHistoricalAnimation, }: {
38
38
  chartData: ChartDataPoint[];
39
39
  forecastData?: ForecastItemData[];
40
40
  hiddenSeries?: Set<string>;
@@ -43,5 +43,6 @@ export declare const ChartLines: import("react").MemoExoticComponent<({ chartDat
43
43
  historicalLineColor?: string;
44
44
  showLegend?: boolean;
45
45
  forecastLineStyle?: "dashed" | "solid";
46
+ disableAnimation?: boolean;
46
47
  disableHistoricalAnimation?: boolean;
47
48
  }) => import("react/jsx-runtime").JSX.Element>;
@@ -8,6 +8,8 @@ export type DriversComparisonViewTab = 'overlapped' | 'lagged';
8
8
  export declare function formatSeriesImportance(value: number | null): string;
9
9
  export declare function resolveDriverLagLabel(driver: {
10
10
  lag?: string | null;
11
+ overall_lag?: string | null;
12
+ overallLag?: string | null;
11
13
  }): string | null;
12
14
  /** Parse lag label to month count; ranges use the upper bound. */
13
15
  export declare function parseLagMonthsFromLabel(lag: string | null | undefined): number | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.55",
3
+ "version": "1.3.56",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -89,6 +89,11 @@
89
89
  .errorMessage
90
90
  color var(--destructive)
91
91
 
92
+ .noLineDrawAnimation
93
+ :global
94
+ .recharts-line-curve
95
+ stroke-dasharray unset !important
96
+
92
97
  .footer
93
98
  display flex
94
99
  align-items center
@@ -14,6 +14,7 @@ interface CssExports {
14
14
  'loaded': string;
15
15
  'loadingOverlay': string;
16
16
  'noLegend': string;
17
+ 'noLineDrawAnimation': string;
17
18
  'root': string;
18
19
  }
19
20
  export const cssExports: CssExports;
@@ -18,21 +18,20 @@ import {
18
18
  } from '#uilib/components/ui/ChartAreaInteractive/ChartLines';
19
19
  import { Skeleton } from '#uilib/components/ui/Skeleton';
20
20
  import { chartRenderQueue } from '#uilib/utils/chartRenderQueue';
21
- import { ComposedChart, LineChart, Tooltip as ChartTooltip } from 'recharts';
22
-
23
- import type { ChartConfig } from '../Chart.types';
24
- import { ChartContainer } from './ChartContainer';
25
- import { ChartTooltipContent } from './ChartTooltipContent';
26
- import { CustomChartLegend } from './CustomChartLegend/CustomChartLegend';
27
- import { QuantileBands } from './QuantileBands';
21
+ import { Tooltip as ChartTooltip, ComposedChart, LineChart } from 'recharts';
28
22
  import { LegendPayload } from 'recharts/types/component/DefaultLegendContent';
29
23
 
24
+ import type { ChartConfig } from '../Chart.types';
30
25
  import { getPlotViewBox, resolveChartMargin } from '../tools/chartPlotGeometry';
31
26
  import { formatDate } from '../tools/formatters';
32
27
  import S from './BaseChartWrapper.styl';
33
28
  import { ChartAxes } from './ChartAxes';
29
+ import { ChartContainer } from './ChartContainer';
34
30
  import { ChartGrid } from './ChartGrid';
31
+ import { ChartTooltipContent } from './ChartTooltipContent';
32
+ import { CustomChartLegend } from './CustomChartLegend/CustomChartLegend';
35
33
  import { LegendSvg } from './LegendSvg/LegendSvg';
34
+ import { QuantileBands } from './QuantileBands';
36
35
 
37
36
  function clampTooltipTranslate(args: {
38
37
  coordinate: { x: number; y: number };
@@ -107,6 +106,11 @@ export interface BaseChartWrapperProps {
107
106
  children?: ReactNode;
108
107
  chartType?: 'composed' | 'line';
109
108
  disableAnimation?: boolean;
109
+ /**
110
+ * Skip left-to-right line path draw; still animate Y on data updates.
111
+ * Independent of `disableAnimation` (full off).
112
+ */
113
+ disableLineDrawAnimation?: boolean;
110
114
  showGrid?: boolean;
111
115
  showAxes?: boolean;
112
116
  showTooltip?: boolean;
@@ -210,6 +214,7 @@ const BaseChartWrapperContent = forwardRef<
210
214
  margin,
211
215
  chartType = 'composed',
212
216
  disableAnimation = false,
217
+ disableLineDrawAnimation = false,
213
218
  showGrid = true,
214
219
  showAxes = true,
215
220
  showTooltip = true,
@@ -240,6 +245,7 @@ const BaseChartWrapperContent = forwardRef<
240
245
  } = props;
241
246
 
242
247
  const [shouldAnimate, setShouldAnimate] = useState(false);
248
+ const lineDataInitializedRef = useRef(false);
243
249
 
244
250
  const rootRef = useRef<HTMLDivElement>(null);
245
251
 
@@ -483,12 +489,20 @@ const BaseChartWrapperContent = forwardRef<
483
489
  }, [forecastData, excludeLegendIds, showLegend]);
484
490
 
485
491
  useEffect(() => {
486
- if (disableAnimation) return;
492
+ if (disableAnimation) {
493
+ setShouldAnimate(false);
494
+ return;
495
+ }
496
+ if (disableLineDrawAnimation && !lineDataInitializedRef.current) {
497
+ lineDataInitializedRef.current = true;
498
+ setShouldAnimate(false);
499
+ return;
500
+ }
487
501
  // Enable animation briefly when line data changes (not quantile band data)
488
502
  setShouldAnimate(true);
489
503
  const timer = setTimeout(() => setShouldAnimate(false), 1000);
490
504
  return () => clearTimeout(timer);
491
- }, [lineDataForAnimation, disableAnimation]);
505
+ }, [lineDataForAnimation, disableAnimation, disableLineDrawAnimation]);
492
506
 
493
507
  const isLoaded = useMemo(
494
508
  () => !loading && chartData.length > 0,
@@ -587,6 +601,7 @@ const BaseChartWrapperContent = forwardRef<
587
601
  S.root,
588
602
  !showLegend && S.noLegend,
589
603
  !showChartAxesLegend && S.hideChartAxesLegend,
604
+ disableLineDrawAnimation && S.noLineDrawAnimation,
590
605
  isLoaded && S.loaded,
591
606
  className,
592
607
  )}
@@ -605,7 +620,11 @@ const BaseChartWrapperContent = forwardRef<
605
620
  className={cn(S.gridLayer, chartClassName)}
606
621
  style={height ? { height: `${height}px` } : undefined}
607
622
  >
608
- <ChartComponent data={chartData} margin={margin}>
623
+ <ChartComponent
624
+ data={chartData}
625
+ margin={margin}
626
+ isAnimationActive={!disableAnimation}
627
+ >
609
628
  <ChartGrid />
610
629
  {showAxes && (
611
630
  <ChartAxes
@@ -633,7 +652,11 @@ const BaseChartWrapperContent = forwardRef<
633
652
  style={height ? { height: `${height}px` } : undefined}
634
653
  {...containerProps}
635
654
  >
636
- <ChartComponent data={chartData} margin={margin}>
655
+ <ChartComponent
656
+ data={chartData}
657
+ margin={margin}
658
+ isAnimationActive={!disableAnimation}
659
+ >
637
660
  {/* Render invisible axes for coordinate system, but hide labels */}
638
661
  {showAxes && (
639
662
  <ChartAxes
@@ -671,9 +694,10 @@ const BaseChartWrapperContent = forwardRef<
671
694
  hiddenSeries={hiddenSeries}
672
695
  isDarkTheme={isDarkTheme}
673
696
  shouldAnimate={shouldAnimate}
697
+ disableAnimation={disableAnimation}
698
+ disableHistoricalAnimation={disableHistoricalAnimation}
674
699
  showLegend={showLegend}
675
700
  forecastLineStyle={forecastLineStyle}
676
- // disableHistoricalAnimation={disableHistoricalAnimation}
677
701
  />
678
702
 
679
703
  {showTooltip && (
@@ -73,6 +73,7 @@ export const ChartLines = memo(
73
73
  historicalLineColor = isDarkTheme ? '#ffffff' : '#000000',
74
74
  showLegend = true,
75
75
  forecastLineStyle = 'dashed',
76
+ disableAnimation = false,
76
77
  disableHistoricalAnimation = false,
77
78
  }: {
78
79
  chartData: ChartDataPoint[];
@@ -83,6 +84,7 @@ export const ChartLines = memo(
83
84
  historicalLineColor?: string;
84
85
  showLegend?: boolean;
85
86
  forecastLineStyle?: 'dashed' | 'solid';
87
+ disableAnimation?: boolean;
86
88
  disableHistoricalAnimation?: boolean;
87
89
  }) => {
88
90
  const dotStroke = isDarkTheme ? '#000000' : '#FFFFFF';
@@ -152,6 +154,14 @@ export const ChartLines = memo(
152
154
 
153
155
  const renderForecastDot = (props: any, id: number, color: string) =>
154
156
  renderDot(props, lastForecastIndices[id] ?? -1, `forecast_${id}`, color);
157
+
158
+ const animationActive = Boolean(shouldAnimate) && !disableAnimation;
159
+ const historicalAnimationActive =
160
+ animationActive && !disableHistoricalAnimation;
161
+ const forecastAnimationActive = animationActive;
162
+ const animationDuration = disableAnimation ? 0 : ANIMATION_DURATION;
163
+ const valueAnimationEnabled = !disableAnimation;
164
+
155
165
  return (
156
166
  <>
157
167
  {/* Historical Data Line */}
@@ -167,10 +177,9 @@ export const ChartLines = memo(
167
177
  legendType={showLegend === false ? 'none' : undefined}
168
178
  strokeDasharray="0"
169
179
  hide={hiddenSeries?.has('historical')}
170
- isAnimationActive={shouldAnimate && !disableHistoricalAnimation}
171
- animationDuration={
172
- disableHistoricalAnimation ? 0 : ANIMATION_DURATION
173
- }
180
+ isAnimationActive={historicalAnimationActive}
181
+ animationDuration={animationDuration}
182
+ animateNewValues={valueAnimationEnabled}
174
183
  animationBegin={0}
175
184
  animationEasing="ease-out"
176
185
  />
@@ -197,12 +206,14 @@ export const ChartLines = memo(
197
206
  name={`${name || `${id}`}`}
198
207
  legendType={showLegend === false ? 'none' : undefined}
199
208
  hide={hiddenSeries?.has(dataKey)}
200
- isAnimationActive={shouldAnimate}
201
- animationDuration={ANIMATION_DURATION}
209
+ isAnimationActive={forecastAnimationActive}
210
+ animationDuration={animationDuration}
211
+ animateNewValues={valueAnimationEnabled}
202
212
  animationBegin={0}
203
213
  animationEasing="ease-out"
204
- className="chart-line-blinking"
205
- // className={pending ? 'chart-line-blinking' : undefined}
214
+ className={
215
+ data.status === 'pending' ? 'chart-line-blinking' : undefined
216
+ }
206
217
  />
207
218
  );
208
219
  })}
@@ -192,7 +192,9 @@ export function DriversComparisonChart({
192
192
  });
193
193
  }, [originalDriversById, sortedDriversWithData, viewTab]);
194
194
 
195
- const seriesInitKeyResolved = `${seriesInitKey ?? `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`}:${viewTab}`;
195
+ const seriesInitKeyResolved =
196
+ seriesInitKey ??
197
+ `${tableSeriesRows.map(r => r.id).join(',') || 'none'}`;
196
198
 
197
199
  const backtestsSeriesInitKeyRef = useRef<string>('');
198
200
 
@@ -238,8 +240,8 @@ export function DriversComparisonChart({
238
240
  )}
239
241
  >
240
242
  <ChartAreaInteractive
241
- key={viewTab}
242
- chartRenderId={`drivers-comparison-${viewTab}`}
243
+ chartRenderId="drivers-comparison"
244
+ disableLineDrawAnimation
243
245
  disableHistoricalAnimation
244
246
  disableTimeRangeSelector
245
247
  enableTimeRangeBrush
@@ -23,12 +23,13 @@ export function formatSeriesImportance(value: number | null): string {
23
23
 
24
24
  export function resolveDriverLagLabel(driver: {
25
25
  lag?: string | null;
26
+ overall_lag?: string | null;
27
+ overallLag?: string | null;
26
28
  }): string | null {
27
29
  if (typeof driver.lag === 'string' && driver.lag.trim()) {
28
30
  return driver.lag;
29
31
  }
30
- const raw = driver as Record<string, unknown>;
31
- const overallLag = raw.overall_lag ?? raw.overallLag;
32
+ const overallLag = driver.overall_lag ?? driver.overallLag;
32
33
  if (typeof overallLag === 'string' && overallLag.trim()) {
33
34
  return overallLag;
34
35
  }