Files
bansonic_beta_main/Assets/EasyChart/Scripts/Runtime/ChartElement.cs
T

1720 lines
66 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
public bool IgnoreProfileSize { get; set; } = false;
public new class UxmlFactory : UxmlFactory<ChartElement, UxmlTraits> { }
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<BaseSeriesRenderer> _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<string> _windowedCategoryLabelsX = new List<string>(16);
private readonly List<string> _windowedCategoryLabelsY = new List<string>(16);
private ChartRefreshPipeline _refresh;
private ChartKernel _kernel;
internal ChartTooltipController TooltipControllerInternal => _tooltipController;
internal List<BaseSeriesRenderer> 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<string> 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<string> 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)
{
_tooltip.userData = this;
ChartTextStyleApplier.ApplyLabel(_tooltip, this, ChartTextRole.Tooltip);
}
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<string> 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<bool> 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<string> 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<bool> 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<string> s_breadcrumbs = new Queue<string>(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<GeometryChangedEvent>(_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<string> 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<bool> 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<GeometryChangedEvent>(_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<string> 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<SerieType, BaseSeriesRenderer> 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<GeometryChangedEvent>(OnGeometryChanged);
RegisterCallback<PointerMoveEvent>(_kernel.OnPointerMove);
RegisterCallback<PointerLeaveEvent>(_kernel.OnPointerLeave);
RegisterCallback<AttachToPanelEvent>((evt) =>
{
_editor.OnAttachToPanel(evt);
_kernel.OnAttachToPanel();
});
RegisterCallback<DetachFromPanelEvent>((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<ChartProfile>(path);
if (profile != null)
{
Profile = profile;
}
}
#else
var profile = Resources.Load<ChartProfile>(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<ChartProfile>(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);
}
/// <summary>
/// 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.
/// </summary>
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<string> GetCategoryLabels(AxisId id)
{
var axis = GetAxis(id);
if (axis == null) return null;
return axis.labels;
}
private List<string> 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);
}
}
}