437 lines
16 KiB
C#
437 lines
16 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using EasyChart;
|
|
|
|
namespace EasyChart.Layers
|
|
{
|
|
public class ScatterSeriesRenderer : BaseSeriesRenderer
|
|
{
|
|
private struct ScatterHit
|
|
{
|
|
public int SerieIndex;
|
|
public int PointIndex;
|
|
public Vector2 PixelPos;
|
|
public float Dist;
|
|
}
|
|
|
|
private ScatterHit? _hover;
|
|
|
|
public ScatterSeriesRenderer()
|
|
{
|
|
schedule.Execute(OnUpdate).Every(16);
|
|
}
|
|
|
|
private static bool HasTextureFillAnimation(TextureFillSettings fill)
|
|
{
|
|
return fill != null && fill.animationType != TextureFillAnimationType.None;
|
|
}
|
|
|
|
private void OnUpdate()
|
|
{
|
|
if (panel == null) return;
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
if (!ProPackage.IsInstalled) return;
|
|
|
|
for (int i = 0; i < Data.Series.Count; i++)
|
|
{
|
|
var s = Data.Series[i];
|
|
if (s == null || !s.visible) continue;
|
|
if (s.type != SerieType.Scatter) continue;
|
|
if (s.settings is not ScatterSettings settings) continue;
|
|
if (settings.point != null && HasTextureFillAnimation(settings.point.textureFill)) { MarkDirtyRepaint(); return; }
|
|
if (settings.hover != null && HasTextureFillAnimation(settings.hover.textureFill)) { MarkDirtyRepaint(); return; }
|
|
if (settings.hover != null && settings.hover.point != null && HasTextureFillAnimation(settings.hover.point.textureFill)) { MarkDirtyRepaint(); return; }
|
|
}
|
|
}
|
|
|
|
private void DrawPointMarkerWithAlpha(MeshGenerationContext context, Painter2D painter, Vector2 pos, float radius, TextureFillSettings fill, Color defaultColor, float alpha, Vector2 uvOffsetAdd)
|
|
{
|
|
if (radius <= 0f) return;
|
|
if (alpha <= 0.001f) 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;
|
|
}
|
|
|
|
offset += uvOffsetAdd;
|
|
color.a *= alpha;
|
|
if (color.a <= 0.001f) return;
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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 bool IsCartesianTransposed()
|
|
{
|
|
if (Data == null) return false;
|
|
AxisId xAxisId = (Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
AxisId yAxisId = (Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft;
|
|
|
|
var xAxis = GetAxis(xAxisId);
|
|
var yAxis = GetAxis(yAxisId);
|
|
var xDim = xAxis != null ? xAxis.axisType : AxisType.Category;
|
|
var yDim = yAxis != null ? yAxis.axisType : AxisType.Value;
|
|
return xDim == AxisType.Value && yDim == AxisType.Category;
|
|
}
|
|
|
|
protected override Vector2 GetPixelPos(Vector2 point, float width, float height)
|
|
{
|
|
if (IsCartesianTransposed())
|
|
{
|
|
return base.GetPixelPos(new Vector2(point.y, point.x), width, height);
|
|
}
|
|
return base.GetPixelPos(point, width, height);
|
|
}
|
|
|
|
private static float GetScatterY(SeriesData p, ScatterSettings settings)
|
|
{
|
|
if (p == null) return 0f;
|
|
|
|
float y = p.y;
|
|
if (Mathf.Approximately(y, 0f) && !Mathf.Approximately(p.value, 0f))
|
|
{
|
|
y = p.value;
|
|
}
|
|
|
|
return y;
|
|
}
|
|
|
|
private float EvalPointSize(SeriesData dataPoint, ScatterSettings settings)
|
|
{
|
|
if (settings == null || settings.point == null) return 0f;
|
|
if (dataPoint == null) return 0f;
|
|
|
|
if (settings.sizeMapping == null || !settings.sizeMapping.enabled)
|
|
{
|
|
return settings.point.size;
|
|
}
|
|
|
|
float minV = settings.sizeMapping.minValue;
|
|
float maxV = settings.sizeMapping.maxValue;
|
|
float t;
|
|
if (Mathf.Approximately(maxV, minV))
|
|
{
|
|
t = 0f;
|
|
}
|
|
else t = (dataPoint.z - minV) / (maxV - minV);
|
|
|
|
if (settings.sizeMapping.clamp) t = Mathf.Clamp01(t);
|
|
|
|
float c = settings.sizeMapping.curve != null ? settings.sizeMapping.curve.Evaluate(t) : t;
|
|
if (settings.sizeMapping.clamp) c = Mathf.Clamp01(c);
|
|
|
|
return Mathf.Lerp(settings.sizeMapping.minSize, settings.sizeMapping.maxSize, c);
|
|
}
|
|
|
|
private float GetPickRadius(ScatterSettings settings)
|
|
{
|
|
if (settings == null || settings.hover == null || !settings.hover.enabled) return 0f;
|
|
return Mathf.Max(0f, settings.hover.pickRadius);
|
|
}
|
|
|
|
private ScatterHit? FindHoverHit(TooltipContext context)
|
|
{
|
|
if (Data == null || Data.Series == null) return null;
|
|
|
|
float width = context.Width;
|
|
float height = context.Height;
|
|
if (width <= 0 || height <= 0) return null;
|
|
|
|
ScatterHit? best = null;
|
|
|
|
for (int si = 0; si < Data.Series.Count; si++)
|
|
{
|
|
var serie = Data.Series[si];
|
|
if (serie == null || !serie.visible) continue;
|
|
if (serie.type != SerieType.Scatter) continue;
|
|
if (serie.settings is not ScatterSettings settings) continue;
|
|
|
|
float pickRadius = GetPickRadius(settings);
|
|
if (pickRadius <= 0f) continue;
|
|
|
|
float bestDistSqForSerie = pickRadius * pickRadius;
|
|
|
|
if (serie.seriesData == null) continue;
|
|
for (int pi = 0; pi < serie.seriesData.Count; pi++)
|
|
{
|
|
var p = serie.seriesData[pi];
|
|
if (p == null) continue;
|
|
var pos = GetPixelPos(new Vector2(p.x, GetScatterY(p, settings)), width, height);
|
|
|
|
float dx = pos.x - context.LocalPos.x;
|
|
float dy = pos.y - context.LocalPos.y;
|
|
float distSq = dx * dx + dy * dy;
|
|
|
|
if (distSq > bestDistSqForSerie) continue;
|
|
|
|
float dist = Mathf.Sqrt(distSq);
|
|
if (best == null || dist < best.Value.Dist)
|
|
{
|
|
best = new ScatterHit
|
|
{
|
|
SerieIndex = si,
|
|
PointIndex = pi,
|
|
PixelPos = pos,
|
|
Dist = dist
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
public override void ClearHover()
|
|
{
|
|
if (_hover != null)
|
|
{
|
|
_hover = null;
|
|
MarkDirtyRepaint();
|
|
}
|
|
}
|
|
|
|
public override void UpdateLabels()
|
|
{
|
|
if (!TryGetChartAndLabelController(out var chart, out var labelController)) return;
|
|
GetSmoothScrollOffsets(chart, out float scrollOffsetX, out float scrollOffsetY);
|
|
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
float w = contentRect.width;
|
|
float h = contentRect.height;
|
|
if (w <= 0 || h <= 0) return;
|
|
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
var xAxis = GetAxisConfig(xAxisId);
|
|
bool xIsCategory = xAxis != null && xAxis.axisType == AxisType.Category && xAxis.labels != null && xAxis.labels.Count > 0;
|
|
|
|
AxisId yAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft;
|
|
var yAxis = GetAxisConfig(yAxisId);
|
|
|
|
for (int si = 0; si < Data.Series.Count; si++)
|
|
{
|
|
var serie = Data.Series[si];
|
|
if (serie == null) continue;
|
|
if (!serie.visible) continue;
|
|
if (serie.type != SerieType.Scatter) continue;
|
|
if (!serie.labelSettings.enabled) continue;
|
|
|
|
var settings = serie.settings as ScatterSettings;
|
|
if (settings == null) continue;
|
|
|
|
var points = serie.seriesData;
|
|
if (points == null) continue;
|
|
|
|
int dpPlaces = Mathf.Clamp(serie.labelSettings != null ? serie.labelSettings.decimalPlaces : 2, 0, 8);
|
|
bool showName = serie.labelSettings != null && serie.labelSettings.showName;
|
|
Vector2 extraOffset = serie.labelSettings != null ? serie.labelSettings.offset : Vector2.zero;
|
|
int fontSizeOverride = serie.labelSettings != null ? serie.labelSettings.fontSize : 0;
|
|
Color colorOverride = serie.labelSettings != null ? serie.labelSettings.color : Color.clear;
|
|
|
|
float baseMarginTop;
|
|
if (serie.labelSettings.position == LabelPosition.Inside)
|
|
baseMarginTop = 10;
|
|
else if (serie.labelSettings.position == LabelPosition.Center)
|
|
baseMarginTop = -7;
|
|
else
|
|
baseMarginTop = -20;
|
|
|
|
for (int pi = 0; pi < points.Count; pi++)
|
|
{
|
|
var point = points[pi];
|
|
if (point == null) continue;
|
|
|
|
float y = GetScatterY(point, settings);
|
|
Vector2 pos = GetPixelPos(new Vector2(point.x, y), w, h);
|
|
if (!IsAnchorVisibleInPlot(pos, w, h, scrollOffsetX, scrollOffsetY)) continue;
|
|
|
|
string text = FormatAxisValue(y, yAxis, dpPlaces);
|
|
if (showName) text = $"{serie.name}\n{text}";
|
|
|
|
string pointId = GetStableKeyForPoint(xIsCategory, point.x, point.id, pi);
|
|
var desc = BuildSeriesLabelDesc(
|
|
$"scatter:{si}:{pointId}",
|
|
text,
|
|
pos,
|
|
new Vector2(-10 + extraOffset.x, -baseMarginTop + extraOffset.y),
|
|
clipToPlot: true,
|
|
fontSizeOverride,
|
|
colorOverride);
|
|
|
|
labelController.Submit(desc);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override bool GetTooltip(TooltipContext context, List<TooltipItem> items, ref Vector2? cursorPosition, ref string categoryLabel)
|
|
{
|
|
var hit = FindHoverHit(context);
|
|
|
|
bool hoverChanged = false;
|
|
if (hit.HasValue)
|
|
{
|
|
if (!_hover.HasValue || _hover.Value.SerieIndex != hit.Value.SerieIndex || _hover.Value.PointIndex != hit.Value.PointIndex)
|
|
{
|
|
_hover = hit;
|
|
hoverChanged = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_hover != null)
|
|
{
|
|
_hover = null;
|
|
hoverChanged = true;
|
|
}
|
|
}
|
|
|
|
if (hoverChanged) MarkDirtyRepaint();
|
|
|
|
if (!hit.HasValue) return false;
|
|
|
|
var serie = Data.Series[hit.Value.SerieIndex];
|
|
if (serie == null || serie.seriesData == null) return false;
|
|
if (hit.Value.PointIndex < 0 || hit.Value.PointIndex >= serie.seriesData.Count) return false;
|
|
|
|
var dp = serie.seriesData[hit.Value.PointIndex];
|
|
if (dp == null) return false;
|
|
|
|
var settings = serie.settings as ScatterSettings;
|
|
float y = GetScatterY(dp, settings);
|
|
|
|
if (cursorPosition == null)
|
|
{
|
|
cursorPosition = hit.Value.PixelPos;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(categoryLabel))
|
|
{
|
|
if (!string.IsNullOrEmpty(dp.name))
|
|
{
|
|
categoryLabel = dp.name;
|
|
}
|
|
else
|
|
{
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
var xAxis = GetAxisConfig(xAxisId);
|
|
categoryLabel = FormatAxisValue(dp.x, xAxis, 2);
|
|
}
|
|
}
|
|
|
|
Color c = Color.white;
|
|
if (settings != null && settings.point != null)
|
|
c = ResolveFillColor(settings.point.textureFill, Color.white);
|
|
|
|
items.Add(new TooltipItem
|
|
{
|
|
Name = serie.name,
|
|
Value = FormatAxisValue(
|
|
y,
|
|
GetAxisConfig((Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft),
|
|
Mathf.Clamp(serie.labelSettings != null ? serie.labelSettings.decimalPlaces : 2, 0, 8)
|
|
),
|
|
Color = c
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
protected override void OnGenerateVisualContent(MeshGenerationContext context)
|
|
{
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
var width = contentRect.width;
|
|
var height = contentRect.height;
|
|
if (width <= 0 || height <= 0) return;
|
|
|
|
var painter = context.painter2D;
|
|
|
|
float pixelClipX = width * _animationProgress;
|
|
|
|
for (int si = 0; si < Data.Series.Count; si++)
|
|
{
|
|
var serie = Data.Series[si];
|
|
if (serie == null || !serie.visible || serie.seriesData == null || serie.seriesData.Count == 0) continue;
|
|
if (serie.type != SerieType.Scatter) continue;
|
|
var settings = serie.settings as ScatterSettings;
|
|
if (settings == null) continue;
|
|
if (settings.point == null || !settings.point.show) continue;
|
|
|
|
var pointFill = settings.point.textureFill;
|
|
|
|
for (int pi = 0; pi < serie.seriesData.Count; pi++)
|
|
{
|
|
var p = serie.seriesData[pi];
|
|
if (p == null) continue;
|
|
Vector2 pos = GetPixelPos(new Vector2(p.x, GetScatterY(p, settings)), width, height);
|
|
if (pos.x > pixelClipX) continue;
|
|
|
|
float size = EvalPointSize(p, settings);
|
|
|
|
bool hoverEnabled = settings.hover != null && settings.hover.enabled;
|
|
bool isHovered = _hover.HasValue && _hover.Value.SerieIndex == si && _hover.Value.PointIndex == pi;
|
|
|
|
if (hoverEnabled && isHovered && settings.hover.point != null)
|
|
{
|
|
var hp = settings.hover.point;
|
|
|
|
if (!hp.show) continue;
|
|
float radius = hp.size * 0.5f;
|
|
DrawPointMarkerWithAlpha(context, painter, pos, radius, hp.textureFill, Color.white, 1f, Vector2.zero);
|
|
}
|
|
else
|
|
{
|
|
TextureFillSettings hoverFill = null;
|
|
if (hoverEnabled && isHovered)
|
|
{
|
|
size *= Mathf.Max(0f, settings.hover.scale);
|
|
hoverFill = settings.hover.textureFill;
|
|
}
|
|
|
|
float radius = size * 0.5f;
|
|
DrawPointMarkerWithAlpha(context, painter, pos, radius, pointFill, Color.white, 1f, Vector2.zero);
|
|
DrawPointOverlay(context, painter, pos, radius, hoverFill);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|