Files

743 lines
27 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using EasyChart;
namespace EasyChart.Layers
{
public abstract class BaseSeriesRenderer : VisualElement
{
public ChartData Data { get; set; }
public Color BackgroundColor { get; set; } = Color.black; // Default
public virtual void ClearHover() { }
// Animation
protected float _animationProgress = 1.0f;
public float AnimationProgress
{
get => _animationProgress;
set
{
if (!Mathf.Approximately(_animationProgress, value))
{
_animationProgress = value;
MarkDirtyRepaint();
// Optional: UpdateLabels() if labels depend on animation?
// Usually labels might fade in or move. For now let's keep labels static or hide them during animation?
// Better to just repaint for now.
}
}
}
// Common properties for range, can be overridden or used by subclasses
protected float _xMin, _xMax;
protected float _yMin, _yMax;
protected VisualElement _labelContainer;
// Label Pooling
private List<Label> _labelPool = new List<Label>();
private int _activeLabelCount = 0;
public BaseSeriesRenderer()
{
this.StretchToParentSize();
pickingMode = PickingMode.Ignore;
style.overflow = Overflow.Visible;
generateVisualContent += OnGenerateVisualContent;
_labelContainer = new VisualElement();
_labelContainer.pickingMode = PickingMode.Ignore;
_labelContainer.StretchToParentSize();
_labelContainer.style.overflow = Overflow.Visible;
Add(_labelContainer);
}
public void SetLabelRoot(VisualElement root)
{
if (root == null) return;
if (_labelContainer == null) return;
if (root.userData is ChartElement)
{
_labelContainer.userData = root.userData;
userData = root.userData;
}
if (_labelContainer.parent != root)
{
_labelContainer.RemoveFromHierarchy();
root.Add(_labelContainer);
}
}
protected void BeginUpdateLabels()
{
_activeLabelCount = 0;
}
protected Label GetLabel()
{
Label lbl;
if (_activeLabelCount < _labelPool.Count)
{
lbl = _labelPool[_activeLabelCount];
lbl.visible = true;
}
else
{
lbl = new Label();
lbl.pickingMode = PickingMode.Ignore;
_labelContainer.Add(lbl);
_labelPool.Add(lbl);
}
_activeLabelCount++;
return lbl;
}
protected void ApplyCommonLabelStyle(Label lbl, Serie serie)
{
if (lbl == null || serie == null) return;
lbl.style.position = Position.Absolute;
lbl.style.color = serie.labelSettings.color;
int fontSize = serie.labelSettings.fontSize;
if (fontSize > 0)
{
lbl.style.fontSize = fontSize;
}
else
{
lbl.style.fontSize = StyleKeyword.Null;
}
ChartTextStyleApplier.ApplyLabel(lbl, _labelContainer != null ? _labelContainer : this, ChartTextRole.SeriesLabel);
}
protected void ApplyLabelOffset(Label lbl, Serie serie, float baseMarginLeft, float baseMarginTop)
{
if (lbl == null || serie == null) return;
Vector2 offset = serie.labelSettings != null ? serie.labelSettings.offset : Vector2.zero;
lbl.style.marginLeft = baseMarginLeft + offset.x;
lbl.style.marginTop = baseMarginTop - offset.y;
}
protected Label DrawSerieLabel(Serie serie, string text, Vector2 position, float baseMarginLeft, float baseMarginTop)
{
if (serie == null) return null;
Label lbl = GetLabel();
lbl.text = text;
ApplyCommonLabelStyle(lbl, serie);
lbl.style.left = position.x;
lbl.style.top = position.y;
ApplyLabelOffset(lbl, serie, baseMarginLeft, baseMarginTop);
return lbl;
}
protected void CalcQuadUV(Vector2 tiling, Vector2 offset, bool flipV, out float u0, out float u1, out float v0, out float v1)
{
u0 = offset.x;
u1 = tiling.x + offset.x;
if (flipV)
{
v0 = tiling.y + offset.y;
v1 = offset.y;
}
else
{
v0 = offset.y;
v1 = tiling.y + offset.y;
}
}
protected void UnpackTextureFill(TextureFillSettings fill, Color defaultColor, out Texture2D texture, out Vector2 tiling, out Vector2 offset, out Color color)
{
if (fill != null)
{
texture = fill.texture;
tiling = fill.tiling;
offset = fill.offset;
color = fill.color;
bool hasPro = ProPackage.IsInstalled;
if (hasPro && fill.animationType != TextureFillAnimationType.None)
{
float t = Time.realtimeSinceStartup;
if (fill.animationType == TextureFillAnimationType.TextureUvMove)
{
offset -= fill.uvMoveSpeed * t;
}
else if (fill.animationType == TextureFillAnimationType.TextureScale)
{
var baseTiling = tiling;
float u = t * fill.scaleSpeed;
float c01;
Vector2 factor;
switch (fill.scaleType)
{
case TextureFillScaleType.ZoomIn:
{
float p = Mathf.Repeat(u, 1f);
var s = Vector2.Lerp(fill.scaleFrom, fill.scaleTo, p);
s.x = Mathf.Max(0.0001f, s.x);
s.y = Mathf.Max(0.0001f, s.y);
factor = new Vector2(1f / s.x, 1f / s.y);
c01 = p;
break;
}
case TextureFillScaleType.ZoomOut:
{
float p = Mathf.Repeat(u, 1f);
var s = Vector2.Lerp(fill.scaleTo, fill.scaleFrom, p);
s.x = Mathf.Max(0.0001f, s.x);
s.y = Mathf.Max(0.0001f, s.y);
factor = new Vector2(1f / s.x, 1f / s.y);
c01 = p;
break;
}
case TextureFillScaleType.PingPong:
{
float p = Mathf.PingPong(u, 1f);
var s = Vector2.Lerp(fill.scaleFrom, fill.scaleTo, p);
s.x = Mathf.Max(0.0001f, s.x);
s.y = Mathf.Max(0.0001f, s.y);
factor = new Vector2(1f / s.x, 1f / s.y);
c01 = p;
break;
}
case TextureFillScaleType.Sin:
default:
{
float s = Mathf.Sin(u);
float p = (s + 1f) * 0.5f;
var sc = Vector2.Lerp(fill.scaleFrom, fill.scaleTo, p);
sc.x = Mathf.Max(0.0001f, sc.x);
sc.y = Mathf.Max(0.0001f, sc.y);
factor = new Vector2(1f / sc.x, 1f / sc.y);
c01 = p;
break;
}
}
factor.x = Mathf.Max(0.0001f, factor.x);
factor.y = Mathf.Max(0.0001f, factor.y);
tiling = new Vector2(baseTiling.x * factor.x, baseTiling.y * factor.y);
offset += (baseTiling - tiling) * 0.5f;
if (fill.colorFadeGradient != null)
color *= fill.colorFadeGradient.Evaluate(c01);
}
else if (fill.animationType == TextureFillAnimationType.TextureFade)
{
float u = t * fill.colorFadeSpeed;
float c01;
switch (fill.colorFadeWrap)
{
case TextureFillColorOverLifeWrap.Loop:
c01 = Mathf.Repeat(u, 1f);
break;
case TextureFillColorOverLifeWrap.Clamp:
c01 = Mathf.Clamp01(u);
break;
case TextureFillColorOverLifeWrap.PingPong:
default:
c01 = Mathf.PingPong(u, 1f);
break;
}
if (fill.colorFadeGradient != null)
color = fill.colorFadeGradient.Evaluate(c01);
else
color = Color.Lerp(fill.colorFadeStart, fill.colorFadeEnd, c01);
}
}
if (texture != null && Application.isPlaying)
{
var desiredWrap = (hasPro && fill.animationType == TextureFillAnimationType.TextureScale)
? TextureWrapMode.Clamp
: TextureWrapMode.Repeat;
if (texture.wrapMode != desiredWrap)
texture.wrapMode = desiredWrap;
}
}
else
{
texture = null;
tiling = Vector2.one;
offset = Vector2.zero;
color = defaultColor;
}
}
protected void UnpackTextureFill(TextureFillSettings fill, out Texture2D texture, out Vector2 tiling, out Vector2 offset, out Color color)
{
UnpackTextureFill(fill, Color.white, out texture, out tiling, out offset, out color);
}
protected Color ResolveFillColor(TextureFillSettings fill, Color defaultColor)
{
return fill != null ? fill.color : defaultColor;
}
protected bool TryResolveTextureFill(TextureFillSettings fill, Color defaultColor, bool requireTexture, out Texture2D texture, out Vector2 tiling, out Vector2 offset, out Color color)
{
if (fill == null || (requireTexture && fill.texture == null))
{
texture = null;
tiling = Vector2.one;
offset = Vector2.zero;
color = defaultColor;
return false;
}
texture = fill.texture;
tiling = fill.tiling;
offset = fill.offset;
color = fill.color;
return true;
}
protected bool DrawTexturedQuad(MeshGenerationContext context, Rect rect, Texture2D texture, Vector2 tiling, Vector2 offset, Color tint, bool flipV)
{
return DrawTexturedQuad(context, rect, (Texture)texture, tiling, offset, tint, flipV);
}
protected bool DrawTexturedQuad(MeshGenerationContext context, Rect rect, Texture texture, Vector2 tiling, Vector2 offset, Color tint, bool flipV)
{
if (texture == null) return false;
CalcQuadUV(tiling, offset, flipV, out float u0, out float u1, out float v0, out float v1);
MeshUtils.WriteTexturedQuad(context, rect.xMin, rect.yMin, rect.xMax, rect.yMax, texture, tint, u0, v0, u1, v1);
return true;
}
protected bool DrawTexturedFan(MeshGenerationContext context, IList<Vector2> points, Rect rect, Texture texture, Vector2 tiling, Vector2 offset, Color tint, bool flipV)
{
if (texture == null) return false;
if (points == null || points.Count < 3) return false;
CalcQuadUV(tiling, offset, flipV, out float u0, out float u1, out float v0, out float v1);
MeshUtils.WriteTexturedFan(context, points, texture, tint, rect, u0, v0, u1, v1);
return true;
}
protected bool DrawTexturedVerticalStrip(MeshGenerationContext context, IList<Vector2> topVertices, float bottomY, Texture texture, Vector2 tiling, Vector2 offset, Color tint, bool doubleSided)
{
if (texture == null) return false;
if (topVertices == null || topVertices.Count < 2) return false;
MeshUtils.WriteTexturedVerticalStrip(context, topVertices, bottomY, texture, tint, tiling, offset, doubleSided);
return true;
}
protected float EvalTextureScaleSizeMul(TextureFillSettings fill)
{
if (fill == null) return 1f;
if (fill.texture == null) return 1f;
bool hasPro = ProPackage.IsInstalled;
if (!hasPro) return 1f;
if (fill.animationType != TextureFillAnimationType.TextureScale) return 1f;
float t = Time.realtimeSinceStartup;
float u = t * fill.scaleSpeed;
float p;
switch (fill.scaleType)
{
case TextureFillScaleType.ZoomIn:
p = Mathf.Repeat(u, 1f);
break;
case TextureFillScaleType.ZoomOut:
p = Mathf.Repeat(u, 1f);
break;
case TextureFillScaleType.PingPong:
p = Mathf.PingPong(u, 1f);
break;
case TextureFillScaleType.Sin:
default:
p = (Mathf.Sin(u) + 1f) * 0.5f;
break;
}
Vector2 sc;
switch (fill.scaleType)
{
case TextureFillScaleType.ZoomOut:
sc = Vector2.Lerp(fill.scaleTo, fill.scaleFrom, p);
break;
case TextureFillScaleType.ZoomIn:
case TextureFillScaleType.PingPong:
case TextureFillScaleType.Sin:
default:
sc = Vector2.Lerp(fill.scaleFrom, fill.scaleTo, p);
break;
}
sc.x = Mathf.Max(0.0001f, sc.x);
sc.y = Mathf.Max(0.0001f, sc.y);
float m = Mathf.Max(sc.x, sc.y);
return Mathf.Max(1f, m);
}
protected void DrawPointMarker(MeshGenerationContext context, Painter2D painter, Vector2 pos, float radius, TextureFillSettings fill, Color defaultColor)
{
if (radius <= 0f) return;
UnpackTextureFill(fill, defaultColor, out var tex, out var tiling, out var offset, out var color);
bool hasPro = ProPackage.IsInstalled;
if (hasPro && fill != null && fill.animationType == TextureFillAnimationType.TextureScale)
{
tiling = fill.tiling;
offset = fill.offset;
}
float r = radius;
if (tex != null) r *= EvalTextureScaleSizeMul(fill);
if (tex != null)
{
DrawTexturedQuad(
context,
new Rect(pos.x - r, pos.y - r, r * 2f, r * 2f),
tex,
tiling,
offset,
color,
true);
}
else
{
painter.fillColor = color;
painter.BeginPath();
painter.Arc(pos, r, 0, 360);
painter.Fill();
}
}
protected void DrawPointOverlay(MeshGenerationContext context, Painter2D painter, Vector2 pos, float radius, TextureFillSettings fill)
{
if (radius <= 0f) return;
if (fill == null || fill.texture == null) return;
UnpackTextureFill(fill, Color.clear, out var tex, out var tiling, out var offset, out var tint);
if (tint.a <= 0f) return;
bool hasPro = ProPackage.IsInstalled;
if (hasPro && fill.animationType == TextureFillAnimationType.TextureScale)
{
tiling = fill.tiling;
offset = fill.offset;
}
float r = radius;
if (tex != null) r *= EvalTextureScaleSizeMul(fill);
if (tex != null)
{
DrawTexturedQuad(
context,
new Rect(pos.x - r, pos.y - r, r * 2f, r * 2f),
tex,
tiling,
offset,
tint,
true);
}
else
{
painter.fillColor = tint;
painter.BeginPath();
painter.Arc(pos, r, 0, 360);
painter.Fill();
}
}
protected void EnsureRepeatWrapMode(Texture texture)
{
if (texture == null) return;
if (!Application.isPlaying) return;
if (texture.wrapMode != TextureWrapMode.Repeat)
{
texture.wrapMode = TextureWrapMode.Repeat;
}
}
protected void EnsureRepeatWrapMode(Texture2D texture)
{
if (texture == null) return;
if (!Application.isPlaying) return;
if (texture.wrapMode != TextureWrapMode.Repeat)
{
texture.wrapMode = TextureWrapMode.Repeat;
}
}
protected void EndUpdateLabels()
{
for (int i = _activeLabelCount; i < _labelPool.Count; i++)
{
_labelPool[i].visible = false;
}
}
public void ClearLabels()
{
// Hide all labels in the pool
for (int i = 0; i < _labelPool.Count; i++)
{
_labelPool[i].visible = false;
}
_activeLabelCount = 0;
}
public virtual void SetRange(float xMin, float xMax, float yMin, float yMax)
{
_xMin = xMin;
_xMax = xMax;
_yMin = yMin;
_yMax = yMax;
MarkDirtyRepaint();
}
public virtual void UpdateLabels()
{
// Base implementation does nothing
}
public virtual bool GetTooltip(TooltipContext context, List<TooltipItem> items, ref Vector2? cursorPosition, ref string categoryLabel)
{
// Base implementation does nothing
return false;
}
protected AxisConfig GetAxisConfig(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 static readonly string[] s_fixedPointFormats =
{
"F0",
"F1",
"F2",
"F3",
"F4",
"F5",
"F6",
"F7",
"F8",
};
protected static string FormatAxisValue(float value, AxisConfig axis, int decimalPlaces)
{
if (axis != null && !string.IsNullOrEmpty(axis.labelFormat))
{
return value.ToString(axis.labelFormat);
}
int p = Mathf.Clamp(decimalPlaces, 0, 8);
return value.ToString(s_fixedPointFormats[p]);
}
private const int k_IntStringCacheSize = 512;
private static readonly string[] s_intStringCache = BuildIntStringCache();
private static string[] BuildIntStringCache()
{
var arr = new string[k_IntStringCacheSize];
for (int i = 0; i < arr.Length; i++)
{
arr[i] = i.ToString();
}
return arr;
}
protected static string FormatIntCached(int value)
{
if (value >= 0 && value < k_IntStringCacheSize)
{
return s_intStringCache[value];
}
return value.ToString();
}
#region Label Descriptor Helpers
protected bool TryGetChartAndLabelController(out ChartElement chart, out ChartLabelController controller)
{
chart = userData as ChartElement;
if (chart == null && _labelContainer != null)
{
chart = _labelContainer.userData as ChartElement;
}
controller = chart != null ? chart.LabelControllerInternal : null;
return controller != null;
}
protected void GetSmoothScrollOffsets(ChartElement chart, out float offsetX, out float offsetY)
{
offsetX = 0f;
offsetY = 0f;
if (chart == null) return;
var scroll = chart.CategoryScrollControllerInternal;
if (scroll == null) return;
if (!scroll.SmoothTranslating) return;
offsetX = scroll.ScrollOffsetX;
offsetY = scroll.ScrollOffsetY;
}
protected static bool IsAnchorVisibleInPlot(Vector2 anchorPx, float plotWidth, float plotHeight, float scrollOffsetX, float scrollOffsetY)
{
float visX = anchorPx.x + scrollOffsetX;
float visY = anchorPx.y + scrollOffsetY;
return visX >= 0f && visX <= plotWidth && visY >= 0f && visY <= plotHeight;
}
protected string GetStableKeyForPoint(bool isCategoryAxis, float categoryIndex, string pointId, int fallbackIndex)
{
if (isCategoryAxis)
{
int idx = Mathf.RoundToInt(categoryIndex);
return FormatIntCached(idx);
}
if (!string.IsNullOrEmpty(pointId)) return pointId;
return fallbackIndex.ToString();
}
protected static LabelDescriptor BuildSeriesLabelDesc(
string key,
string text,
Vector2 anchorPx,
Vector2 offsetPx,
bool clipToPlot,
int fontSizeOverride,
Color colorOverride,
ChartLabelAnchor anchor = ChartLabelAnchor.TopLeft,
Color backgroundColor = default,
Texture2D backgroundTexture = null)
{
return new LabelDescriptor
{
key = key,
text = text,
visible = true,
anchorPx = anchorPx,
offsetPx = offsetPx,
anchor = anchor,
role = ChartTextRole.SeriesLabel,
zOrder = 0,
priority = 0,
clipToPlot = clipToPlot,
fontSizeOverride = fontSizeOverride,
colorOverride = colorOverride,
rotationDeg = 0f,
backgroundColor = backgroundColor,
backgroundTexture = backgroundTexture,
};
}
#endregion
protected virtual Vector2 GetPixelPos(Vector2 point, float width, float height)
{
if (Mathf.Approximately(_xMax, _xMin)) return new Vector2(0, height);
if (Mathf.Approximately(_yMax, _yMin)) return new Vector2(point.x, height);
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
AxisId yAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft;
var xAxis = GetAxisConfig(xAxisId);
var yAxis = GetAxisConfig(yAxisId);
bool xIsCategory = xAxis != null && xAxis.axisType == AxisType.Category && xAxis.labels != null && xAxis.labels.Count > 0;
bool yIsCategory = yAxis != null && yAxis.axisType == AxisType.Category && yAxis.labels != null && yAxis.labels.Count > 0;
float xVal = point.x;
if (xIsCategory)
{
int labelCount = xAxis.labels.Count;
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
if (span < 1) span = 1;
int preloadExtra = labelCount > span ? 1 : 0;
float effectiveMax = _xMax + preloadExtra;
bool wraps = effectiveMax >= labelCount;
if (wraps && xVal < _xMin)
{
xVal += labelCount;
}
}
float xRatio;
if (xIsCategory && xAxis.labelPlacement == CategoryLabelPlacement.CellCenter)
{
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
if (span < 1) span = 1;
xRatio = (xVal - _xMin + 0.5f) / span;
}
else
{
xRatio = (xVal - _xMin) / (_xMax - _xMin);
}
float yVal = point.y;
if (yIsCategory)
{
int labelCount = yAxis.labels.Count;
int span = Mathf.RoundToInt(_yMax - _yMin + 1);
if (span < 1) span = 1;
int preloadExtra = labelCount > span ? 1 : 0;
float effectiveMax = _yMax + preloadExtra;
bool wraps = effectiveMax >= labelCount;
if (wraps && yVal < _yMin)
{
yVal += labelCount;
}
}
float yRatio;
if (yIsCategory && yAxis.labelPlacement == CategoryLabelPlacement.CellCenter)
{
int span = Mathf.RoundToInt(_yMax - _yMin + 1);
if (span < 1) span = 1;
yRatio = (yVal - _yMin + 0.5f) / span;
}
else
{
yRatio = (yVal - _yMin) / (_yMax - _yMin);
}
if (float.IsNaN(xRatio)) xRatio = 0;
if (float.IsNaN(yRatio)) yRatio = 0;
float pixelX = xRatio * width;
bool xOnTop = xAxisId == AxisId.XTop;
float pixelY = (yIsCategory && xOnTop) ? (yRatio * height) : (height - (yRatio * height));
return new Vector2(pixelX, pixelY);
}
protected abstract void OnGenerateVisualContent(MeshGenerationContext context);
}
}