拼ui 一些业务逻辑x实现
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace EasyChart
|
||||
{
|
||||
public class AxisLayer : VisualElement
|
||||
{
|
||||
private const float LabelClipMarginPx = 2000f;
|
||||
private const float LabelEdgeClipMarginPx = 20f;
|
||||
|
||||
private VisualElement _xAxisViewport;
|
||||
private VisualElement _yAxisViewport;
|
||||
private VisualElement _xAxisContainer;
|
||||
private VisualElement _yAxisContainer;
|
||||
|
||||
// These are now mostly redundant if Settings are used, but kept for fallback
|
||||
public Color AxisColor { get; set; } = Color.white;
|
||||
public float AxisLineWidth { get; set; } = 2.0f;
|
||||
|
||||
// Data for labels
|
||||
public List<string> XLabels { get; set; } = new List<string>();
|
||||
public List<string> YLabels { get; set; } = new List<string>();
|
||||
|
||||
public float XMin { get; set; } = 0;
|
||||
public float XMax { get; set; } = 100;
|
||||
public int XSplitCount { get; set; } = 6;
|
||||
public float YMin { get; set; } = 0;
|
||||
public float YMax { get; set; } = 100;
|
||||
public int YSplitCount { get; set; } = 6;
|
||||
|
||||
public bool XIsCategory { get; set; } = true;
|
||||
public bool YIsCategory { get; set; } = false;
|
||||
|
||||
public AxisId XAxisId { get; set; } = AxisId.XBottom;
|
||||
public AxisId YAxisId { get; set; } = AxisId.YLeft;
|
||||
|
||||
public AxisConfig XAxisConfig { get; set; }
|
||||
public AxisConfig YAxisConfig { get; set; }
|
||||
|
||||
public AxisLayer()
|
||||
{
|
||||
this.StretchToParentSize();
|
||||
pickingMode = PickingMode.Ignore;
|
||||
|
||||
_xAxisViewport = new VisualElement { name = "xAxisViewport", pickingMode = PickingMode.Ignore };
|
||||
_yAxisViewport = new VisualElement { name = "yAxisViewport", pickingMode = PickingMode.Ignore };
|
||||
|
||||
_xAxisContainer = new VisualElement { name = "xAxis", pickingMode = PickingMode.Ignore };
|
||||
_yAxisContainer = new VisualElement { name = "yAxis", pickingMode = PickingMode.Ignore };
|
||||
|
||||
// X axis labels should be clipped horizontally to plot width, but may extend above/below.
|
||||
// We achieve this with a tall viewport (extra margin on Y) and an inner container aligned to plot coords.
|
||||
_xAxisViewport.style.position = Position.Absolute;
|
||||
_xAxisViewport.style.left = -LabelEdgeClipMarginPx;
|
||||
_xAxisViewport.style.right = -LabelEdgeClipMarginPx;
|
||||
_xAxisViewport.style.top = -LabelClipMarginPx;
|
||||
_xAxisViewport.style.bottom = -LabelClipMarginPx;
|
||||
_xAxisViewport.style.overflow = Overflow.Hidden;
|
||||
|
||||
// Y axis labels should be clipped vertically to plot height, but may extend left/right.
|
||||
// We achieve this with a wide viewport (extra margin on X) and an inner container aligned to plot coords.
|
||||
_yAxisViewport.style.position = Position.Absolute;
|
||||
_yAxisViewport.style.left = -LabelClipMarginPx;
|
||||
_yAxisViewport.style.right = -LabelClipMarginPx;
|
||||
_yAxisViewport.style.top = -LabelEdgeClipMarginPx;
|
||||
_yAxisViewport.style.bottom = -LabelEdgeClipMarginPx;
|
||||
_yAxisViewport.style.overflow = Overflow.Hidden;
|
||||
|
||||
// Containers stretch to parent, labels positioned with pixel values
|
||||
_xAxisContainer.style.position = Position.Absolute;
|
||||
_xAxisContainer.style.left = LabelEdgeClipMarginPx;
|
||||
_xAxisContainer.style.right = LabelEdgeClipMarginPx;
|
||||
_xAxisContainer.style.top = LabelClipMarginPx;
|
||||
_xAxisContainer.style.bottom = LabelClipMarginPx;
|
||||
_xAxisContainer.style.overflow = Overflow.Visible;
|
||||
|
||||
_yAxisContainer.style.position = Position.Absolute;
|
||||
_yAxisContainer.style.left = LabelClipMarginPx;
|
||||
_yAxisContainer.style.right = LabelClipMarginPx;
|
||||
_yAxisContainer.style.top = LabelEdgeClipMarginPx;
|
||||
_yAxisContainer.style.bottom = LabelEdgeClipMarginPx;
|
||||
_yAxisContainer.style.overflow = Overflow.Visible;
|
||||
|
||||
_xAxisViewport.Add(_xAxisContainer);
|
||||
_yAxisViewport.Add(_yAxisContainer);
|
||||
|
||||
Add(_xAxisViewport);
|
||||
Add(_yAxisViewport);
|
||||
|
||||
generateVisualContent += OnGenerateVisualContent;
|
||||
}
|
||||
|
||||
public void SetCategoryScrollOffset(float xPx, float yPx)
|
||||
{
|
||||
if (_xAxisContainer != null)
|
||||
{
|
||||
_xAxisContainer.style.translate = new Translate(xPx, 0, 0);
|
||||
}
|
||||
if (_yAxisContainer != null)
|
||||
{
|
||||
_yAxisContainer.style.translate = new Translate(0, yPx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static int ClampCategoryVisibleCount(AxisConfig axis, int labelsCount)
|
||||
{
|
||||
if (labelsCount <= 0) return 0;
|
||||
// When autoTicks is true, use all labels
|
||||
if (axis != null && axis.autoTicks) return labelsCount;
|
||||
int v = axis != null ? axis.splitCount : 0;
|
||||
if (v < 2) v = 2;
|
||||
if (v > labelsCount) v = labelsCount;
|
||||
return v;
|
||||
}
|
||||
|
||||
private void OnGenerateVisualContent(MeshGenerationContext context)
|
||||
{
|
||||
var w = contentRect.width;
|
||||
var h = contentRect.height;
|
||||
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
var painter = context.painter2D;
|
||||
|
||||
// Use config if available, else fallback
|
||||
var xColor = XAxisConfig != null ? XAxisConfig.color : AxisColor;
|
||||
var xWidth = XAxisConfig != null ? XAxisConfig.width : AxisLineWidth;
|
||||
var yColor = YAxisConfig != null ? YAxisConfig.color : AxisColor;
|
||||
var yWidth = YAxisConfig != null ? YAxisConfig.width : AxisLineWidth;
|
||||
|
||||
bool showX = XAxisConfig != null ? XAxisConfig.visible : true;
|
||||
bool showY = YAxisConfig != null ? YAxisConfig.visible : true;
|
||||
|
||||
painter.BeginPath();
|
||||
|
||||
float xAxisY = XAxisId == AxisId.XTop ? 0 : h;
|
||||
float yAxisX = YAxisId == AxisId.YRight ? w : 0;
|
||||
|
||||
// X Axis Line
|
||||
if (showX)
|
||||
{
|
||||
painter.strokeColor = xColor;
|
||||
painter.lineWidth = xWidth;
|
||||
painter.MoveTo(new Vector2(0, xAxisY));
|
||||
painter.LineTo(new Vector2(w, xAxisY));
|
||||
painter.Stroke(); // Stroke separately to support different colors
|
||||
painter.BeginPath();
|
||||
}
|
||||
|
||||
// Y Axis Line
|
||||
if (showY)
|
||||
{
|
||||
painter.strokeColor = yColor;
|
||||
painter.lineWidth = yWidth;
|
||||
painter.MoveTo(new Vector2(yAxisX, 0));
|
||||
painter.LineTo(new Vector2(yAxisX, h));
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyAxisLabelBaseStyle(Label label)
|
||||
{
|
||||
label.style.marginLeft = 0;
|
||||
label.style.marginRight = 0;
|
||||
label.style.marginTop = 0;
|
||||
label.style.marginBottom = 0;
|
||||
|
||||
label.style.paddingLeft = 0;
|
||||
label.style.paddingRight = 0;
|
||||
label.style.paddingTop = 0;
|
||||
label.style.paddingBottom = 0;
|
||||
|
||||
label.style.borderLeftWidth = 0;
|
||||
label.style.borderRightWidth = 0;
|
||||
label.style.borderTopWidth = 0;
|
||||
label.style.borderBottomWidth = 0;
|
||||
|
||||
label.style.flexGrow = 0;
|
||||
label.style.flexShrink = 0;
|
||||
label.style.whiteSpace = WhiteSpace.NoWrap;
|
||||
}
|
||||
|
||||
private static string FormatNumericTick(float val, string labelFormat)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(labelFormat)) return val.ToString(labelFormat);
|
||||
|
||||
float v = Mathf.Round(val * 1000f) / 1000f;
|
||||
float r = Mathf.Round(v);
|
||||
if (Mathf.Abs(v - r) < 0.0001f)
|
||||
{
|
||||
return ((int)r).ToString();
|
||||
}
|
||||
|
||||
return v.ToString("0.###");
|
||||
}
|
||||
|
||||
public void RefreshLabels()
|
||||
{
|
||||
_xAxisContainer.Clear();
|
||||
_yAxisContainer.Clear();
|
||||
|
||||
float w = contentRect.width;
|
||||
float h = contentRect.height;
|
||||
|
||||
var plotViewport = parent != null ? parent.Q<VisualElement>("plot-viewport") : null;
|
||||
float plotX0 = 0f;
|
||||
float plotW = w;
|
||||
if (plotViewport != null)
|
||||
{
|
||||
var rs = plotViewport.resolvedStyle;
|
||||
plotX0 = plotViewport.layout.x + rs.borderLeftWidth + rs.paddingLeft;
|
||||
plotW = plotViewport.contentRect.width;
|
||||
}
|
||||
|
||||
bool xOnTop = XAxisId == AxisId.XTop;
|
||||
bool yOnRight = YAxisId == AxisId.YRight;
|
||||
float xAxisY = xOnTop ? 0 : h;
|
||||
float yAxisX = yOnRight ? w : 0;
|
||||
|
||||
// --- X Axis Labels ---
|
||||
if (XAxisConfig != null && (XAxisConfig.labelStyle != null ? XAxisConfig.labelStyle.enabled : XAxisConfig.showLabels))
|
||||
{
|
||||
var xLabelStyle = XAxisConfig.labelStyle;
|
||||
var xLabels = XLabels;
|
||||
if ((xLabels == null || xLabels.Count == 0) && XAxisConfig != null && XAxisConfig.labels != null && XAxisConfig.labels.Count > 0)
|
||||
{
|
||||
xLabels = XAxisConfig.labels;
|
||||
}
|
||||
|
||||
if (xLabels != null && xLabels.Count > 0)
|
||||
{
|
||||
var sourceLabels = xLabels;
|
||||
if (XAxisConfig != null && XAxisConfig.axisType == AxisType.Category && XAxisConfig.labels != null && XAxisConfig.labels.Count > 0)
|
||||
{
|
||||
sourceLabels = XAxisConfig.labels;
|
||||
}
|
||||
|
||||
int totalCount = sourceLabels != null ? sourceLabels.Count : 0;
|
||||
if (totalCount <= 0) return;
|
||||
|
||||
int visibleCount = ClampCategoryVisibleCount(XAxisConfig, totalCount);
|
||||
int startIndex = 0;
|
||||
if (totalCount > visibleCount)
|
||||
{
|
||||
startIndex = Mathf.RoundToInt(XMin);
|
||||
startIndex = Mathf.Clamp(startIndex, 0, totalCount - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < visibleCount; i++)
|
||||
{
|
||||
int labelIndex = (totalCount > visibleCount) ? ((startIndex + i) % totalCount) : i;
|
||||
var label = new Label(sourceLabels[labelIndex]);
|
||||
ApplyAxisLabelBaseStyle(label);
|
||||
int fs = xLabelStyle != null ? xLabelStyle.fontSize : XAxisConfig.fontSize;
|
||||
if (fs > 0) label.style.fontSize = fs;
|
||||
else label.style.fontSize = StyleKeyword.Null;
|
||||
label.style.color = xLabelStyle != null ? xLabelStyle.color : XAxisConfig.labelColor;
|
||||
label.style.position = Position.Absolute;
|
||||
label.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
|
||||
ChartTextStyleApplier.ApplyLabel(label, this, ChartTextRole.AxisLabel);
|
||||
|
||||
float xPos;
|
||||
if (XAxisConfig != null && XAxisConfig.labelPlacement == CategoryLabelPlacement.CellCenter)
|
||||
{
|
||||
float step = visibleCount > 0 ? (plotW / visibleCount) : 0f;
|
||||
xPos = plotX0 + (i + 0.5f) * step;
|
||||
}
|
||||
else
|
||||
{
|
||||
float denom = Mathf.Max(1, visibleCount - 1);
|
||||
float step = denom > 0 ? (plotW / denom) : 0f;
|
||||
xPos = plotX0 + i * step;
|
||||
}
|
||||
label.style.left = xPos;
|
||||
|
||||
// Position Vertical (relative to X axis line)
|
||||
var xPosMode = xLabelStyle != null ? xLabelStyle.position : XAxisConfig.labelPosition;
|
||||
if (xPosMode == LabelPosition.Inside)
|
||||
{
|
||||
label.style.top = xOnTop ? xAxisY + 5 : xAxisY - 5;
|
||||
label.style.translate = xOnTop
|
||||
? new Translate(new Length(-50, LengthUnit.Percent), 0, 0)
|
||||
: new Translate(new Length(-50, LengthUnit.Percent), new Length(-100, LengthUnit.Percent), 0);
|
||||
}
|
||||
else if (xPosMode == LabelPosition.Center)
|
||||
{
|
||||
label.style.top = xAxisY;
|
||||
label.style.translate = new Translate(new Length(-50, LengthUnit.Percent), new Length(-50, LengthUnit.Percent), 0);
|
||||
}
|
||||
else // Outside
|
||||
{
|
||||
label.style.top = xOnTop ? xAxisY - 5 : xAxisY + 5;
|
||||
label.style.translate = xOnTop
|
||||
? new Translate(new Length(-50, LengthUnit.Percent), new Length(-100, LengthUnit.Percent), 0)
|
||||
: new Translate(new Length(-50, LengthUnit.Percent), 0, 0);
|
||||
}
|
||||
|
||||
// Apply Offset
|
||||
var xOffset = xLabelStyle != null ? xLabelStyle.offset : XAxisConfig.labelOffset;
|
||||
if (xOffset != Vector2.zero)
|
||||
{
|
||||
label.style.marginLeft = xOffset.x;
|
||||
label.style.marginTop = xOffset.y;
|
||||
}
|
||||
|
||||
_xAxisContainer.Add(label);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Numeric X axis labels (used by HorizontalBar)
|
||||
int split = Mathf.Max(1, XSplitCount);
|
||||
|
||||
bool cellCenter = XAxisConfig != null && XAxisConfig.labelPlacement == CategoryLabelPlacement.CellCenter;
|
||||
int labelCount = cellCenter ? split : (split + 1);
|
||||
for (int i = 0; i < labelCount; i++)
|
||||
{
|
||||
float t = cellCenter ? ((i + 0.5f) / split) : ((float)i / split);
|
||||
float val = Mathf.Lerp(XMin, XMax, t);
|
||||
string text = FormatNumericTick(val, XAxisConfig.labelFormat);
|
||||
|
||||
var label = new Label(text);
|
||||
ApplyAxisLabelBaseStyle(label);
|
||||
int fs = xLabelStyle != null ? xLabelStyle.fontSize : XAxisConfig.fontSize;
|
||||
if (fs > 0) label.style.fontSize = fs;
|
||||
else label.style.fontSize = StyleKeyword.Null;
|
||||
label.style.color = xLabelStyle != null ? xLabelStyle.color : XAxisConfig.labelColor;
|
||||
label.style.position = Position.Absolute;
|
||||
label.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
|
||||
ChartTextStyleApplier.ApplyLabel(label, this, ChartTextRole.AxisLabel);
|
||||
|
||||
float xPos = plotX0 + t * plotW;
|
||||
label.style.left = xPos;
|
||||
|
||||
var xPosMode = xLabelStyle != null ? xLabelStyle.position : XAxisConfig.labelPosition;
|
||||
if (xPosMode == LabelPosition.Inside)
|
||||
{
|
||||
label.style.top = xOnTop ? xAxisY + 5 : xAxisY - 5;
|
||||
label.style.translate = xOnTop
|
||||
? new Translate(new Length(-50, LengthUnit.Percent), 0, 0)
|
||||
: new Translate(new Length(-50, LengthUnit.Percent), new Length(-100, LengthUnit.Percent), 0);
|
||||
}
|
||||
else if (xPosMode == LabelPosition.Center)
|
||||
{
|
||||
label.style.top = xAxisY;
|
||||
label.style.translate = new Translate(new Length(-50, LengthUnit.Percent), new Length(-50, LengthUnit.Percent), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
label.style.top = xOnTop ? xAxisY - 5 : xAxisY + 5;
|
||||
label.style.translate = xOnTop
|
||||
? new Translate(new Length(-50, LengthUnit.Percent), new Length(-100, LengthUnit.Percent), 0)
|
||||
: new Translate(new Length(-50, LengthUnit.Percent), 0, 0);
|
||||
}
|
||||
|
||||
var xOffset = xLabelStyle != null ? xLabelStyle.offset : XAxisConfig.labelOffset;
|
||||
if (xOffset != Vector2.zero)
|
||||
{
|
||||
label.style.marginLeft = xOffset.x;
|
||||
label.style.marginTop = xOffset.y;
|
||||
}
|
||||
|
||||
_xAxisContainer.Add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (XAxisConfig != null && XAxisConfig.axisType == AxisType.Value && XAxisConfig.showUnit && !string.IsNullOrEmpty(XAxisConfig.unitText))
|
||||
{
|
||||
var unitStyle = XAxisConfig.unitLabelStyle;
|
||||
var unitLabel = new Label(XAxisConfig.unitText);
|
||||
ApplyAxisLabelBaseStyle(unitLabel);
|
||||
int fs = unitStyle != null ? unitStyle.fontSize : XAxisConfig.fontSize;
|
||||
if (fs > 0) unitLabel.style.fontSize = fs;
|
||||
else unitLabel.style.fontSize = StyleKeyword.Null;
|
||||
unitLabel.style.color = unitStyle != null ? unitStyle.color : XAxisConfig.labelColor;
|
||||
unitLabel.style.position = Position.Absolute;
|
||||
unitLabel.style.left = w;
|
||||
unitLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
|
||||
|
||||
ChartTextStyleApplier.ApplyLabel(unitLabel, this, ChartTextRole.AxisLabel);
|
||||
|
||||
var xPosMode = unitStyle != null ? unitStyle.position : LabelPosition.Outside;
|
||||
if (xPosMode == LabelPosition.Inside)
|
||||
{
|
||||
unitLabel.style.top = xOnTop ? xAxisY + 5 : xAxisY - 5;
|
||||
unitLabel.style.translate = xOnTop
|
||||
? new Translate(0, 0, 0)
|
||||
: new Translate(0, new Length(-100, LengthUnit.Percent), 0);
|
||||
}
|
||||
else if (xPosMode == LabelPosition.Center)
|
||||
{
|
||||
unitLabel.style.top = xAxisY;
|
||||
unitLabel.style.translate = new Translate(0, new Length(-50, LengthUnit.Percent), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
unitLabel.style.top = xOnTop ? xAxisY - 5 : xAxisY + 5;
|
||||
unitLabel.style.translate = xOnTop
|
||||
? new Translate(0, new Length(-100, LengthUnit.Percent), 0)
|
||||
: new Translate(0, 0, 0);
|
||||
}
|
||||
|
||||
var offset = unitStyle != null ? unitStyle.offset : Vector2.zero;
|
||||
if (offset != Vector2.zero)
|
||||
{
|
||||
unitLabel.style.marginLeft = offset.x;
|
||||
unitLabel.style.marginTop = offset.y;
|
||||
}
|
||||
|
||||
_xAxisContainer.Add(unitLabel);
|
||||
}
|
||||
|
||||
// --- Y Axis Labels ---
|
||||
if (YAxisConfig != null && (YAxisConfig.labelStyle != null ? YAxisConfig.labelStyle.enabled : YAxisConfig.showLabels))
|
||||
{
|
||||
var yLabelStyle = YAxisConfig.labelStyle;
|
||||
var yLabels = YLabels;
|
||||
if ((yLabels == null || yLabels.Count == 0) && YAxisConfig != null && YAxisConfig.labels != null && YAxisConfig.labels.Count > 0)
|
||||
{
|
||||
yLabels = YAxisConfig.labels;
|
||||
}
|
||||
|
||||
if (yLabels != null && yLabels.Count > 0)
|
||||
{
|
||||
var sourceLabels = yLabels;
|
||||
if (YAxisConfig != null && YAxisConfig.axisType == AxisType.Category && YAxisConfig.labels != null && YAxisConfig.labels.Count > 0)
|
||||
{
|
||||
sourceLabels = YAxisConfig.labels;
|
||||
}
|
||||
|
||||
int totalCount = sourceLabels != null ? sourceLabels.Count : 0;
|
||||
if (totalCount <= 0) return;
|
||||
|
||||
int visibleCount = ClampCategoryVisibleCount(YAxisConfig, totalCount);
|
||||
int startIndex = 0;
|
||||
if (totalCount > visibleCount)
|
||||
{
|
||||
startIndex = Mathf.RoundToInt(YMin);
|
||||
startIndex = Mathf.Clamp(startIndex, 0, totalCount - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < visibleCount; i++)
|
||||
{
|
||||
int labelIndex = (totalCount > visibleCount) ? ((startIndex + i) % totalCount) : i;
|
||||
var label = new Label(sourceLabels[labelIndex]);
|
||||
ApplyAxisLabelBaseStyle(label);
|
||||
int fs = yLabelStyle != null ? yLabelStyle.fontSize : YAxisConfig.fontSize;
|
||||
if (fs > 0) label.style.fontSize = fs;
|
||||
else label.style.fontSize = StyleKeyword.Null;
|
||||
label.style.color = yLabelStyle != null ? yLabelStyle.color : YAxisConfig.labelColor;
|
||||
label.style.position = Position.Absolute;
|
||||
|
||||
ChartTextStyleApplier.ApplyLabel(label, this, ChartTextRole.AxisLabel);
|
||||
|
||||
float t;
|
||||
if (YAxisConfig != null && YAxisConfig.labelPlacement == CategoryLabelPlacement.CellCenter)
|
||||
{
|
||||
t = (i + 0.5f) / Mathf.Max(1, visibleCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
t = visibleCount > 1 ? (float)i / (visibleCount - 1) : 0;
|
||||
}
|
||||
|
||||
float yPos;
|
||||
if (YIsCategory)
|
||||
{
|
||||
yPos = h > 0 ? (xOnTop ? (t * h) : ((1.0f - t) * h)) : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
yPos = h > 0 ? (1.0f - t) * h : 0;
|
||||
}
|
||||
label.style.top = yPos;
|
||||
|
||||
label.style.left = yOnRight ? yAxisX + 5 : yAxisX - 5;
|
||||
label.style.unityTextAlign = yOnRight ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight;
|
||||
label.style.translate = yOnRight
|
||||
? new Translate(0, new Length(-50, LengthUnit.Percent), 0)
|
||||
: new Translate(new Length(-100, LengthUnit.Percent), new Length(-50, LengthUnit.Percent), 0);
|
||||
|
||||
var yOffset = yLabelStyle != null ? yLabelStyle.offset : YAxisConfig.labelOffset;
|
||||
if (yOffset != Vector2.zero)
|
||||
{
|
||||
label.style.marginLeft = yOffset.x;
|
||||
label.style.marginTop = yOffset.y;
|
||||
}
|
||||
|
||||
_yAxisContainer.Add(label);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
bool cellCenter = YAxisConfig != null && YAxisConfig.labelPlacement == CategoryLabelPlacement.CellCenter;
|
||||
int labelCount = cellCenter ? Mathf.Max(1, YSplitCount) : (Mathf.Max(1, YSplitCount) + 1);
|
||||
for (int i = 0; i < labelCount; i++)
|
||||
{
|
||||
float split = Mathf.Max(1, YSplitCount);
|
||||
float t = cellCenter ? ((i + 0.5f) / split) : ((float)i / split);
|
||||
float val = Mathf.Lerp(YMin, YMax, t);
|
||||
string text = FormatNumericTick(val, YAxisConfig.labelFormat);
|
||||
|
||||
var label = new Label(text);
|
||||
ApplyAxisLabelBaseStyle(label);
|
||||
int fs = yLabelStyle != null ? yLabelStyle.fontSize : YAxisConfig.fontSize;
|
||||
if (fs > 0) label.style.fontSize = fs;
|
||||
else label.style.fontSize = StyleKeyword.Null;
|
||||
label.style.color = yLabelStyle != null ? yLabelStyle.color : YAxisConfig.labelColor;
|
||||
label.style.position = Position.Absolute;
|
||||
|
||||
ChartTextStyleApplier.ApplyLabel(label, this, ChartTextRole.AxisLabel);
|
||||
|
||||
// Use pixel position based on actual height
|
||||
float yPos = h > 0 ? (1.0f - t) * h : 0;
|
||||
label.style.top = yPos;
|
||||
|
||||
// Position Horizontal (relative to Y axis line)
|
||||
var yPosMode = yLabelStyle != null ? yLabelStyle.position : YAxisConfig.labelPosition;
|
||||
if (yPosMode == LabelPosition.Inside)
|
||||
{
|
||||
label.style.left = yOnRight ? yAxisX - 5 : yAxisX + 5;
|
||||
label.style.unityTextAlign = yOnRight ? TextAnchor.MiddleRight : TextAnchor.MiddleLeft;
|
||||
label.style.translate = yOnRight
|
||||
? new Translate(new Length(-100, LengthUnit.Percent), new Length(-50, LengthUnit.Percent), 0)
|
||||
: new Translate(0, new Length(-50, LengthUnit.Percent), 0);
|
||||
}
|
||||
else if (yPosMode == LabelPosition.Center)
|
||||
{
|
||||
label.style.left = yAxisX;
|
||||
label.style.translate = new Translate(new Length(-50, LengthUnit.Percent), new Length(-50, LengthUnit.Percent), 0);
|
||||
}
|
||||
else // Outside (Default) - left of axis
|
||||
{
|
||||
label.style.left = yOnRight ? yAxisX + 5 : yAxisX - 5;
|
||||
label.style.unityTextAlign = yOnRight ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight;
|
||||
label.style.translate = yOnRight
|
||||
? new Translate(0, new Length(-50, LengthUnit.Percent), 0)
|
||||
: new Translate(new Length(-100, LengthUnit.Percent), new Length(-50, LengthUnit.Percent), 0);
|
||||
}
|
||||
|
||||
// Apply Offset
|
||||
var yOffset = yLabelStyle != null ? yLabelStyle.offset : YAxisConfig.labelOffset;
|
||||
if (yOffset != Vector2.zero)
|
||||
{
|
||||
label.style.marginLeft = yOffset.x;
|
||||
label.style.marginTop = yOffset.y;
|
||||
}
|
||||
|
||||
_yAxisContainer.Add(label);
|
||||
}
|
||||
}
|
||||
|
||||
if (YAxisConfig != null && YAxisConfig.axisType == AxisType.Value && YAxisConfig.showUnit && !string.IsNullOrEmpty(YAxisConfig.unitText))
|
||||
{
|
||||
var unitStyle = YAxisConfig.unitLabelStyle;
|
||||
var unitLabel = new Label(YAxisConfig.unitText);
|
||||
ApplyAxisLabelBaseStyle(unitLabel);
|
||||
int fs = unitStyle != null ? unitStyle.fontSize : YAxisConfig.fontSize;
|
||||
if (fs > 0) unitLabel.style.fontSize = fs;
|
||||
else unitLabel.style.fontSize = StyleKeyword.Null;
|
||||
unitLabel.style.color = unitStyle != null ? unitStyle.color : YAxisConfig.labelColor;
|
||||
unitLabel.style.position = Position.Absolute;
|
||||
|
||||
ChartTextStyleApplier.ApplyLabel(unitLabel, this, ChartTextRole.AxisLabel);
|
||||
|
||||
unitLabel.style.top = 0;
|
||||
|
||||
var yPosMode = unitStyle != null ? unitStyle.position : LabelPosition.Outside;
|
||||
if (yPosMode == LabelPosition.Inside)
|
||||
{
|
||||
unitLabel.style.left = yOnRight ? yAxisX - 5 : yAxisX + 5;
|
||||
unitLabel.style.unityTextAlign = yOnRight ? TextAnchor.MiddleRight : TextAnchor.MiddleLeft;
|
||||
unitLabel.style.translate = yOnRight
|
||||
? new Translate(new Length(-100, LengthUnit.Percent), new Length(-100, LengthUnit.Percent), 0)
|
||||
: new Translate(0, new Length(-100, LengthUnit.Percent), 0);
|
||||
}
|
||||
else if (yPosMode == LabelPosition.Center)
|
||||
{
|
||||
unitLabel.style.left = yAxisX;
|
||||
unitLabel.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
unitLabel.style.translate = new Translate(new Length(-50, LengthUnit.Percent), new Length(-100, LengthUnit.Percent), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
unitLabel.style.left = yOnRight ? yAxisX + 5 : yAxisX - 5;
|
||||
unitLabel.style.unityTextAlign = yOnRight ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight;
|
||||
unitLabel.style.translate = yOnRight
|
||||
? new Translate(0, new Length(-100, LengthUnit.Percent), 0)
|
||||
: new Translate(new Length(-100, LengthUnit.Percent), new Length(-100, LengthUnit.Percent), 0);
|
||||
}
|
||||
|
||||
var offset = unitStyle != null ? unitStyle.offset : Vector2.zero;
|
||||
if (offset != Vector2.zero)
|
||||
{
|
||||
unitLabel.style.marginLeft = offset.x;
|
||||
unitLabel.style.marginTop = offset.y;
|
||||
}
|
||||
|
||||
_yAxisContainer.Add(unitLabel);
|
||||
}
|
||||
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b949ca50968ffc4984c13c1cdd3d864
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/AxisLayer.cs
|
||||
uploadId: 857482
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3aae7d72877981241b6141c106009a42
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/BarSeriesRenderer.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,742 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8e394fcbb327c34390e1702cd61712b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/BaseSeriesRenderer.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,133 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace EasyChart.Layers
|
||||
{
|
||||
internal sealed class DashedLineElement : VisualElement
|
||||
{
|
||||
private Color _color = Color.yellow;
|
||||
private float _lineWidth = 1f;
|
||||
private bool _dashed;
|
||||
private float _dashLength = 4f;
|
||||
private float _dashGap = 2f;
|
||||
private float _dashOffset;
|
||||
|
||||
public Color Color
|
||||
{
|
||||
get => _color;
|
||||
set
|
||||
{
|
||||
if (_color == value) return;
|
||||
_color = value;
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
public float LineWidth
|
||||
{
|
||||
get => _lineWidth;
|
||||
set
|
||||
{
|
||||
float v = Mathf.Max(0f, value);
|
||||
if (Mathf.Approximately(_lineWidth, v)) return;
|
||||
_lineWidth = v;
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dashed
|
||||
{
|
||||
get => _dashed;
|
||||
set
|
||||
{
|
||||
if (_dashed == value) return;
|
||||
_dashed = value;
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
public float DashLength
|
||||
{
|
||||
get => _dashLength;
|
||||
set
|
||||
{
|
||||
float v = Mathf.Max(0f, value);
|
||||
if (Mathf.Approximately(_dashLength, v)) return;
|
||||
_dashLength = v;
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
public float DashGap
|
||||
{
|
||||
get => _dashGap;
|
||||
set
|
||||
{
|
||||
float v = Mathf.Max(0f, value);
|
||||
if (Mathf.Approximately(_dashGap, v)) return;
|
||||
_dashGap = v;
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
public float DashOffset
|
||||
{
|
||||
get => _dashOffset;
|
||||
set
|
||||
{
|
||||
if (Mathf.Approximately(_dashOffset, value)) return;
|
||||
_dashOffset = value;
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
public DashedLineElement()
|
||||
{
|
||||
pickingMode = PickingMode.Ignore;
|
||||
generateVisualContent += OnGenerateVisualContent;
|
||||
}
|
||||
|
||||
private void OnGenerateVisualContent(MeshGenerationContext context)
|
||||
{
|
||||
var width = contentRect.width;
|
||||
var height = contentRect.height;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
var painter = context.painter2D;
|
||||
painter.lineWidth = Mathf.Max(1f, LineWidth);
|
||||
painter.strokeColor = Color;
|
||||
|
||||
bool isVertical = height >= width;
|
||||
if (isVertical)
|
||||
{
|
||||
float x = width * 0.5f;
|
||||
var a = new Vector2(x, 0);
|
||||
var b = new Vector2(x, height);
|
||||
|
||||
if (Dashed) DashedLineUtils.DrawDashedLine(painter, a, b, DashLength, DashGap, DashOffset);
|
||||
else
|
||||
{
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(a);
|
||||
painter.LineTo(b);
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float y = height * 0.5f;
|
||||
var a = new Vector2(0, y);
|
||||
var b = new Vector2(width, y);
|
||||
|
||||
if (Dashed) DashedLineUtils.DrawDashedLine(painter, a, b, DashLength, DashGap, DashOffset);
|
||||
else
|
||||
{
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(a);
|
||||
painter.LineTo(b);
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0360c8637b46ac94398389593a032f5e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/DashedLineElement.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,54 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace EasyChart.Layers
|
||||
{
|
||||
internal static class DashedLineUtils
|
||||
{
|
||||
public static void DrawDashedLine(Painter2D painter, Vector2 start, Vector2 end, float dashLength, float gapLength, float offset)
|
||||
{
|
||||
if (painter == null) return;
|
||||
|
||||
Vector2 delta = end - start;
|
||||
float len = delta.magnitude;
|
||||
if (len <= 0.0001f) return;
|
||||
|
||||
float period = dashLength + gapLength;
|
||||
if (dashLength <= 0f || gapLength < 0f || period <= 0f)
|
||||
{
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(start);
|
||||
painter.LineTo(end);
|
||||
painter.Stroke();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 dir = delta / len;
|
||||
float normalizedOffset = offset;
|
||||
if (!Mathf.Approximately(period, 0f))
|
||||
{
|
||||
normalizedOffset = offset % period;
|
||||
if (normalizedOffset < 0f) normalizedOffset += period;
|
||||
}
|
||||
|
||||
float pos = -normalizedOffset;
|
||||
while (pos < len)
|
||||
{
|
||||
float segStart = Mathf.Max(pos, 0f);
|
||||
float segEnd = Mathf.Min(pos + dashLength, len);
|
||||
|
||||
if (segEnd > segStart)
|
||||
{
|
||||
Vector2 a = start + dir * segStart;
|
||||
Vector2 b = start + dir * segEnd;
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(a);
|
||||
painter.LineTo(b);
|
||||
painter.Stroke();
|
||||
}
|
||||
|
||||
pos += period;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d5a77b83e0d1204a8367328f264b951
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/DashedLineUtils.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,163 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace EasyChart.Layers
|
||||
{
|
||||
public class GridLayer : VisualElement
|
||||
{
|
||||
public int HorizontalGridCount { get; set; } = 5;
|
||||
public int VerticalGridCount { get; set; } = 5;
|
||||
|
||||
public bool VerticalPreload { get; set; } = false;
|
||||
public bool HorizontalPreload { get; set; } = false;
|
||||
|
||||
public Color XGridColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.2f);
|
||||
public float XGridLineWidth { get; set; } = 1.0f;
|
||||
public bool XGridDashed { get; set; } = false;
|
||||
public float XGridDashLength { get; set; } = 4f;
|
||||
public float XGridDashGap { get; set; } = 2f;
|
||||
public float XGridDashOffset { get; set; } = 0f;
|
||||
public Color YGridColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.2f);
|
||||
public float YGridLineWidth { get; set; } = 1.0f;
|
||||
public bool YGridDashed { get; set; } = false;
|
||||
public float YGridDashLength { get; set; } = 4f;
|
||||
public float YGridDashGap { get; set; } = 2f;
|
||||
public float YGridDashOffset { get; set; } = 0f;
|
||||
|
||||
public GridLayer()
|
||||
{
|
||||
// Make it fill the parent
|
||||
this.StretchToParentSize();
|
||||
pickingMode = PickingMode.Ignore; // Background doesn't need to block clicks
|
||||
style.overflow = Overflow.Visible; // Allow drawing outside contentRect for preload
|
||||
generateVisualContent += OnGenerateVisualContent;
|
||||
}
|
||||
|
||||
private void OnGenerateVisualContent(MeshGenerationContext context)
|
||||
{
|
||||
var width = contentRect.width;
|
||||
var height = contentRect.height;
|
||||
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
var painter = context.painter2D;
|
||||
|
||||
// Calculate extended width/height for preload
|
||||
float extendedWidth = width;
|
||||
float extendedHeight = height;
|
||||
if (VerticalPreload && VerticalGridCount > 0)
|
||||
{
|
||||
float vStep = width / VerticalGridCount;
|
||||
extendedWidth = width + vStep;
|
||||
}
|
||||
if (HorizontalPreload && HorizontalGridCount > 0)
|
||||
{
|
||||
float hStep = height / HorizontalGridCount;
|
||||
extendedHeight = height + hStep;
|
||||
}
|
||||
|
||||
// Draw Horizontal Lines (Y Axis steps)
|
||||
// Usually split into N parts means N+1 lines? Or N lines?
|
||||
// For Y axis, usually 0..Count means Count+1 lines including 0 and Max.
|
||||
if (HorizontalGridCount > 0)
|
||||
{
|
||||
painter.lineWidth = YGridLineWidth;
|
||||
painter.strokeColor = YGridColor;
|
||||
float hStep = height / HorizontalGridCount;
|
||||
// When preload is enabled, draw one extra line outside the visible area
|
||||
int lineCount = HorizontalGridCount + (HorizontalPreload ? 1 : 0);
|
||||
for (int i = 0; i <= lineCount; i++)
|
||||
{
|
||||
float y = i * hStep;
|
||||
// Use extendedWidth so horizontal lines cover the preload area
|
||||
var a = new Vector2(0, y);
|
||||
var b = new Vector2(extendedWidth, y);
|
||||
|
||||
if (YGridDashed)
|
||||
{
|
||||
DashedLineUtils.DrawDashedLine(painter, a, b, YGridDashLength, YGridDashGap, YGridDashOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(a);
|
||||
painter.LineTo(b);
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw Vertical Lines (X Axis steps)
|
||||
// For Category Axis, we want lines exactly at 0, 1, 2... (LabelCount - 1)
|
||||
// So VerticalGridCount should be set to (LabelCount > 1 ? LabelCount - 1 : 1)
|
||||
// to match the logic: width / Count * i
|
||||
// BUT wait: if we have 5 labels (0,1,2,3,4), we want 5 lines at 0%, 25%, 50%, 75%, 100%.
|
||||
// This corresponds to dividing width by 4 (Count-1).
|
||||
|
||||
if (VerticalGridCount > 0)
|
||||
{
|
||||
painter.lineWidth = XGridLineWidth;
|
||||
painter.strokeColor = XGridColor;
|
||||
// If VerticalGridCount means "Number of segments", step = width / Count.
|
||||
// If VerticalGridCount means "Number of labels/lines - 1", step = width / Count.
|
||||
// Let's standardize: VerticalGridCount passed in will be (LabelCount - 1).
|
||||
// If LabelCount=1, GridCount=0 (avoid div by zero).
|
||||
|
||||
float vStep = width / VerticalGridCount;
|
||||
// When preload is enabled, draw one extra line outside the visible area
|
||||
int lineCount = VerticalGridCount + (VerticalPreload ? 1 : 0);
|
||||
|
||||
for (int i = 0; i <= lineCount; i++)
|
||||
{
|
||||
float x = i * vStep;
|
||||
// Use extendedHeight so vertical lines cover the preload area
|
||||
var a = new Vector2(x, 0);
|
||||
var b = new Vector2(x, extendedHeight);
|
||||
|
||||
if (XGridDashed)
|
||||
{
|
||||
DashedLineUtils.DrawDashedLine(painter, a, b, XGridDashLength, XGridDashGap, XGridDashOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(a);
|
||||
painter.LineTo(b);
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (VerticalGridCount == 0) // Special case: Single line or just edges?
|
||||
{
|
||||
painter.lineWidth = XGridLineWidth;
|
||||
painter.strokeColor = XGridColor;
|
||||
// Maybe just draw left and right edge?
|
||||
// For now do nothing or draw at 0.
|
||||
var a = new Vector2(0, 0);
|
||||
var b = new Vector2(0, height);
|
||||
|
||||
if (XGridDashed)
|
||||
{
|
||||
DashedLineUtils.DrawDashedLine(painter, a, b, XGridDashLength, XGridDashGap, XGridDashOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(a);
|
||||
painter.LineTo(b);
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Redraw()
|
||||
{
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
|
||||
public void SetScrollOffset(float xPx, float yPx)
|
||||
{
|
||||
style.translate = new Translate(xPx, yPx, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46b8094e31c281d45bd2e61e491a5fff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/GridLayer.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace EasyChart.Layers
|
||||
{
|
||||
public interface IExclusiveTooltipRenderer
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f8ac8cee20ce9a4a806c79588314d11
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/IExclusiveTooltipRenderer.cs
|
||||
uploadId: 857482
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab7e86d2c4b7d9b42892b9298b27e015
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/LineSeriesRenderer.cs
|
||||
uploadId: 857482
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d04d79a51c7a0404c880abefc93cded9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/PieSeriesRenderer.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,740 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using EasyChart;
|
||||
|
||||
namespace EasyChart.Layers
|
||||
{
|
||||
public class RadarSeriesRenderer : BaseSeriesRenderer
|
||||
{
|
||||
private struct RadarHit
|
||||
{
|
||||
public int SerieIndex;
|
||||
public int PointIndex;
|
||||
public Vector2 PixelPos;
|
||||
public float Dist;
|
||||
}
|
||||
|
||||
private RadarHit? _hover;
|
||||
|
||||
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 static string ResolveDimensionLabel(List<string> categoryLabels, Serie serie, int index)
|
||||
{
|
||||
if (categoryLabels != null && index >= 0 && index < categoryLabels.Count)
|
||||
{
|
||||
string l = categoryLabels[index];
|
||||
if (!string.IsNullOrEmpty(l)) return l;
|
||||
}
|
||||
|
||||
if (serie != null && serie.seriesData != null && index >= 0 && index < serie.seriesData.Count)
|
||||
{
|
||||
var dp = serie.seriesData[index];
|
||||
if (dp != null && !string.IsNullOrEmpty(dp.name)) return dp.name;
|
||||
}
|
||||
|
||||
return $"Dim {index}";
|
||||
}
|
||||
|
||||
private float GetPickRadius(RadarSettings settings)
|
||||
{
|
||||
if (settings == null || settings.point == null || !settings.point.show) return 0f;
|
||||
float baseRadius = Mathf.Max(0f, settings.point.size) * 0.75f;
|
||||
return Mathf.Max(6f, baseRadius);
|
||||
}
|
||||
|
||||
private List<Serie> GetRadarSeries()
|
||||
{
|
||||
if (Data == null || Data.Series == null) return null;
|
||||
var list = new List<Serie>();
|
||||
for (int i = 0; i < Data.Series.Count; i++)
|
||||
{
|
||||
var s = Data.Series[i];
|
||||
if (s != null && s.visible && s.type == SerieType.Radar)
|
||||
{
|
||||
list.Add(s);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private int GetRadarSerieIndex(Serie target)
|
||||
{
|
||||
if (Data == null || Data.Series == null || target == null) return -1;
|
||||
for (int i = 0; i < Data.Series.Count; i++)
|
||||
{
|
||||
if (ReferenceEquals(Data.Series[i], target)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private RadarHit? FindHoverHit(TooltipContext context)
|
||||
{
|
||||
if (Data == null) return null;
|
||||
|
||||
float width = context.Width;
|
||||
float height = context.Height;
|
||||
if (width <= 0 || height <= 0) return null;
|
||||
|
||||
var series = GetRadarSeries();
|
||||
if (series == null || series.Count == 0) return null;
|
||||
Serie primarySerie = series[0];
|
||||
if (primarySerie == null) return null;
|
||||
|
||||
var labels = GetCategoryLabels();
|
||||
int dimensionCount = labels != null ? labels.Count : 0;
|
||||
if (dimensionCount <= 0) dimensionCount = primarySerie.seriesData != null ? primarySerie.seriesData.Count : 0;
|
||||
if (dimensionCount <= 2) return null;
|
||||
|
||||
var radiusAxis = Data.PolarAxes != null ? Data.PolarAxes.radiusAxis : null;
|
||||
|
||||
GetPolarLayout(primarySerie, width, height, out var center, out var outerRadiusPx, out var innerRadiusPx, out var startAngleDeg, out var clockwise);
|
||||
if (outerRadiusPx <= 0f) return null;
|
||||
|
||||
ResolveValueRange(series, radiusAxis, dimensionCount, out float minV, out float maxV);
|
||||
|
||||
float step = 360f / dimensionCount;
|
||||
float sign = clockwise ? 1f : -1f;
|
||||
|
||||
float bestDistSq = float.MaxValue;
|
||||
RadarHit? best = null;
|
||||
|
||||
for (int si = 0; si < series.Count; si++)
|
||||
{
|
||||
var serie = series[si];
|
||||
if (serie == null || serie.seriesData == null || serie.seriesData.Count == 0) continue;
|
||||
if (serie.settings is not RadarSettings settings) continue;
|
||||
|
||||
float pickRadius = GetPickRadius(settings);
|
||||
if (pickRadius <= 0f) continue;
|
||||
float thresholdSq = pickRadius * pickRadius;
|
||||
|
||||
int serieIndex = GetRadarSerieIndex(serie);
|
||||
if (serieIndex < 0) continue;
|
||||
|
||||
for (int i = 0; i < dimensionCount; i++)
|
||||
{
|
||||
float value = 0f;
|
||||
if (i < serie.seriesData.Count)
|
||||
{
|
||||
var dp = serie.seriesData[i];
|
||||
if (dp != null) value = dp.value;
|
||||
}
|
||||
|
||||
float tt = (value - minV) / (maxV - minV);
|
||||
tt = Mathf.Clamp01(float.IsNaN(tt) ? 0f : tt);
|
||||
|
||||
float rr = Mathf.Lerp(innerRadiusPx, outerRadiusPx, tt);
|
||||
rr = Mathf.Lerp(innerRadiusPx, rr, _animationProgress);
|
||||
|
||||
float angle = startAngleDeg + sign * (i * step);
|
||||
Vector2 pos = center + AngleToDir(angle) * rr;
|
||||
|
||||
float dx = pos.x - context.LocalPos.x;
|
||||
float dy = pos.y - context.LocalPos.y;
|
||||
float distSq = dx * dx + dy * dy;
|
||||
if (distSq > thresholdSq) continue;
|
||||
if (distSq >= bestDistSq) continue;
|
||||
|
||||
bestDistSq = distSq;
|
||||
best = new RadarHit
|
||||
{
|
||||
SerieIndex = serieIndex,
|
||||
PointIndex = i,
|
||||
PixelPos = pos,
|
||||
Dist = Mathf.Sqrt(distSq)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
public override void ClearHover()
|
||||
{
|
||||
if (_hover != null)
|
||||
{
|
||||
_hover = null;
|
||||
MarkDirtyRepaint();
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> GetCategoryLabels()
|
||||
{
|
||||
if (Data == null) return null;
|
||||
|
||||
var angleAxis = Data.PolarAxes != null ? Data.PolarAxes.angleAxis : null;
|
||||
if (angleAxis != null && angleAxis.labels != null && angleAxis.labels.Count > 0)
|
||||
{
|
||||
return angleAxis.labels;
|
||||
}
|
||||
|
||||
if (Data.Axes == null) return null;
|
||||
|
||||
AxisId preferredAxisId = Data.XAxisId; // Use XAxisId for radar dimension labels
|
||||
for (int i = 0; i < Data.Axes.Count; i++)
|
||||
{
|
||||
var a = Data.Axes[i];
|
||||
if (a != null && a.id == preferredAxisId && a.axisType == AxisType.Category)
|
||||
{
|
||||
return a.labels;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < Data.Axes.Count; i++)
|
||||
{
|
||||
var a = Data.Axes[i];
|
||||
if (a != null && a.axisType == AxisType.Category)
|
||||
{
|
||||
return a.labels;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ResolveValueRange(List<Serie> series, PolarAxisStyle radiusAxis, int dimensionCount, out float minV, out float maxV)
|
||||
{
|
||||
minV = 0f;
|
||||
maxV = 1f;
|
||||
|
||||
RangeUtils.ResolveAutoRange(
|
||||
series,
|
||||
Mathf.Max(0, dimensionCount),
|
||||
dp => dp != null ? dp.value : float.NaN,
|
||||
true,
|
||||
0f,
|
||||
1f,
|
||||
out minV,
|
||||
out maxV);
|
||||
|
||||
bool autoMin = radiusAxis == null || radiusAxis.autoRangeMin;
|
||||
bool autoMax = radiusAxis == null || radiusAxis.autoRangeMax;
|
||||
if (!autoMin && radiusAxis != null) minV = radiusAxis.minValue;
|
||||
if (!autoMax && radiusAxis != null) maxV = radiusAxis.maxValue;
|
||||
|
||||
if (radiusAxis != null && (autoMin || autoMax))
|
||||
{
|
||||
float unit;
|
||||
switch (radiusAxis.autoRangeRounding)
|
||||
{
|
||||
case AutoRangeRoundingMode.Integer: unit = 1f; break;
|
||||
case AutoRangeRoundingMode.Tens: unit = 10f; break;
|
||||
case AutoRangeRoundingMode.Hundreds: unit = 100f; break;
|
||||
case AutoRangeRoundingMode.Custom: unit = radiusAxis.autoRangeUnit; break;
|
||||
default: unit = 0f; break;
|
||||
}
|
||||
|
||||
unit = Mathf.Abs(unit);
|
||||
if (unit > 0f)
|
||||
{
|
||||
if (autoMin) minV = Mathf.Floor(minV / unit) * unit;
|
||||
if (autoMax) maxV = Mathf.Ceil(maxV / unit) * unit;
|
||||
}
|
||||
}
|
||||
|
||||
if (Mathf.Approximately(minV, maxV))
|
||||
{
|
||||
maxV = minV + 1f;
|
||||
}
|
||||
}
|
||||
|
||||
private static float ResolveOuterRadius(float outerRadiusSetting, float maxAutoRadius)
|
||||
{
|
||||
if (maxAutoRadius <= 0f) return 0f;
|
||||
if (outerRadiusSetting <= 0f) return maxAutoRadius;
|
||||
return Mathf.Min(outerRadiusSetting, maxAutoRadius);
|
||||
}
|
||||
|
||||
private static float ResolveInnerRadius(float innerRadiusSetting, float resolvedOuterRadius)
|
||||
{
|
||||
if (resolvedOuterRadius <= 0f) return 0f;
|
||||
if (innerRadiusSetting <= 0f) return 0f;
|
||||
return Mathf.Clamp(innerRadiusSetting, 0f, resolvedOuterRadius);
|
||||
}
|
||||
|
||||
private static Vector2 AngleToDir(float angleDeg)
|
||||
{
|
||||
float rad = angleDeg * Mathf.Deg2Rad;
|
||||
return new Vector2(Mathf.Cos(rad), Mathf.Sin(rad));
|
||||
}
|
||||
|
||||
private void GetPolarLayout(Serie serie, float width, float height, out Vector2 center, out float outerRadiusPx, out float innerRadiusPx, out float startAngleDeg, out bool clockwise)
|
||||
{
|
||||
var settings = serie != null ? serie.settings as RadarSettings : null;
|
||||
var layout = settings != null ? settings.radar : null;
|
||||
var plot = layout != null ? layout.plot : null;
|
||||
|
||||
float padding = plot != null ? plot.padding : 0f;
|
||||
float outerRadius = layout != null ? layout.outerRadius : 0f;
|
||||
float innerRadius = layout != null ? layout.innerRadius : 0f;
|
||||
|
||||
startAngleDeg = layout != null ? layout.startAngleDeg : -90f;
|
||||
clockwise = layout == null || layout.clockwise;
|
||||
|
||||
Vector2 centerOffset = plot != null ? plot.centerOffset : Vector2.zero;
|
||||
|
||||
float maxAutoRadius = Mathf.Max(0, Mathf.Min(width, height) * 0.5f - padding);
|
||||
outerRadiusPx = ResolveOuterRadius(outerRadius, maxAutoRadius);
|
||||
innerRadiusPx = ResolveInnerRadius(innerRadius, outerRadiusPx);
|
||||
|
||||
center = new Vector2(width / 2f, height / 2f) + new Vector2(centerOffset.x, -centerOffset.y);
|
||||
}
|
||||
|
||||
private static void ApplyAxisLabelStyle(Label label, PolarAxisStyle axis)
|
||||
{
|
||||
label.style.position = Position.Absolute;
|
||||
label.style.whiteSpace = WhiteSpace.NoWrap;
|
||||
|
||||
if (axis != null)
|
||||
{
|
||||
var s = axis.labelStyle;
|
||||
label.style.fontSize = s != null ? s.fontSize : axis.fontSize;
|
||||
label.style.color = s != null ? s.color : axis.labelColor;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyDataLabelStyle(Label label, SerieLabelSettings settings)
|
||||
{
|
||||
label.style.position = Position.Absolute;
|
||||
label.style.whiteSpace = WhiteSpace.NoWrap;
|
||||
|
||||
if (settings != null)
|
||||
{
|
||||
label.style.fontSize = settings.fontSize;
|
||||
label.style.color = settings.color;
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateLabels()
|
||||
{
|
||||
if (!TryGetChartAndLabelController(out var chart, out var labelController)) return;
|
||||
|
||||
if (Data == null) return;
|
||||
|
||||
float w = contentRect.width;
|
||||
float h = contentRect.height;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
var series = GetRadarSeries();
|
||||
if (series == null || series.Count == 0) return;
|
||||
|
||||
var primarySerie = series[0];
|
||||
if (primarySerie == null) return;
|
||||
|
||||
var labels = GetCategoryLabels();
|
||||
int dimensionCount = labels != null ? labels.Count : 0;
|
||||
if (dimensionCount <= 0)
|
||||
{
|
||||
for (int si = 0; si < series.Count; si++)
|
||||
{
|
||||
var s = series[si];
|
||||
if (s != null && s.seriesData != null)
|
||||
dimensionCount = Mathf.Max(dimensionCount, s.seriesData.Count);
|
||||
}
|
||||
}
|
||||
if (dimensionCount <= 0) return;
|
||||
|
||||
var angleAxis = Data.PolarAxes != null ? Data.PolarAxes.angleAxis : null;
|
||||
var radiusAxis = Data.PolarAxes != null ? Data.PolarAxes.radiusAxis : null;
|
||||
|
||||
GetPolarLayout(primarySerie, w, h, out var center, out var outerRadiusPx, out var innerRadiusPx, out var startAngleDeg, out var clockwise);
|
||||
if (outerRadiusPx <= 0f) return;
|
||||
|
||||
ResolveValueRange(series, radiusAxis, dimensionCount, out float minV, out float maxV);
|
||||
|
||||
float step = 360f / dimensionCount;
|
||||
float sign = clockwise ? 1f : -1f;
|
||||
|
||||
// 1) Dimension labels (Angle Axis)
|
||||
bool showAngleLabels = angleAxis != null && (angleAxis.labelStyle != null ? angleAxis.labelStyle.enabled : angleAxis.showLabels);
|
||||
if (angleAxis != null && angleAxis.visible && showAngleLabels)
|
||||
{
|
||||
int fontSize = angleAxis.labelStyle != null ? angleAxis.labelStyle.fontSize : angleAxis.fontSize;
|
||||
var labelColor = angleAxis.labelStyle != null ? angleAxis.labelStyle.color : angleAxis.labelColor;
|
||||
float labelRadius = outerRadiusPx + Mathf.Max(6f, fontSize * 0.6f);
|
||||
Vector2 axisOffset = angleAxis.labelStyle != null ? angleAxis.labelStyle.offset : angleAxis.labelOffset;
|
||||
|
||||
for (int i = 0; i < dimensionCount; i++)
|
||||
{
|
||||
float angle = startAngleDeg + sign * (i * step);
|
||||
Vector2 dir = AngleToDir(angle);
|
||||
string text = ResolveDimensionLabel(labels, primarySerie, i);
|
||||
|
||||
Vector2 pos = center + dir * labelRadius + axisOffset;
|
||||
|
||||
float ax = Mathf.Abs(dir.x);
|
||||
float ay = Mathf.Abs(dir.y);
|
||||
bool isVertical = ay >= ax;
|
||||
bool isRight = dir.x > 0f;
|
||||
|
||||
ChartLabelAnchor anchor = isVertical ? ChartLabelAnchor.Center : (isRight ? ChartLabelAnchor.Left : ChartLabelAnchor.Right);
|
||||
Vector2 anchorOffset = isVertical ? Vector2.zero : new Vector2(isRight ? 2f : -2f, 0f);
|
||||
|
||||
var desc = new LabelDescriptor
|
||||
{
|
||||
key = $"radar:axis:{i}",
|
||||
text = text,
|
||||
visible = true,
|
||||
anchorPx = pos,
|
||||
offsetPx = new Vector2(anchorOffset.x, -anchorOffset.y),
|
||||
anchor = anchor,
|
||||
role = ChartTextRole.AxisLabel,
|
||||
zOrder = 0,
|
||||
priority = 0,
|
||||
clipToPlot = false,
|
||||
fontSizeOverride = fontSize,
|
||||
colorOverride = labelColor,
|
||||
rotationDeg = 0f,
|
||||
};
|
||||
|
||||
labelController.Submit(desc);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Data point labels (per serie)
|
||||
for (int si = 0; si < series.Count; si++)
|
||||
{
|
||||
var s = series[si];
|
||||
if (s == null || s.seriesData == null || s.seriesData.Count == 0) continue;
|
||||
if (s.labelSettings == null || !s.labelSettings.enabled) continue;
|
||||
|
||||
int serieIndex = GetRadarSerieIndex(s);
|
||||
if (serieIndex < 0) continue;
|
||||
|
||||
// Use final position (animationProgress = 1) for labels to avoid position drift during animation
|
||||
var valueVertices = BuildVertices(s, dimensionCount, minV, maxV, center, outerRadiusPx, innerRadiusPx, startAngleDeg, sign, step, 1f);
|
||||
|
||||
for (int i = 0; i < dimensionCount; i++)
|
||||
{
|
||||
var dp = i < s.seriesData.Count ? s.seriesData[i] : null;
|
||||
float y = dp != null ? dp.value : 0f;
|
||||
|
||||
string dimLabel = ResolveDimensionLabel(labels, s, i);
|
||||
int dpPlaces = Mathf.Clamp(s.labelSettings.decimalPlaces, 0, 8);
|
||||
string yText = (radiusAxis != null && !string.IsNullOrEmpty(radiusAxis.labelFormat))
|
||||
? y.ToString(radiusAxis.labelFormat)
|
||||
: FormatAxisValue(y, null, dpPlaces);
|
||||
string text = s.labelSettings.showName ? $"{dimLabel}: {yText}" : yText;
|
||||
|
||||
float angle = startAngleDeg + sign * (i * step);
|
||||
Vector2 dir = AngleToDir(angle);
|
||||
|
||||
Vector2 pos = (i < valueVertices.Count) ? valueVertices[i] : center;
|
||||
|
||||
// Direction from chart center to data point
|
||||
Vector2 radialDir = (pos - center).normalized;
|
||||
if (radialDir.sqrMagnitude < 0.001f) radialDir = dir;
|
||||
|
||||
float shift = Mathf.Max(10f, s.labelSettings.fontSize * 1.2f);
|
||||
if (s.labelSettings.position == LabelPosition.Outside) pos += radialDir * shift;
|
||||
else if (s.labelSettings.position == LabelPosition.Inside) pos -= radialDir * shift;
|
||||
pos += s.labelSettings.offset;
|
||||
|
||||
// Always use Center anchor for consistent positioning
|
||||
ChartLabelAnchor anchor = ChartLabelAnchor.Center;
|
||||
Vector2 anchorOffset = Vector2.zero;
|
||||
|
||||
var bg = s.labelSettings.background;
|
||||
var bgColor = bg != null ? bg.color : default;
|
||||
var bgTex = bg != null ? bg.texture : null;
|
||||
var desc = BuildSeriesLabelDesc(
|
||||
$"radar:{serieIndex}:{i}",
|
||||
text,
|
||||
pos,
|
||||
new Vector2(anchorOffset.x, -anchorOffset.y),
|
||||
clipToPlot: false,
|
||||
s.labelSettings.fontSize,
|
||||
s.labelSettings.color,
|
||||
anchor,
|
||||
bgColor,
|
||||
bgTex);
|
||||
|
||||
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;
|
||||
|
||||
if (Data == null || Data.Series == null) return false;
|
||||
if (hit.Value.SerieIndex < 0 || hit.Value.SerieIndex >= Data.Series.Count) return false;
|
||||
var serie = Data.Series[hit.Value.SerieIndex];
|
||||
if (serie == null || serie.type != SerieType.Radar || !serie.visible || 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 radiusAxis = Data.PolarAxes != null ? Data.PolarAxes.radiusAxis : null;
|
||||
|
||||
var labels = GetCategoryLabels();
|
||||
string dimLabel = ResolveDimensionLabel(labels, serie, hit.Value.PointIndex);
|
||||
if (string.IsNullOrEmpty(categoryLabel)) categoryLabel = dimLabel;
|
||||
|
||||
Color c = Color.white;
|
||||
if (serie.settings is RadarSettings rs)
|
||||
{
|
||||
if (rs.point != null) c = ResolveFillColor(rs.point.textureFill, Color.white);
|
||||
else if (rs.stroke != null) c = rs.stroke.color;
|
||||
}
|
||||
|
||||
items.Add(new TooltipItem
|
||||
{
|
||||
Name = serie.name,
|
||||
Value = (radiusAxis != null && !string.IsNullOrEmpty(radiusAxis.labelFormat))
|
||||
? dp.value.ToString(radiusAxis.labelFormat)
|
||||
: FormatAxisValue(dp.value, null, Mathf.Clamp(serie.labelSettings != null ? serie.labelSettings.decimalPlaces : 2, 0, 8)),
|
||||
Color = c
|
||||
});
|
||||
|
||||
// For Radar/Polar charts, keep cursor line hidden.
|
||||
cursorPosition = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<Vector2> BuildVertices(Serie serie, int dimensionCount, float minV, float maxV, Vector2 center, float outerRadiusPx, float innerRadiusPx, float startAngleDeg, float sign, float step, float animationProgress)
|
||||
{
|
||||
var vertices = new List<Vector2>(dimensionCount);
|
||||
if (serie == null || serie.seriesData == null) return vertices;
|
||||
|
||||
int valueCount = Mathf.Min(dimensionCount, serie.seriesData.Count);
|
||||
for (int i = 0; i < dimensionCount; i++)
|
||||
{
|
||||
float value = 0f;
|
||||
if (i < valueCount)
|
||||
{
|
||||
var dp = serie.seriesData[i];
|
||||
if (dp != null) value = dp.value;
|
||||
}
|
||||
|
||||
float tt = (value - minV) / (maxV - minV);
|
||||
tt = Mathf.Clamp01(float.IsNaN(tt) ? 0f : tt);
|
||||
|
||||
float rr = Mathf.Lerp(innerRadiusPx, outerRadiusPx, tt);
|
||||
rr = Mathf.Lerp(innerRadiusPx, rr, animationProgress);
|
||||
|
||||
float angle = startAngleDeg + sign * (i * step);
|
||||
vertices.Add(center + AngleToDir(angle) * rr);
|
||||
}
|
||||
|
||||
return vertices;
|
||||
}
|
||||
|
||||
protected override void OnGenerateVisualContent(MeshGenerationContext context)
|
||||
{
|
||||
if (Data == null) return;
|
||||
|
||||
float width = contentRect.width;
|
||||
float height = contentRect.height;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
var series = GetRadarSeries();
|
||||
if (series == null || series.Count == 0) return;
|
||||
var primarySerie = series[0];
|
||||
if (primarySerie == null || primarySerie.seriesData == null || primarySerie.seriesData.Count == 0) return;
|
||||
|
||||
var labels = GetCategoryLabels();
|
||||
int dimensionCount = labels != null ? labels.Count : 0;
|
||||
if (dimensionCount <= 0) dimensionCount = primarySerie.seriesData != null ? primarySerie.seriesData.Count : 0;
|
||||
if (dimensionCount <= 2) return;
|
||||
|
||||
var angleAxis = Data.PolarAxes != null ? Data.PolarAxes.angleAxis : null;
|
||||
var radiusAxis = Data.PolarAxes != null ? Data.PolarAxes.radiusAxis : null;
|
||||
|
||||
GetPolarLayout(primarySerie, width, height, out var center, out var outerRadiusPx, out var innerRadiusPx, out var startAngleDeg, out var clockwise);
|
||||
if (outerRadiusPx <= 0f) return;
|
||||
|
||||
ResolveValueRange(series, radiusAxis, dimensionCount, out float minV, out float maxV);
|
||||
|
||||
float step = 360f / dimensionCount;
|
||||
float sign = clockwise ? 1f : -1f;
|
||||
|
||||
var painter = context.painter2D;
|
||||
|
||||
// Background fill (from first serie's settings)
|
||||
var primarySettings = primarySerie.settings as RadarSettings;
|
||||
var radarLayout = primarySettings != null ? primarySettings.radar : null;
|
||||
if (radarLayout != null && radarLayout.background != null)
|
||||
{
|
||||
UnpackTextureFill(radarLayout.background, out var bgTex, out var bgTiling, out var bgOffset, out var bgColor);
|
||||
if (bgColor.a > 0f || bgTex != null)
|
||||
{
|
||||
// Build polygon vertices
|
||||
var bgVertices = new List<Vector2>(dimensionCount);
|
||||
for (int i = 0; i < dimensionCount; i++)
|
||||
{
|
||||
float angle = startAngleDeg + sign * (i * step);
|
||||
Vector2 p = center + AngleToDir(angle) * outerRadiusPx;
|
||||
bgVertices.Add(p);
|
||||
}
|
||||
|
||||
if (bgTex != null)
|
||||
{
|
||||
// Draw textured polygon
|
||||
Rect bounds = new Rect(center.x - outerRadiusPx, center.y - outerRadiusPx, outerRadiusPx * 2f, outerRadiusPx * 2f);
|
||||
DrawTexturedFan(context, bgVertices, bounds, bgTex, bgTiling, bgOffset, bgColor, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Draw solid color polygon
|
||||
painter.fillColor = bgColor;
|
||||
painter.BeginPath();
|
||||
for (int i = 0; i < bgVertices.Count; i++)
|
||||
{
|
||||
if (i == 0) painter.MoveTo(bgVertices[i]);
|
||||
else painter.LineTo(bgVertices[i]);
|
||||
}
|
||||
painter.ClosePath();
|
||||
painter.Fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grid: polygon rings
|
||||
int ringCount = radiusAxis != null ? Mathf.Max(1, radiusAxis.splitCount) : 5;
|
||||
if (radiusAxis != null && radiusAxis.visible)
|
||||
{
|
||||
painter.lineWidth = Mathf.Max(0f, radiusAxis.width);
|
||||
|
||||
for (int r = 1; r <= ringCount; r++)
|
||||
{
|
||||
float t = r / (float)ringCount;
|
||||
float rr = Mathf.Lerp(innerRadiusPx, outerRadiusPx, t);
|
||||
|
||||
// Use edgeColor for the outermost ring
|
||||
bool isOuterRing = r == ringCount;
|
||||
painter.strokeColor = isOuterRing ? radiusAxis.edgeColor : radiusAxis.color;
|
||||
|
||||
painter.BeginPath();
|
||||
for (int i = 0; i < dimensionCount; i++)
|
||||
{
|
||||
float angle = startAngleDeg + sign * (i * step);
|
||||
Vector2 p = center + AngleToDir(angle) * rr;
|
||||
if (i == 0) painter.MoveTo(p);
|
||||
else painter.LineTo(p);
|
||||
}
|
||||
painter.ClosePath();
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// Axis spokes
|
||||
if (angleAxis != null && angleAxis.visible)
|
||||
{
|
||||
painter.strokeColor = angleAxis.color;
|
||||
painter.lineWidth = Mathf.Max(0f, angleAxis.width);
|
||||
|
||||
for (int i = 0; i < dimensionCount; i++)
|
||||
{
|
||||
float angle = startAngleDeg + sign * (i * step);
|
||||
Vector2 dir = AngleToDir(angle);
|
||||
Vector2 p0 = center + dir * innerRadiusPx;
|
||||
Vector2 p1 = center + dir * outerRadiusPx;
|
||||
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(p0);
|
||||
painter.LineTo(p1);
|
||||
painter.Stroke();
|
||||
}
|
||||
}
|
||||
|
||||
for (int si = 0; si < series.Count; si++)
|
||||
{
|
||||
var serie = series[si];
|
||||
if (serie == null || serie.seriesData == null || serie.seriesData.Count == 0) continue;
|
||||
if (serie.settings is not RadarSettings settings) continue;
|
||||
|
||||
int valueCount = Mathf.Min(dimensionCount, serie.seriesData.Count);
|
||||
if (valueCount <= 2) continue;
|
||||
|
||||
var vertices = BuildVertices(serie, dimensionCount, minV, maxV, center, outerRadiusPx, innerRadiusPx, startAngleDeg, sign, step, _animationProgress);
|
||||
|
||||
// Fill
|
||||
if (settings.area != null && settings.area.show)
|
||||
{
|
||||
UnpackTextureFill(settings.area.textureFill, out var _areaTex, out var _areaTiling, out var _areaOffset, out var _areaColor);
|
||||
painter.fillColor = _areaColor;
|
||||
painter.BeginPath();
|
||||
for (int i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
if (i == 0) painter.MoveTo(vertices[i]);
|
||||
else painter.LineTo(vertices[i]);
|
||||
}
|
||||
painter.ClosePath();
|
||||
painter.Fill();
|
||||
}
|
||||
|
||||
// Stroke
|
||||
if (settings.stroke != null)
|
||||
{
|
||||
painter.strokeColor = settings.stroke.color;
|
||||
painter.lineWidth = Mathf.Max(0f, settings.stroke.width);
|
||||
painter.BeginPath();
|
||||
for (int i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
if (i == 0) painter.MoveTo(vertices[i]);
|
||||
else painter.LineTo(vertices[i]);
|
||||
}
|
||||
painter.ClosePath();
|
||||
painter.Stroke();
|
||||
}
|
||||
|
||||
// Points
|
||||
if (settings.point != null && settings.point.show)
|
||||
{
|
||||
float size = Mathf.Max(0f, settings.point.size);
|
||||
float pr = size * 0.5f;
|
||||
int serieIndex = GetRadarSerieIndex(serie);
|
||||
|
||||
for (int i = 0; i < vertices.Count; i++)
|
||||
{
|
||||
var pos = vertices[i];
|
||||
|
||||
float localPr = pr;
|
||||
if (_hover.HasValue && _hover.Value.SerieIndex == serieIndex && _hover.Value.PointIndex == i)
|
||||
{
|
||||
localPr *= 1.6f;
|
||||
}
|
||||
|
||||
DrawPointMarker(context, painter, pos, localPr, settings.point.textureFill, Color.white);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7d1affc17b84b443aea37512d223e55
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/RadarSeriesRenderer.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace EasyChart.Layers
|
||||
{
|
||||
public static class RoundedRectUtils
|
||||
{
|
||||
private static readonly List<Vector2> s_tempRoundedPoints = new List<Vector2>(64);
|
||||
|
||||
public static void BuildRoundedRectPoints(List<Vector2> points, Rect rect, float radius, int segments, bool roundTL, bool roundTR, bool roundBR, bool roundBL)
|
||||
{
|
||||
if (points == null) return;
|
||||
points.Clear();
|
||||
|
||||
float x = rect.xMin;
|
||||
float y = rect.yMin;
|
||||
float w = rect.width;
|
||||
float h = rect.height;
|
||||
|
||||
if (w <= 0f || h <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float r = Mathf.Max(0f, radius);
|
||||
r = Mathf.Min(r, Mathf.Min(w, h) * 0.5f);
|
||||
int seg = Mathf.Clamp(segments, 1, 16);
|
||||
|
||||
void AddPoint(Vector2 p)
|
||||
{
|
||||
if (points.Count > 0)
|
||||
{
|
||||
var last = points[points.Count - 1];
|
||||
if (Mathf.Approximately(last.x, p.x) && Mathf.Approximately(last.y, p.y)) return;
|
||||
}
|
||||
points.Add(p);
|
||||
}
|
||||
|
||||
void AddArc(Vector2 center, float startDeg, float endDeg)
|
||||
{
|
||||
for (int i = 1; i <= seg; i++)
|
||||
{
|
||||
float t = (float)i / seg;
|
||||
float a = Mathf.Deg2Rad * Mathf.Lerp(startDeg, endDeg, t);
|
||||
float px = center.x + Mathf.Cos(a) * r;
|
||||
float py = center.y + Mathf.Sin(a) * r;
|
||||
AddPoint(new Vector2(px, py));
|
||||
}
|
||||
}
|
||||
|
||||
bool useTL = roundTL && r > 0f;
|
||||
bool useTR = roundTR && r > 0f;
|
||||
bool useBR = roundBR && r > 0f;
|
||||
bool useBL = roundBL && r > 0f;
|
||||
|
||||
AddPoint(new Vector2(x + (useTL ? r : 0f), y));
|
||||
AddPoint(new Vector2(x + w - (useTR ? r : 0f), y));
|
||||
if (useTR) AddArc(new Vector2(x + w - r, y + r), 270f, 360f);
|
||||
else AddPoint(new Vector2(x + w, y));
|
||||
|
||||
AddPoint(new Vector2(x + w, y + h - (useBR ? r : 0f)));
|
||||
if (useBR) AddArc(new Vector2(x + w - r, y + h - r), 0f, 90f);
|
||||
else AddPoint(new Vector2(x + w, y + h));
|
||||
|
||||
AddPoint(new Vector2(x + (useBL ? r : 0f), y + h));
|
||||
if (useBL) AddArc(new Vector2(x + r, y + h - r), 90f, 180f);
|
||||
else AddPoint(new Vector2(x, y + h));
|
||||
|
||||
AddPoint(new Vector2(x, y + (useTL ? r : 0f)));
|
||||
if (useTL) AddArc(new Vector2(x + r, y + r), 180f, 270f);
|
||||
else AddPoint(new Vector2(x, y));
|
||||
}
|
||||
|
||||
public static void BeginRoundedRectPath(Painter2D painter, Rect rect, float radius, int segments, bool roundTL, bool roundTR, bool roundBR, bool roundBL)
|
||||
{
|
||||
if (painter == null) return;
|
||||
|
||||
s_tempRoundedPoints.Clear();
|
||||
BuildRoundedRectPoints(s_tempRoundedPoints, rect, radius, segments, roundTL, roundTR, roundBR, roundBL);
|
||||
if (s_tempRoundedPoints.Count == 0) return;
|
||||
|
||||
painter.BeginPath();
|
||||
painter.MoveTo(s_tempRoundedPoints[0]);
|
||||
for (int i = 1; i < s_tempRoundedPoints.Count; i++)
|
||||
{
|
||||
painter.LineTo(s_tempRoundedPoints[i]);
|
||||
}
|
||||
painter.ClosePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2fa72412056f244c8879ba32664c8b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/RoundedRectUtils.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,436 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71792e8ce3f7e3743874e377af453c1c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/ScatterSeriesRenderer.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace EasyChart.Layers
|
||||
{
|
||||
public static class SerieRendererRegistry
|
||||
{
|
||||
private static readonly Dictionary<EasyChart.SerieType, Func<BaseSeriesRenderer>> s_factories = new Dictionary<EasyChart.SerieType, Func<BaseSeriesRenderer>>();
|
||||
|
||||
public static void Register(EasyChart.SerieType type, Func<BaseSeriesRenderer> factory)
|
||||
{
|
||||
if (factory == null) return;
|
||||
s_factories[type] = factory;
|
||||
}
|
||||
|
||||
public static bool TryCreate(EasyChart.SerieType type, out BaseSeriesRenderer renderer)
|
||||
{
|
||||
renderer = null;
|
||||
if (s_factories.TryGetValue(type, out var f) && f != null)
|
||||
{
|
||||
renderer = f();
|
||||
return renderer != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasFactory(EasyChart.SerieType type)
|
||||
{
|
||||
return s_factories.ContainsKey(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e4fa67c258725234f94b8de1c4d6e2d9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/SerieRendererRegistry.cs
|
||||
uploadId: 857482
|
||||
@@ -0,0 +1,18 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace EasyChart.Layers
|
||||
{
|
||||
public struct TooltipItem
|
||||
{
|
||||
public string Name;
|
||||
public string Value;
|
||||
public Color Color;
|
||||
}
|
||||
|
||||
public class TooltipContext
|
||||
{
|
||||
public Vector2 LocalPos;
|
||||
public float Width;
|
||||
public float Height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ad5df64b623c9a4a82c31971b58c3a2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
AssetOrigin:
|
||||
serializedVersion: 1
|
||||
productId: 359794
|
||||
packageName: Easy Chart Lite
|
||||
packageVersion: 1.0
|
||||
assetPath: Assets/EasyChart/Scripts/Runtime/Layers/TooltipModels.cs
|
||||
uploadId: 857482
|
||||
Reference in New Issue
Block a user