/************************************************/
/* */
/* Copyright (c) 2018 - 2021 monitor1394 */
/* https://github.com/monitor1394 */
/* */
/************************************************/
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace XCharts
{
///
/// 标线类型
///
public enum MarkLineType
{
None,
///
/// 最小值。
///
Min,
///
/// 最大值。
///
Max,
///
/// 平均值。
///
Average,
///
/// 中位数。
///
Median
}
///
/// Data of marking line.
/// 图表标线的数据。
///
[System.Serializable]
public class MarkLineData : SubComponent
{
[SerializeField] private string m_Name;
[SerializeField] private MarkLineType m_Type = MarkLineType.None;
[SerializeField] private int m_Dimension = 1;
[SerializeField] private float m_XPosition;
[SerializeField] private float m_YPosition;
[SerializeField] private double m_XValue;
[SerializeField] private double m_YValue;
[SerializeField] private int m_Group = 0;
[SerializeField] private bool m_ZeroPosition = false;
[SerializeField] private SerieSymbol m_StartSymbol = new SerieSymbol();
[SerializeField] private SerieSymbol m_EndSymbol = new SerieSymbol();
[SerializeField] private LineStyle m_LineStyle = new LineStyle();
[SerializeField] private SerieLabel m_Label = new SerieLabel();
//[SerializeField] private Emphasis m_Emphasis = new Emphasis();
public int index { get; set; }
public Vector3 runtimeStartPosition { get; internal set; }
public Vector3 runtimeEndPosition { get; internal set; }
public Vector3 runtimeCurrentEndPosition { get; internal set; }
public ChartLabel runtimeLabel { get; internal set; }
public double runtimeValue { get; internal set; }
///
/// Name of the marker, which will display as a label.
/// 标线名称,将会作为文字显示。label的formatter可通过{b}显示名称,通过{c}显示数值。
///
public string name
{
get { return m_Name; }
set { if (PropertyUtil.SetClass(ref m_Name, value)) SetVerticesDirty(); }
}
///
/// Special label types, are used to label maximum value, minimum value and so on.
/// 特殊的标线类型,用于标注最大值最小值等。
///
public MarkLineType type
{
get { return m_Type; }
set { if (PropertyUtil.SetStruct(ref m_Type, value)) SetVerticesDirty(); }
}
///
/// From which dimension of data to calculate the maximum and minimum value and so on.
/// 从哪个维度的数据计算最大最小值等。
///
public int dimension
{
get { return m_Dimension; }
set { if (PropertyUtil.SetStruct(ref m_Dimension, value)) SetVerticesDirty(); }
}
///
/// The x coordinate relative to the origin, in pixels.
/// 相对原点的 x 坐标,单位像素。当type为None时有效。
///
public float xPosition
{
get { return m_XPosition; }
set { if (PropertyUtil.SetStruct(ref m_XPosition, value)) SetVerticesDirty(); }
}
///
/// The y coordinate relative to the origin, in pixels.
/// 相对原点的 y 坐标,单位像素。当type为None时有效。
///
public float yPosition
{
get { return m_YPosition; }
set { if (PropertyUtil.SetStruct(ref m_YPosition, value)) SetVerticesDirty(); }
}
///
/// The value specified on the X-axis. A value specified when the X-axis is the category axis represents the index of the category axis data, otherwise a specific value.
/// X轴上的指定值。当X轴为类目轴时指定值表示类目轴数据的索引,否则为具体的值。当type为None时有效。
///
public double xValue
{
get { return m_XValue; }
set { if (PropertyUtil.SetStruct(ref m_XValue, value)) SetVerticesDirty(); }
}
///
/// That's the value on the Y-axis. The value specified when the Y axis is the category axis represents the index of the category axis data, otherwise the specific value.
/// Y轴上的指定值。当Y轴为类目轴时指定值表示类目轴数据的索引,否则为具体的值。当type为None时有效。
///
public double yValue
{
get { return m_YValue; }
set { if (PropertyUtil.SetStruct(ref m_YValue, value)) SetVerticesDirty(); }
}
///
/// Grouping. When the group is not 0, it means that this data is the starting point or end point of the marking line. Data consistent with the group form a marking line.
/// 分组。当group不为0时,表示这个data是标线的起点或终点,group一致的data组成一条标线。
///
public int group
{
get { return m_Group; }
set { if (PropertyUtil.SetStruct(ref m_Group, value)) SetVerticesDirty(); }
}
///
/// Is the origin of the coordinate system.
/// 是否为坐标系原点。
///
public bool zeroPosition
{
get { return m_ZeroPosition; }
set { if (PropertyUtil.SetStruct(ref m_ZeroPosition, value)) SetVerticesDirty(); }
}
///
/// The symbol of the start point of markline.
/// 起始点的图形标记。
///
public SerieSymbol startSymbol
{
get { return m_StartSymbol; }
set { if (PropertyUtil.SetClass(ref m_StartSymbol, value)) SetVerticesDirty(); }
}
///
/// The symbol of the end point of markline.
/// 结束点的图形标记。
///
public SerieSymbol endSymbol
{
get { return m_EndSymbol; }
set { if (PropertyUtil.SetClass(ref m_EndSymbol, value)) SetVerticesDirty(); }
}
///
/// The line style of markline.
/// 标线样式。
///
public LineStyle lineStyle
{
get { return m_LineStyle; }
set { if (PropertyUtil.SetClass(ref m_LineStyle, value)) SetVerticesDirty(); }
}
///
/// Text styles of label. You can set position to Start, Middle, and End to display text in different locations.
/// 文本样式。可设置position为Start、Middle和End在不同的位置显示文本。
///
public SerieLabel label
{
get { return m_Label; }
set { if (PropertyUtil.SetClass(ref m_Label, value)) SetVerticesDirty(); }
}
// public Emphasis emphasis
// {
// get { return m_Emphasis; }
// set { if (PropertyUtil.SetClass(ref m_Emphasis, value)) SetVerticesDirty(); }
// }
}
///
/// Use a line in the chart to illustrate.
/// 图表标线。
///
[System.Serializable]
public class MarkLine : SubComponent
{
[SerializeField] private bool m_Show;
[SerializeField] private SerieAnimation m_Animation = new SerieAnimation();
[SerializeField] private List m_Data = new List();
///
/// Whether to display the marking line.
/// 是否显示标线。
///
public bool show
{
get { return m_Show; }
set { if (PropertyUtil.SetStruct(ref m_Show, value)) SetVerticesDirty(); }
}
///
/// The animation of markline.
/// 标线的动画样式。
///
public SerieAnimation animation
{
get { return m_Animation; }
set { if (PropertyUtil.SetClass(ref m_Animation, value)) SetVerticesDirty(); }
}
///
/// A list of marked data. When the group of data item is 0, each data item represents a line;
/// When the group is not 0, two data items of the same group represent the starting point and
/// the ending point of the line respectively to form a line. In this case, the relevant style
/// parameters of the line are the parameters of the starting point.
/// 标线的数据列表。当数据项的group为0时,每个数据项表示一条标线;当group不为0时,相同group的两个数据项分别表
/// 示标线的起始点和终止点来组成一条标线,此时标线的相关样式参数取起始点的参数。
///
public List data
{
get { return m_Data; }
set { if (PropertyUtil.SetClass(ref m_Data, value)) SetVerticesDirty(); }
}
public static MarkLine defaultMarkLine
{
get
{
var markLine = new MarkLine
{
m_Show = false,
m_Data = new List()
};
var data = new MarkLineData();
data.name = "average";
data.type = MarkLineType.Average;
data.lineStyle.type = LineStyle.Type.Dashed;
data.lineStyle.color = Color.blue;
data.startSymbol.show = true;
data.startSymbol.type = SerieSymbolType.Circle;
data.endSymbol.show = true;
data.endSymbol.type = SerieSymbolType.Arrow;
data.label.show = true;
data.label.numericFormatter = "f1";
data.label.formatter = "{c}";
markLine.data.Add(data);
return markLine;
}
}
}
internal class MarkLineHandler : IComponentHandler
{
public CoordinateChart chart;
private GameObject m_MarkLineLabelRoot;
private bool m_RefreshLabel = false;
public MarkLineHandler(CoordinateChart chart)
{
this.chart = chart;
}
public void DrawBase(VertexHelper vh)
{
}
public void DrawTop(VertexHelper vh)
{
DrawMarkLine(vh);
}
public void Init()
{
m_MarkLineLabelRoot = ChartHelper.AddObject("markline", chart.transform, chart.chartMinAnchor,
chart.chartMaxAnchor, chart.chartPivot, chart.chartSizeDelta);
m_MarkLineLabelRoot.hideFlags = chart.chartHideFlags;
ChartHelper.HideAllObject(m_MarkLineLabelRoot);
foreach (var serie in chart.series.list) InitMarkLine(serie);
}
public void OnBeginDrag(PointerEventData eventData)
{
}
public void OnDrag(PointerEventData eventData)
{
}
public void OnEndDrag(PointerEventData eventData)
{
}
public void OnPointerDown(PointerEventData eventData)
{
}
public void OnScroll(PointerEventData eventData)
{
}
public void Update()
{
if (m_RefreshLabel)
{
m_RefreshLabel = false;
foreach (var serie in chart.series.list)
{
if (!serie.show || !serie.markLine.show) continue;
foreach (var data in serie.markLine.data)
{
if (data.runtimeLabel != null)
{
data.runtimeLabel.SetPosition(MarkLineHelper.GetLabelPosition(data));
data.runtimeLabel.SetText(MarkLineHelper.GetFormatterContent(serie, data));
}
}
}
}
}
private void InitMarkLine(Serie serie)
{
if (!serie.show || !serie.markLine.show) return;
ResetTempMarkLineGroupData(serie.markLine);
var serieColor = (Color)chart.theme.GetColor(chart.GetLegendRealShowNameIndex(serie.name));
if (m_TempGroupData.Count > 0)
{
foreach (var kv in m_TempGroupData)
{
if (kv.Value.Count >= 2)
{
var data = kv.Value[0];
InitMarkLineLabel(serie, data, serieColor);
}
}
}
foreach (var data in serie.markLine.data)
{
if (data.group != 0) continue;
InitMarkLineLabel(serie, data, serieColor);
}
}
private void InitMarkLineLabel(Serie serie, MarkLineData data, Color serieColor)
{
data.painter = chart.m_PainterTop;
data.refreshComponent = delegate ()
{
var label = data.label;
var textName = string.Format("markLine_{0}_{1}", serie.index, data.index);
var color = !ChartHelper.IsClearColor(label.textStyle.color) ? label.textStyle.color : chart.theme.axis.textColor;
var element = ChartHelper.AddSerieLabel(textName, m_MarkLineLabelRoot.transform, label.backgroundWidth,
label.backgroundHeight, color, label.textStyle, chart.theme);
var isAutoSize = label.backgroundWidth == 0 || label.backgroundHeight == 0;
var item = new ChartLabel();
item.SetLabel(element, isAutoSize, label.paddingLeftRight, label.paddingTopBottom);
item.SetIconActive(false);
item.SetActive(data.label.show);
item.SetPosition(MarkLineHelper.GetLabelPosition(data));
item.SetText(MarkLineHelper.GetFormatterContent(serie, data));
data.runtimeLabel = item;
};
data.refreshComponent();
}
private void DrawMarkLine(VertexHelper vh)
{
foreach (var serie in chart.series.list)
{
DrawMarkLine(vh, serie);
}
}
private Dictionary> m_TempGroupData = new Dictionary>();
private void DrawMarkLine(VertexHelper vh, Serie serie)
{
if (!serie.show || !serie.markLine.show) return;
if (serie.markLine.data.Count == 0) return;
var yAxis = chart.GetSerieYAxisOrDefault(serie);
var xAxis = chart.GetSerieXAxisOrDefault(serie);
var grid = chart.GetSerieGridOrDefault(serie);
var dataZoom = DataZoomHelper.GetAxisRelatedDataZoom(xAxis, chart.dataZooms);
var animation = serie.markLine.animation;
var showData = serie.GetDataList(dataZoom);
var sp = Vector3.zero;
var ep = Vector3.zero;
var colorIndex = chart.GetLegendRealShowNameIndex(serie.name);
var serieColor = SerieHelper.GetLineColor(serie, chart.theme, colorIndex, false);
animation.InitProgress(1, 0, 1f);
ResetTempMarkLineGroupData(serie.markLine);
if (m_TempGroupData.Count > 0)
{
foreach (var kv in m_TempGroupData)
{
if (kv.Value.Count >= 2)
{
sp = GetSinglePos(xAxis, yAxis, grid, serie, dataZoom, kv.Value[0], showData.Count);
ep = GetSinglePos(xAxis, yAxis, grid, serie, dataZoom, kv.Value[1], showData.Count);
kv.Value[0].runtimeStartPosition = sp;
kv.Value[1].runtimeEndPosition = ep;
DrawMakLineData(vh, kv.Value[0], animation, serie, grid, serieColor, sp, ep);
}
}
}
foreach (var data in serie.markLine.data)
{
if (data.group != 0) continue;
switch (data.type)
{
case MarkLineType.Min:
data.runtimeValue = SerieHelper.GetMinData(serie, data.dimension, dataZoom);
GetStartEndPos(xAxis, yAxis, grid, data.runtimeValue, ref sp, ref ep);
break;
case MarkLineType.Max:
data.runtimeValue = SerieHelper.GetMaxData(serie, data.dimension, dataZoom);
GetStartEndPos(xAxis, yAxis, grid, data.runtimeValue, ref sp, ref ep);
break;
case MarkLineType.Average:
data.runtimeValue = SerieHelper.GetAverageData(serie, data.dimension, dataZoom);
GetStartEndPos(xAxis, yAxis, grid, data.runtimeValue, ref sp, ref ep);
break;
case MarkLineType.Median:
data.runtimeValue = SerieHelper.GetMedianData(serie, data.dimension, dataZoom);
GetStartEndPos(xAxis, yAxis, grid, data.runtimeValue, ref sp, ref ep);
break;
case MarkLineType.None:
if (data.xPosition != 0)
{
data.runtimeValue = data.xPosition;
var pX = grid.runtimeX + data.xPosition;
sp = new Vector3(pX, grid.runtimeY);
ep = new Vector3(pX, grid.runtimeY + grid.runtimeHeight);
}
else if (data.yPosition != 0)
{
data.runtimeValue = data.yPosition;
var pY = grid.runtimeY + data.yPosition;
sp = new Vector3(grid.runtimeX, pY);
ep = new Vector3(grid.runtimeX + grid.runtimeWidth, pY);
}
else if (data.yValue != 0)
{
data.runtimeValue = data.yValue;
if (yAxis.IsCategory())
{
var pY = AxisHelper.GetAxisPosition(grid, yAxis, data.yValue, showData.Count, dataZoom);
sp = new Vector3(grid.runtimeX, pY);
ep = new Vector3(grid.runtimeX + grid.runtimeWidth, pY);
}
else
{
GetStartEndPos(xAxis, yAxis, grid, data.yValue, ref sp, ref ep);
}
}
else
{
data.runtimeValue = data.xValue;
if (xAxis.IsCategory())
{
var pX = AxisHelper.GetAxisPosition(grid, xAxis, data.xValue, showData.Count, dataZoom);
sp = new Vector3(pX, grid.runtimeY);
ep = new Vector3(pX, grid.runtimeY + grid.runtimeHeight);
}
else
{
GetStartEndPos(xAxis, yAxis, grid, data.xValue, ref sp, ref ep);
}
}
break;
default:
break;
}
data.runtimeStartPosition = sp;
data.runtimeEndPosition = ep;
DrawMakLineData(vh, data, animation, serie, grid, serieColor, sp, ep);
}
if (!animation.IsFinish())
{
animation.CheckProgress(1f);
chart.RefreshTopPainter();
}
}
private void ResetTempMarkLineGroupData(MarkLine markLine)
{
m_TempGroupData.Clear();
for (int i = 0; i < markLine.data.Count; i++)
{
var data = markLine.data[i];
data.index = i;
if (data.group == 0) continue;
if (!m_TempGroupData.ContainsKey(data.group))
{
m_TempGroupData[data.group] = new List();
}
m_TempGroupData[data.group].Add(data);
}
}
private void DrawMakLineData(VertexHelper vh, MarkLineData data, SerieAnimation animation, Serie serie,
Grid grid, Color32 serieColor, Vector3 sp, Vector3 ep)
{
if (!animation.IsFinish())
ep = Vector3.Lerp(sp, ep, animation.GetCurrDetail());
data.runtimeCurrentEndPosition = ep;
if (sp != Vector3.zero || ep != Vector3.zero)
{
m_RefreshLabel = true;
chart.ClampInChart(ref sp);
chart.ClampInChart(ref ep);
var theme = chart.theme.axis;
var lineColor = ChartHelper.IsClearColor(data.lineStyle.color) ? serieColor : data.lineStyle.color;
var lineWidth = data.lineStyle.width == 0 ? theme.lineWidth : data.lineStyle.width;
ChartDrawer.DrawLineStyle(vh, data.lineStyle, sp, ep, lineColor, lineWidth, LineStyle.Type.Dashed);
if (data.startSymbol != null && data.startSymbol.show)
{
DrawMarkLineSymbol(vh, data.startSymbol, serie, grid, chart.theme, sp, sp, lineColor);
}
if (data.endSymbol != null && data.endSymbol.show)
{
DrawMarkLineSymbol(vh, data.endSymbol, serie, grid, chart.theme, ep, sp, lineColor);
}
}
}
private void DrawMarkLineSymbol(VertexHelper vh, SerieSymbol symbol, Serie serie, Grid grid, ChartTheme theme,
Vector3 pos, Vector3 startPos, Color32 lineColor)
{
var symbolSize = symbol.GetSize(null, theme.serie.lineSymbolSize);
var tickness = SerieHelper.GetSymbolBorder(serie, null, theme, false);
var cornerRadius = SerieHelper.GetSymbolCornerRadius(serie, null, false);
chart.Internal_CheckClipAndDrawSymbol(vh, symbol.type, symbolSize, tickness, pos, lineColor, lineColor,
symbol.gap, true, cornerRadius, grid, startPos);
}
private void GetStartEndPos(Axis xAxis, Axis yAxis, Grid grid, double value, ref Vector3 sp, ref Vector3 ep)
{
if (xAxis.IsCategory())
{
var pY = AxisHelper.GetAxisPosition(grid, yAxis, value);
sp = new Vector3(grid.runtimeX, pY);
ep = new Vector3(grid.runtimeX + grid.runtimeWidth, pY);
}
else
{
var pX = AxisHelper.GetAxisPosition(grid, xAxis, value);
sp = new Vector3(pX, grid.runtimeY);
ep = new Vector3(pX, grid.runtimeY + grid.runtimeHeight);
}
}
private float GetAxisPosition(Grid grid, Axis axis, DataZoom dataZoom, int dataCount, double value)
{
return AxisHelper.GetAxisPosition(grid, axis, value, dataCount, dataZoom);
}
private Vector3 GetSinglePos(Axis xAxis, Axis yAxis, Grid grid, Serie serie, DataZoom dataZoom, MarkLineData data,
int serieDataCount)
{
switch (data.type)
{
case MarkLineType.Min:
var serieData = SerieHelper.GetMinSerieData(serie, data.dimension, dataZoom);
data.runtimeValue = serieData.GetData(data.dimension);
var pX = GetAxisPosition(grid, xAxis, dataZoom, serieDataCount, serieData.index);
var pY = GetAxisPosition(grid, yAxis, dataZoom, serieDataCount, data.runtimeValue);
return new Vector3(pX, pY);
case MarkLineType.Max:
serieData = SerieHelper.GetMaxSerieData(serie, data.dimension, dataZoom);
data.runtimeValue = serieData.GetData(data.dimension);
pX = GetAxisPosition(grid, xAxis, dataZoom, serieDataCount, serieData.index);
pY = GetAxisPosition(grid, yAxis, dataZoom, serieDataCount, data.runtimeValue);
return new Vector3(pX, pY);
case MarkLineType.None:
if (data.zeroPosition)
{
data.runtimeValue = 0;
return grid.runtimePosition;
}
else
{
pX = data.xPosition != 0 ? grid.runtimeX + data.xPosition :
GetAxisPosition(grid, xAxis, dataZoom, serieDataCount, data.xValue);
pY = data.yPosition != 0 ? grid.runtimeY + data.yPosition :
GetAxisPosition(grid, yAxis, dataZoom, serieDataCount, data.yValue);
data.runtimeValue = data.yValue;
return new Vector3(pX, pY);
}
default:
return grid.runtimePosition;
}
}
}
internal static class MarkLineHelper
{
public static string GetFormatterContent(Serie serie, MarkLineData data)
{
var serieLabel = data.label;
var numericFormatter = serieLabel.numericFormatter;
if (serieLabel.formatterFunction != null)
{
return serieLabel.formatterFunction(data.index, data.runtimeValue);
}
if (string.IsNullOrEmpty(serieLabel.formatter))
return ChartCached.NumberToStr(data.runtimeValue, numericFormatter);
else
{
var content = serieLabel.formatter;
FormatterHelper.ReplaceSerieLabelContent(ref content, numericFormatter, data.runtimeValue,
0, serie.name, data.name, Color.clear);
return content;
}
}
public static Vector3 GetLabelPosition(MarkLineData data)
{
if (!data.label.show) return Vector3.zero;
var dir = (data.runtimeEndPosition - data.runtimeStartPosition).normalized;
var horizontal = Mathf.Abs(Vector3.Dot(dir, Vector3.right)) == 1;
var labelWidth = data.runtimeLabel == null ? 50 : data.runtimeLabel.GetLabelWidth();
var labelHeight = data.runtimeLabel == null ? 20 : data.runtimeLabel.GetLabelHeight();
switch (data.label.position)
{
case SerieLabel.Position.Start:
if (horizontal) return data.runtimeStartPosition + data.label.offset + labelWidth / 2 * Vector3.left;
else return data.runtimeStartPosition + data.label.offset + labelHeight / 2 * Vector3.down;
case SerieLabel.Position.Middle:
var center = (data.runtimeStartPosition + data.runtimeCurrentEndPosition) / 2;
if (horizontal) return center + data.label.offset + labelHeight / 2 * Vector3.up;
else return center + data.label.offset + labelWidth / 2 * Vector3.right;
default:
if (horizontal) return data.runtimeCurrentEndPosition + data.label.offset + labelWidth / 2 * Vector3.right;
else return data.runtimeCurrentEndPosition + data.label.offset + labelHeight / 2 * Vector3.up;
}
}
}
}