using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; using EasyChart.Layers; #if UNITY_EDITOR using UnityEditor; #endif namespace EasyChart { public class ChartElement : VisualElement { /// /// When true, ignores chartWidth/chartHeight from ChartProfile and uses 100% of parent container. /// Useful when embedding in UGUIChartBridge or other containers that control sizing externally. /// public bool IgnoreProfileSize { get; set; } = false; public new class UxmlFactory : UxmlFactory { } public new class UxmlTraits : VisualElement.UxmlTraits { UxmlStringAttributeDescription m_ProfileName = new UxmlStringAttributeDescription { name = "profile-name", defaultValue = "" }; UxmlStringAttributeDescription m_ProfileGuid = new UxmlStringAttributeDescription { name = "profile-guid", defaultValue = "" }; public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc) { base.Init(ve, bag, cc); var chart = ve as ChartElement; string profileGuid = m_ProfileGuid.GetValueFromBag(bag, cc); #if UNITY_EDITOR if (!string.IsNullOrEmpty(profileGuid)) { if (chart.LoadProfileByGuid(profileGuid)) { return; } } #endif string profileName = m_ProfileName.GetValueFromBag(bag, cc); if (!string.IsNullOrEmpty(profileName)) { chart.LoadProfileByName(profileName); } } } private void ScheduleLayoutRefresh() { bool showCartesian = Data != null && Data.CoordinateSystem == CoordinateSystemType.Cartesian2D; _labelRefresh.Request( this, showCartesian ? _axisLayer : null, _renderers, _editor.IsCoordSwitchInProgress, EditorDiagnosticsEnabled ); } // Layers private GridLayer _gridLayer; private AxisLayer _axisLayer; private VisualElement _chartArea; private VisualElement _chartContent; private VisualElement _plotViewport; private VisualElement _plotContentRoot; private VisualElement _backgroundLayer; private VisualElement _labelOverlay; // Dynamic Series Renderers private readonly ChartRendererController _rendererController = new ChartRendererController(); private List _renderers => _rendererController.Renderers; private BarSeriesRenderer _barRenderer; private LineSeriesRenderer _lineRenderer; private ScatterSeriesRenderer _scatterRenderer; private RadarSeriesRenderer _radarRenderer; private PieSeriesRenderer _pieRenderer; private Label _tooltip; private VisualElement _cursorLine; private readonly List _windowedCategoryLabelsX = new List(16); private readonly List _windowedCategoryLabelsY = new List(16); private ChartRefreshPipeline _refresh; private ChartKernel _kernel; internal ChartTooltipController TooltipControllerInternal => _tooltipController; internal List RenderersInternal => _renderers; internal ChartRendererController RendererControllerInternal => _rendererController; internal VisualElement ChartAreaInternal => _chartArea; internal Vector4 PaddingInternal => _padding; internal bool IsCartesianTransposedInternal => IsCartesianTransposed(); internal bool EditorIsDeferredCoordSwitchApplyingInternal => _editor.IsDeferredCoordSwitchApplying; internal bool IsCategorySmoothTranslatingInternal => _categoryScroll != null && _categoryScroll.SmoothTranslating; internal Label TooltipLabelInternal => _tooltip; internal VisualElement CursorLineInternal => _cursorLine; internal GridLayer GridLayerInternal => _gridLayer; internal AxisLayer AxisLayerInternal => _axisLayer; internal ChartAxisGridController AxisGridControllerInternal => _axisGrid; internal ChartLegendController LegendControllerInternal => _legend; internal ChartProfile ChartProfileInternal => _chartProfile; internal IChartLayoutModel LayoutModelInternal => _layoutModel; internal CategoryScrollController CategoryScrollControllerInternal => _categoryScroll; internal float XMinInternal { get => _xMin; set => _xMin = value; } internal float XMaxInternal { get => _xMax; set => _xMax = value; } internal float YMinInternal { get => _yMin; set => _yMin = value; } internal float YMaxInternal { get => _yMax; set => _yMax = value; } internal float CategoryScrollOffsetXInternal => _categoryScroll != null ? _categoryScroll.ScrollOffsetX : 0f; internal float CategoryScrollOffsetYInternal => _categoryScroll != null ? _categoryScroll.ScrollOffsetY : 0f; internal void ApplyCategoryScrollTranslateInternal(float xPx, float yPx) => ApplyCategoryScrollTranslate(xPx, yPx); internal VisualElement BackgroundLayerInternal => _backgroundLayer; internal VisualElement PlotViewportInternal => _plotViewport; internal VisualElement LabelOverlayInternal => _labelOverlay; internal ChartLabelController LabelControllerInternal => _labelController; internal AxisId GetMappedXAxisIdInternal() => GetMappedXAxisId(); internal AxisId GetMappedYAxisIdInternal() => GetMappedYAxisId(); internal AxisConfig GetAxisInternal(AxisId id) => GetAxis(id); internal List GetCategoryLabelsWindowedInternal(AxisId id) => GetCategoryLabelsWindowed(id); internal bool EditorShouldDeferVisualWorkInternal() => _editor.ShouldDeferVisualWork(); internal bool EditorTryDeferVisualWorkInternal(string breadcrumbIfCoordChanged) => _editor.TryDeferVisualWork(breadcrumbIfCoordChanged); internal void EditorRefreshLegendDeferredInternal(System.Action refreshLegend) => _editor.RefreshLegendDeferred(refreshLegend); internal void EditorGetLegendTraceInternal(out bool trace, out System.Action pushBreadcrumb) => _editor.GetLegendTrace(out trace, out pushBreadcrumb); internal void EditorOnRebuildRenderersStartInternal() => _editor.OnRebuildRenderersStart(); internal void EditorOnRebuildRenderersDoneInternal(int rendererCount, bool showCartesian) => _editor.OnRebuildRenderersDone(rendererCount, showCartesian); internal void SetTooltipVisualsInternal(Label tooltip, VisualElement cursorLine) { _tooltip = tooltip; _cursorLine = cursorLine; } internal bool EditorHandleGeometryChangedInternal(GeometryChangedEvent evt) { return _editor.HandleGeometryChanged(evt); } // Data private ChartProfile _chartProfile; private ChartTheme _theme; public ChartTheme Theme { get => _theme; set { if (ReferenceEquals(_theme, value)) return; _theme = value; if (_tooltip != null) { ChartTooltipModule.ApplyTooltipStyle(_tooltip, this); } ScheduleLayoutRefresh(); if (_kernel != null) { _kernel.QueueStages(ChartRefreshStage.AxisLayer | ChartRefreshStage.SeriesRenderers | ChartRefreshStage.Legend); } } } internal ChartTheme GetEffectiveThemeInternal() { return _theme != null ? _theme : ChartThemeRegistry.GlobalTheme; } // Animation private readonly ChartAnimationController _animation = new ChartAnimationController(); private readonly CategoryScrollController _categoryScroll = new CategoryScrollController(); private readonly ChartTooltipController _tooltipController = new ChartTooltipController(); private readonly ChartLegendController _legend = new ChartLegendController(); private readonly ChartAxisGridController _axisGrid = new ChartAxisGridController(); private readonly ChartLabelController _labelController = new ChartLabelController(); private ChartInteractionState _interactionState = new ChartInteractionState(); private interface IEditorExecutionPolicy { bool IsCoordSwitchInProgress { get; } bool IsDeferredCoordSwitchApplying { get; } bool IsCoordSwitchBusy { get; } float GetCategoryScrollDeltaTime(float runtimeDeltaTime); void OnPlayAnimationStart(); System.Action GetEditorAnimationUpdateCallback(); void OnSetDataStart(ChartData prev, ChartData next, out bool coordChanged); bool TryDeferCoordSwitchApply(bool coordChanged); bool TryDeferVisualWork(string breadcrumbIfCoordChanged); bool ShouldDeferVisualWork(); void RefreshLegendDeferred(System.Action refreshLegend); void ScheduleRuntimeAnimationLoop(System.Action onAnimationUpdate); void OnTextureFillAnimationFlagsUpdated(bool has); void OnProfileSameAssigned(ChartProfile profile); bool HandleGeometryChanged(GeometryChangedEvent evt); void GetLegendTrace(out bool trace, out System.Action pushBreadcrumb); bool ShouldDeferInEditorRefreshPipeline(); void SchedulePostRefresh(); void OnAttachToPanel(AttachToPanelEvent evt); void OnDetachFromPanel(DetachFromPanelEvent evt); void OnCtorStart(); void OnApplyProfile(ChartProfile profile); void OnSetDataApplyNow(); void OnRebuildRenderersStart(); void OnRebuildRenderersDone(int rendererCount, bool showCartesian); ChartRefreshPipeline CreateRefreshPipeline( ChartElement owner, System.Func hasData, System.Action rebuildRenderers, System.Action calculateRange, System.Action refreshAxisLayer, System.Action refreshGridLayer, System.Action refreshSeriesRenderers, System.Action refreshLayersNoLegend, System.Action refreshLegendDeferred, System.Action playAnimation); } private sealed class NullEditorExecutionPolicy : IEditorExecutionPolicy { private readonly ChartElement _owner; public bool IsCoordSwitchInProgress => false; public bool IsDeferredCoordSwitchApplying => false; public bool IsCoordSwitchBusy => false; public NullEditorExecutionPolicy(ChartElement owner) { _owner = owner; } public float GetCategoryScrollDeltaTime(float runtimeDeltaTime) => runtimeDeltaTime; public void OnPlayAnimationStart() { } public System.Action GetEditorAnimationUpdateCallback() => null; public void OnSetDataStart(ChartData prev, ChartData next, out bool coordChanged) => coordChanged = false; public bool TryDeferCoordSwitchApply(bool coordChanged) => false; public bool TryDeferVisualWork(string breadcrumbIfCoordChanged) => false; public bool ShouldDeferVisualWork() => false; public void RefreshLegendDeferred(System.Action refreshLegend) => refreshLegend?.Invoke(); public void ScheduleRuntimeAnimationLoop(System.Action onAnimationUpdate) { if (_owner == null) return; _owner.schedule.Execute(onAnimationUpdate).Every(16); } public void OnTextureFillAnimationFlagsUpdated(bool has) { } public void OnProfileSameAssigned(ChartProfile profile) { } public bool HandleGeometryChanged(GeometryChangedEvent evt) => false; public void GetLegendTrace(out bool trace, out System.Action pushBreadcrumb) { trace = false; pushBreadcrumb = null; } public bool ShouldDeferInEditorRefreshPipeline() => false; public void SchedulePostRefresh() { } public void OnAttachToPanel(AttachToPanelEvent evt) { } public void OnDetachFromPanel(DetachFromPanelEvent evt) { } public void OnCtorStart() { } public void OnApplyProfile(ChartProfile profile) { } public void OnSetDataApplyNow() { } public void OnRebuildRenderersStart() { } public void OnRebuildRenderersDone(int rendererCount, bool showCartesian) { } public ChartRefreshPipeline CreateRefreshPipeline( ChartElement owner, System.Func hasData, System.Action rebuildRenderers, System.Action calculateRange, System.Action refreshAxisLayer, System.Action refreshGridLayer, System.Action refreshSeriesRenderers, System.Action refreshLayersNoLegend, System.Action refreshLegendDeferred, System.Action playAnimation) { return new ChartRefreshPipeline( owner, hasData, rebuildRenderers, calculateRange, refreshAxisLayer, refreshGridLayer, refreshSeriesRenderers, refreshLayersNoLegend, refreshLegendDeferred, playAnimation #if UNITY_EDITOR , null, null #endif ); } } private readonly IEditorExecutionPolicy _editor; private const string EditorDiagnosticsPrefsKey = "EasyChart.ChartElement.EditorDiagnosticsEnabled"; internal static bool EditorDiagnosticsEnabled { get { #if UNITY_EDITOR return UnityEditor.EditorPrefs.GetBool(EditorDiagnosticsPrefsKey, false); #else return false; #endif } set { #if UNITY_EDITOR UnityEditor.EditorPrefs.SetBool(EditorDiagnosticsPrefsKey, value); #endif } } #if UNITY_EDITOR [UnityEditor.InitializeOnLoadMethod] private static void InitEditorDiagnosticsDefault() { // Default to quiet logs. Developers can turn this on temporarily when debugging. UnityEditor.EditorPrefs.SetBool(EditorDiagnosticsPrefsKey, false); } private static bool s_assertHooked; private static bool s_dumpingAssert; private static double s_lastAssertDumpTime; private static readonly Queue s_breadcrumbs = new Queue(32); private static void PushBreadcrumb(string msg) { if (!EditorDiagnosticsEnabled) return; try { string line = $"{EditorApplication.timeSinceStartup:F3} {msg}"; s_breadcrumbs.Enqueue(line); while (s_breadcrumbs.Count > 32) s_breadcrumbs.Dequeue(); } catch { } } private static void EnsureAssertHooked() { if (!EditorDiagnosticsEnabled) return; if (s_assertHooked) return; s_assertHooked = true; Application.logMessageReceived += OnLogMessageReceived; } private static void OnLogMessageReceived(string condition, string stackTrace, LogType type) { if (!EditorDiagnosticsEnabled) return; if (s_dumpingAssert) return; if (string.IsNullOrEmpty(condition)) return; if (!condition.StartsWith("Assertion failed")) return; double now = EditorApplication.timeSinceStartup; if (now - s_lastAssertDumpTime < 0.2) return; s_lastAssertDumpTime = now; s_dumpingAssert = true; try { var dump = string.Join("\n", s_breadcrumbs.ToArray()); Debug.Log("[EasyChart][AssertionBreadcrumbs]\n" + dump); } finally { s_dumpingAssert = false; } } private sealed class EditorExecutionPolicy : IEditorExecutionPolicy { private readonly ChartElement _owner; private double _categoryScrollLastTime; private bool _debugCoordChangedInLastSetData; private CoordinateSystemType _debugPrevCoord; private CoordinateSystemType _debugNewCoord; private bool _debugLogThisAnimation; private bool _debugAnimEndLogged; private bool _playAnimationAfterDeferredApply; private bool _postRefreshScheduled; private bool _postRefreshAwaitingAttach; private bool _deferredVisualRefreshScheduled; private bool _coordSwitchInProgress; private int _coordSwitchGeometryChangedCount; private bool _geometryCallbackSuppressed; private bool _deferredCoordSwitchApplyPending; private bool _deferredCoordSwitchApplying; public bool IsCoordSwitchInProgress => _coordSwitchInProgress; public bool IsDeferredCoordSwitchApplying => _deferredCoordSwitchApplying; public bool IsCoordSwitchBusy => _coordSwitchInProgress || _deferredCoordSwitchApplying; public EditorExecutionPolicy(ChartElement owner) { _owner = owner; } public float GetCategoryScrollDeltaTime(float runtimeDeltaTime) { if (Application.isPlaying) return runtimeDeltaTime; double now = EditorApplication.timeSinceStartup; if (_categoryScrollLastTime <= 0) _categoryScrollLastTime = now; float dt = (float)(now - _categoryScrollLastTime); _categoryScrollLastTime = now; return dt; } public void OnPlayAnimationStart() { _debugLogThisAnimation = !Application.isPlaying && _debugCoordChangedInLastSetData; _debugAnimEndLogged = false; if (_debugLogThisAnimation && EditorDiagnosticsEnabled) { Debug.Log($"[ChartElement] Preview animation start (coord change: {_debugPrevCoord} -> {_debugNewCoord}, duration={_owner.Data?.animationDuration})"); } _debugCoordChangedInLastSetData = false; } public void OnEditorAnimationUpdate() { bool ended = _owner._animation.TickEditorAnimation(_owner, _owner.Data, _owner._renderers); if (ended && _debugLogThisAnimation && !Application.isPlaying && !_debugAnimEndLogged && EditorDiagnosticsEnabled) { _debugAnimEndLogged = true; Debug.Log("[ChartElement] Preview animation end"); } } private void OnEditorTextureFillAnimationUpdate() { _owner._animation.TickEditorTextureFillAnimation(_owner, _owner._renderers, _owner._backgroundLayer); } public System.Action GetEditorAnimationUpdateCallback() { return OnEditorAnimationUpdate; } public void OnSetDataStart(ChartData prev, ChartData next, out bool coordChanged) { bool hasPrev = prev != null; var prevCoord = hasPrev ? prev.CoordinateSystem : default; coordChanged = hasPrev && next != null && prevCoord != next.CoordinateSystem; _debugCoordChangedInLastSetData = !Application.isPlaying && coordChanged; if (!Application.isPlaying) { if (EditorDiagnosticsEnabled) { PushBreadcrumb($"SetData start prevCoord={prevCoord} newCoord={(next != null ? next.CoordinateSystem : default)} coordChanged={coordChanged}"); } } if (_debugCoordChangedInLastSetData) { _debugPrevCoord = prevCoord; _debugNewCoord = next.CoordinateSystem; _coordSwitchInProgress = true; _coordSwitchGeometryChangedCount = 0; if (!_geometryCallbackSuppressed) { _geometryCallbackSuppressed = true; _owner.UnregisterCallback(_owner.OnGeometryChanged); if (EditorDiagnosticsEnabled) Debug.Log("[ChartElement] Coord switch: unregister GeometryChanged"); } } } public bool TryDeferCoordSwitchApply(bool coordChanged) { if (Application.isPlaying) return false; if (!coordChanged) return false; // Defer expensive UI tree rebuild to next editor update to avoid ProcessEvent-time assertions. _owner._animation.StopEditorAnimation(); _owner._animation.ResetRuntimeState(); _playAnimationAfterDeferredApply = false; _deferredCoordSwitchApplyPending = true; if (EditorDiagnosticsEnabled) Debug.Log("[ChartElement] Coord switch: defer renderer rebuild to post refresh"); PushBreadcrumb("SetData deferred apply pending"); SchedulePostRefresh(); _debugCoordChangedInLastSetData = false; return true; } public bool TryDeferVisualWork(string breadcrumbIfCoordChanged) { if (Application.isPlaying) return false; if (_debugCoordChangedInLastSetData) { if (!string.IsNullOrEmpty(breadcrumbIfCoordChanged)) PushBreadcrumb(breadcrumbIfCoordChanged); SchedulePostRefresh(); return true; } return _coordSwitchInProgress || _deferredCoordSwitchApplying; } public bool ShouldDeferVisualWork() { return TryDeferVisualWork(null); } public void RefreshLegendDeferred(System.Action refreshLegend) { if (Application.isPlaying) { refreshLegend?.Invoke(); return; } if (_owner.panel == null) { refreshLegend?.Invoke(); return; } ScheduleDeferredVisualRefresh(refreshLegend); } private void ScheduleDeferredVisualRefresh(System.Action action) { if (Application.isPlaying) { action?.Invoke(); return; } if (_owner.panel == null) { action?.Invoke(); return; } if (_deferredVisualRefreshScheduled) return; _deferredVisualRefreshScheduled = true; _owner.schedule.Execute(() => { _deferredVisualRefreshScheduled = false; if (_owner.panel == null) return; action?.Invoke(); }).ExecuteLater(0); } public void ScheduleRuntimeAnimationLoop(System.Action onAnimationUpdate) { if (Application.isPlaying) { _owner.schedule.Execute(onAnimationUpdate).Every(16); } } public void OnTextureFillAnimationFlagsUpdated(bool has) { if (Application.isPlaying) return; _owner._animation.SetHasTextureFillAnimations(has, _owner, OnEditorTextureFillAnimationUpdate); } public void OnProfileSameAssigned(ChartProfile profile) { if (Application.isPlaying) return; if (profile == null) return; _owner.ApplyProfileToElement(); } public bool HandleGeometryChanged(GeometryChangedEvent evt) { return HandleGeometryChangedInEditor(evt); } public void GetLegendTrace(out bool trace, out System.Action pushBreadcrumb) { trace = !Application.isPlaying && IsCoordSwitchBusy; pushBreadcrumb = trace ? PushBreadcrumb : null; } public bool ShouldDeferInEditorRefreshPipeline() { return _coordSwitchInProgress || _deferredCoordSwitchApplying; } public void SchedulePostRefresh() { if (Application.isPlaying) return; if (_postRefreshScheduled) return; _postRefreshScheduled = true; if (_owner.panel != null) { PushBreadcrumb("SchedulePostRefresh via schedule"); _owner.schedule.Execute(OnPostRefreshScheduled).ExecuteLater(0); } else { _postRefreshAwaitingAttach = true; PushBreadcrumb("SchedulePostRefresh awaiting attach"); } } public void OnAttachToPanel(AttachToPanelEvent evt) { _owner._animation.OnAttachToPanel(_owner, OnEditorTextureFillAnimationUpdate); if (!_postRefreshAwaitingAttach) return; if (!_postRefreshScheduled) return; _postRefreshAwaitingAttach = false; PushBreadcrumb("AttachToPanel -> run scheduled post refresh"); _owner.schedule.Execute(OnPostRefreshScheduled).ExecuteLater(0); } public void OnDetachFromPanel(DetachFromPanelEvent evt) { _postRefreshAwaitingAttach = false; _owner._animation.OnDetachFromPanel(); } public void OnCtorStart() { EnsureAssertHooked(); PushBreadcrumb("ChartElement.ctor"); } public void OnApplyProfile(ChartProfile profile) { if (Application.isPlaying) return; if (profile == null) return; PushBreadcrumb($"Profile set name={profile.name} coord={profile.coordinateSystem}"); } public void OnSetDataApplyNow() { if (Application.isPlaying) return; if (!EditorDiagnosticsEnabled) return; PushBreadcrumb("SetData apply now: RebuildRenderers"); } public void OnRebuildRenderersStart() { if (Application.isPlaying) return; if (!EditorDiagnosticsEnabled) return; PushBreadcrumb("RebuildRenderers start"); } public void OnRebuildRenderersDone(int rendererCount, bool showCartesian) { if (Application.isPlaying) return; if (!EditorDiagnosticsEnabled) return; PushBreadcrumb($"RebuildRenderers done renderers={rendererCount} showCartesian={showCartesian}"); } public ChartRefreshPipeline CreateRefreshPipeline( ChartElement owner, System.Func hasData, System.Action rebuildRenderers, System.Action calculateRange, System.Action refreshAxisLayer, System.Action refreshGridLayer, System.Action refreshSeriesRenderers, System.Action refreshLayersNoLegend, System.Action refreshLegendDeferred, System.Action playAnimation) { return new ChartRefreshPipeline( owner, hasData, rebuildRenderers, calculateRange, refreshAxisLayer, refreshGridLayer, refreshSeriesRenderers, refreshLayersNoLegend, refreshLegendDeferred, playAnimation, ShouldDeferInEditorRefreshPipeline, SchedulePostRefresh); } private void OnPostRefreshScheduled() { PushBreadcrumb("OnEditorPostRefresh(scheduled) start"); _postRefreshScheduled = false; if (_owner.panel == null) return; if (_owner.Data == null) return; OnPostRefreshBody(); } private void OnPostRefreshBody() { if (_geometryCallbackSuppressed) { _geometryCallbackSuppressed = false; _owner.RegisterCallback(_owner.OnGeometryChanged); if (EditorDiagnosticsEnabled) Debug.Log("[ChartElement] Coord switch: re-register GeometryChanged"); } bool showCartesian = _owner.Data.CoordinateSystem == CoordinateSystemType.Cartesian2D; if ((_owner._refresh == null || !_owner._refresh.HasPending) && !_deferredCoordSwitchApplyPending) { if (showCartesian) { if (_owner._axisLayer != null) _owner._axisLayer.RefreshLabels(); } } if (_deferredCoordSwitchApplyPending) { _deferredCoordSwitchApplyPending = false; _deferredCoordSwitchApplying = true; PushBreadcrumb("OnEditorPostRefresh applying deferred rebuild"); _owner._legend.ClearHiddenPieSliceIds(); PushBreadcrumb("Deferred: RebuildRenderers"); if (_owner._kernel != null) _owner._kernel.ExecuteRebuildRenderers(); _deferredCoordSwitchApplying = false; PushBreadcrumb("Deferred: QueuePendingRefresh"); if (_owner._kernel != null) { _owner._kernel.QueueStages(ChartRefreshStage.CalculateRange | ChartRefreshStage.LayersNoLegend | ChartRefreshStage.Legend); } if (_playAnimationAfterDeferredApply) { _playAnimationAfterDeferredApply = false; _owner.schedule.Execute(() => { if (_owner.panel == null || _owner.Data == null) return; if (_owner.Data.animationDuration <= 0f) return; _owner.PlayAnimation(); }).ExecuteLater(32); } } if (_owner._refresh == null || !_owner._refresh.HasPending) { foreach (var r in _owner._renderers) { if (r == null) continue; if (r.panel == null) continue; if (r.parent == null) continue; r.SetRange(_owner._xMin, _owner._xMax, _owner._yMin, _owner._yMax); if (!r.visible) continue; r.UpdateLabels(); } // Always do legend/repaint via UIElements scheduler in editor to avoid ProcessEvent-time assertions. if (!Application.isPlaying) { ScheduleDeferredVisualRefresh(() => { bool sc = _owner.Data != null && _owner.Data.CoordinateSystem == CoordinateSystemType.Cartesian2D; PushBreadcrumb("DeferredVisual RefreshLegend"); RefreshLegendNow(); if (sc && _owner._gridLayer != null) _owner._gridLayer.Redraw(); }); } else { PushBreadcrumb("OnEditorPostRefresh RefreshLegend"); RefreshLegendNow(); if (showCartesian && _owner._gridLayer != null) _owner._gridLayer.Redraw(); PushBreadcrumb("OnEditorPostRefresh MarkDirtyRepaint"); _owner.MarkDirtyRepaint(); } } if (_coordSwitchInProgress) { if (EditorDiagnosticsEnabled) Debug.Log($"[ChartElement] Coord switch geometryChanged count: {_coordSwitchGeometryChangedCount}"); _coordSwitchInProgress = false; _coordSwitchGeometryChangedCount = 0; if (EditorDiagnosticsEnabled) Debug.Log("[ChartElement] Coord switch post refresh applied"); PushBreadcrumb("OnEditorPostRefresh coord switch done"); } if (_owner._refresh != null && _owner._refresh.HasPending) { PushBreadcrumb("OnEditorPostRefresh FlushPendingRefresh"); _owner._refresh.Flush(); } } private void RefreshLegendNow() { GetLegendTrace(out bool trace, out System.Action pushBreadcrumb); _owner._legend.RefreshLegend( _owner.Data, trace, pushBreadcrumb, (reason) => { if (_owner._kernel != null) _owner._kernel.Invalidate(reason, immediate: true); }); } public bool HandleGeometryChangedInEditor(GeometryChangedEvent evt) { if (Application.isPlaying) return false; if (_coordSwitchInProgress) { _coordSwitchGeometryChangedCount++; if (EditorDiagnosticsEnabled && _coordSwitchGeometryChangedCount <= 3) { Debug.Log($"[ChartElement] OnGeometryChanged (coord switch) #{_coordSwitchGeometryChangedCount}: {evt.oldRect} -> {evt.newRect}"); } return true; } // In editor, avoid rebuilding legend / forcing grid redraw from geometry events. // These can fire during GUIUtility:ProcessEvent and trigger assertion spam. _owner.ScheduleLayoutRefresh(); return true; } } #endif public ChartProfile Profile { get => _chartProfile; set { if (_chartProfile == value) { _editor.OnProfileSameAssigned(_chartProfile); return; } _chartProfile = value; ApplyProfileToElement(); } } private void ApplyProfileToElement() { if (_chartProfile == null) return; _editor.OnApplyProfile(_chartProfile); _chartProfile.EnsureAxes(); var theme = GetEffectiveThemeInternal(); if (!IgnoreProfileSize) { if (_chartProfile.chartWidth > 0) style.width = _chartProfile.chartWidth; if (_chartProfile.chartHeight > 0) style.height = _chartProfile.chartHeight; } if (_chartProfile.background == null) _chartProfile.background = new BackgroundSettings { show = true }; style.backgroundColor = new StyleColor(theme != null ? theme.backgroundColor : Color.clear); style.backgroundImage = StyleKeyword.Null; if (_plotViewport != null) { // Plot settings: profile override takes precedence over theme var plotSettings = _chartProfile.plotSettings; bool useOverride = plotSettings != null && plotSettings.overrideTheme; var plotBg = useOverride ? plotSettings.backgroundColor : (theme != null ? theme.plotBackgroundColor : Color.clear); _plotViewport.style.backgroundColor = new StyleColor(plotBg); float bw = useOverride ? Mathf.Max(0f, plotSettings.borderWidth) : (theme != null ? Mathf.Max(0f, theme.plotBorderWidth) : 0f); var bc = useOverride ? plotSettings.borderColor : (theme != null ? theme.plotBorderColor : Color.clear); _plotViewport.style.borderLeftWidth = bw; _plotViewport.style.borderRightWidth = bw; _plotViewport.style.borderTopWidth = bw; _plotViewport.style.borderBottomWidth = bw; _plotViewport.style.borderLeftColor = new StyleColor(bc); _plotViewport.style.borderRightColor = new StyleColor(bc); _plotViewport.style.borderTopColor = new StyleColor(bc); _plotViewport.style.borderBottomColor = new StyleColor(bc); } if (_backgroundLayer != null) _backgroundLayer.MarkDirtyRepaint(); UpdateTextureFillAnimationFlags(); _padding = _chartProfile.padding; if (_chartArea != null) { _chartArea.style.left = _padding.x; _chartArea.style.right = _padding.y; _chartArea.style.top = _padding.z; _chartArea.style.bottom = _padding.w; ScheduleLayoutRefresh(); } if (theme != null && theme.seriesColors != null && theme.seriesColors.Count > 0 && _chartProfile.series != null) { int paletteCount = theme.seriesColors.Count; int nextPaletteIndex = 0; for (int i = 0; i < _chartProfile.series.Count; i++) { var serie = _chartProfile.series[i]; if (serie == null) continue; Color paletteColor = theme.seriesColors[(nextPaletteIndex + theme.paletteSeed) % paletteCount]; bool used = false; if (serie.type == SerieType.Line && serie.settings is LineSettings ls) { if (ls.stroke == null) ls.stroke = new LineStrokeSettings(); if (ls.stroke.color == Color.white) { ls.stroke.color = paletteColor; used = true; } } else if (serie.type == SerieType.Radar && serie.settings is RadarSettings rs) { if (rs.stroke == null) rs.stroke = new LineStrokeSettings(); if (rs.stroke.color == Color.white) { rs.stroke.color = paletteColor; used = true; } } else if ((serie.type == SerieType.Bar || serie.type == SerieType.HorizontalBar) && serie.settings is BarSettings bs) { if (bs.textureFill == null) bs.textureFill = new TextureFillSettings(); if (bs.textureFill.color == Color.white) { bs.textureFill.color = paletteColor; used = true; } } else if (serie.type == SerieType.Scatter && serie.settings is ScatterSettings ss) { if (ss.point == null) ss.point = new PointSettings(); if (ss.point.textureFill == null) ss.point.textureFill = new TextureFillSettings(); if (ss.point.textureFill.color == Color.white) { ss.point.textureFill.color = paletteColor; used = true; } } if (used) nextPaletteIndex++; } } var data = _chartProfile.ToChartData(); bool showCartesian = data != null && data.CoordinateSystem == CoordinateSystemType.Cartesian2D; if (showCartesian) { if (_gridLayer != null) { var grid = _chartProfile.cartesianGrid; if (grid != null) { _gridLayer.XGridColor = grid.xGridColor; _gridLayer.XGridLineWidth = grid.xGridLineWidth; _gridLayer.XGridDashed = grid.xGridDashed; _gridLayer.XGridDashLength = grid.xGridDashLength; _gridLayer.XGridDashGap = grid.xGridDashGap; _gridLayer.XGridDashOffset = grid.xGridDashOffset; _gridLayer.YGridColor = grid.yGridColor; _gridLayer.YGridLineWidth = grid.yGridLineWidth; _gridLayer.YGridDashed = grid.yGridDashed; _gridLayer.YGridDashLength = grid.yGridDashLength; _gridLayer.YGridDashGap = grid.yGridDashGap; _gridLayer.YGridDashOffset = grid.yGridDashOffset; } } if (_cursorLine is DashedLineElement dashed && _chartProfile.hover != null) { var hover = _chartProfile.hover; dashed.Color = hover.cursorLineColor; dashed.LineWidth = hover.cursorLineWidth; dashed.Dashed = hover.cursorLineDashed; dashed.DashLength = hover.cursorLineDashLength; dashed.DashGap = hover.cursorLineDashGap; dashed.DashOffset = hover.cursorLineDashOffset; } } SetData(data); } public ChartData Data { get; private set; } public ChartServices Services { get => _services; set { _services = value; ApplyServices(_services); } } public ChartServices GetEffectiveServicesSnapshot() { return new ChartServices { RendererSelectionPolicy = _rendererController.RendererSelectionPolicy, DynamicRendererFactory = _rendererController.DynamicRendererFactory, InteractionState = _interactionState, TooltipPolicy = _tooltipController.TooltipPolicy, HitTestPolicy = _tooltipController.HitTestPolicy, InteractionPolicy = _interactionPolicy, LegendTogglePolicy = _legend.LegendTogglePolicy, LayoutModel = _layoutModel, }; } public IChartRendererSelectionPolicy RendererSelectionPolicy { get => _rendererController.RendererSelectionPolicy; set { _rendererController.RendererSelectionPolicy = value; if (_services != null) _services.RendererSelectionPolicy = value; } } public System.Func DynamicRendererFactory { get => _rendererController.DynamicRendererFactory; set { _rendererController.DynamicRendererFactory = value; if (_services != null) _services.DynamicRendererFactory = value; } } public ChartInteractionState InteractionState { get => _interactionState; set { _interactionState = value ?? new ChartInteractionState(); _legend.SetInteractionState(_interactionState); _rendererController.SetInteractionState(_interactionState); if (_services != null) _services.InteractionState = _interactionState; } } public IChartTooltipPolicy TooltipPolicy { get => _tooltipController.TooltipPolicy; set { _tooltipController.TooltipPolicy = value; if (_services != null) _services.TooltipPolicy = value; } } public IChartHitTestPolicy HitTestPolicy { get => _tooltipController.HitTestPolicy; set { _tooltipController.HitTestPolicy = value; if (_services != null) _services.HitTestPolicy = value; } } public IChartInteractionPolicy InteractionPolicy { get => _interactionPolicy; set { _interactionPolicy = value ?? DefaultChartInteractionPolicy.Instance; if (_services != null) _services.InteractionPolicy = _interactionPolicy; } } public IChartLegendTogglePolicy LegendTogglePolicy { get => _legend.LegendTogglePolicy; set { _legend.LegendTogglePolicy = value; if (_services != null) _services.LegendTogglePolicy = value; } } public IChartLayoutModel LayoutModel { get => _layoutModel; set { _layoutModel = value ?? new DefaultChartLayoutModel(); if (_services != null) _services.LayoutModel = _layoutModel; } } // Layout Settings private Vector4 _padding = new Vector4(40f, 30f, 50f, 50f); private readonly ChartLabelRefreshController _labelRefresh = new ChartLabelRefreshController(); private IChartLayoutModel _layoutModel = new DefaultChartLayoutModel(); private IChartInteractionPolicy _interactionPolicy = DefaultChartInteractionPolicy.Instance; private ChartServices _services; private void ApplyServices(ChartServices services) { if (services == null) return; if (services.RendererSelectionPolicy == null) { services.RendererSelectionPolicy = _rendererController.RendererSelectionPolicy; } if (services.DynamicRendererFactory == null) { services.DynamicRendererFactory = _rendererController.DynamicRendererFactory; } if (services.InteractionState == null) { services.InteractionState = _interactionState; } if (services.TooltipPolicy == null) { services.TooltipPolicy = _tooltipController.TooltipPolicy; } if (services.HitTestPolicy == null) { services.HitTestPolicy = _tooltipController.HitTestPolicy; } if (services.InteractionPolicy == null) { services.InteractionPolicy = _interactionPolicy; } if (services.LegendTogglePolicy == null) { services.LegendTogglePolicy = _legend.LegendTogglePolicy; } if (services.LayoutModel == null) { services.LayoutModel = _layoutModel; } if (services.RendererSelectionPolicy != null) { _rendererController.RendererSelectionPolicy = services.RendererSelectionPolicy; } if (services.DynamicRendererFactory != null) { _rendererController.DynamicRendererFactory = services.DynamicRendererFactory; } if (services.InteractionState != null) { _interactionState = services.InteractionState; _legend.SetInteractionState(_interactionState); _rendererController.SetInteractionState(_interactionState); } if (services.TooltipPolicy != null) { _tooltipController.TooltipPolicy = services.TooltipPolicy; } if (services.HitTestPolicy != null) { _tooltipController.HitTestPolicy = services.HitTestPolicy; } if (services.InteractionPolicy != null) { _interactionPolicy = services.InteractionPolicy; } if (services.LegendTogglePolicy != null) { _legend.LegendTogglePolicy = services.LegendTogglePolicy; } if (services.LayoutModel != null) { _layoutModel = services.LayoutModel; } } // Computed Range private float _xMin, _xMax; private float _yMin, _yMax; private AxisId GetMappedXAxisId() { return _layoutModel.GetMappedXAxisId(Data); } private AxisId GetMappedYAxisId() { return _layoutModel.GetMappedYAxisId(Data); } private bool IsCartesianTransposed() { return _layoutModel.IsCartesianTransposed(Data, GetAxis); } private static IEditorExecutionPolicy CreateEditorPolicy(ChartElement owner) { #if UNITY_EDITOR return new EditorExecutionPolicy(owner); #else return new NullEditorExecutionPolicy(owner); #endif } public ChartElement() { _editor = CreateEditorPolicy(this); _editor.OnCtorStart(); _rendererController.RendererSelectionPolicy = DefaultChartRendererSelectionPolicy.Instance; style.width = 300; style.height = 200; style.backgroundColor = new StyleColor(Color.clear); ChartKernel kernel = null; var callbacks = new ChartRefreshCallbacks( null, null, null, null, null, null, null, null); _refresh = _editor.CreateRefreshPipeline( this, () => Data != null, () => kernel?.ExecuteRebuildRenderers(), () => kernel?.ExecuteCalculateRange(), () => kernel?.ExecuteRefreshAxisLayer(), () => kernel?.ExecuteRefreshGridLayer(), () => kernel?.ExecuteRefreshSeriesRenderers(), () => kernel?.ExecuteRefreshLayersNoLegend(), () => kernel?.ExecuteRefreshLegendDeferred(), () => kernel?.ExecutePlayAnimation()); _kernel = kernel = new ChartKernel(this, _refresh, callbacks); _backgroundLayer = new VisualElement(); _backgroundLayer.pickingMode = PickingMode.Ignore; _backgroundLayer.StretchToParentSize(); Add(_backgroundLayer); _gridLayer = new GridLayer(); _axisLayer = new AxisLayer(); _axisLayer.userData = this; _chartArea = new VisualElement(); _chartArea.name = "chart-area"; _chartArea.style.position = Position.Absolute; _chartArea.style.left = _padding.x; _chartArea.style.right = _padding.y; _chartArea.style.top = _padding.z; _chartArea.style.bottom = _padding.w; _chartArea.style.overflow = Overflow.Visible; _chartArea.pickingMode = PickingMode.Ignore; var plotViewport = new VisualElement(); plotViewport.name = "plot-viewport"; plotViewport.style.position = Position.Absolute; plotViewport.style.left = 0; plotViewport.style.right = 0; plotViewport.style.top = 0; plotViewport.style.bottom = 0; plotViewport.style.overflow = Overflow.Hidden; plotViewport.pickingMode = PickingMode.Ignore; var plotContentRoot = new VisualElement(); plotContentRoot.name = "plot-content"; plotContentRoot.style.position = Position.Absolute; plotContentRoot.style.left = 0; plotContentRoot.style.right = 0; plotContentRoot.style.top = 0; plotContentRoot.style.bottom = 0; plotContentRoot.style.overflow = Overflow.Visible; plotContentRoot.pickingMode = PickingMode.Ignore; _plotViewport = plotViewport; _plotContentRoot = plotContentRoot; _chartContent = plotViewport; _labelOverlay = new VisualElement(); _labelOverlay.name = "label-overlay"; _labelOverlay.style.position = Position.Absolute; _labelOverlay.style.left = 0; _labelOverlay.style.right = 0; _labelOverlay.style.top = 0; _labelOverlay.style.bottom = 0; _labelOverlay.style.overflow = Overflow.Visible; _labelOverlay.pickingMode = PickingMode.Ignore; _labelOverlay.userData = this; _labelController.Bind(_labelOverlay); plotContentRoot.Add(_gridLayer); _barRenderer = new BarSeriesRenderer(); _lineRenderer = new LineSeriesRenderer(); _scatterRenderer = new ScatterSeriesRenderer(); _radarRenderer = new RadarSeriesRenderer(); _pieRenderer = new PieSeriesRenderer(); _barRenderer.visible = false; _lineRenderer.visible = false; _scatterRenderer.visible = false; _radarRenderer.visible = false; _pieRenderer.visible = false; plotContentRoot.Add(_barRenderer); plotContentRoot.Add(_lineRenderer); plotContentRoot.Add(_scatterRenderer); plotContentRoot.Add(_radarRenderer); plotContentRoot.Add(_pieRenderer); _barRenderer.SetLabelRoot(_labelOverlay); _lineRenderer.SetLabelRoot(_labelOverlay); _scatterRenderer.SetLabelRoot(_labelOverlay); _radarRenderer.SetLabelRoot(_labelOverlay); _pieRenderer.SetLabelRoot(_labelOverlay); _rendererController.Bind( _plotViewport, _plotContentRoot, _labelOverlay, _gridLayer, _axisLayer, _barRenderer, _lineRenderer, _scatterRenderer, _radarRenderer, _pieRenderer); plotViewport.Add(plotContentRoot); _chartArea.Add(plotViewport); _chartArea.Add(_axisLayer); Add(_chartArea); _legend.EnsureContainer(_chartArea); _chartArea.Add(_labelOverlay); _legend.SetInteractionState(_interactionState); _rendererController.SetInteractionState(_interactionState); _kernel.InstallDefaultModules(); _kernel.OnAttachToPanel(); RegisterCallback(OnGeometryChanged); RegisterCallback(_kernel.OnPointerMove); RegisterCallback(_kernel.OnPointerLeave); RegisterCallback((evt) => { _editor.OnAttachToPanel(evt); _kernel.OnAttachToPanel(); }); RegisterCallback((evt) => { _kernel.OnDetachFromPanel(); _editor.OnDetachFromPanel(evt); }); // Animation Loop _editor.ScheduleRuntimeAnimationLoop(OnAnimationUpdate); if (Data == null) SetData(new ChartData()); } public void PlayAnimation() { _editor.OnPlayAnimationStart(); _animation.Play(this, Application.isPlaying, _renderers, _editor.GetEditorAnimationUpdateCallback()); } internal void PlayAnimationAutoInternal() { if (!Application.isPlaying && _animation.IsAnimating) return; PlayAnimation(); } private void OnAnimationUpdate() { if (panel == null) { _animation.ResetRuntimeState(); } if (Data == null) return; _animation.TickRuntime(Data, _renderers, _backgroundLayer); } public void LoadProfileByName(string name) { #if UNITY_EDITOR string[] guids = AssetDatabase.FindAssets($"t:ChartProfile {name}"); if (guids.Length > 0) { string path = AssetDatabase.GUIDToAssetPath(guids[0]); var profile = AssetDatabase.LoadAssetAtPath(path); if (profile != null) { Profile = profile; } } #else var profile = Resources.Load(name); if (profile != null) Profile = profile; #endif } #if UNITY_EDITOR public bool LoadProfileByGuid(string guid) { if (string.IsNullOrEmpty(guid)) return false; string path = AssetDatabase.GUIDToAssetPath(guid); if (string.IsNullOrEmpty(path)) return false; var profile = AssetDatabase.LoadAssetAtPath(path); if (profile != null) { Profile = profile; return true; } return false; } #endif public void SetData(ChartData data) { _editor.OnSetDataStart(Data, data, out bool coordChanged); Data = data; UpdateTextureFillAnimationFlags(); if (_editor.TryDeferCoordSwitchApply(coordChanged)) return; _editor.OnSetDataApplyNow(); if (_kernel != null) _kernel.InvalidateDataAssignedWithAnimation(data, immediate: true); } private static bool HasTextureFillAnimation(TextureFillSettings fill) { return fill != null && fill.animationType != TextureFillAnimationType.None; } private void UpdateTextureFillAnimationFlags() { if (!ProPackage.IsInstalled) { _animation.SetHasTextureFillAnimations(false); _editor.OnTextureFillAnimationFlagsUpdated(false); return; } bool has = false; if (_chartProfile != null) { var bg = _chartProfile.background; if (bg != null && bg.show && HasTextureFillAnimation(bg.textureFill)) has = true; } if (!has && Data != null && Data.Series != null) { for (int i = 0; i < Data.Series.Count; i++) { var s = Data.Series[i]; if (s == null) continue; if (s.settings is LineSettings ls) { if (HasTextureFillAnimation(ls.stroke != null ? ls.stroke.textureFill : null)) { has = true; break; } if (HasTextureFillAnimation(ls.area != null ? ls.area.textureFill : null)) { has = true; break; } if (ls.point != null && HasTextureFillAnimation(ls.point.textureFill)) { has = true; break; } } else if (s.settings is ScatterSettings ss) { if (ss.point != null && HasTextureFillAnimation(ss.point.textureFill)) { has = true; break; } if (ss.hover != null && HasTextureFillAnimation(ss.hover.textureFill)) { has = true; break; } } else if (s.settings is BarSettings bs) { if (HasTextureFillAnimation(bs.textureFill)) { has = true; break; } if (bs.background != null && HasTextureFillAnimation(bs.background.textureFill)) { has = true; break; } if (bs.hover != null && HasTextureFillAnimation(bs.hover.textureFill)) { has = true; break; } } else if (s.settings is RadarSettings rs) { if (HasTextureFillAnimation(rs.stroke != null ? rs.stroke.textureFill : null)) { has = true; break; } if (HasTextureFillAnimation(rs.area != null ? rs.area.textureFill : null)) { has = true; break; } if (rs.point != null && HasTextureFillAnimation(rs.point.textureFill)) { has = true; break; } } } } _animation.SetHasTextureFillAnimations(has); _editor.OnTextureFillAnimationFlagsUpdated(has); } public void RefreshData(bool rebuildRenderers = false, bool playAnimation = true) { if (Data == null) return; if (_kernel != null) _kernel.InvalidateDataMutated(rebuildRenderers, playAnimation, immediate: true); } /// /// Force re-apply the current profile to the element, refreshing all data and visuals. /// Use this when the profile's data has been modified in-place. /// public void ForceRefreshProfile() { ApplyProfileToElement(); } private AxisConfig GetAxis(AxisId id) { if (Data == null || Data.Axes == null) return null; for (int i = 0; i < Data.Axes.Count; i++) { var a = Data.Axes[i]; if (a != null && a.id == id) return a; } return null; } private List GetCategoryLabels(AxisId id) { var axis = GetAxis(id); if (axis == null) return null; return axis.labels; } private List GetCategoryLabelsWindowed(AxisId id) { var axis = GetAxis(id); if (axis == null) return null; int start = (id == AxisId.XBottom || id == AxisId.XTop) ? _categoryScroll.WindowStartX : _categoryScroll.WindowStartY; var buffer = (id == AxisId.XBottom || id == AxisId.XTop) ? _windowedCategoryLabelsX : _windowedCategoryLabelsY; return _layoutModel.GetCategoryLabelsWindowed(id, axis, start, buffer); } private void ApplyCategoryScrollTranslate(float xPx, float yPx) { if (_plotContentRoot != null) { _plotContentRoot.style.translate = new Translate(xPx, yPx, 0); } if (_labelOverlay != null) { _labelOverlay.style.translate = new Translate(xPx, yPx, 0); } if (_axisLayer != null) { _axisLayer.SetCategoryScrollOffset(xPx, yPx); } } private void UpdateCategoryAxisRangeOnly() { int wx = _categoryScroll.WindowStartX; int wy = _categoryScroll.WindowStartY; float xMin = _xMin; float xMax = _xMax; float yMin = _yMin; float yMax = _yMax; _layoutModel.UpdateCategoryAxisRangeOnly(Data, GetAxis, ref wx, ref wy, ref xMin, ref xMax, ref yMin, ref yMax); _categoryScroll.WindowStartX = wx; _categoryScroll.WindowStartY = wy; _xMin = xMin; _xMax = xMax; _yMin = yMin; _yMax = yMax; } internal void OnCategoryScrollUpdate() { if (panel == null) return; if (Data == null) return; if (Data.CoordinateSystem != CoordinateSystemType.Cartesian2D) { if (_categoryScroll.ScrollOffsetX != 0f || _categoryScroll.ScrollOffsetY != 0f) { _categoryScroll.ResetOffsets(); ApplyCategoryScrollTranslate(0, 0); } return; } float dt = _editor.GetCategoryScrollDeltaTime(Time.unscaledDeltaTime); if (dt <= 0f || dt > 1f) dt = 0.016f; var xAxisId = GetMappedXAxisId(); var yAxisId = GetMappedYAxisId(); var xAxis = GetAxis(xAxisId); var yAxis = GetAxis(yAxisId); float prevOffsetX = _categoryScroll.ScrollOffsetX; float prevOffsetY = _categoryScroll.ScrollOffsetY; float plotWidth = _chartContent != null ? _chartContent.contentRect.width : 0f; float plotHeight = _chartContent != null ? _chartContent.contentRect.height : 0f; var update = _categoryScroll.Update(xAxisId, xAxis, yAxis, plotWidth, plotHeight, dt); if (update.NeedsRangeUpdate) { UpdateCategoryAxisRangeOnly(); } if (update.NeedsWindowRefresh) { if (_kernel != null) _kernel.Invalidate(ChartDirtyReason.CategoryWindowChanged, immediate: true); } if (_categoryScroll.SmoothTranslating) { HideTooltip(); ApplyCategoryScrollTranslate(_categoryScroll.ScrollOffsetX, _categoryScroll.ScrollOffsetY); } else { if (prevOffsetX != 0f || prevOffsetY != 0f) { ApplyCategoryScrollTranslate(0, 0); } } } private void OnGeometryChanged(GeometryChangedEvent evt) { _kernel?.OnGeometryChanged(evt); } private void HideTooltip() { _tooltipController.Hide(_tooltip, _cursorLine); } } }