786 lines
28 KiB
C#
786 lines
28 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
|
|
namespace EasyChart
|
|
{
|
|
[CreateAssetMenu(fileName = "NewChartProfile", menuName = "EasyChart/Chart Profile")]
|
|
public class ChartProfile : ScriptableObject
|
|
{
|
|
// Cache to avoid redundant EnsureRuntimeData() calls in the same frame
|
|
[NonSerialized] private double _lastEnsureTime;
|
|
[NonSerialized] private int _lastEnsureVersion;
|
|
private static object DeepCloneObject(object source, Dictionary<object, object> visited)
|
|
{
|
|
if (source == null) return null;
|
|
|
|
var type = source.GetType();
|
|
|
|
// Keep UnityEngine.Object references as-is (Texture2D, etc.).
|
|
if (typeof(UnityEngine.Object).IsAssignableFrom(type)) return source;
|
|
|
|
// Immutable / value types.
|
|
if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal)) return source;
|
|
if (type.IsValueType) return source;
|
|
|
|
if (visited != null && visited.TryGetValue(source, out var existing)) return existing;
|
|
visited ??= new Dictionary<object, object>();
|
|
|
|
// Arrays
|
|
if (type.IsArray)
|
|
{
|
|
var arr = (Array)source;
|
|
var elementType = type.GetElementType();
|
|
var cloneArr = Array.CreateInstance(elementType, arr.Length);
|
|
visited[source] = cloneArr;
|
|
for (int i = 0; i < arr.Length; i++)
|
|
{
|
|
cloneArr.SetValue(DeepCloneObject(arr.GetValue(i), visited), i);
|
|
}
|
|
return cloneArr;
|
|
}
|
|
|
|
// Lists / collections
|
|
if (source is IList list)
|
|
{
|
|
var cloneList = (IList)Activator.CreateInstance(type);
|
|
visited[source] = cloneList;
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
cloneList.Add(DeepCloneObject(list[i], visited));
|
|
}
|
|
return cloneList;
|
|
}
|
|
|
|
// Plain classes
|
|
var clone = Activator.CreateInstance(type);
|
|
visited[source] = clone;
|
|
|
|
// Unity serializes public instance fields (and [SerializeField] privates).
|
|
// Here we copy all instance fields to avoid missing important data.
|
|
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
|
var fields = type.GetFields(flags);
|
|
for (int i = 0; i < fields.Length; i++)
|
|
{
|
|
var f = fields[i];
|
|
if (f.IsStatic) continue;
|
|
var v = f.GetValue(source);
|
|
f.SetValue(clone, DeepCloneObject(v, visited));
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
|
|
private static T DeepCloneSettings<T>(T source) where T : class
|
|
{
|
|
if (source == null) return null;
|
|
return (T)DeepCloneObject(source, null);
|
|
}
|
|
|
|
public string chartName;
|
|
public string chartId;
|
|
|
|
public CoordinateSystemType coordinateSystem = CoordinateSystemType.Cartesian2D;
|
|
public CartesianMapping cartesian = new CartesianMapping();
|
|
public List<AxisConfig> axes = new List<AxisConfig>();
|
|
|
|
// New axis fields (X/Y based)
|
|
public AxisId xAxisId = AxisId.XBottom;
|
|
public AxisId yAxisId = AxisId.YLeft;
|
|
|
|
public bool axisSelectionInitialized = false;
|
|
public bool axisTypeSelectionInitialized = false;
|
|
public float animationDuration = 1.0f;
|
|
public BackgroundSettings background = new BackgroundSettings
|
|
{
|
|
show = true,
|
|
textureFill = new TextureFillSettings { color = new Color(0.15f, 0.15f, 0.15f, 1f) }
|
|
};
|
|
|
|
public float chartWidth = 450f;
|
|
public float chartHeight = 300f;
|
|
|
|
[Padding4] public Vector4 padding = new Vector4(40f, 30f, 50f, 50f);
|
|
public bool paddingInitialized = false;
|
|
|
|
public Color xGridColor = new Color(0.5f, 0.5f, 0.5f, 0.2f);
|
|
public float xGridLineWidth = 1.0f;
|
|
public Color yGridColor = new Color(0.5f, 0.5f, 0.5f, 0.2f);
|
|
public float yGridLineWidth = 1.0f;
|
|
|
|
public CartesianGridSettings cartesianGrid = new CartesianGridSettings();
|
|
public HoverSettings hover = new HoverSettings();
|
|
public PolarAxesSettings polarAxes = new PolarAxesSettings();
|
|
public bool gridSettingsInitialized = false;
|
|
public bool hoverSettingsInitialized = false;
|
|
|
|
public LegendSettings legendSettings = new LegendSettings();
|
|
|
|
public PlotSettings plotSettings = new PlotSettings();
|
|
|
|
[Header("Series Data")] public List<Serie> series = new List<Serie>();
|
|
|
|
private AxisConfig GetAxis(AxisId id)
|
|
{
|
|
if (axes == null) return null;
|
|
for (int i = 0; i < axes.Count; i++)
|
|
{
|
|
var a = axes[i];
|
|
if (a != null && a.id == id) return a;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void EnsureAxes()
|
|
{
|
|
EnsureAxesIntegrity();
|
|
}
|
|
|
|
public bool EnsureAxesIntegrity()
|
|
{
|
|
bool changed = false;
|
|
|
|
var beforePadding = padding;
|
|
var beforeAxisSelectionInitialized = axisSelectionInitialized;
|
|
var beforeAxisTypeSelectionInitialized = axisTypeSelectionInitialized;
|
|
var beforeXAxisId = xAxisId;
|
|
var beforeYAxisId = yAxisId;
|
|
var beforeGridSettingsInitialized = gridSettingsInitialized;
|
|
AxisId beforeCartesianX = cartesian != null ? cartesian.xAxisId : AxisId.XBottom;
|
|
AxisId beforeCartesianY = cartesian != null ? cartesian.yAxisId : AxisId.YLeft;
|
|
int beforeAxesCount = axes != null ? axes.Count : 0;
|
|
|
|
if (axes == null)
|
|
{
|
|
axes = new List<AxisConfig>();
|
|
changed = true;
|
|
}
|
|
|
|
if (cartesian == null)
|
|
{
|
|
cartesian = new CartesianMapping();
|
|
changed = true;
|
|
}
|
|
|
|
if (background == null)
|
|
{
|
|
background = new BackgroundSettings { show = true };
|
|
changed = true;
|
|
}
|
|
|
|
if (!paddingInitialized)
|
|
{
|
|
if (padding == Vector4.zero)
|
|
{
|
|
padding = new Vector4(40f, 30f, 50f, 50f);
|
|
changed = true;
|
|
}
|
|
paddingInitialized = true;
|
|
changed = true;
|
|
}
|
|
|
|
if (cartesianGrid == null)
|
|
{
|
|
cartesianGrid = new CartesianGridSettings();
|
|
changed = true;
|
|
}
|
|
if (hover == null)
|
|
{
|
|
hover = new HoverSettings();
|
|
changed = true;
|
|
}
|
|
if (polarAxes == null)
|
|
{
|
|
polarAxes = new PolarAxesSettings();
|
|
changed = true;
|
|
}
|
|
|
|
if (!gridSettingsInitialized)
|
|
{
|
|
cartesianGrid.xGridColor = xGridColor;
|
|
cartesianGrid.xGridLineWidth = xGridLineWidth;
|
|
cartesianGrid.yGridColor = yGridColor;
|
|
cartesianGrid.yGridLineWidth = yGridLineWidth;
|
|
gridSettingsInitialized = true;
|
|
}
|
|
|
|
if (!hoverSettingsInitialized)
|
|
{
|
|
hoverSettingsInitialized = true;
|
|
changed = true;
|
|
}
|
|
|
|
bool IsXAxis(AxisId id) => id == AxisId.XBottom || id == AxisId.XTop;
|
|
bool IsYAxis(AxisId id) => id == AxisId.YLeft || id == AxisId.YRight;
|
|
|
|
// Initialize axis selection from existing cartesian mapping if not yet done
|
|
if (!axisSelectionInitialized)
|
|
{
|
|
xAxisId = cartesian.xAxisId;
|
|
yAxisId = cartesian.yAxisId;
|
|
axisSelectionInitialized = true;
|
|
}
|
|
|
|
// Ensure xAxisId/yAxisId are valid (one X, one Y)
|
|
if (!IsXAxis(xAxisId))
|
|
{
|
|
xAxisId = AxisId.XBottom;
|
|
changed = true;
|
|
}
|
|
if (!IsYAxis(yAxisId))
|
|
{
|
|
yAxisId = AxisId.YLeft;
|
|
changed = true;
|
|
}
|
|
|
|
// Sync to cartesian mapping
|
|
if (cartesian.xAxisId != xAxisId || cartesian.yAxisId != yAxisId)
|
|
{
|
|
cartesian.xAxisId = xAxisId;
|
|
cartesian.yAxisId = yAxisId;
|
|
changed = true;
|
|
}
|
|
|
|
// Ensure axes referenced by mapping exist with default axisType
|
|
if (GetAxis(xAxisId) == null)
|
|
{
|
|
axes.Add(new AxisConfig
|
|
{
|
|
id = xAxisId,
|
|
axisType = AxisType.Category // Default: X axis is Category
|
|
});
|
|
changed = true;
|
|
}
|
|
|
|
if (GetAxis(yAxisId) == null)
|
|
{
|
|
axes.Add(new AxisConfig
|
|
{
|
|
id = yAxisId,
|
|
axisType = AxisType.Value // Default: Y axis is Value
|
|
});
|
|
changed = true;
|
|
}
|
|
|
|
// One-time default axis type initialization:
|
|
// - X defaults to Category
|
|
// - Y defaults to Value
|
|
// Only do this if both axes are still at the default Value (uninitialized state),
|
|
// so we don't override user configurations (e.g. transposed charts or dual-category heatmaps).
|
|
if (!axisTypeSelectionInitialized)
|
|
{
|
|
var xAxis = GetAxis(xAxisId);
|
|
var yAxis = GetAxis(yAxisId);
|
|
if (xAxis != null && yAxis != null
|
|
&& xAxis.axisType == AxisType.Value
|
|
&& yAxis.axisType == AxisType.Value)
|
|
{
|
|
xAxis.axisType = AxisType.Category;
|
|
yAxis.axisType = AxisType.Value;
|
|
changed = true;
|
|
}
|
|
|
|
axisTypeSelectionInitialized = true;
|
|
changed = true;
|
|
}
|
|
|
|
// Ensure labels list exists for all axes
|
|
for (int i = 0; i < axes.Count; i++)
|
|
{
|
|
if (axes[i] != null && axes[i].labels == null)
|
|
{
|
|
axes[i].labels = new List<string>();
|
|
changed = true;
|
|
}
|
|
}
|
|
for (int i = 0; i < axes.Count; i++)
|
|
{
|
|
var a = axes[i];
|
|
if (a == null) continue;
|
|
if (a.labelStyle != null) continue;
|
|
|
|
a.labelStyle = new LabelStyleSettings
|
|
{
|
|
enabled = a.showLabels,
|
|
fontSize = a.fontSize,
|
|
color = a.labelColor,
|
|
position = a.labelPosition,
|
|
offset = a.labelOffset
|
|
};
|
|
changed = true;
|
|
}
|
|
|
|
if (padding != beforePadding) changed = true;
|
|
if (gridSettingsInitialized != beforeGridSettingsInitialized) changed = true;
|
|
if (axisSelectionInitialized != beforeAxisSelectionInitialized) changed = true;
|
|
if (axisTypeSelectionInitialized != beforeAxisTypeSelectionInitialized) changed = true;
|
|
if (xAxisId != beforeXAxisId) changed = true;
|
|
if (yAxisId != beforeYAxisId) changed = true;
|
|
if (cartesian.xAxisId != beforeCartesianX) changed = true;
|
|
if (cartesian.yAxisId != beforeCartesianY) changed = true;
|
|
if (axes.Count != beforeAxesCount) changed = true;
|
|
|
|
return changed;
|
|
}
|
|
|
|
private int GetDataVersion()
|
|
{
|
|
// Simple hash based on key fields to detect data changes
|
|
unchecked
|
|
{
|
|
int hash = 17;
|
|
hash = hash * 31 + (int)coordinateSystem;
|
|
hash = hash * 31 + (series?.Count ?? 0);
|
|
hash = hash * 31 + (axes?.Count ?? 0);
|
|
hash = hash * 31 + (polarAxes?.angleAxis?.labels?.Count ?? 0);
|
|
return hash;
|
|
}
|
|
}
|
|
|
|
public bool EnsureRuntimeData()
|
|
{
|
|
#if UNITY_EDITOR
|
|
// Cache optimization: skip if called within 1 second and data hasn't changed
|
|
if (!UnityEngine.Application.isPlaying)
|
|
{
|
|
double now = UnityEditor.EditorApplication.timeSinceStartup;
|
|
int currentVersion = GetDataVersion();
|
|
|
|
bool needsIdentityFix = string.IsNullOrWhiteSpace(chartId) || string.IsNullOrWhiteSpace(chartName);
|
|
|
|
if (!needsIdentityFix && now - _lastEnsureTime < 1.0 && currentVersion == _lastEnsureVersion)
|
|
{
|
|
return false; // No changes, skip execution
|
|
}
|
|
|
|
_lastEnsureTime = now;
|
|
_lastEnsureVersion = currentVersion;
|
|
}
|
|
#endif
|
|
|
|
bool changed = false;
|
|
|
|
if (string.IsNullOrWhiteSpace(chartName))
|
|
{
|
|
chartName = name;
|
|
changed = true;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(chartId))
|
|
{
|
|
chartId = Guid.NewGuid().ToString("N");
|
|
changed = true;
|
|
}
|
|
|
|
if (EnsureAxesIntegrity()) changed = true;
|
|
|
|
if (polarAxes == null)
|
|
{
|
|
polarAxes = new PolarAxesSettings();
|
|
changed = true;
|
|
}
|
|
if (polarAxes.angleAxis == null)
|
|
{
|
|
polarAxes.angleAxis = new PolarAxisStyle();
|
|
changed = true;
|
|
}
|
|
if (polarAxes.radiusAxis == null)
|
|
{
|
|
polarAxes.radiusAxis = new PolarAxisStyle();
|
|
changed = true;
|
|
}
|
|
if (polarAxes.angleAxis.labels == null)
|
|
{
|
|
polarAxes.angleAxis.labels = new List<string>();
|
|
changed = true;
|
|
}
|
|
if (polarAxes.radiusAxis.labels == null)
|
|
{
|
|
polarAxes.radiusAxis.labels = new List<string>();
|
|
changed = true;
|
|
}
|
|
|
|
if (polarAxes.angleAxis.labelStyle == null)
|
|
{
|
|
polarAxes.angleAxis.labelStyle = new LabelStyleSettings
|
|
{
|
|
enabled = polarAxes.angleAxis.showLabels,
|
|
fontSize = polarAxes.angleAxis.fontSize,
|
|
color = polarAxes.angleAxis.labelColor,
|
|
position = polarAxes.angleAxis.labelPosition,
|
|
offset = polarAxes.angleAxis.labelOffset
|
|
};
|
|
changed = true;
|
|
}
|
|
|
|
if (polarAxes.radiusAxis.labelStyle == null)
|
|
{
|
|
polarAxes.radiusAxis.labelStyle = new LabelStyleSettings
|
|
{
|
|
enabled = polarAxes.radiusAxis.showLabels,
|
|
fontSize = polarAxes.radiusAxis.fontSize,
|
|
color = polarAxes.radiusAxis.labelColor,
|
|
position = polarAxes.radiusAxis.labelPosition,
|
|
offset = polarAxes.radiusAxis.labelOffset
|
|
};
|
|
changed = true;
|
|
}
|
|
|
|
// One-time migration: Polar2D (Radar dimensions) should be self-consistent.
|
|
// If angle axis labels are empty but legacy Category axis labels exist, copy them.
|
|
if (polarAxes.angleAxis.labels.Count == 0)
|
|
{
|
|
List<string> ResolveLegacyCategoryLabels()
|
|
{
|
|
if (axes == null) return null;
|
|
|
|
// Use xAxisId as preferred for legacy category labels
|
|
AxisId preferred = xAxisId;
|
|
for (int i = 0; i < axes.Count; i++)
|
|
{
|
|
var a = axes[i];
|
|
if (a != null && a.id == preferred && a.axisType == AxisType.Category)
|
|
{
|
|
return a.labels;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < axes.Count; i++)
|
|
{
|
|
var a = axes[i];
|
|
if (a != null && a.axisType == AxisType.Category)
|
|
{
|
|
return a.labels;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
var legacy = ResolveLegacyCategoryLabels();
|
|
if (legacy != null && legacy.Count > 0)
|
|
{
|
|
polarAxes.angleAxis.labels = new List<string>(legacy);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (legendSettings == null)
|
|
{
|
|
legendSettings = new LegendSettings();
|
|
changed = true;
|
|
}
|
|
|
|
if (plotSettings == null)
|
|
{
|
|
plotSettings = new PlotSettings();
|
|
changed = true;
|
|
}
|
|
|
|
if (series == null)
|
|
{
|
|
series = new List<Serie>();
|
|
changed = true;
|
|
}
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
if (series[i] == null)
|
|
{
|
|
series[i] = new Serie { name = $"Serie {i + 1}" };
|
|
changed = true;
|
|
}
|
|
|
|
if (series[i] != null && series[i].EnsureIntegrity())
|
|
{
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var s = series[i];
|
|
if (s == null) continue;
|
|
if (s.type != SerieType.Heatmap) continue;
|
|
if (s.seriesData == null || s.seriesData.Count == 0) continue;
|
|
|
|
bool looksLegacyHeatmap = false;
|
|
for (int pi = 0; pi < s.seriesData.Count; pi++)
|
|
{
|
|
var p = s.seriesData[pi];
|
|
if (p == null) continue;
|
|
if (!Mathf.Approximately(p.z, 0f))
|
|
{
|
|
looksLegacyHeatmap = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!looksLegacyHeatmap) continue;
|
|
|
|
for (int pi = 0; pi < s.seriesData.Count; pi++)
|
|
{
|
|
var p = s.seriesData[pi];
|
|
if (p == null) continue;
|
|
|
|
float legacyY = p.value;
|
|
float legacyZ = p.z;
|
|
|
|
p.y = legacyY;
|
|
p.value = legacyZ;
|
|
p.z = 0f;
|
|
}
|
|
|
|
changed = true;
|
|
if (s.EnsureIntegrity()) changed = true;
|
|
}
|
|
|
|
var radarLabels = polarAxes != null && polarAxes.angleAxis != null ? polarAxes.angleAxis.labels : null;
|
|
if (radarLabels != null && radarLabels.Count > 0)
|
|
{
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var s = series[i];
|
|
if (s == null) continue;
|
|
if (s.type != SerieType.Radar) continue;
|
|
|
|
bool empty = s.seriesData == null || s.seriesData.Count == 0;
|
|
if (!empty) continue;
|
|
|
|
s.seriesData = new List<SeriesData>(radarLabels.Count);
|
|
for (int di = 0; di < radarLabels.Count; di++)
|
|
{
|
|
float t = radarLabels.Count > 1 ? (float)di / (radarLabels.Count - 1) : 0.5f;
|
|
s.seriesData.Add(new SeriesData
|
|
{
|
|
x = di,
|
|
value = Mathf.Lerp(0f, 100f, t),
|
|
name = radarLabels[di],
|
|
id = Guid.NewGuid().ToString("N")
|
|
});
|
|
}
|
|
|
|
changed = true;
|
|
if (s.EnsureIntegrity()) changed = true;
|
|
}
|
|
}
|
|
|
|
// Ensure each serie has its own settings instance.
|
|
// UI/editor workflows (and array element duplication) can cause multiple series
|
|
// to reference the same managedReference object, which makes edits leak across series.
|
|
var seenSettings = new HashSet<object>();
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var s = series[i];
|
|
if (s == null || s.settings == null) continue;
|
|
|
|
if (seenSettings.Contains(s.settings))
|
|
{
|
|
var cloned = DeepCloneSettings(s.settings);
|
|
if (cloned != null)
|
|
{
|
|
s.settings = cloned;
|
|
changed = true;
|
|
|
|
if (s.EnsureIntegrity()) changed = true;
|
|
}
|
|
}
|
|
|
|
seenSettings.Add(s.settings);
|
|
}
|
|
|
|
bool hasPolar = false;
|
|
bool hasCartesian = false;
|
|
bool hasPie = false;
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var s = series[i];
|
|
if (s == null || !s.visible) continue;
|
|
if (s.type == SerieType.Pie || s.type == SerieType.RingChart || s.type == SerieType.Pie3D)
|
|
{
|
|
hasPie = true;
|
|
continue;
|
|
}
|
|
|
|
if (s.type == SerieType.Radar) hasPolar = true;
|
|
else if (s.type == SerieType.Line || s.type == SerieType.Bar || s.type == SerieType.Scatter || s.type == SerieType.Heatmap) hasCartesian = true;
|
|
}
|
|
|
|
// CoordinateSystem is a user-chosen plot space. Do not rewrite it automatically.
|
|
// Incompatible series are filtered at render time (ChartElement), and the editor UI filters selectable types.
|
|
bool warn = true;
|
|
#if UNITY_EDITOR
|
|
warn = Application.isPlaying;
|
|
#endif
|
|
if (warn)
|
|
{
|
|
if (hasPolar && hasCartesian)
|
|
{
|
|
Debug.LogWarning("[EasyChart] Mixed Polar (Radar) and Cartesian (Line/Bar/Scatter) series detected in one ChartProfile. This may produce semantically inconsistent axes/grid if CoordinateSystem/Axes settings are not aligned.");
|
|
}
|
|
else if (coordinateSystem == CoordinateSystemType.Cartesian2D && hasPolar)
|
|
{
|
|
Debug.LogWarning("[EasyChart] CoordinateSystem is Cartesian2D but a Radar series exists. Radar will be rendered, but PolarAxes/Radar settings may be semantically inconsistent with the selected CoordinateSystem.");
|
|
}
|
|
else if (coordinateSystem == CoordinateSystemType.Polar2D && hasCartesian)
|
|
{
|
|
Debug.LogWarning("[EasyChart] CoordinateSystem is Polar2D but Line/Bar/Scatter series exist. They will be rendered, but Cartesian axes/grid settings may be semantically inconsistent with the selected CoordinateSystem.");
|
|
}
|
|
else if (coordinateSystem == CoordinateSystemType.None && (hasPolar || hasCartesian))
|
|
{
|
|
Debug.LogWarning("[EasyChart] CoordinateSystem is None but coordinate-based series exist. They will be rendered, but axis/grid settings may be semantically inconsistent.");
|
|
}
|
|
else if (coordinateSystem != CoordinateSystemType.None && hasPie && !hasPolar && !hasCartesian)
|
|
{
|
|
Debug.LogWarning("[EasyChart] Only Pie series exist but CoordinateSystem is not None. Consider switching to None coordinate system.");
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
void Reset()
|
|
{
|
|
EnsureAxes();
|
|
|
|
xAxisId = AxisId.XBottom;
|
|
yAxisId = AxisId.YLeft;
|
|
EnsureAxes();
|
|
|
|
var xAxis = GetAxis(this.xAxisId);
|
|
if (xAxis != null)
|
|
{
|
|
xAxis.axisType = AxisType.Category; // Default: X is Category
|
|
xAxis.labels = new List<string> { "A", "B", "C", "D", "E" };
|
|
}
|
|
|
|
var yAxis = GetAxis(this.yAxisId);
|
|
if (yAxis != null)
|
|
{
|
|
yAxis.axisType = AxisType.Value; // Default: Y is Value
|
|
}
|
|
|
|
var s = new Serie { name = "Series 1" };
|
|
s.type = SerieType.Line;
|
|
s.settings = new LineSettings { stroke = new LineStrokeSettings { color = Color.cyan } };
|
|
|
|
s.seriesData.Add(new SeriesData { x = 0, value = 10 });
|
|
s.seriesData.Add(new SeriesData { x = 1, value = 20 });
|
|
s.seriesData.Add(new SeriesData { x = 2, value = 15 });
|
|
s.seriesData.Add(new SeriesData { x = 3, value = 30 });
|
|
s.seriesData.Add(new SeriesData { x = 4, value = 25 });
|
|
series.Add(s);
|
|
}
|
|
|
|
public ChartData ToChartData()
|
|
{
|
|
var data = new ChartData();
|
|
EnsureRuntimeData();
|
|
data.CoordinateSystem = coordinateSystem;
|
|
data.Cartesian = cartesian;
|
|
data.Axes = axes;
|
|
|
|
data.XAxisId = xAxisId;
|
|
data.YAxisId = yAxisId;
|
|
|
|
data.CartesianGrid = cartesianGrid;
|
|
data.Hover = hover;
|
|
data.PolarAxes = polarAxes;
|
|
data.Plot = plotSettings;
|
|
|
|
data.Series = this.series;
|
|
data.legend = this.legendSettings;
|
|
data.animationDuration = this.animationDuration;
|
|
return data;
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
EnsureRuntimeData();
|
|
|
|
// Sync legend offset when position changes
|
|
if (legendSettings != null)
|
|
{
|
|
legendSettings.SyncOffsetToPosition();
|
|
}
|
|
|
|
if (series == null) return;
|
|
AxisConfig GetAxis(AxisId id)
|
|
{
|
|
if (axes == null) return null;
|
|
for (int i = 0; i < axes.Count; i++)
|
|
{
|
|
var a = axes[i];
|
|
if (a != null && a.id == id) return a;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
bool transposed = false;
|
|
if (coordinateSystem == CoordinateSystemType.Cartesian2D && cartesian != null)
|
|
{
|
|
var xAxis = GetAxis(cartesian.xAxisId);
|
|
var yAxis = GetAxis(cartesian.yAxisId);
|
|
|
|
bool hasHeatmap = false;
|
|
if (series != null)
|
|
{
|
|
for (int i = 0; i < series.Count; i++)
|
|
{
|
|
var s = series[i];
|
|
if (s != null && s.visible && s.type == SerieType.Heatmap)
|
|
{
|
|
hasHeatmap = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasHeatmap)
|
|
{
|
|
for (int i = 0; i < axes.Count; i++)
|
|
{
|
|
var a = axes[i];
|
|
if (a == null) continue;
|
|
if (a.axisType != AxisType.Category) continue;
|
|
a.labelPlacement = CategoryLabelPlacement.CellCenter;
|
|
}
|
|
}
|
|
|
|
var xDim = xAxis != null ? xAxis.axisType : AxisType.Category;
|
|
var yDim = yAxis != null ? yAxis.axisType : AxisType.Value;
|
|
transposed = xDim == AxisType.Value && yDim == AxisType.Category;
|
|
}
|
|
|
|
// Data point convention:
|
|
// - Normal: p.x = category index, p.y = value
|
|
// - Transposed: p.y = category index, p.x = value
|
|
bool categoryOnY = transposed;
|
|
|
|
foreach (var serie in series)
|
|
{
|
|
if (serie == null) continue;
|
|
if (serie.seriesData == null) continue;
|
|
|
|
for (int i = 0; i < serie.seriesData.Count; i++)
|
|
{
|
|
var p = serie.seriesData[i];
|
|
if (p == null) continue;
|
|
if (i > 0)
|
|
{
|
|
var prev = serie.seriesData[i - 1];
|
|
if (prev == null) continue;
|
|
float pCat = categoryOnY ? p.y : p.x;
|
|
float prevCat = categoryOnY ? prev.y : prev.x;
|
|
if (Mathf.Approximately(pCat, prevCat))
|
|
{
|
|
if (categoryOnY) p.y = i;
|
|
else p.x = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |