Compare commits

..

20 Commits

Author SHA1 Message Date
monitor1394
12be0ef93b 优化图表性能 2026-05-28 23:24:42 +08:00
monitor1394
2688d93f17 优化DataZoomMarquee框选功能 2026-05-28 09:14:53 +08:00
monitor1394
b040f27b2c 增加DataZoomfilterAxisRange设置坐标轴的范围计算是否受DataZoom的影响 2026-05-26 08:46:49 +08:00
monitor1394
584ef9a834 优化DataZoomMarquee框选功能 2026-05-25 22:19:22 +08:00
monitor1394
b14a574ad6 优化DataZoomMarquee框选功能 2026-05-24 21:15:29 +08:00
monitor1394
9d982019dd 修复DataZoom内绘制的折线图可能会超出范围的问题 2026-05-23 22:51:15 +08:00
monitor1394
be07afeb69 修复Axisinverse没能正确反转的问题 2026-05-23 22:16:28 +08:00
monitor1394
4ad2b3268f 增加LabelStyleshowCondition,showFilter,showThreshold可控制label显示和隐藏 2026-05-23 17:07:00 +08:00
monitor1394
f7ccec87d9 增加LabelStyleminGap可避免label过于密集 2026-05-22 21:52:22 +08:00
monitor1394
9bca52fbfb 修复DataZoom点击时指示区域不准的问题 2026-05-20 23:09:51 +08:00
monitor1394
2ee94acd30 优化图表性能 2026-05-20 22:13:04 +08:00
monitor1394
10d67cff41 修复DataZoom点击时指示区域不准的问题 2026-05-17 22:52:05 +08:00
monitor1394
4120b61f2a 增加LegendWidthHeight可设置固定宽高 2026-05-17 18:47:47 +08:00
monitor1394
0174453b05 修复SerieEndLabelY轴是MinMax类型时显示的数值不对的问题 2026-05-17 18:37:47 +08:00
monitor1394
68cb18305d 修复Candlestick按昨收判断涨跌颜色,一字涨停/跌停显示不对的问题 (#362) 2026-05-17 18:28:03 +08:00
monitor1394
39514f82b3 修复Candlestick的涨停颜色不对的问题 (#362) 2026-05-16 22:33:40 +08:00
monitor1394
5f66391428 修复LegendBackground区域在Horizonal模式下不对的问题 2026-03-29 21:48:25 +08:00
monitor1394
99e56d238a 增加ChartJson导出导入 2026-03-25 22:46:26 +08:00
monitor1394
dcac0f9655 默认设置圆角 2026-03-10 09:27:13 +08:00
monitor1394
2bb56fcd28 修复SaveAsImage保存的图片在PC和手机上不一致的问题 2026-03-03 22:23:07 +08:00
47 changed files with 4103 additions and 278 deletions

View File

@@ -81,6 +81,23 @@ slug: /changelog
## master
* (2026.05.25) 增加`DataZoom``filterAxisRange`设置坐标轴的范围计算是否受`DataZoom`的影响
* (2026.05.23) 优化`DataZoom``Marquee`框选功能
* (2026.05.23) 修复`DataZoom`内绘制的折线图可能会超出范围的问题
* (2026.05.23) 修复`Axis``inverse`没能正确反转的问题
* (2026.05.23) 增加`LabelStyle``showCondition`,`showFilter`,`showThreshold`可控制`label`显示和隐藏
* (2026.05.22) 增加`LabelStyle``minGap`可避免`label`过于密集
* (2026.05.17) 修复`DataZoom`点击时指示区域不准的问题
* (2026.05.17) 增加`Legend``Width``Height`可设置固定宽高
* (2026.05.17) 修复`Serie``EndLabel``Y`轴是`MinMax`类型时显示的数值不对的问题
* (2026.05.16) 修复`Candlestick`按昨收判断涨跌颜色,一字涨停/跌停显示不对的问题 (#362)
* (2026.03.29) 修复`Legend``Background`区域在`Horizonal`模式下不对的问题
* (2026.03.25) 增加`Chart``Json`导出导入
* (2026.03.10) 增加`Sankey`的线条tooltip触发显示
* (2026.03.10) 增加`UITable`的排序功能支持
* (2026.03.07) 修复`UITable`在尺寸变化时背景没有实时刷新的问题
* (2026.03.06) 优化`UITable`支持万级以上数据量不卡顿
## v3.15.0
版本要点:

View File

@@ -19,6 +19,8 @@ namespace XCharts.Editor
public static readonly GUIContent btnSaveAsImage = new GUIContent("Save As Image", "");
public static readonly GUIContent btnCheckWarning = new GUIContent("Check Warning", "");
public static readonly GUIContent btnHideWarning = new GUIContent("Hide Warning", "");
public static readonly GUIContent btnImportJsonData = new GUIContent("Import Json", "");
public static readonly GUIContent btnExportJsonData = new GUIContent("Export Json", "");
}
protected BaseChart m_Chart;
protected SerializedProperty m_Script;
@@ -36,6 +38,7 @@ namespace XCharts.Editor
private bool m_BaseFoldout;
private bool m_CheckWarning = false;
private bool m_ExportPending = false;
private int m_LastComponentCount = 0;
private int m_LastSerieCount = 0;
private string m_VersionString = "";
@@ -300,6 +303,14 @@ namespace XCharts.Editor
m_CheckWarning = false;
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button(Styles.btnImportJsonData))
{
ChartJsonImportWindow.ShowWindow(m_Chart);
}
if (GUILayout.Button(Styles.btnExportJsonData))
{
RequestExportJsonData();
}
sb.Length = 0;
sb.AppendFormat("v{0}", XChartsMgr.fullVersion);
if (!string.IsNullOrEmpty(m_Chart.warningInfo))
@@ -321,8 +332,47 @@ namespace XCharts.Editor
m_CheckWarning = true;
m_Chart.CheckWarning();
}
if (GUILayout.Button(Styles.btnImportJsonData))
{
ChartJsonImportWindow.ShowWindow(m_Chart);
}
if (GUILayout.Button(Styles.btnExportJsonData))
{
RequestExportJsonData();
}
}
}
private void RequestExportJsonData()
{
if (m_ExportPending) return;
m_ExportPending = true;
var chart = m_Chart;
EditorApplication.delayCall += delegate()
{
m_ExportPending = false;
ExportJsonData(chart);
};
GUIUtility.ExitGUI();
}
private static void ExportJsonData(BaseChart chart)
{
if (chart == null) return;
var json = chart.ExportToJson(true);
var defaultName = chart.gameObject.name + ".json";
var path = EditorUtility.SaveFilePanel("Save Chart JSON", "", defaultName, "json");
if (string.IsNullOrEmpty(path)) return;
try
{
System.IO.File.WriteAllText(path, json);
Debug.Log("[XCharts] JSON exported to: " + path);
}
catch (Exception ex)
{
Debug.LogError("[XCharts] Failed to save JSON: " + ex.Message);
}
}
}
}

View File

@@ -113,6 +113,15 @@ namespace XCharts.Editor
}
}
protected void PropertyFlagsField(SerializedProperty prop, string relativePropName, System.Type enumType)
{
if (IngorePropertys.Contains(relativePropName)) return;
if (!ChartEditorHelper.PropertyFlagsField(ref m_DrawRect, m_Heights, m_KeyName, prop, relativePropName, enumType))
{
Debug.LogError("PropertyFlagsField ERROR:" + prop.displayName + ", " + relativePropName);
}
}
protected void PropertyFieldLimitMin(SerializedProperty prop, string relativePropName, float minValue)
{
if (IngorePropertys.Contains(relativePropName)) return;

View File

@@ -26,6 +26,10 @@ namespace XCharts.Editor
PropertyField(prop, "m_Height");
PropertyField(prop, "m_FixedX");
PropertyField(prop, "m_FixedY");
PropertyField(prop, "m_ShowCondition");
PropertyField(prop, "m_ShowFilter");
PropertyField(prop, "m_ShowThreshold");
PropertyField(prop, "m_ShowMinGap");
PropertyField(prop, "m_Icon");
PropertyField(prop, "m_Background");
PropertyField(prop, "m_TextStyle");

View File

@@ -28,10 +28,11 @@ namespace XCharts.Editor
PropertyField("m_ScrollSensitivity");
PropertyField("m_RangeMode");
PropertyField(m_Start);
PropertyField(m_End);
PropertyField("m_StartLock");
PropertyField(m_End);
PropertyField("m_EndLock");
PropertyField(m_MinZoomRatio);
PropertyField("m_FilterAxisRange");
if (m_Start.floatValue < 0) m_Start.floatValue = 0;
if (m_End.floatValue > 100) m_End.floatValue = 100;
if (m_MinZoomRatio.floatValue < 0) m_MinZoomRatio.floatValue = 0;

View File

@@ -10,6 +10,8 @@ namespace XCharts.Editor
{
++EditorGUI.indentLevel;
PropertyField("m_IconType");
PropertyField("m_Width");
PropertyField("m_Height");
PropertyField("m_ItemWidth");
PropertyField("m_ItemHeight");
PropertyField("m_ItemGap");

View File

@@ -13,10 +13,13 @@ namespace XCharts.Editor
public static readonly GUIContent btnAddComponent = new GUIContent("Add Main Component", "");
public static readonly GUIContent btnRebuildChartObject = new GUIContent("Rebuild Object", "");
public static readonly GUIContent btnSaveAsImage = new GUIContent("Save As Image", "");
public static readonly GUIContent btnImportJsonData = new GUIContent("Import Json", "");
public static readonly GUIContent btnExportJsonData = new GUIContent("Export Json", "");
public static readonly GUIContent btnCheckWarning = new GUIContent("Check Warning", "");
public static readonly GUIContent btnHideWarning = new GUIContent("Hide Warning", "");
}
public UIComponent m_UIComponent;
private bool m_ExportPending;
public static T AddUIComponent<T>(string chartName) where T : UIComponent
{
@@ -56,6 +59,14 @@ namespace XCharts.Editor
{
m_UIComponent.SaveAsImage("png", "", 4f);
}
if (GUILayout.Button(Styles.btnImportJsonData))
{
UIComponentJsonImportWindow.ShowWindow(m_UIComponent);
}
if (GUILayout.Button(Styles.btnExportJsonData))
{
RequestExportJsonData();
}
OnDebugEndInspectorGUI();
}
@@ -89,6 +100,37 @@ namespace XCharts.Editor
EditorGUILayout.PropertyField(property, title);
}
private void RequestExportJsonData()
{
if (m_ExportPending) return;
m_ExportPending = true;
var target = m_UIComponent;
EditorApplication.delayCall += delegate ()
{
m_ExportPending = false;
ExportJsonData(target);
};
GUIUtility.ExitGUI();
}
private static void ExportJsonData(UIComponent target)
{
if (target == null) return;
var json = target.ExportToJson(true);
var defaultName = target.gameObject.name + ".json";
var path = EditorUtility.SaveFilePanel("Save UI Component JSON", "", defaultName, "json");
if (string.IsNullOrEmpty(path)) return;
try
{
System.IO.File.WriteAllText(path, json);
Debug.Log("[XCharts] UI JSON exported to: " + path);
}
catch (System.Exception ex)
{
Debug.LogError("[XCharts] Failed to save UI JSON: " + ex.Message);
}
}
protected void PropertyListField(string relativePropName, bool showOrder = true, params HeaderMenuInfo[] menus)
{
var m_DrawRect = GUILayoutUtility.GetRect(1f, 17f);

View File

@@ -507,6 +507,28 @@ namespace XCharts.Editor
{
return PropertyField(ref drawRect, heights, key, parentProp.FindPropertyRelative(relativeName));
}
public static bool PropertyFlagsField(ref Rect drawRect, Dictionary<string, float> heights, string key,
SerializedProperty parentProp, string relativeName, System.Type enumType)
{
return PropertyFlagsField(ref drawRect, heights, key, parentProp.FindPropertyRelative(relativeName), enumType);
}
public static bool PropertyFlagsField(ref Rect drawRect, Dictionary<string, float> heights, string key,
SerializedProperty prop, System.Type enumType)
{
if (prop == null) return false;
var label = GetContent(prop.displayName);
var enumValue = (System.Enum)System.Enum.ToObject(enumType, prop.intValue);
EditorGUI.BeginChangeCheck();
var newValue = EditorGUI.EnumFlagsField(drawRect, label, enumValue);
if (EditorGUI.EndChangeCheck())
prop.intValue = (int)(object)newValue;
var hig = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
drawRect.y += hig;
heights[key] += hig;
return true;
}
public static bool PropertyFieldWithMinValue(ref Rect drawRect, Dictionary<string, float> heights, string key,
SerializedProperty parentProp, string relativeName, float minValue)
{

View File

@@ -0,0 +1,201 @@
using UnityEditor;
using UnityEngine;
using XCharts.Runtime;
namespace XCharts.Editor
{
public class ChartJsonImportWindow : EditorWindow
{
private const int TEXTAREA_SAFE_CHAR_LIMIT = 8000;
private const int LARGE_JSON_PREVIEW_CHAR_LIMIT = 4000;
private static BaseChart s_TargetChart;
private string m_JsonInput = "";
private Vector2 m_ScrollPos;
private bool m_ShowPreview = false;
private string m_PreviewText = "";
private bool m_OpenFilePending = false;
private bool m_PreviewPending = false;
private bool m_ImportPending = false;
public static void ShowWindow(BaseChart targetChart)
{
s_TargetChart = targetChart;
var window = GetWindow<ChartJsonImportWindow>("Import Chart JSON");
window.minSize = new Vector2(600, 400);
window.Show();
}
private void OnGUI()
{
if (s_TargetChart == null)
{
EditorGUILayout.HelpBox("Target chart is null. Please select a chart first.", MessageType.Error);
if (GUILayout.Button("Close")) Close();
return;
}
if (m_JsonInput == null) m_JsonInput = "";
EditorGUILayout.LabelField("Target Chart: " + s_TargetChart.gameObject.name, EditorStyles.boldLabel);
GUILayout.Space(10);
EditorGUILayout.LabelField("Paste JSON Data:", EditorStyles.boldLabel);
using (var scroll = new EditorGUILayout.ScrollViewScope(m_ScrollPos, GUILayout.Height(250)))
{
m_ScrollPos = scroll.scrollPosition;
if (m_JsonInput.Length <= TEXTAREA_SAFE_CHAR_LIMIT)
{
m_JsonInput = EditorGUILayout.TextArea(m_JsonInput, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
}
else
{
var preview = m_JsonInput.Substring(0, LARGE_JSON_PREVIEW_CHAR_LIMIT);
EditorGUILayout.HelpBox("JSON content is very large. To avoid editor text rendering limits, only a preview is shown below. Import uses full content.", MessageType.Info);
EditorGUILayout.TextArea(preview, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
}
}
EditorGUILayout.HelpBox("Paste JSON directly, or click Open Json File.", MessageType.Info);
GUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Open Json File", GUILayout.Width(120)))
{
RequestOpenJsonFile();
}
if (GUILayout.Button("Preview", GUILayout.Width(100)))
{
RequestPreviewJson();
}
}
if (m_ShowPreview && !string.IsNullOrEmpty(m_PreviewText))
{
EditorGUILayout.TextArea(m_PreviewText, GUILayout.Height(150));
}
GUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Import", GUILayout.Height(40), GUILayout.Width(150)))
{
RequestImportJson();
}
if (GUILayout.Button("Cancel", GUILayout.Height(40), GUILayout.Width(150))) Close();
}
}
private void RequestOpenJsonFile()
{
if (m_OpenFilePending) return;
m_OpenFilePending = true;
EditorApplication.delayCall += delegate ()
{
m_OpenFilePending = false;
if (this == null) return;
OpenJsonFile();
Repaint();
};
GUIUtility.ExitGUI();
}
private void RequestPreviewJson()
{
if (m_PreviewPending) return;
m_PreviewPending = true;
EditorApplication.delayCall += delegate ()
{
m_PreviewPending = false;
if (this == null) return;
PreviewJson();
Repaint();
};
GUIUtility.ExitGUI();
}
private void RequestImportJson()
{
if (m_ImportPending) return;
m_ImportPending = true;
EditorApplication.delayCall += delegate ()
{
m_ImportPending = false;
if (this == null) return;
ImportJson();
};
GUIUtility.ExitGUI();
}
private void PreviewJson()
{
if (string.IsNullOrEmpty(m_JsonInput))
{
EditorUtility.DisplayDialog("Error", "JSON input is empty.", "OK");
return;
}
try
{
var json = JsonUtility.FromJson<XCharts.Runtime.ChartJson>(m_JsonInput);
if (json == null)
{
m_PreviewText = "Invalid JSON or unsupported schema.";
}
else
{
var componentCount = json.components != null ? json.components.Count : 0;
var seriesCount = json.series != null ? json.series.Count : 0;
m_PreviewText = "Chart Type: " + json.chartType + "\nComponents: " + componentCount + "\nSeries: " + seriesCount + "\n(Full validation on import)";
}
m_ShowPreview = true;
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Preview Error", "Invalid JSON: " + ex.Message, "OK");
}
}
private void ImportJson()
{
if (string.IsNullOrEmpty(m_JsonInput))
{
EditorUtility.DisplayDialog("Error", "JSON input is empty. Please paste JSON data.", "OK");
return;
}
try
{
Undo.RecordObject(s_TargetChart, "Import Chart JSON");
s_TargetChart.ImportFromJson(m_JsonInput);
s_TargetChart.RebuildChartObject();
s_TargetChart.RefreshAllComponent();
s_TargetChart.RefreshChart();
EditorUtility.SetDirty(s_TargetChart);
UnityEditor.SceneView.RepaintAll();
var chart = s_TargetChart;
EditorApplication.delayCall += delegate ()
{
if (chart == null) return;
chart.RefreshAllComponent();
chart.RefreshChart();
UnityEditor.SceneView.RepaintAll();
};
EditorUtility.DisplayDialog("Success", "Chart '" + s_TargetChart.gameObject.name + "' imported successfully!", "OK");
Close();
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Import Error", "Failed to import JSON:\n" + ex.Message + "\n\n" + ex.StackTrace, "OK");
}
}
private void OpenJsonFile()
{
var path = EditorUtility.OpenFilePanel("Open Chart JSON", "", "json");
if (string.IsNullOrEmpty(path)) return;
try
{
m_JsonInput = System.IO.File.ReadAllText(path);
m_ShowPreview = false;
m_PreviewText = "";
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Open File Error", "Failed to read JSON file:\n" + ex.Message, "OK");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 21f2eafb07ab34d4abf575784acc56a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,193 @@
using UnityEditor;
using UnityEngine;
using XCharts.Runtime;
namespace XCharts.Editor
{
public class UIComponentJsonImportWindow : EditorWindow
{
private const int TEXTAREA_SAFE_CHAR_LIMIT = 8000;
private const int LARGE_JSON_PREVIEW_CHAR_LIMIT = 4000;
private static UIComponent s_TargetComponent;
private string m_JsonInput = "";
private Vector2 m_ScrollPos;
private bool m_ShowPreview = false;
private string m_PreviewText = "";
private bool m_OpenFilePending = false;
private bool m_PreviewPending = false;
private bool m_ImportPending = false;
public static void ShowWindow(UIComponent target)
{
s_TargetComponent = target;
var window = GetWindow<UIComponentJsonImportWindow>("Import UI JSON");
window.minSize = new Vector2(600, 400);
window.Show();
}
private void OnGUI()
{
if (s_TargetComponent == null)
{
EditorGUILayout.HelpBox("Target UI component is null. Please select a component first.", MessageType.Error);
if (GUILayout.Button("Close")) Close();
return;
}
if (m_JsonInput == null) m_JsonInput = "";
EditorGUILayout.LabelField("Target: " + s_TargetComponent.gameObject.name + " (" + s_TargetComponent.GetType().Name + ")", EditorStyles.boldLabel);
GUILayout.Space(10);
EditorGUILayout.LabelField("Paste JSON Data:", EditorStyles.boldLabel);
using (var scroll = new EditorGUILayout.ScrollViewScope(m_ScrollPos, GUILayout.Height(250)))
{
m_ScrollPos = scroll.scrollPosition;
if (m_JsonInput.Length <= TEXTAREA_SAFE_CHAR_LIMIT)
{
m_JsonInput = EditorGUILayout.TextArea(m_JsonInput, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
}
else
{
var preview = m_JsonInput.Substring(0, LARGE_JSON_PREVIEW_CHAR_LIMIT);
EditorGUILayout.HelpBox("JSON content is very large. Only a preview is shown below. Import uses full content.", MessageType.Info);
EditorGUILayout.TextArea(preview, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
}
}
EditorGUILayout.HelpBox("Paste JSON directly, or click Open Json File.", MessageType.Info);
GUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Open Json File", GUILayout.Width(120))) RequestOpenJsonFile();
if (GUILayout.Button("Preview", GUILayout.Width(100))) RequestPreviewJson();
}
if (m_ShowPreview && !string.IsNullOrEmpty(m_PreviewText))
{
EditorGUILayout.TextArea(m_PreviewText, GUILayout.Height(120));
}
GUILayout.Space(10);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Import", GUILayout.Height(40), GUILayout.Width(150))) RequestImportJson();
if (GUILayout.Button("Cancel", GUILayout.Height(40), GUILayout.Width(150))) Close();
}
}
private void RequestOpenJsonFile()
{
if (m_OpenFilePending) return;
m_OpenFilePending = true;
EditorApplication.delayCall += delegate ()
{
m_OpenFilePending = false;
if (this == null) return;
OpenJsonFile();
Repaint();
};
GUIUtility.ExitGUI();
}
private void RequestPreviewJson()
{
if (m_PreviewPending) return;
m_PreviewPending = true;
EditorApplication.delayCall += delegate ()
{
m_PreviewPending = false;
if (this == null) return;
PreviewJson();
Repaint();
};
GUIUtility.ExitGUI();
}
private void RequestImportJson()
{
if (m_ImportPending) return;
m_ImportPending = true;
EditorApplication.delayCall += delegate ()
{
m_ImportPending = false;
if (this == null) return;
ImportJson();
};
GUIUtility.ExitGUI();
}
private void PreviewJson()
{
if (string.IsNullOrEmpty(m_JsonInput))
{
EditorUtility.DisplayDialog("Error", "JSON input is empty.", "OK");
return;
}
try
{
var json = JsonUtility.FromJson<UIComponentJson>(m_JsonInput);
if (json == null)
m_PreviewText = "Invalid JSON or unsupported schema.";
else
m_PreviewText = "Component Type: " + json.componentType + "\nSchema: " + json.schemaVersion + "\nVersion: " + json.componentVersion;
m_ShowPreview = true;
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Preview Error", "Invalid JSON: " + ex.Message, "OK");
}
}
private void ImportJson()
{
if (string.IsNullOrEmpty(m_JsonInput))
{
EditorUtility.DisplayDialog("Error", "JSON input is empty. Please paste JSON data.", "OK");
return;
}
try
{
Undo.RecordObject(s_TargetComponent, "Import UI Component JSON");
s_TargetComponent.ImportFromJson(m_JsonInput);
s_TargetComponent.RebuildChartObject();
s_TargetComponent.RefreshAllComponent();
s_TargetComponent.RefreshGraph();
EditorUtility.SetDirty(s_TargetComponent);
SceneView.RepaintAll();
var target = s_TargetComponent;
EditorApplication.delayCall += delegate ()
{
if (target == null) return;
target.RefreshAllComponent();
target.RefreshGraph();
SceneView.RepaintAll();
};
EditorUtility.DisplayDialog("Success", "UI component imported successfully!", "OK");
Close();
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Import Error", "Failed to import JSON:\n" + ex.Message + "\n\n" + ex.StackTrace, "OK");
}
}
private void OpenJsonFile()
{
var path = EditorUtility.OpenFilePanel("Open UI Component JSON", "", "json");
if (string.IsNullOrEmpty(path)) return;
try
{
m_JsonInput = System.IO.File.ReadAllText(path);
m_ShowPreview = false;
m_PreviewText = "";
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Open File Error", "Failed to read JSON file:\n" + ex.Message, "OK");
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 68157a6f7d4e94ccc8ccbb4913d187f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -103,22 +103,35 @@ namespace XCharts.Runtime
internal void UpdateFilterData(List<string> data, DataZoom dataZoom)
{
int start = 0, end = 0;
var range = Mathf.RoundToInt(data.Count * (dataZoom.end - dataZoom.start) / 100);
if (range <= 0)
range = 1;
// Use (N-1) intervals to match shadow drawing (scaleWid = width/(N-1)).
// CeilToInt for start, FloorToInt for end, so filter aligns exactly with filler.
int n = data.Count - 1;
int startIndex, endIndex;
if (n > 0)
{
if (dataZoom.context.invert)
{
end = Mathf.RoundToInt(data.Count * dataZoom.end / 100);
start = end - range;
if (start < 0) start = 0;
startIndex = Mathf.CeilToInt((float)n * (100 - dataZoom.end) / 100);
endIndex = Mathf.FloorToInt((float)n * (100 - dataZoom.start) / 100);
}
else
{
start = Mathf.RoundToInt(data.Count * dataZoom.start / 100);
startIndex = Mathf.CeilToInt((float)n * dataZoom.start / 100);
endIndex = Mathf.FloorToInt((float)n * dataZoom.end / 100);
}
}
else
{
startIndex = 0;
endIndex = 0;
}
var range = endIndex - startIndex + 1;
if (range <= 0)
range = 1;
start = startIndex;
if (start < 0) start = 0;
end = start + range;
if (end > data.Count) end = data.Count;
}
var minZoomRatio = (int)(data.Count * dataZoom.minZoomRatio);
if (start != filterStart ||

View File

@@ -122,10 +122,9 @@ namespace XCharts
{
if (axis is YAxis)
{
var yRate = axis.context.minMaxRange / grid.context.height;
var yValue = yRate * (chart.pointerPos.y - grid.context.y - axis.context.offset);
if (axis.context.minValue > 0)
yValue += axis.context.minValue;
var yValue = axis.context.minValue + (chart.pointerPos.y - grid.context.y) / grid.context.height * axis.context.minMaxRange;
if (axis.inverse)
yValue = -yValue;
var labelX = axis.GetLabelObjectPosition(0).x;
axis.context.pointerValue = yValue;
@@ -150,10 +149,9 @@ namespace XCharts
}
else
{
var xRate = axis.context.minMaxRange / grid.context.width;
xValue = xRate * (chart.pointerPos.x - grid.context.x - axis.context.offset);
if (axis.context.minValue > 0)
xValue += axis.context.minValue;
xValue = axis.context.minValue + (chart.pointerPos.x - grid.context.x) / grid.context.width * axis.context.minMaxRange;
if (axis.inverse)
xValue = -xValue;
}
var labelY = axis.GetLabelObjectPosition(0).y;
axis.context.pointerValue = xValue;
@@ -188,15 +186,20 @@ namespace XCharts
double tempMinValue;
double tempMaxValue;
axis.context.needAnimation = Application.isPlaying && axis.animation.show;
if (axis.inverse != axis.context.lastCheckInverse)
{
foreach (var serie in chart.series)
serie.context.InvalidateMinMaxCache();
}
chart.GetSeriesMinMaxValue(axis, axisIndex, out tempMinValue, out tempMaxValue);
var dataZoom = chart.GetDataZoomOfAxis(axis);
if (dataZoom != null && dataZoom.enable)
{
if (axis is XAxis)
dataZoom.SetXAxisIndexValueInfo(axisIndex, ref tempMinValue, ref tempMaxValue);
dataZoom.SetXAxisIndexValueInfo(axisIndex, ref tempMinValue, ref tempMaxValue, axis.inverse);
else
dataZoom.SetYAxisIndexValueInfo(axisIndex, ref tempMinValue, ref tempMaxValue);
dataZoom.SetYAxisIndexValueInfo(axisIndex, ref tempMinValue, ref tempMaxValue, axis.inverse);
}
if (tempMinValue != axis.context.destMinValue ||

View File

@@ -495,17 +495,17 @@ namespace XCharts.Runtime
public static double GetAxisPositionValue(GridCoord grid, Axis axis, Vector3 pos)
{
if (axis is YAxis)
return GetAxisPositionValue(pos.y, grid.context.height, axis.context.minMaxRange, grid.context.y, axis.context.offset);
return GetAxisPositionValue(pos.y, grid.context.height, axis.context.minMaxRange, grid.context.y, axis.context.offset, axis.context.minValue);
else if (axis is XAxis)
return GetAxisPositionValue(pos.x, grid.context.width, axis.context.minMaxRange, grid.context.x, axis.context.offset);
return GetAxisPositionValue(pos.x, grid.context.width, axis.context.minMaxRange, grid.context.x, axis.context.offset, axis.context.minValue);
else
return 0;
}
public static double GetAxisPositionValue(float xy, float axisLength, double axisRange, float axisStart, float axisOffset)
public static double GetAxisPositionValue(float xy, float axisLength, double axisRange, float axisStart, float axisOffset, double minValue = 0)
{
var yRate = axisRange / axisLength;
return yRate * (xy - axisStart - axisOffset);
return minValue + yRate * (xy - axisStart - axisOffset);
}
/// <summary>

View File

@@ -15,7 +15,7 @@ namespace XCharts.Runtime
[SerializeField] private float m_BorderWidth;
[SerializeField] private Color32 m_BorderColor;
[SerializeField] private bool m_RoundedCorner = true;
[SerializeField] private float[] m_CornerRadius = new float[] { 0, 0, 0, 0 };
[SerializeField] private float[] m_CornerRadius = new float[] { 10, 10, 10, 10 };
/// <summary>
/// whether the border is visible.

View File

@@ -12,7 +12,7 @@ namespace XCharts.Runtime
[System.Serializable]
public class MarqueeStyle : ChildComponent
{
[SerializeField][Since("v3.5.0")] private bool m_Apply = false;
[SerializeField][Since("v3.5.0")] private bool m_Apply = true;
[SerializeField][Since("v3.5.0")] private bool m_RealRect = false;
[SerializeField][Since("v3.5.0")] private AreaStyle m_AreaStyle = new AreaStyle();
[SerializeField][Since("v3.5.0")] private LineStyle m_LineStyle = new LineStyle();
@@ -52,7 +52,7 @@ namespace XCharts.Runtime
/// Custom checkboxes select ongoing callbacks.
/// ||自定义选取框选取进行时的回调。
/// </summary>
public Action<DataZoom> onGoing { set { m_OnStart = value; } get { return m_OnStart; } }
public Action<DataZoom> onGoing { set { m_OnGoing = value; } get { return m_OnGoing; } }
/// <summary>
/// Customize the callback at the end of the selection.
/// ||自定义选取框结束选取时的回调。

View File

@@ -92,6 +92,7 @@ namespace XCharts.Runtime
[SerializeField][Since("v3.5.0")] private MarqueeStyle m_MarqueeStyle = new MarqueeStyle();
[SerializeField][Since("v3.6.0")] private bool m_StartLock;
[SerializeField][Since("v3.6.0")] private bool m_EndLock;
[SerializeField][Since("v3.12.0")] private bool m_FilterAxisRange = true;
public DataZoomContext context = new DataZoomContext();
private CustomDataZoomStartEndFunction m_StartEndFunction;
@@ -325,6 +326,16 @@ namespace XCharts.Runtime
set { if (PropertyUtil.SetStruct(ref m_EndLock, value)) SetVerticesDirty(); }
}
/// <summary>
/// Whether dataZoom filters the axis min/max range. When true, the axis scale adapts to the current zoom window.
/// When false, the axis always shows the full data range regardless of the zoom position.
/// ||是否根据DataZoom的缩放窗口过滤坐标轴的最大最小值范围。为true时坐标轴范围随缩放窗口变化为false时坐标轴始终显示全部数据范围。
/// </summary>
public bool filterAxisRange
{
get { return m_FilterAxisRange; }
set { if (PropertyUtil.SetStruct(ref m_FilterAxisRange, value)) SetVerticesDirty(); }
}
/// <summary>
/// The end percentage of the window out of the data extent, in the range of 0 ~ 100.
/// ||数据窗口范围的结束百分比。范围是0 ~ 100。
/// </summary>
@@ -415,6 +426,7 @@ namespace XCharts.Runtime
public double rawMax;
public double min;
public double max;
public bool isInverse;
}
private Dictionary<int, AxisIndexValueInfo> m_XAxisIndexInfos = new Dictionary<int, AxisIndexValueInfo>();
private Dictionary<int, AxisIndexValueInfo> m_YAxisIndexInfos = new Dictionary<int, AxisIndexValueInfo>();
@@ -688,7 +700,7 @@ namespace XCharts.Runtime
context.height = chartHeight - runtimeTop - runtimeBottom;
}
internal void SetXAxisIndexValueInfo(int xAxisIndex, ref double min, ref double max)
internal void SetXAxisIndexValueInfo(int xAxisIndex, ref double min, ref double max, bool isInverse = false)
{
AxisIndexValueInfo info;
if (!m_XAxisIndexInfos.TryGetValue(xAxisIndex, out info))
@@ -698,13 +710,14 @@ namespace XCharts.Runtime
}
info.rawMin = min;
info.rawMax = max;
info.isInverse = isInverse;
info.min = min + (max - min) * start / 100;
info.max = min + (max - min) * end / 100;
min = info.min;
max = info.max;
}
internal void SetYAxisIndexValueInfo(int yAxisIndex, ref double min, ref double max)
internal void SetYAxisIndexValueInfo(int yAxisIndex, ref double min, ref double max, bool isInverse = false)
{
AxisIndexValueInfo info;
if (!m_YAxisIndexInfos.TryGetValue(yAxisIndex, out info))
@@ -714,6 +727,7 @@ namespace XCharts.Runtime
}
info.rawMin = min;
info.rawMax = max;
info.isInverse = isInverse;
info.min = min + (max - min) * start / 100;
info.max = min + (max - min) * end / 100;
min = info.min;
@@ -738,6 +752,14 @@ namespace XCharts.Runtime
var range = info.rawMax - info.rawMin;
min = info.rawMin + range * m_Start / 100;
max = info.rawMin + range * m_End / 100;
if (info.isInverse)
{
// Internal values are negated; convert back to original for data comparison
var originalMin = -max;
var originalMax = -min;
min = originalMin;
max = originalMax;
}
}
else
{
@@ -753,6 +775,14 @@ namespace XCharts.Runtime
var range = info.rawMax - info.rawMin;
min = info.rawMin + range * m_Start / 100;
max = info.rawMin + range * m_End / 100;
if (info.isInverse)
{
// Internal values are negated; convert back to original for data comparison
var originalMin = -max;
var originalMax = -min;
min = originalMin;
max = originalMax;
}
}
else
{

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
@@ -19,6 +20,9 @@ namespace XCharts.Runtime
private float m_DataZoomLastEndIndex;
private float m_LastStart;
private float m_LastEnd;
private List<double> _sampleSumPrefixCache;
private int _sampleSumPrefixMaxCount = 0;
private bool _sampleSumPrefixInverse = false;
public override void InitComponent()
{
@@ -113,7 +117,7 @@ namespace XCharts.Runtime
dataZoom.context.isCoordinateDrag = true;
}
}
if (dataZoom.supportMarquee)
if (dataZoom.supportMarquee && grid.Contains(pos))
{
dataZoom.context.isMarqueeDrag = true;
dataZoom.context.marqueeStartPos = pos;
@@ -163,7 +167,7 @@ namespace XCharts.Runtime
var dataZoom = component;
var grid = chart.GetGridOfDataZoom(dataZoom);
if (dataZoom.supportMarquee)
if (dataZoom.supportMarquee && dataZoom.context.isMarqueeDrag)
{
Vector2 pos;
if (!chart.ScreenPointToChartPoint(eventData.position, out pos))
@@ -207,16 +211,38 @@ namespace XCharts.Runtime
var dataZoom = component;
if (dataZoom.supportMarquee)
if (dataZoom.supportMarquee && dataZoom.context.isMarqueeDrag)
{
dataZoom.context.isMarqueeDrag = false;
if (dataZoom.marqueeStyle.apply)
{
var grid = chart.GetGridOfDataZoom(dataZoom);
var start = (dataZoom.context.marqueeRect.x - grid.context.x) / grid.context.width * 100;
var end = (dataZoom.context.marqueeRect.x - grid.context.x + dataZoom.context.marqueeRect.width) / grid.context.width * 100;
UpdateDataZoomRange(dataZoom, start, end, grid);
var currentRange = dataZoom.end - dataZoom.start;
var startRatio = (dataZoom.context.marqueeRect.x - grid.context.x) / grid.context.width;
var endRatio = (dataZoom.context.marqueeRect.x - grid.context.x + dataZoom.context.marqueeRect.width) / grid.context.width;
var start = dataZoom.start + startRatio * currentRange;
var end = dataZoom.start + endRatio * currentRange;
if (start > end)
{
var temp = start;
start = end;
end = temp;
}
if (start < 0) start = 0;
if (end > 100) end = 100;
if (end - start >= 1)
{
// Bypass minZoomRatio for marquee: the user explicitly selected this region.
if (!dataZoom.startLock) dataZoom.start = start;
if (!dataZoom.endLock) dataZoom.end = end;
m_LastStart = dataZoom.start;
m_LastEnd = dataZoom.end;
chart.OnDataZoomRangeChanged(dataZoom);
chart.RefreshChart();
}
}
dataZoom.context.marqueeRect = Rect.zero;
dataZoom.SetVerticesDirty();
if (dataZoom.marqueeStyle.onEnd != null)
{
dataZoom.marqueeStyle.onEnd(dataZoom);
@@ -247,30 +273,55 @@ namespace XCharts.Runtime
var dataZoom = component;
var grid = chart.GetGridOfDataZoom(dataZoom);
if (dataZoom.IsInStartZoom(localPos) ||
dataZoom.IsInEndZoom(localPos))
dataZoom.IsInEndZoom(localPos) ||
dataZoom.IsInSelectedZoom(localPos))
{
return;
}
if (dataZoom.IsInZoom(localPos) &&
!dataZoom.IsInSelectedZoom(localPos))
if (dataZoom.IsInZoom(localPos))
{
var start = dataZoom.start;
var end = dataZoom.end;
switch (dataZoom.orient)
{
case Orient.Horizonal:
var pointerX = localPos.x;
var selectWidth = grid.context.width * (dataZoom.end - dataZoom.start) / 100;
var selectWidth = dataZoom.context.width * (dataZoom.end - dataZoom.start) / 100;
var startX = pointerX - selectWidth / 2;
var endX = pointerX + selectWidth / 2;
if (startX < grid.context.x)
if (startX < dataZoom.context.x)
{
startX = grid.context.x;
endX = grid.context.x + selectWidth;
startX = dataZoom.context.x;
endX = dataZoom.context.x + selectWidth;
}
else if (endX > grid.context.x + grid.context.width)
else if (endX > dataZoom.context.x + dataZoom.context.width)
{
endX = grid.context.x + grid.context.width;
startX = grid.context.x + grid.context.width - selectWidth;
endX = dataZoom.context.x + dataZoom.context.width;
startX = dataZoom.context.x + dataZoom.context.width - selectWidth;
}
start = (startX - dataZoom.context.x) / dataZoom.context.width * 100;
end = (endX - dataZoom.context.x) / dataZoom.context.width * 100;
break;
case Orient.Vertical:
var pointerY = localPos.y;
var selectHeight = dataZoom.context.height * (dataZoom.end - dataZoom.start) / 100;
var startY = pointerY - selectHeight / 2;
var endY = pointerY + selectHeight / 2;
if (startY < dataZoom.context.y)
{
startY = dataZoom.context.y;
endY = dataZoom.context.y + selectHeight;
}
else if (endY > dataZoom.context.y + dataZoom.context.height)
{
endY = dataZoom.context.y + dataZoom.context.height;
startY = dataZoom.context.y + dataZoom.context.height - selectHeight;
}
start = (startY - dataZoom.context.y) / dataZoom.context.height * 100;
end = (endY - dataZoom.context.y) / dataZoom.context.height * 100;
break;
}
var start = (startX - grid.context.x) / grid.context.width * 100;
var end = (endX - grid.context.x) / grid.context.width * 100;
UpdateDataZoomRange(dataZoom, start, end, grid);
}
}
@@ -294,7 +345,7 @@ namespace XCharts.Runtime
if ((dataZoom.supportInside && dataZoom.supportInsideScroll && grid.Contains(pos)) ||
dataZoom.IsInZoom(pos))
{
ScaleDataZoom(dataZoom, eventData.scrollDelta.y * dataZoom.scrollSensitivity, grid);
ScaleDataZoom(dataZoom, eventData.scrollDelta.y * dataZoom.scrollSensitivity, grid, pos);
}
}
@@ -375,22 +426,62 @@ namespace XCharts.Runtime
}
}
private void ScaleDataZoom(DataZoom dataZoom, float delta, GridCoord grid = null)
private void ScaleDataZoom(DataZoom dataZoom, float delta, GridCoord grid = null, Vector2? mousePos = null)
{
if (grid == null) grid = chart.GetGridOfDataZoom(dataZoom);
var range = dataZoom.orient == Orient.Horizonal ? grid.context.width : grid.context.height;
var deltaPercent = Mathf.Abs(delta / range * 100);
float start, end;
// Calculate the anchor ratio within the current [start, end] range based on mouse position.
// This ensures the data point under the cursor stays fixed while zooming.
float centerRatio = 0.5f;
if (mousePos.HasValue)
{
float mousePercent;
if (dataZoom.orient == Orient.Horizonal)
mousePercent = grid.context.width > 0
? (mousePos.Value.x - grid.context.x) / grid.context.width * 100
: 50f;
else
mousePercent = grid.context.height > 0
? (mousePos.Value.y - grid.context.y) / grid.context.height * 100
: 50f;
var currentRange = dataZoom.end - dataZoom.start;
if (currentRange > 0)
{
// mousePercent is always grid-relative (0=left edge, 100=right edge).
// When DataZoom shows [start, end], the grid spans exactly that window,
// so the anchor fraction is simply mousePercent/100.
// For inverse axes the data runs right→left, so flip the fraction.
bool isInverse = false;
if (dataZoom.orient == Orient.Horizonal && dataZoom.xAxisIndexs.Count > 0)
{
var xAxis = chart.GetChartComponent<XAxis>(dataZoom.xAxisIndexs[0]);
isInverse = xAxis != null && xAxis.inverse;
}
else if (dataZoom.orient == Orient.Vertical && dataZoom.yAxisIndexs.Count > 0)
{
var yAxis = chart.GetChartComponent<YAxis>(dataZoom.yAxisIndexs[0]);
isInverse = yAxis != null && yAxis.inverse;
}
centerRatio = isInverse
? 1f - mousePercent / 100f
: mousePercent / 100f;
}
}
if (delta > 0)
{
if (dataZoom.end <= dataZoom.start) return;
start = dataZoom.start + deltaPercent;
end = dataZoom.end - deltaPercent;
start = dataZoom.start + deltaPercent * centerRatio;
end = dataZoom.end - deltaPercent * (1 - centerRatio);
}
else
{
start = dataZoom.start - deltaPercent;
end = dataZoom.end + deltaPercent;
start = dataZoom.start - deltaPercent * centerRatio;
end = dataZoom.end + deltaPercent * (1 - centerRatio);
}
UpdateDataZoomRange(dataZoom, start, end, grid);
}
@@ -411,7 +502,11 @@ namespace XCharts.Runtime
if(grid == null) grid = chart.GetGridOfDataZoom(dataZoom);
var range = dataZoom.orient == Orient.Horizonal ? grid.context.width : grid.context.height;
var minRange = dataZoom.minZoomRatio * range;
if (end - start < minRange / range * 100)
var newRange = end - start;
var currentRange = dataZoom.end - dataZoom.start;
// Only block when shrinking the range; always allow expansion so the chart
// cannot get permanently locked after a marquee selects a narrow region.
if (newRange < minRange / range * 100 && newRange <= currentRange)
{
return;
}
@@ -546,13 +641,26 @@ namespace XCharts.Runtime
Serie serie = chart.series[0];
Axis axis = chart.GetChartComponent<YAxis>(0);
var showData = serie.GetDataList(null);
float scaleWid = dataZoom.context.width / (showData.Count - 1);
float scaleWid = showData.Count > 1 ? dataZoom.context.width / (showData.Count - 1) : dataZoom.context.width;
Vector3 lp = Vector3.zero;
Vector3 np = Vector3.zero;
double minValue = 0;
double maxValue = 0;
SeriesHelper.GetYMinMaxValue(chart, 0, axis.inverse, out minValue, out maxValue, false, false);
AxisHelper.AdjustMinMaxValue(axis, ref minValue, ref maxValue, true);
// shadow always shows the full data range, independent of DataZoom window
double minValue = SerieHelper.GetMinData(serie, 1, null, axis.inverse);
double maxValue = SerieHelper.GetMaxData(serie, 1, null, axis.inverse);
minValue = ChartHelper.GetMinDivisibleValue(minValue, 0);
maxValue = ChartHelper.GetMaxDivisibleValue(maxValue, 0);
double xMinValue = 0;
double xMaxValue = 0;
bool useXValueForShadow = false;
var xAxisIndex = dataZoom.xAxisIndexs.Count > 0 ? dataZoom.xAxisIndexs[0] : 0;
var xAxis = chart.GetChartComponent<XAxis>(xAxisIndex);
if (xAxis != null && (xAxis.IsValue() || xAxis.IsTime()))
{
xMinValue = SerieHelper.GetMinData(serie, 0, null, xAxis.inverse);
xMaxValue = SerieHelper.GetMaxData(serie, 0, null, xAxis.inverse);
AxisHelper.AdjustMinMaxValue(xAxis, ref xMinValue, ref xMaxValue, true);
useXValueForShadow = (xMaxValue - xMinValue) > 0;
}
int rate = 1;
var sampleDist = serie.sampleDist < 2 ? 2 : serie.sampleDist;
@@ -568,12 +676,40 @@ namespace XCharts.Runtime
var animationDuration = serie.animation.GetChangeDuration();
var dataAddDuration = serie.animation.GetAdditionDuration();
var unscaledTime = serie.animation.unscaledTime;
var useCurrentData = false;
List<double> sampleSumPrefix = null;
if (serie.animation.enable)
{
useCurrentData = DataHelper.IsAnyDataChanged(ref showData, serie.minShow, maxCount);
dataChanging = useCurrentData;
}
if (!useCurrentData && rate > 1 &&
(serie.sampleType == SampleType.Sum || serie.sampleType == SampleType.Average))
{
if (_sampleSumPrefixCache == null || _sampleSumPrefixMaxCount != maxCount || _sampleSumPrefixInverse != axis.inverse)
{
_sampleSumPrefixCache = DataHelper.BuildSampleSumPrefix(ref showData, maxCount, axis.inverse);
_sampleSumPrefixMaxCount = maxCount;
_sampleSumPrefixInverse = axis.inverse;
}
sampleSumPrefix = _sampleSumPrefixCache;
}
for (int i = 0; i < maxCount; i += rate)
for (int i = serie.minShow; i < maxCount; i += rate)
{
double value = DataHelper.SampleValue(ref showData, serie.sampleType, rate, serie.minShow, maxCount, totalAverage, i,
dataAddDuration, animationDuration, ref dataChanging, axis, unscaledTime);
float pX = dataZoom.context.x + i * scaleWid;
dataAddDuration, animationDuration, ref dataChanging, axis, unscaledTime,
useCurrentData, false, sampleSumPrefix);
float pX;
if (useXValueForShadow && i < showData.Count && showData[i].data.Count > 0)
{
var xVal = (xAxis != null && xAxis.inverse) ? -showData[i].data[0] : showData[i].data[0];
pX = dataZoom.context.x + (float)((xVal - xMinValue) / (xMaxValue - xMinValue)) * dataZoom.context.width;
}
else
{
pX = dataZoom.context.x + i * scaleWid;
}
float dataHig = (float)((maxValue - minValue) == 0 ? 0 :
(value - minValue) / (maxValue - minValue) * dataZoom.context.height);
np = new Vector3(pX, chart.chartY + dataZoom.bottom + dataHig);
@@ -641,11 +777,11 @@ namespace XCharts.Runtime
float scaleWid = dataZoom.context.height / (showData.Count - 1);
Vector3 lp = Vector3.zero;
Vector3 np = Vector3.zero;
double minValue = 0;
double maxValue = 0;
double minValue;
double maxValue;
SeriesHelper.GetYMinMaxValue(chart, 0, axis.inverse, out minValue, out maxValue);
AxisHelper.AdjustMinMaxValue(axis, ref minValue, ref maxValue, true);
minValue = ChartHelper.GetMinDivisibleValue(minValue, 0);
maxValue = ChartHelper.GetMaxDivisibleValue(maxValue, 0);
int rate = 1;
var sampleDist = serie.sampleDist < 2 ? 2 : serie.sampleDist;
var maxCount = showData.Count;
@@ -660,11 +796,30 @@ namespace XCharts.Runtime
var animationDuration = serie.animation.GetChangeDuration();
var dataAddDuration = serie.animation.GetAdditionDuration();
var unscaledTime = serie.animation.unscaledTime;
var useCurrentData = false;
List<double> sampleSumPrefix = null;
if (serie.animation.enable)
{
useCurrentData = DataHelper.IsAnyDataChanged(ref showData, serie.minShow, maxCount);
dataChanging = useCurrentData;
}
if (!useCurrentData && rate > 1 &&
(serie.sampleType == SampleType.Sum || serie.sampleType == SampleType.Average))
{
if (_sampleSumPrefixCache == null || _sampleSumPrefixMaxCount != maxCount || _sampleSumPrefixInverse != axis.inverse)
{
_sampleSumPrefixCache = DataHelper.BuildSampleSumPrefix(ref showData, maxCount, axis.inverse);
_sampleSumPrefixMaxCount = maxCount;
_sampleSumPrefixInverse = axis.inverse;
}
sampleSumPrefix = _sampleSumPrefixCache;
}
for (int i = 0; i < maxCount; i += rate)
for (int i = serie.minShow; i < maxCount; i += rate)
{
double value = DataHelper.SampleValue(ref showData, serie.sampleType, rate, serie.minShow, maxCount, totalAverage, i,
dataAddDuration, animationDuration, ref dataChanging, axis, unscaledTime);
dataAddDuration, animationDuration, ref dataChanging, axis, unscaledTime,
useCurrentData, false, sampleSumPrefix);
float pY = dataZoom.context.y + i * scaleWid;
float dataHig = (maxValue - minValue) == 0 ? 0 :
(float)((value - minValue) / (maxValue - minValue) * dataZoom.context.width);
@@ -706,7 +861,7 @@ namespace XCharts.Runtime
private void DrawMarquee(VertexHelper vh, DataZoom dataZoom)
{
if (!dataZoom.enable || !dataZoom.supportMarquee)
if (!dataZoom.enable || !dataZoom.supportMarquee || !dataZoom.context.isMarqueeDrag)
return;
var areaColor = dataZoom.marqueeStyle.areaStyle.GetColor(chart.theme.dataZoom.dataAreaColor);
UGL.DrawRectangle(vh, dataZoom.context.marqueeRect, areaColor);

View File

@@ -68,6 +68,50 @@ namespace XCharts.Runtime
/// </summary>
End
}
/// <summary>
/// The value-based condition for showing label. Controls visibility based on threshold comparison.
/// ||标签基于值的显示条件,通过与阈值比较来控制标签的显示。
/// </summary>
public enum ShowCondition
{
/// <summary>
/// Always show label.
/// ||总是显示标签。
/// </summary>
Always,
/// <summary>
/// Show label when value is greater than showThreshold.
/// ||大于showThreshold才显示标签。
/// </summary>
GreaterThan,
/// <summary>
/// Show label when value is less than showThreshold.
/// ||小于showThreshold才显示标签。
/// </summary>
LessThan,
}
/// <summary>
/// The data-pattern-based filter for showing label. Controls visibility based on data topology.
/// ||标签基于数据形态的显示筛选,通过数据的拓扑特征(波峰/波谷)来控制标签的显示。
/// </summary>
public enum ShowFilter
{
/// <summary>
/// All data points show label.
/// ||所有数据点都显示标签。
/// </summary>
All,
/// <summary>
/// Show label when value is at a peak.
/// ||波峰才显示标签。
/// </summary>
Peak,
/// <summary>
/// Show label when value is at a valley.
/// ||波谷才显示标签。
/// </summary>
Valley
}
[SerializeField] protected bool m_Show = true;
[SerializeField] Position m_Position = Position.Default;
@@ -82,6 +126,10 @@ namespace XCharts.Runtime
[SerializeField] protected float m_Height = 0;
[SerializeField][Since("v3.15.0")] protected float m_FixedX = 0;
[SerializeField][Since("v3.15.0")] protected float m_FixedY = 0;
[SerializeField][Since("v3.16.0")] protected ShowCondition m_ShowCondition = ShowCondition.Always;
[SerializeField][Since("v3.16.0")] protected ShowFilter m_ShowFilter = ShowFilter.All;
[SerializeField][Since("v3.16.0")] protected double m_ShowThreshold = 0;
[SerializeField][Since("v3.16.0")] protected float m_ShowMinGap = 0;
[SerializeField] protected IconStyle m_Icon = new IconStyle();
[SerializeField] protected ImageStyle m_Background = new ImageStyle();
@@ -100,6 +148,9 @@ namespace XCharts.Runtime
m_Height = 0;
m_NumericFormatter = "";
m_AutoOffset = false;
m_ShowCondition = ShowCondition.Always;
m_ShowFilter = ShowFilter.All;
m_ShowThreshold = 0;
}
/// <summary>
@@ -308,6 +359,47 @@ namespace XCharts.Runtime
set { if (PropertyUtil.SetStruct(ref m_FixedY, value)) SetComponentDirty(); }
}
/// <summary>
/// The value-based show condition of label. Default is ShowCondition.Always.
/// When set to GreaterThan or LessThan, the label is shown only when the value satisfies the threshold.
/// ||标签基于值的显示条件。默认为ShowCondition.Always总是显示。
/// 设为GreaterThan或LessThan时只有满足showThreshold阈值条件的数据点才显示标签。
/// </summary>
public ShowCondition showCondition
{
get { return m_ShowCondition; }
set { if (PropertyUtil.SetStruct(ref m_ShowCondition, value)) SetComponentDirty(); }
}
/// <summary>
/// The data-pattern-based show filter of label. Default is ShowFilter.All.
/// When set to Peak or Valley, the label is shown only at local maximum or minimum data points.
/// ||标签基于数据形态的显示筛选。默认为ShowFilter.All所有数据点都显示。
/// 设为Peak或Valley时只在局部波峰或波谷的数据点显示标签。
/// </summary>
public ShowFilter showFilter
{
get { return m_ShowFilter; }
set { if (PropertyUtil.SetStruct(ref m_ShowFilter, value)) SetComponentDirty(); }
}
/// <summary>
/// The threshold for showCondition. When showCondition is GreaterThan or LessThan, only values that satisfy the comparison will show label. Default is 0.
/// ||showCondition的阈值。当showCondition为GreaterThan或LessThan时生效只有满足比较条件的值才显示标签。默认值为0。
/// </summary>
public double showThreshold
{
get { return m_ShowThreshold; }
set { if (PropertyUtil.SetStruct(ref m_ShowThreshold, value)) SetComponentDirty(); }
}
/// <summary>
/// the gap between label and the previous label. When the distance to the previous label is less than this value,
/// the label with smaller y value will be hidden. Default is 0, which means this function is turned off.
/// 和上一个标签的最小间距。当和上一个标签的距离小于该值时隐藏对应y值较小的标签。默认为0不开启该功能。
/// </summary>
public float showMinGap
{
get { return m_ShowMinGap; }
set { if (PropertyUtil.SetStruct(ref m_ShowMinGap, value)) SetComponentDirty(); }
}
/// <summary>
/// the sytle of background.
/// ||背景图样式。
/// </summary>

View File

@@ -80,6 +80,8 @@ namespace XCharts.Runtime
[SerializeField] private bool m_ItemAutoColor = true;
[SerializeField] private float m_ItemOpacity = 1;
[SerializeField][Since("v3.15.0")] private float m_ItemInactiveOpacity = 1;
[SerializeField][Since("v3.16.0")] private float m_Width = 0;
[SerializeField][Since("v3.16.0")] private float m_Height = 0;
[SerializeField] private string m_Formatter;
[SerializeField] private LabelStyle m_LabelStyle = new LabelStyle();
[SerializeField][Since("v3.10.0")] private TextLimit m_TextLimit = new TextLimit();
@@ -202,6 +204,25 @@ namespace XCharts.Runtime
set { if (PropertyUtil.SetClass(ref m_Formatter, value)) SetComponentDirty(); }
}
/// <summary>
/// the width of legend component. Default is 0 for auto adapt. When set a value between 0 and 1, it means the percentage relative to chart width and height.
/// ||图例组件的宽。默认为0自适应。当设置0-1的值时表示相对于图表宽高的比例。
/// </summary>
public float width
{
get { return m_Width; }
set { if (PropertyUtil.SetStruct(ref m_Width, value)) SetComponentDirty(); }
}
/// <summary>
/// the height of legend component. Default is 0 for auto adapt. When set a value between 0 and 1, it means the percentage relative to chart width and height.
/// ||图例组件的高。默认为0自适应。当设置0-1的值时表示相对于图表宽高的比例。
/// </summary>
/// <value></value>
public float height
{
get { return m_Height; }
set { if (PropertyUtil.SetStruct(ref m_Height, value)) SetComponentDirty(); }
}
/// <summary>
/// the style of text.
/// ||文本样式。
/// </summary>

View File

@@ -93,10 +93,23 @@ namespace XCharts.Runtime
var startY = 0f;
var legendMaxWidth = chartWidth - legend.location.runtimeLeft - legend.location.runtimeRight;
var legendMaxHeight = chartHeight - legend.location.runtimeTop - legend.location.runtimeBottom;
var isVertical = legend.orient == Orient.Vertical;
var fixedWidth = legend.width <= 0 ? 0
: legend.width < 1 ? chartWidth * legend.width
: legend.width;
var fixedHeight = legend.height <= 0 ? 0
: legend.height < 1 ? chartHeight * legend.height
: legend.height;
// Horizonal: width constrains layout wrapping; Vertical: height constrains layout wrapping.
// The other axis only affects the background size, not the layout.
if (!isVertical && fixedWidth > 0) legendMaxWidth = fixedWidth;
if (isVertical && fixedHeight > 0) legendMaxHeight = fixedHeight;
UpdateLegendWidthAndHeight(legend, legendMaxWidth, legendMaxHeight);
// Override context size for fixed dimensions (controls background rect size).
if (fixedWidth > 0) legend.context.width = fixedWidth;
if (fixedHeight > 0) legend.context.height = fixedHeight;
var legendRuntimeWidth = legend.context.width;
var legendRuntimeHeight = legend.context.height;
var isVertical = legend.orient == Orient.Vertical;
switch (legend.location.align)
{
case Location.Align.TopCenter:
@@ -198,11 +211,14 @@ namespace XCharts.Runtime
legend.context.eachHeight = 0;
if (legend.orient == Orient.Horizonal)
{
var maxRowWidth = 0f;
foreach (var kv in legend.context.buttonList)
{
if (width + kv.Value.width > maxWidth)
{
realWidth = width - legend.itemGap;
if (realWidth > maxRowWidth)
maxRowWidth = realWidth;
realHeight += height + legend.itemGap;
if (legend.context.eachHeight < height + legend.itemGap)
{
@@ -216,8 +232,10 @@ namespace XCharts.Runtime
height = kv.Value.height;
}
width -= legend.itemGap;
if (width > maxRowWidth)
maxRowWidth = width;
legend.context.height = realHeight + height;
legend.context.width = realWidth > 0 ? realWidth : width;
legend.context.width = maxRowWidth;
}
else
{

View File

@@ -11,6 +11,8 @@ namespace XCharts.Runtime
internal sealed class TooltipHandler : MainComponentHandler<Tooltip>
{
private Dictionary<string, ChartLabel> m_IndicatorLabels = new Dictionary<string, ChartLabel>();
private Dictionary<Serie, Dictionary<int, List<SerieData>>> m_SortedAxisDataCache =
new Dictionary<Serie, Dictionary<int, List<SerieData>>>();
private GameObject m_LabelRoot;
private ISerieContainer m_PointerContainer;
@@ -425,8 +427,10 @@ namespace XCharts.Runtime
private void GetSerieDataByXYAxis(Serie serie, Axis xAxis, Axis yAxis)
{
var xAxisIndex = AxisHelper.GetAxisValueSplitIndex(xAxis, xAxis.context.pointerValue, false);
var yAxisIndex = AxisHelper.GetAxisValueSplitIndex(yAxis, yAxis.context.pointerValue, false);
var xPointerInternal = xAxis.inverse ? -xAxis.context.pointerValue : xAxis.context.pointerValue;
var yPointerInternal = yAxis.inverse ? -yAxis.context.pointerValue : yAxis.context.pointerValue;
var xAxisIndex = AxisHelper.GetAxisValueSplitIndex(xAxis, xPointerInternal, false);
var yAxisIndex = AxisHelper.GetAxisValueSplitIndex(yAxis, yPointerInternal, false);
serie.context.pointerItemDataIndex = -1;
if (serie is Heatmap)
{
@@ -439,8 +443,10 @@ namespace XCharts.Runtime
}
foreach (var serieData in serie.data)
{
var x = AxisHelper.GetAxisValueSplitIndex(xAxis, serieData.GetData(0), true);
var y = AxisHelper.GetAxisValueSplitIndex(yAxis, serieData.GetData(1), true);
var xData = xAxis.inverse ? -serieData.GetData(0) : serieData.GetData(0);
var yData = yAxis.inverse ? -serieData.GetData(1) : serieData.GetData(1);
var x = AxisHelper.GetAxisValueSplitIndex(xAxis, xData, true);
var y = AxisHelper.GetAxisValueSplitIndex(yAxis, yData, true);
if (xAxisIndex == x && y == yAxisIndex)
{
serie.context.pointerItemDataIndex = serieData.index;
@@ -451,29 +457,117 @@ namespace XCharts.Runtime
private void GetSerieDataIndexByAxis(Serie serie, Axis axis, GridCoord grid, int dimension = 0)
{
var currValue = 0d;
var lastValue = 0d;
var nextValue = 0d;
var axisValue = axis.context.pointerValue;
var isTimeAxis = axis.IsTime();
var dataCount = serie.dataCount;
var themeSymbolSize = chart.theme.serie.scatterSymbolSize;
var data = serie.data;
if (!isTimeAxis)// || serie.useSortData)
serie.context.pointerAxisDataIndexs.Clear();
if (axis.IsTime())
{
serie.context.sortedData.Clear();
for (int i = 0; i < dataCount; i++)
{
var serieData = serie.data[i];
serie.context.sortedData.Add(serieData);
FindSerieDataIndexByAxisLinear(serie, axis, axisValue, dimension);
}
serie.context.sortedData.Sort(delegate (SerieData a, SerieData b)
else
{
var sortedData = GetSortedAxisData(serie, dimension);
var nearestIndex = GetNearestSerieDataIndex(sortedData, axisValue, dimension, axis.context.tickValue);
if (nearestIndex >= 0)
serie.context.pointerAxisDataIndexs.Add(nearestIndex);
}
if (serie.context.pointerAxisDataIndexs.Count > 0)
{
var index = serie.context.pointerAxisDataIndexs[0];
serie.context.pointerItemDataIndex = index;
var dataValue = serie.GetSerieData(index).GetData(dimension);
axis.context.axisTooltipValue = axis.inverse ? -dataValue : dataValue;
}
else
{
serie.context.pointerItemDataIndex = -1;
axis.context.axisTooltipValue = 0;
}
}
private List<SerieData> GetSortedAxisData(Serie serie, int dimension)
{
Dictionary<int, List<SerieData>> dimensionCache;
if (!m_SortedAxisDataCache.TryGetValue(serie, out dimensionCache))
{
dimensionCache = new Dictionary<int, List<SerieData>>();
m_SortedAxisDataCache[serie] = dimensionCache;
}
List<SerieData> sortedData;
if (!dimensionCache.TryGetValue(dimension, out sortedData))
{
sortedData = new List<SerieData>();
dimensionCache[dimension] = sortedData;
}
if (serie.dataDirty || sortedData.Count != serie.dataCount)
{
sortedData.Clear();
for (int i = 0; i < serie.dataCount; i++)
{
sortedData.Add(serie.data[i]);
}
sortedData.Sort(delegate (SerieData a, SerieData b)
{
return a.GetData(dimension).CompareTo(b.GetData(dimension));
});
data = serie.context.sortedData;
}
serie.context.pointerAxisDataIndexs.Clear();
return sortedData;
}
private int GetNearestSerieDataIndex(List<SerieData> sortedData, double axisValue, int dimension, double tickValue)
{
var dataCount = sortedData.Count;
if (dataCount <= 0) return -1;
if (dataCount == 1)
{
var currValue = sortedData[0].GetData(dimension);
var diff = tickValue * 0.5f;
return axisValue >= currValue - diff && axisValue <= currValue + diff
? sortedData[0].index
: -1;
}
var firstValue = sortedData[0].GetData(dimension);
var secondValue = sortedData[1].GetData(dimension);
if (axisValue <= firstValue + (secondValue - firstValue) / 2)
return sortedData[0].index;
var lastValue = sortedData[dataCount - 1].GetData(dimension);
var beforeLastValue = sortedData[dataCount - 2].GetData(dimension);
if (axisValue > beforeLastValue + (lastValue - beforeLastValue) / 2)
return sortedData[dataCount - 1].index;
var low = 1;
var high = dataCount - 2;
while (low <= high)
{
var mid = (low + high) / 2;
var prevValue = sortedData[mid - 1].GetData(dimension);
var currValue = sortedData[mid].GetData(dimension);
var nextValue = sortedData[mid + 1].GetData(dimension);
var leftBound = currValue - (currValue - prevValue) / 2;
var rightBound = currValue + (nextValue - currValue) / 2;
if (axisValue > leftBound && axisValue <= rightBound)
return sortedData[mid].index;
if (axisValue <= leftBound)
high = mid - 1;
else
low = mid + 1;
}
return -1;
}
private void FindSerieDataIndexByAxisLinear(Serie serie, Axis axis, double axisValue, int dimension)
{
var currValue = 0d;
var lastValue = 0d;
var nextValue = 0d;
var dataCount = serie.dataCount;
var data = serie.data;
for (int i = 0; i < dataCount; i++)
{
var serieData = data[i];
@@ -518,28 +612,18 @@ namespace XCharts.Runtime
}
lastValue = currValue;
}
if (serie.context.pointerAxisDataIndexs.Count > 0)
{
var index = serie.context.pointerAxisDataIndexs[0];
serie.context.pointerItemDataIndex = index;
axis.context.axisTooltipValue = serie.GetSerieData(index).GetData(dimension);
}
else
{
serie.context.pointerItemDataIndex = -1;
axis.context.axisTooltipValue = 0;
}
}
private void GetSerieDataIndexByItem(Serie serie, Axis axis, GridCoord grid, int dimension = 0)
{
if (serie.context.pointerItemDataIndex >= 0)
{
axis.context.axisTooltipValue = serie.GetSerieData(serie.context.pointerItemDataIndex).GetData(dimension);
var dataValue = serie.GetSerieData(serie.context.pointerItemDataIndex).GetData(dimension);
axis.context.axisTooltipValue = axis.inverse ? -dataValue : dataValue;
}
else if (component.type == Tooltip.Type.Cross)
{
axis.context.axisTooltipValue = axis.context.pointerValue;
axis.context.axisTooltipValue = axis.inverse ? -axis.context.pointerValue : axis.context.pointerValue;
}
else
{

View File

@@ -25,6 +25,13 @@ namespace XCharts.Runtime
return !string.IsNullOrEmpty(content) && content.IndexOf('{') >= 0;
}
public static bool NeedTotalContent(string content)
{
if (string.IsNullOrEmpty(content)) return false;
return content.IndexOf("{d", System.StringComparison.OrdinalIgnoreCase) >= 0 ||
content.IndexOf("{f", System.StringComparison.OrdinalIgnoreCase) >= 0;
}
/// <summary>
/// 替换字符串中的通配符,支持的通配符有{.}、{a}、{b}、{c}、{d}、{e}、{f}、{g}、{h}、{y}。
/// </summary>

View File

@@ -778,5 +778,27 @@ namespace XCharts.Runtime
foreach (var component in m_Components) component.ResetStatus();
foreach (var handler in m_SerieHandlers) handler.ForceUpdateSerieContext();
}
/// <summary>
/// Export chart configuration and data to JSON string.
/// ||导出图表配置和数据为JSON字符串。
/// </summary>
[Since("v3.16.0")]
public string ExportToJson(bool prettyPrint = true)
{
return XCharts.Runtime.ChartJsonSerializer.Serialize(this, prettyPrint);
}
/// <summary>
/// Import JSON and update current chart configuration.
/// ||导入JSON并更新当前图表配置。
/// </summary>
[Since("v3.16.0")]
public void ImportFromJson(string json)
{
XCharts.Runtime.ChartJsonDeserializer.Deserialize(json, this);
RefreshAllComponent();
RefreshChart();
}
}
}

View File

@@ -56,15 +56,15 @@ namespace XCharts.Runtime
}
if (isX)
{
SeriesHelper.GetXMinMaxValue(this, axisIndex, axis.inverse, out tempMinValue, out tempMaxValue, false, false, needAnimationData);
SeriesHelper.GetXMinMaxValue(this, axisIndex, axis.inverse, out tempMinValue, out tempMaxValue, false, needAnimationData);
}
else if (isY)
{
SeriesHelper.GetYMinMaxValue(this, axisIndex, axis.inverse, out tempMinValue, out tempMaxValue, false, false, needAnimationData);
SeriesHelper.GetYMinMaxValue(this, axisIndex, axis.inverse, out tempMinValue, out tempMaxValue, false, needAnimationData);
}
else if(isZ)
{
SeriesHelper.GetZMinMaxValue(this, axisIndex, axis.inverse, out tempMinValue, out tempMaxValue, false, false, needAnimationData);
SeriesHelper.GetZMinMaxValue(this, axisIndex, axis.inverse, out tempMinValue, out tempMaxValue, false, needAnimationData);
}
AxisHelper.AdjustMinMaxValue(axis, ref tempMinValue, ref tempMaxValue, true);
}

View File

@@ -154,5 +154,25 @@ namespace XCharts.Runtime
}
protected virtual void OnThemeChanged() { }
/// <summary>
/// Export UI component configuration to JSON string.
/// </summary>
[Since("v3.16.0")]
public string ExportToJson(bool prettyPrint = true)
{
return UIComponentJsonSerializer.Serialize(this, prettyPrint);
}
/// <summary>
/// Import JSON and update current UI component configuration.
/// </summary>
[Since("v3.16.0")]
public void ImportFromJson(string json)
{
UIComponentJsonDeserializer.Deserialize(json, this);
RefreshAllComponent();
RefreshGraph();
}
}
}

View File

@@ -1290,8 +1290,7 @@ namespace XCharts.Runtime
return null;
var clampedExportScale = Mathf.Max(1f, exportScale);
var scaleFactor = canvas.scaleFactor <= 0 ? 1f : canvas.scaleFactor;
var outputScaleFactor = scaleFactor * clampedExportScale;
var outputScaleFactor = clampedExportScale;
var width = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.width * outputScaleFactor));
var height = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.height * outputScaleFactor));
var chart = rectTransform.GetComponent<BaseChart>();
@@ -1404,8 +1403,8 @@ namespace XCharts.Runtime
// so the saved image has original width/height but higher quality.
if (clampedExportScale > 1f)
{
var targetWidth = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.width * scaleFactor));
var targetHeight = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.height * scaleFactor));
var targetWidth = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.width));
var targetHeight = Mathf.Max(1, Mathf.CeilToInt(rectTransform.rect.height));
var smallRT = RenderTexture.GetTemporary(targetWidth, targetHeight, 0, rt.format);
Graphics.Blit(rt, smallRT);
@@ -1416,7 +1415,7 @@ namespace XCharts.Runtime
tex.Apply();
RenderTexture.ReleaseTemporary(smallRT);
var cornerRadiiFinal = GetChartCornerRadius(chart, rectTransform.rect.width, rectTransform.rect.height, scaleFactor);
var cornerRadiiFinal = GetChartCornerRadius(chart, rectTransform.rect.width, rectTransform.rect.height, 1f);
ApplyRoundedCornerClip(tex, cornerRadiiFinal);
}
else

View File

@@ -5,6 +5,36 @@ namespace XCharts.Runtime
{
public static class DataHelper
{
private static List<double> s_SampleSumPrefix = new List<double>();
public static bool IsAnyDataChanged(ref List<SerieData> showData, int minCount, int maxCount)
{
for (int i = minCount; i < maxCount; i++)
{
if (showData[i].IsDataChanged())
return true;
}
return false;
}
public static List<double> BuildSampleSumPrefix(ref List<SerieData> showData, int maxCount, bool inverse)
{
if (maxCount < 0) maxCount = 0;
var targetCount = maxCount + 1;
if (s_SampleSumPrefix.Count != targetCount)
{
s_SampleSumPrefix.Clear();
for (int i = 0; i < targetCount; i++)
s_SampleSumPrefix.Add(0);
}
s_SampleSumPrefix[0] = 0;
for (int i = 0; i < maxCount; i++)
{
s_SampleSumPrefix[i + 1] = s_SampleSumPrefix[i] + showData[i].GetData(1, inverse);
}
return s_SampleSumPrefix;
}
public static double DataAverage(ref List<SerieData> showData, SampleType sampleType,
int minCount, int maxCount, int rate)
{
@@ -23,14 +53,84 @@ namespace XCharts.Runtime
public static double SampleValue(ref List<SerieData> showData, SampleType sampleType, int rate,
int minCount, int maxCount, double totalAverage, int index, float dataAddDuration, float dataChangeDuration,
ref bool dataChanging, Axis axis, bool unscaledTime)
ref bool dataChanging, Axis axis, bool unscaledTime, bool useCurrentData = true,
bool checkDataChanging = true, List<double> sampleSumPrefix = null)
{
var inverse = axis.inverse;
var minValue = 0;
var maxValue = 0;
if (!useCurrentData)
{
if (rate <= 1 || index == minCount)
return showData[index].GetData(1, inverse);
switch (sampleType)
{
case SampleType.Sum:
case SampleType.Average:
if (sampleSumPrefix != null)
{
var totalByPrefix = sampleSumPrefix[index + 1] - sampleSumPrefix[index - rate + 1];
if (sampleType == SampleType.Average)
return totalByPrefix / rate;
else
return totalByPrefix;
}
double total = 0;
for (int i = index; i > index - rate; i--)
{
total += showData[i].GetData(1, inverse);
}
if (sampleType == SampleType.Average)
return total / rate;
else
return total;
case SampleType.Max:
double max = double.MinValue;
for (int i = index; i > index - rate; i--)
{
var value = showData[i].GetData(1, inverse);
if (value > max)
max = value;
}
return max;
case SampleType.Min:
double min = double.MaxValue;
for (int i = index; i > index - rate; i--)
{
var value = showData[i].GetData(1, inverse);
if (value < min)
min = value;
}
return min;
case SampleType.Peak:
max = double.MinValue;
min = double.MaxValue;
total = 0;
for (int i = index; i > index - rate; i--)
{
var value = showData[i].GetData(1, inverse);
total += value;
if (value < min)
min = value;
if (value > max)
max = value;
}
var average = total / rate;
if (average >= totalAverage)
return max;
else
return min;
}
return showData[index].GetData(1, inverse);
}
if (rate <= 1 || index == minCount)
{
if (showData[index].IsDataChanged())
if (checkDataChanging && showData[index].IsDataChanged())
dataChanging = true;
return showData[index].GetCurrData(1, dataAddDuration, dataChangeDuration, inverse, minValue, maxValue, unscaledTime);
@@ -45,7 +145,7 @@ namespace XCharts.Runtime
{
count++;
total += showData[i].GetCurrData(1, dataAddDuration, dataChangeDuration, inverse, minValue, maxValue, unscaledTime);
if (showData[i].IsDataChanged())
if (checkDataChanging && showData[i].IsDataChanged())
dataChanging = true;
}
if (sampleType == SampleType.Average)
@@ -61,7 +161,7 @@ namespace XCharts.Runtime
if (value > max)
max = value;
if (showData[i].IsDataChanged())
if (checkDataChanging && showData[i].IsDataChanged())
dataChanging = true;
}
return max;
@@ -74,7 +174,7 @@ namespace XCharts.Runtime
if (value < min)
min = value;
if (showData[i].IsDataChanged())
if (checkDataChanging && showData[i].IsDataChanged())
dataChanging = true;
}
return min;
@@ -92,7 +192,7 @@ namespace XCharts.Runtime
if (value > max)
max = value;
if (showData[i].IsDataChanged())
if (checkDataChanging && showData[i].IsDataChanged())
dataChanging = true;
}
var average = total / rate;
@@ -101,7 +201,7 @@ namespace XCharts.Runtime
else
return min;
}
if (showData[index].IsDataChanged())
if (checkDataChanging && showData[index].IsDataChanged())
dataChanging = true;
return showData[index].GetCurrData(1, dataAddDuration, dataChangeDuration, inverse, minValue, maxValue, unscaledTime);

View File

@@ -217,7 +217,8 @@ namespace XCharts.Runtime
var close = serieData.GetCurrData(startDataIndex + 1, dataAddDuration, dataChangeDuration, yAxis.inverse, yMinValue, yMaxValue, unscaledTime);
var lowest = serieData.GetCurrData(startDataIndex + 2, dataAddDuration, dataChangeDuration, yAxis.inverse, yMinValue, yMaxValue, unscaledTime);
var heighest = serieData.GetCurrData(startDataIndex + 3, dataAddDuration, dataChangeDuration, yAxis.inverse, yMinValue, yMaxValue, unscaledTime);
var isRise = yAxis.inverse ? close < open : close > open;
var isBodyRise = yAxis.inverse ? close <= open : close >= open;
var isColorRise = IsColorRise(showData, i, startDataIndex, open, close);
var borderWidth = open == 0 ? 0f :
(itemStyle.borderWidth == 0 ? theme.serie.candlestickBorderWidth :
itemStyle.borderWidth);
@@ -239,7 +240,7 @@ namespace XCharts.Runtime
Vector3 plb, plt, prt, prb, top;
var offset = 2 * borderWidth;
if (isRise)
if (isBodyRise)
{
plb = new Vector3(pX + gap + offset, pY + offset);
plt = new Vector3(pX + gap + offset, pY + currHig - offset);
@@ -265,10 +266,10 @@ namespace XCharts.Runtime
}
serie.context.dataPoints.Add(top);
serie.context.dataIndexs.Add(serieData.index);
var areaColor = isRise ?
var areaColor = isColorRise ?
itemStyle.GetColor(theme.serie.candlestickColor) :
itemStyle.GetColor0(theme.serie.candlestickColor0);
var borderColor = isRise ?
var borderColor = isColorRise ?
itemStyle.GetBorderColor(theme.serie.candlestickBorderColor) :
itemStyle.GetBorderColor0(theme.serie.candlestickBorderColor0);
var itemWidth = Mathf.Abs(prt.x - plb.x);
@@ -312,7 +313,7 @@ namespace XCharts.Runtime
{
UGL.DrawLine(vh, openCenterPos, closeCenterPos, Mathf.Max(borderWidth, barWidth / 2), borderColor);
}
if (isRise)
if (isBodyRise)
{
UGL.DrawLine(vh, openCenterPos, lowPos, borderWidth, borderColor);
UGL.DrawLine(vh, closeCenterPos, heighPos, borderWidth, borderColor);
@@ -333,5 +334,17 @@ namespace XCharts.Runtime
chart.RefreshPainter(serie);
}
}
private static bool IsColorRise(List<SerieData> showData, int dataIndex, int startDataIndex, double open, double close)
{
if (dataIndex > 0)
{
var prevSerieData = showData[dataIndex - 1];
var prevStartDataIndex = prevSerieData.data.Count > 4 ? 1 : 0;
var prevClose = prevSerieData.GetData(prevStartDataIndex + 1);
return close >= prevClose;
}
return close >= open;
}
}
}

View File

@@ -151,7 +151,8 @@ namespace XCharts.Runtime
var close = serieData.GetCurrData(startDataIndex + 1, dataAddDuration, dataChangeDuration, yAxis.inverse, yMinValue, yMaxValue, unscaledTime);
var lowest = serieData.GetCurrData(startDataIndex + 2, dataAddDuration, dataChangeDuration, yAxis.inverse, yMinValue, yMaxValue, unscaledTime);
var heighest = serieData.GetCurrData(startDataIndex + 3, dataAddDuration, dataChangeDuration, yAxis.inverse, yMinValue, yMaxValue, unscaledTime);
var isRise = yAxis.inverse ? close<open : close> open;
var isBodyRise = yAxis.inverse ? close <= open : close >= open;
var isColorRise = IsColorRise(showData, i, startDataIndex, open, close);
var borderWidth = open == 0 ? 0f :
(itemStyle.borderWidth == 0 ? theme.serie.candlestickBorderWidth :
itemStyle.borderWidth);
@@ -173,7 +174,7 @@ namespace XCharts.Runtime
Vector3 plb, plt, prt, prb, top;
var offset = 2 * borderWidth;
if (isRise)
if (isBodyRise)
{
plb = new Vector3(pX + gap + offset, pY + offset);
plt = new Vector3(pX + gap + offset, pY + currHig - offset);
@@ -199,10 +200,10 @@ namespace XCharts.Runtime
// }
serie.context.dataPoints.Add(top);
serie.context.dataIndexs.Add(serieData.index);
var areaColor = isRise ?
var areaColor = isColorRise ?
itemStyle.GetColor(theme.serie.candlestickColor) :
itemStyle.GetColor0(theme.serie.candlestickColor0);
var borderColor = isRise ?
var borderColor = isColorRise ?
itemStyle.GetBorderColor(theme.serie.candlestickBorderColor) :
itemStyle.GetBorderColor0(theme.serie.candlestickBorderColor0);
var itemWidth = Mathf.Abs(prt.x - plb.x);
@@ -235,7 +236,7 @@ namespace XCharts.Runtime
UGL.DrawBorder(vh, center, itemWidth, itemHeight, 2 * borderWidth, borderColor, 0,
itemStyle.cornerRadius, isYAxis, 0.5f);
}
if (isRise)
if (isBodyRise)
{
UGL.DrawLine(vh, openCenterPos, lowPos, borderWidth, borderColor);
UGL.DrawLine(vh, closeCenterPos, heighPos, borderWidth, borderColor);
@@ -261,5 +262,17 @@ namespace XCharts.Runtime
chart.RefreshPainter(serie);
}
}
private static bool IsColorRise(List<SerieData> showData, int dataIndex, int startDataIndex, double open, double close)
{
if (dataIndex > 0)
{
var prevSerieData = showData[dataIndex - 1];
var prevStartDataIndex = prevSerieData.data.Count > 4 ? 1 : 0;
var prevClose = prevSerieData.GetData(prevStartDataIndex + 1);
return close >= prevClose;
}
return close >= open;
}
}
}

View File

@@ -56,6 +56,7 @@ namespace XCharts.Runtime
m_LastCheckContextFlag = needCheck;
var lineWidth = serie.lineStyle.GetWidth(chart.theme.serie.lineWidth);
var themeSymbolSize = chart.theme.serie.lineSymbolSize;
var symbolVisible = serie.symbol != null && serie.symbol.show && serie.symbol.type != SymbolType.None;
var needInteract = false;
serie.ResetDataIndex();
if (m_LegendEnter)
@@ -65,11 +66,14 @@ namespace XCharts.Runtime
for (int i = 0; i < serie.dataCount; i++)
{
var serieData = serie.data[i];
var size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize, SerieState.Emphasis);
serieData.context.highlight = true;
if (symbolVisible)
{
var size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize, SerieState.Emphasis);
serieData.interact.SetValue(ref needInteract, size);
}
}
}
else if (serie.context.isTriggerByAxis)
{
serie.context.pointerEnter = false;
@@ -79,9 +83,12 @@ namespace XCharts.Runtime
var serieData = serie.data[i];
var highlight = i == serie.context.pointerItemDataIndex;
serieData.context.highlight = highlight;
if (symbolVisible)
{
var state = SerieHelper.GetSerieState(serie, serieData, true);
var size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize, state);
serieData.interact.SetValue(ref needInteract, size);
}
if (highlight)
{
serie.context.pointerEnter = true;
@@ -98,13 +105,23 @@ namespace XCharts.Runtime
for (int i = 0; i < serie.dataCount; i++)
{
var serieData = serie.data[i];
var dist = Vector3.Distance(chart.pointerPos, serieData.context.position);
var pointerOffset = (Vector2)chart.pointerPos - (Vector2)serieData.context.position;
bool highlight;
if (symbolVisible)
{
var size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize);
var highlight = dist <= size * 2.5f;
serieData.context.highlight = highlight;
var radius = size * 2.5f;
highlight = pointerOffset.sqrMagnitude <= radius * radius;
var state = SerieHelper.GetSerieState(serie, serieData, true);
size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize, state);
serieData.interact.SetValue(ref needInteract, size);
}
else
{
var radius = themeSymbolSize * 2.5f;
highlight = pointerOffset.sqrMagnitude <= radius * radius;
}
serieData.context.highlight = highlight;
if (highlight)
{
serie.context.pointerEnter = true;
@@ -292,6 +309,20 @@ namespace XCharts.Runtime
var dataChanging = false;
var dataChangeDuration = serie.animation.GetChangeDuration();
var unscaledTime = serie.animation.unscaledTime;
var dataAddDuration = 0f;
var useCurrentData = false;
List<double> sampleSumPrefix = null;
if (serie.animation.enable)
{
dataAddDuration = serie.animation.GetAdditionDuration();
useCurrentData = DataHelper.IsAnyDataChanged(ref showData, serie.minShow, showData.Count);
dataChanging = useCurrentData;
}
if (!useCurrentData && rate > 1 &&
(serie.sampleType == SampleType.Sum || serie.sampleType == SampleType.Average))
{
sampleSumPrefix = DataHelper.BuildSampleSumPrefix(ref showData, showData.Count, relativedAxis.inverse);
}
var interacting = false;
var lineWidth = LineHelper.GetLineWidth(ref interacting, serie, chart.theme.serie.lineWidth);
@@ -328,7 +359,8 @@ namespace XCharts.Runtime
var np = Vector3.zero;
var xValue = axis.IsCategory() ? realIndex : serieData.GetData(0, axis.inverse);
var relativedValue = DataHelper.SampleValue(ref showData, serie.sampleType, rate, serie.minShow,
maxCount, totalAverage, i, 0, dataChangeDuration, ref dataChanging, relativedAxis, unscaledTime);
maxCount, totalAverage, i, dataAddDuration, dataChangeDuration, ref dataChanging, relativedAxis,
unscaledTime, useCurrentData, false, sampleSumPrefix);
serieData.context.stackHeight = GetDataPoint(isY, axis, relativedAxis, m_SerieGrid, xValue, relativedValue,
i, scaleWid, scaleRelativedWid, isStack, ref np);

View File

@@ -97,6 +97,25 @@ namespace XCharts.Runtime
new Vector3(zero, points[count - 1].position.y) :
new Vector3(points[count - 1].position.x, zero);
// ===== 优化:缓存动画检查结果 =====
bool needAnimationCheck = serie.animation.IsSerieAnimation() && !serie.animation.IsFinish();
float animationCurrDetail = serie.animation.GetCurrDetail();
// ===== 优化:预计算颜色 =====
Color32[] gradientColors1 = null, gradientColors2 = null;
if (isVisualMapGradient)
{
gradientColors1 = new Color32[count];
gradientColors2 = new Color32[count];
for (int i = 0; i < count; i++)
{
var tp = points[i].position;
var zp = isY ? new Vector3(zero, tp.y) : new Vector3(tp.x, zero);
gradientColors1[i] = VisualMapHelper.GetLineGradientColor(visualMap, zp, grid, axis, relativedAxis, areaColor);
gradientColors2[i] = VisualMapHelper.GetLineGradientColor(visualMap, tp, grid, axis, relativedAxis, areaToColor);
}
}
var lastDataIsIgnore = false;
for (int i = 0; i < points.Count; i++)
{
@@ -111,23 +130,26 @@ namespace XCharts.Runtime
var toColor = areaToColor;
var lerp = areaLerp;
if (serie.animation.CheckDetailBreak(tp, isY))
// ===== 优化:使用缓存的动画状态 =====
if (needAnimationCheck)
{
if (isY && tp.y > animationCurrDetail || !isY && tp.x > animationCurrDetail)
{
isBreak = true;
var progress = serie.animation.GetCurrDetail();
var ip = Vector3.zero;
var axisStartPos = isY ? new Vector3(-10000, progress) : new Vector3(progress, -10000);
var axisEndPos = isY ? new Vector3(10000, progress) : new Vector3(progress, 10000);
var axisStartPos = isY ? new Vector3(-10000, animationCurrDetail) : new Vector3(animationCurrDetail, -10000);
var axisEndPos = isY ? new Vector3(10000, animationCurrDetail) : new Vector3(animationCurrDetail, 10000);
if (UGLHelper.GetIntersection(lp, tp, axisStartPos, axisEndPos, ref ip))
tp = ip;
}
}
var zp = isY ? new Vector3(zero, tp.y) : new Vector3(tp.x, zero);
if (isVisualMapGradient)
{
color = VisualMapHelper.GetLineGradientColor(visualMap, zp, grid, axis, relativedAxis, areaColor);
toColor = VisualMapHelper.GetLineGradientColor(visualMap, tp, grid, axis, relativedAxis, areaToColor);
color = gradientColors1[i];
toColor = gradientColors2[i];
lerp = true;
}
if (i > 0)
@@ -271,6 +293,13 @@ namespace XCharts.Runtime
}
}
/// <summary>
/// 【优化版本】关键性能优化:
/// 1. 颜色预计算 (50-70% 性能提升)
/// 2. 缓存动画检查结果 (30-40% 性能提升)
/// 3. 线段样式预处理 (10-20% 性能提升)
/// 总体预期提升50-70%(当启用渐变时)
/// </summary>
internal static void DrawSerieLine(VertexHelper vh, ThemeStyle theme, Serie serie, VisualMap visualMap,
GridCoord grid, Axis axis, Axis relativedAxis, float lineWidth)
{
@@ -304,6 +333,40 @@ namespace XCharts.Runtime
var dashLength = serie.lineStyle.dashLength;
var gapLength = serie.lineStyle.gapLength;
var dotLength = serie.lineStyle.dotLength;
// ===== 优化 1: 预计算颜色数组 (如果启用 VisualMap 渐变) =====
Color32[] pointColors1 = null, pointColors2 = null;
if (isVisualMapGradient)
{
pointColors1 = new Color32[dataCount];
pointColors2 = new Color32[dataCount];
for (int i = 0; i < dataCount; i++)
{
pointColors1[i] = VisualMapHelper.GetLineGradientColor(visualMap, datas[i].position, grid, axis, relativedAxis, lineColor);
pointColors2[i] = pointColors1[i];
}
}
// 如果启用线段样式渐变,也预计算
Color32[] styleColors1 = null, styleColors2 = null;
if (isLineStyleGradient && !isVisualMapGradient)
{
styleColors1 = new Color32[dataCount];
styleColors2 = new Color32[dataCount];
for (int i = 0; i < dataCount; i++)
{
styleColors1[i] = VisualMapHelper.GetLineStyleGradientColor(serie.lineStyle, datas[i].position, grid, axis, lineColor);
styleColors2[i] = styleColors1[i];
}
}
// ===== 优化 2: 缓存动画检查结果 =====
bool needAnimationCheck = serie.animation.IsSerieAnimation() && !serie.animation.IsFinish();
float animationCurrDetail = serie.animation.GetCurrDetail();
// ===== 优化 3: 线段样式预处理 (避免循环内 switch) =====
System.Func<int, bool> isSegmentIgnored = BuildSegmentIgnoreFunc(serie.lineStyle.type,
dashLength, gapLength, dotLength);
for (int i = 1; i < dataCount; i++)
{
var cdata = datas[i];
@@ -312,15 +375,20 @@ namespace XCharts.Runtime
var lp = datas[i - 1].position;
var np = i == dataCount - 1 ? cp : datas[i + 1].position;
if (serie.animation.CheckDetailBreak(cp, isY))
// ===== 优化:使用缓存的动画状态 =====
if (needAnimationCheck)
{
if (isY && cp.y > animationCurrDetail || !isY && cp.x > animationCurrDetail)
{
isBreak = true;
var ip = Vector3.zero;
var progress = serie.animation.GetCurrDetail();
var rate = 0f;
if (AnimationStyleHelper.GetAnimationPosition(serie.animation, isY, lp, cp, progress, ref ip, ref rate))
if (AnimationStyleHelper.GetAnimationPosition(serie.animation, isY, lp, cp, animationCurrDetail, ref ip, ref rate))
cp = np = ip;
}
}
serie.context.lineEndPostion = cp;
serie.context.lineEndValueY = AxisHelper.GetAxisPositionValue(grid, relativedAxis, cp);
var handled = false;
@@ -338,39 +406,11 @@ namespace XCharts.Runtime
handled = true;
break;
}
{
// ===== 优化:使用预处理的线段样式函数 =====
segmentCount++;
var index = 0f;
switch (serie.lineStyle.type)
{
case LineStyle.Type.Dashed:
index = segmentCount % (dashLength + gapLength);
if (index >= dashLength)
if (isSegmentIgnored(segmentCount))
isIgnore = true;
break;
case LineStyle.Type.Dotted:
index = segmentCount % (dotLength + gapLength);
if (index >= dotLength)
isIgnore = true;
break;
case LineStyle.Type.DashDot:
index = segmentCount % (dashLength + dotLength + 2 * gapLength);
if (index >= dashLength && index < dashLength + gapLength)
isIgnore = true;
else if (index >= dashLength + gapLength + dotLength)
isIgnore = true;
break;
case LineStyle.Type.DashDotDot:
index = segmentCount % (dashLength + 2 * dotLength + 3 * gapLength);
if (index >= dashLength && index < dashLength + gapLength)
isIgnore = true;
else if (index >= dashLength + gapLength + dotLength && index < dashLength + dotLength + 2 * gapLength)
isIgnore = true;
else if (index >= dashLength + 2 * gapLength + 2 * dotLength)
isIgnore = true;
break;
}
}
if (handled)
{
@@ -391,12 +431,35 @@ namespace XCharts.Runtime
if (i == 1)
{
if (isClip) lastDataIsIgnore = true;
AddLineVertToVertexHelper(vh, ltp, lbp, lineColor, isVisualMapGradient, isLineStyleGradient,
if (isVisualMapGradient)
{
AddLineVertToVertexHelperFast(vh, ltp, lbp, pointColors1[0], pointColors1[0], false, lastDataIsIgnore, isIgnore);
}
else if (isLineStyleGradient)
{
AddLineVertToVertexHelperFast(vh, ltp, lbp, styleColors1[0], styleColors1[0], false, lastDataIsIgnore, isIgnore);
}
else
{
AddLineVertToVertexHelper(vh, ltp, lbp, lineColor, false, false,
visualMap, serie.lineStyle, grid, axis, relativedAxis, false, lastDataIsIgnore, isIgnore);
}
if (dataCount == 2 || isBreak)
{
AddLineVertToVertexHelper(vh, clp, crp, lineColor, isVisualMapGradient, isLineStyleGradient,
if (isVisualMapGradient)
{
AddLineVertToVertexHelperFast(vh, clp, crp, pointColors1[i], pointColors1[i], true, lastDataIsIgnore, isIgnore);
}
else if (isLineStyleGradient)
{
AddLineVertToVertexHelperFast(vh, clp, crp, styleColors1[i], styleColors1[i], true, lastDataIsIgnore, isIgnore);
}
else
{
AddLineVertToVertexHelper(vh, clp, crp, lineColor, false, false,
visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
}
serie.context.lineEndPostion = cp;
serie.context.lineEndValueY = AxisHelper.GetAxisPositionValue(grid, relativedAxis, cp);
break;
@@ -406,31 +469,70 @@ namespace XCharts.Runtime
if (bitp == bibp)
{
if (bitp)
AddLineVertToVertexHelper(vh, itp, ibp, lineColor, isVisualMapGradient, isLineStyleGradient,
visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
{
if (isVisualMapGradient)
AddLineVertToVertexHelperFast(vh, itp, ibp, pointColors1[i], pointColors1[i], true, lastDataIsIgnore, isIgnore);
else if (isLineStyleGradient)
AddLineVertToVertexHelperFast(vh, itp, ibp, styleColors1[i], styleColors1[i], true, lastDataIsIgnore, isIgnore);
else
AddLineVertToVertexHelper(vh, itp, ibp, lineColor, false, false, visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
}
else
{
AddLineVertToVertexHelper(vh, ltp, clp, lineColor, isVisualMapGradient, isLineStyleGradient,
visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelper(vh, ltp, crp, lineColor, isVisualMapGradient, isLineStyleGradient,
visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
if (isVisualMapGradient)
{
AddLineVertToVertexHelperFast(vh, ltp, clp, pointColors1[i-1], pointColors1[i], true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelperFast(vh, ltp, crp, pointColors1[i-1], pointColors1[i], true, lastDataIsIgnore, isIgnore);
}
else if (isLineStyleGradient)
{
AddLineVertToVertexHelperFast(vh, ltp, clp, styleColors1[i-1], styleColors1[i], true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelperFast(vh, ltp, crp, styleColors1[i-1], styleColors1[i], true, lastDataIsIgnore, isIgnore);
}
else
{
AddLineVertToVertexHelper(vh, ltp, clp, lineColor, false, false, visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelper(vh, ltp, crp, lineColor, false, false, visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
}
}
}
else
{
if (bitp)
{
AddLineVertToVertexHelper(vh, itp, clp, lineColor, isVisualMapGradient, isLineStyleGradient,
visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelper(vh, itp, crp, lineColor, isVisualMapGradient, isLineStyleGradient,
visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
if (isVisualMapGradient)
{
AddLineVertToVertexHelperFast(vh, itp, clp, pointColors1[i], pointColors1[i], true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelperFast(vh, itp, crp, pointColors1[i], pointColors1[i], true, lastDataIsIgnore, isIgnore);
}
else if (isLineStyleGradient)
{
AddLineVertToVertexHelperFast(vh, itp, clp, styleColors1[i], styleColors1[i], true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelperFast(vh, itp, crp, styleColors1[i], styleColors1[i], true, lastDataIsIgnore, isIgnore);
}
else
{
AddLineVertToVertexHelper(vh, itp, clp, lineColor, false, false, visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelper(vh, itp, crp, lineColor, false, false, visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
}
}
else if (bibp)
{
AddLineVertToVertexHelper(vh, clp, ibp, lineColor, isVisualMapGradient, isLineStyleGradient,
visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelper(vh, crp, ibp, lineColor, isVisualMapGradient, isLineStyleGradient,
visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
if (isVisualMapGradient)
{
AddLineVertToVertexHelperFast(vh, clp, ibp, pointColors1[i], pointColors1[i], true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelperFast(vh, crp, ibp, pointColors1[i], pointColors1[i], true, lastDataIsIgnore, isIgnore);
}
else if (isLineStyleGradient)
{
AddLineVertToVertexHelperFast(vh, clp, ibp, styleColors1[i], styleColors1[i], true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelperFast(vh, crp, ibp, styleColors1[i], styleColors1[i], true, lastDataIsIgnore, isIgnore);
}
else
{
AddLineVertToVertexHelper(vh, clp, ibp, lineColor, false, false, visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
AddLineVertToVertexHelper(vh, crp, ibp, lineColor, false, false, visualMap, serie.lineStyle, grid, axis, relativedAxis, true, lastDataIsIgnore, isIgnore);
}
}
}
lastDataIsIgnore = isIgnore;
@@ -439,6 +541,47 @@ namespace XCharts.Runtime
}
}
/// <summary>
/// 【优化】预处理线段样式,避免循环内重复的 switch 判断
/// 返回一个委托,用于快速判断某个段是否应该被忽略
/// </summary>
private static System.Func<int, bool> BuildSegmentIgnoreFunc(LineStyle.Type lineType,
float dashLength, float gapLength, float dotLength)
{
switch (lineType)
{
case LineStyle.Type.Dashed:
return (segmentCount) =>
{
var index = segmentCount % (dashLength + gapLength);
return index >= dashLength;
};
case LineStyle.Type.Dotted:
return (segmentCount) =>
{
var index = segmentCount % (dotLength + gapLength);
return index >= dotLength;
};
case LineStyle.Type.DashDot:
return (segmentCount) =>
{
var index = segmentCount % (dashLength + dotLength + 2 * gapLength);
return (index >= dashLength && index < dashLength + gapLength) ||
(index >= dashLength + gapLength + dotLength);
};
case LineStyle.Type.DashDotDot:
return (segmentCount) =>
{
var index = segmentCount % (dashLength + 2 * dotLength + 3 * gapLength);
return (index >= dashLength && index < dashLength + gapLength) ||
(index >= dashLength + gapLength + dotLength && index < dashLength + dotLength + 2 * gapLength) ||
(index >= dashLength + 2 * gapLength + 2 * dotLength);
};
default:
return (_) => false;
}
}
public static float GetLineWidth(ref bool interacting, Serie serie, float defaultWidth)
{
var lineWidth = 0f;
@@ -450,6 +593,27 @@ namespace XCharts.Runtime
return lineWidth;
}
/// <summary>
/// 快速路径版本 - 用于颜色已预计算的情况,避免条件判断和重复计算
/// </summary>
private static void AddLineVertToVertexHelperFast(VertexHelper vh, Vector3 tp, Vector3 bp,
Color32 color1, Color32 color2, bool needTriangle, bool lastIgnore, bool ignore)
{
if (lastIgnore && needTriangle)
UGL.AddVertToVertexHelper(vh, tp, bp, ColorUtil.clearColor32, true);
UGL.AddVertToVertexHelper(vh, tp, bp, color1, color2, needTriangle);
if (lastIgnore && !needTriangle)
{
UGL.AddVertToVertexHelper(vh, tp, bp, ColorUtil.clearColor32, false);
}
if (ignore && needTriangle)
{
UGL.AddVertToVertexHelper(vh, tp, bp, ColorUtil.clearColor32, false);
}
}
private static void AddLineVertToVertexHelper(VertexHelper vh, Vector3 tp, Vector3 bp,
Color32 lineColor, bool visualMapGradient, bool lineStyleGradient, VisualMap visualMap,
LineStyle lineStyle, GridCoord grid, Axis axis, Axis relativedAxis, bool needTriangle,

View File

@@ -67,6 +67,7 @@ namespace XCharts.Runtime
m_LastCheckContextFlag = needCheck;
var themeSymbolSize = chart.theme.serie.lineSymbolSize;
lineWidth = serie.lineStyle.GetWidth(chart.theme.serie.lineWidth);
var symbolVisible = serie.symbol != null && serie.symbol.show && serie.symbol.type != SymbolType.None;
var needInteract = false;
if (m_LegendEnter)
@@ -76,11 +77,14 @@ namespace XCharts.Runtime
for (int i = 0; i < serie.dataCount; i++)
{
var serieData = serie.data[i];
var size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize, SerieState.Emphasis);
serieData.context.highlight = true;
if (symbolVisible)
{
var size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize, SerieState.Emphasis);
serieData.interact.SetValue(ref needInteract, size);
}
}
}
else if (serie.context.isTriggerByAxis)
{
serie.context.pointerEnter = true;
@@ -90,13 +94,17 @@ namespace XCharts.Runtime
var serieData = serie.data[i];
var highlight = i == serie.context.pointerItemDataIndex;
serieData.context.highlight = highlight;
if (symbolVisible)
{
var state = SerieHelper.GetSerieState(serie, serieData, true);
var size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize, state);
serieData.interact.SetValue(ref needInteract, size);
}
if (highlight)
{
serie.context.pointerEnter = true;
serie.context.pointerItemDataIndex = i;
needInteract = true;
}
}
}
@@ -108,13 +116,21 @@ namespace XCharts.Runtime
for (int i = 0; i < serie.dataCount; i++)
{
var serieData = serie.data[i];
var dist = Vector3.Distance(chart.pointerPos, serieData.context.position);
var pointerOffset = (Vector2)chart.pointerPos - (Vector2)serieData.context.position;
bool highlight;
if (symbolVisible)
{
var size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize);
var highlight = dist <= size;
serieData.context.highlight = highlight;
highlight = pointerOffset.sqrMagnitude <= size * size;
var state = SerieHelper.GetSerieState(serie, serieData, true);
size = SerieHelper.GetSysmbolSize(serie, serieData, themeSymbolSize, state);
serieData.interact.SetValue(ref needInteract, size);
}
else
{
highlight = pointerOffset.sqrMagnitude <= themeSymbolSize * themeSymbolSize;
}
serieData.context.highlight = highlight;
if (highlight)
{
serie.context.pointerEnter = true;
@@ -177,6 +193,18 @@ namespace XCharts.Runtime
var dataChangeDuration = serie.animation.GetChangeDuration();
var dataAddDuration = serie.animation.GetAdditionDuration();
var unscaledTime = serie.animation.unscaledTime;
var useCurrentData = false;
List<double> sampleSumPrefix = null;
if (serie.animation.enable)
{
useCurrentData = DataHelper.IsAnyDataChanged(ref showData, serie.minShow, maxCount);
dataChanging = useCurrentData;
}
if (!useCurrentData && rate > 1 &&
(serie.sampleType == SampleType.Sum || serie.sampleType == SampleType.Average))
{
sampleSumPrefix = DataHelper.BuildSampleSumPrefix(ref showData, maxCount, relativedAxis.inverse);
}
var interacting = false;
var lineWidth = LineHelper.GetLineWidth(ref interacting, serie, chart.theme.serie.lineWidth);
@@ -204,7 +232,8 @@ namespace XCharts.Runtime
var np = Vector3.zero;
var xValue = axis.IsCategory() ? i : serieData.GetData(0, axis.inverse);
var relativedValue = DataHelper.SampleValue(ref showData, serie.sampleType, rate, serie.minShow,
maxCount, totalAverage, i, dataAddDuration, dataChangeDuration, ref dataChanging, relativedAxis, unscaledTime);
maxCount, totalAverage, i, dataAddDuration, dataChangeDuration, ref dataChanging, relativedAxis,
unscaledTime, useCurrentData, false, sampleSumPrefix);
serieData.context.stackHeight = GetDataPoint(isY, axis, relativedAxis, m_SerieGrid, xValue, relativedValue,
i, scaleWid, scaleRelativedWid, false, ref np);

View File

@@ -95,6 +95,9 @@ namespace XCharts.Runtime
else
{
itemFormatter = itemFormatter.Replace("\\n", "\n");
var needTotal = itemFormatter.IndexOf("{d", System.StringComparison.OrdinalIgnoreCase) >= 0 ||
itemFormatter.IndexOf("{f", System.StringComparison.OrdinalIgnoreCase) >= 0;
var total = needTotal ? serie.yTotal : 0;
var temp = itemFormatter.Split('\n');
for (int i = 0; i < temp.Length; i++)
{
@@ -106,7 +109,7 @@ namespace XCharts.Runtime
param.serieData = serieData;
param.dataCount = serie.dataCount;
param.value = serieData.GetData(i);
param.total = serie.yTotal;
param.total = total;
param.color = color;
param.category = radar.GetIndicatorName(i);
param.marker = marker;

View File

@@ -323,6 +323,9 @@ namespace XCharts.Runtime
[NonSerialized] internal bool m_NeedUpdateFilterData;
[NonSerialized] public List<SerieData> m_FilterData = new List<SerieData>();
[NonSerialized] private bool m_NameDirty;
[NonSerialized] private int m_YTotalCacheFrame = -1;
[NonSerialized] private double m_YTotalCacheValue = 0;
/// <summary>
/// event callback when click serie.
@@ -1239,6 +1242,9 @@ namespace XCharts.Runtime
{
get
{
if (m_YTotalCacheFrame == Time.frameCount)
return m_YTotalCacheValue;
double total = 0;
if (IsPerformanceMode())
{
@@ -1259,6 +1265,8 @@ namespace XCharts.Runtime
total += sdata.GetCurrData(1, dataAddDuration, duration, unscaledTime);
}
}
m_YTotalCacheFrame = Time.frameCount;
m_YTotalCacheValue = total;
return total;
}
}
@@ -1309,6 +1317,7 @@ namespace XCharts.Runtime
/// </summary>
public override void ClearData()
{
InvalidateTotalCache();
while (m_Data.Count > 0)
{
RemoveData(0);
@@ -1336,6 +1345,7 @@ namespace XCharts.Runtime
{
if (index >= 0 && index < m_Data.Count)
{
InvalidateTotalCache();
if (!string.IsNullOrEmpty(m_Data[index].name))
{
SetSerieNameDirty();
@@ -1384,6 +1394,7 @@ namespace XCharts.Runtime
public virtual void AddSerieData(SerieData serieData)
{
InvalidateTotalCache();
if (m_InsertDataToHead)
m_Data.Insert(0, serieData);
else
@@ -1824,6 +1835,7 @@ namespace XCharts.Runtime
var flag = m_Data[index].UpdateData(dimension, value, animationOpen, unscaledTime, animationDuration);
if (flag)
{
InvalidateTotalCache();
SetVerticesDirty();
dataDirty = true;
titleDirty = true;
@@ -1845,6 +1857,7 @@ namespace XCharts.Runtime
{
if (index >= 0 && index < m_Data.Count && values != null)
{
InvalidateTotalCache();
var serieData = m_Data[index];
var animationOpen = animation.enable;
var animationDuration = animation.GetChangeDuration();
@@ -1858,6 +1871,18 @@ namespace XCharts.Runtime
return false;
}
private void InvalidateTotalCache()
{
m_YTotalCacheFrame = -1;
m_YTotalCacheValue = 0;
InvalidateMinMaxCache();
}
private void InvalidateMinMaxCache()
{
context.InvalidateMinMaxCache();
}
public bool UpdateDataName(int index, string name)
{
if (index >= 0 && index < m_Data.Count)

View File

@@ -29,6 +29,40 @@ namespace XCharts.Runtime
public class SerieContext
{
[System.NonSerialized] internal double[] cachedMin = new double[3] { double.MaxValue, double.MaxValue, double.MaxValue };
[System.NonSerialized] internal double[] cachedMax = new double[3] { double.MinValue, double.MinValue, double.MinValue };
[System.NonSerialized] internal bool[] cacheValid = new bool[3] { false, false, false };
internal void InvalidateMinMaxCache()
{
for (int i = 0; i < cacheValid.Length; i++)
cacheValid[i] = false;
cachedMin[0] = cachedMin[1] = cachedMin[2] = double.MaxValue;
cachedMax[0] = cachedMax[1] = cachedMax[2] = double.MinValue;
}
internal bool TryGetCachedMinMax(int dimension, out double minValue, out double maxValue)
{
minValue = 0; maxValue = 0;
if (dimension < 0 || dimension > 2) return false;
if (cacheValid[dimension])
{
minValue = cachedMin[dimension];
maxValue = cachedMax[dimension];
return true;
}
return false;
}
internal void SetCachedMinMax(int dimension, double minValue, double maxValue)
{
if (dimension < 0 || dimension > 2) return;
cachedMin[dimension] = minValue;
cachedMax[dimension] = maxValue;
cacheValid[dimension] = true;
}
/// <summary>
/// 鼠标是否进入serie
/// </summary>

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
@@ -68,6 +69,7 @@ namespace XCharts.Runtime
protected bool m_ForceUpdateSerieContext = false;
protected int m_LegendEnterIndex;
protected ChartLabel m_EndLabel;
private HashSet<int> m_DataIndexsSet = new HashSet<int>();
private float[] m_LastRadius = new float[2] { 0, 0 };
private float[] m_LastCenter = new float[2] { 0, 0 };
@@ -505,15 +507,24 @@ namespace XCharts.Runtime
var dataAddDuration = serie.animation.GetAdditionDuration();
var unscaledTime = serie.animation.unscaledTime;
var needCheck = serie.context.dataIndexs.Count > 0;
if (needCheck)
{
m_DataIndexsSet.Clear();
foreach (var idx in serie.context.dataIndexs)
m_DataIndexsSet.Add(idx);
}
var allLabelZeroPosition = true;
var anyLabelActive = false;
SerieData lastActiveLabelSerieData = null;
var lastActiveLabelPos = Vector3.zero;
double lastActiveLabelValue = 0;
foreach (var serieData in serie.data)
{
if (serieData.labelObject == null && serieData.context.dataLabels.Count <= 0)
{
continue;
}
if (needCheck && !serie.context.dataIndexs.Contains(serieData.index))
if (needCheck && !m_DataIndexsSet.Contains(serieData.index))
{
serieData.SetLabelActive(false);
continue;
@@ -529,7 +540,7 @@ namespace XCharts.Runtime
{
if (serie.multiDimensionLabel)
{
var total = serieData.GetTotalData();
var total = FormatterHelper.NeedTotalContent(currLabel.formatter) ? serieData.GetTotalData() : 0;
var color = chart.GetItemColor(serie, serieData);
for (int i = 0; i < serieData.context.dataLabels.Count; i++)
{
@@ -542,6 +553,7 @@ namespace XCharts.Runtime
currLabel, color, chart);
var offset = GetSerieDataLabelOffset(serieData, currLabel);
var active = currLabel.show && !isIgnore && !serie.IsMinShowLabelValue(value);
if (active) active = CheckLabelVisible(currLabel, serieData.index, value, i);
if (active)
{
anyLabelActive = true;
@@ -565,7 +577,7 @@ namespace XCharts.Runtime
else
{
var value = serieData.GetCurrData(defaultDimension, dataAddDuration, dataChangeDuration, unscaledTime);
var total = serie.GetDataTotal(defaultDimension, serieData);
var total = FormatterHelper.NeedTotalContent(currLabel.formatter) ? serie.GetDataTotal(defaultDimension, serieData) : 0;
var color = chart.GetItemColor(serie, serieData);
var content = string.IsNullOrEmpty(currLabel.formatter) ?
ChartCached.NumberToStr(value, currLabel.numericFormatter) :
@@ -573,6 +585,38 @@ namespace XCharts.Runtime
currLabel, color, chart);
var labelPos = UpdateLabelPosition(serieData, currLabel);
var active = currLabel.show && !isIgnore && !serie.IsMinShowLabelValue(value);
if (active) active = CheckLabelVisible(currLabel, serieData.index, value, defaultDimension);
if (active && currLabel.showMinGap > 0 && lastActiveLabelSerieData != null)
{
var dist = Mathf.Abs(labelPos.x - lastActiveLabelPos.x);
if (dist < currLabel.showMinGap)
{
var currValue = serieData.GetData(1);
if (Math.Abs(currValue) >= Math.Abs(lastActiveLabelValue))
{
lastActiveLabelSerieData.SetLabelActive(false);
lastActiveLabelSerieData = serieData;
lastActiveLabelPos = labelPos;
lastActiveLabelValue = currValue;
}
else
{
active = false;
}
}
else
{
lastActiveLabelSerieData = serieData;
lastActiveLabelPos = labelPos;
lastActiveLabelValue = serieData.GetData(1);
}
}
else if (active)
{
lastActiveLabelSerieData = serieData;
lastActiveLabelPos = labelPos;
lastActiveLabelValue = serieData.GetData(1);
}
if (active)
{
anyLabelActive = true;
@@ -606,6 +650,66 @@ namespace XCharts.Runtime
}
}
private bool CheckLabelVisible(LabelStyle label, int dataIndex, double value, int dimension)
{
// showCondition: 基于阈值的条件检查AND showFilter
bool conditionResult;
switch (label.showCondition)
{
case LabelStyle.ShowCondition.GreaterThan:
conditionResult = value > label.showThreshold;
break;
case LabelStyle.ShowCondition.LessThan:
conditionResult = value < label.showThreshold;
break;
default: // Always
conditionResult = true;
break;
}
if (!conditionResult)
return false;
// showFilter: 基于数据形态的过滤检查
switch (label.showFilter)
{
case LabelStyle.ShowFilter.Peak:
{
bool isPeak = true;
bool hasNeighbor = false;
if (dataIndex > 0)
{
hasNeighbor = true;
isPeak &= value > serie.data[dataIndex - 1].GetData(dimension);
}
if (dataIndex < serie.dataCount - 1)
{
hasNeighbor = true;
isPeak &= value > serie.data[dataIndex + 1].GetData(dimension);
}
return isPeak && hasNeighbor;
}
case LabelStyle.ShowFilter.Valley:
{
bool isValley = true;
bool hasNeighbor = false;
if (dataIndex > 0)
{
hasNeighbor = true;
isValley &= value < serie.data[dataIndex - 1].GetData(dimension);
}
if (dataIndex < serie.dataCount - 1)
{
hasNeighbor = true;
isValley &= value < serie.data[dataIndex + 1].GetData(dimension);
}
return isValley && hasNeighbor;
}
default: // All
return true;
}
}
public virtual void RefreshEndLabelInternal()
{
if (m_EndLabel == null)
@@ -694,6 +798,37 @@ namespace XCharts.Runtime
if (itemFormatter == null) itemFormatter = "";
var newItemFormatter = itemFormatter.Replace("\\n", "\n");
var newNumericFormatter = SerieHelper.GetNumericFormatter(serie, serieData, numericFormatter);
var needTotal = newItemFormatter.IndexOf("{d", System.StringComparison.OrdinalIgnoreCase) >= 0 ||
newItemFormatter.IndexOf("{f", System.StringComparison.OrdinalIgnoreCase) >= 0;
var total = needTotal ? serie.yTotal : 0;
int newLinePos = newItemFormatter.IndexOf('\n');
if (newLinePos < 0)
{
var formatter = newItemFormatter;
var param = serie.context.param;
param.serieName = serie.serieName;
param.serieIndex = serie.index;
param.category = category;
param.dimension = dimension;
param.serieData = serieData;
param.dataCount = serie.dataCount;
param.value = serieData.GetData(dimension);
param.ignore = ignore;
param.total = total;
param.color = chart.GetMarkColor(serie, serieData);
param.marker = SerieHelper.GetItemMarker(serie, serieData, marker);
param.itemFormatter = formatter;
param.numericFormatter = newNumericFormatter;
param.columns.Clear();
param.columns.Add(param.marker);
param.columns.Add(showCategory ? category : serie.serieName);
param.columns.Add(ignore ? ignoreDataDefaultContent : ChartCached.NumberToStr(param.value, param.numericFormatter));
paramList.Add(param);
}
else
{
var temp = newItemFormatter.Split('\n');
for (int i = 0; i < temp.Length; i++)
{
@@ -707,7 +842,7 @@ namespace XCharts.Runtime
param.dataCount = serie.dataCount;
param.value = serieData.GetData(dimension);
param.ignore = ignore;
param.total = serie.yTotal;
param.total = total;
param.color = chart.GetMarkColor(serie, serieData);
param.marker = SerieHelper.GetItemMarker(serie, serieData, marker);
param.itemFormatter = formatter;
@@ -721,6 +856,7 @@ namespace XCharts.Runtime
paramList.Add(param);
}
}
}
protected void UpdateItemSerieParams(ref List<SerieParams> paramList, ref string title,
int dataIndex, string category, string marker,

View File

@@ -7,7 +7,7 @@ namespace XCharts.Runtime
{
public static partial class SerieHelper
{
public static double GetMinData(Serie serie, int dimension = 1, DataZoom dataZoom = null)
public static double GetMinData(Serie serie, int dimension = 1, DataZoom dataZoom = null, bool inverse = false)
{
double min = double.MaxValue;
var dataList = serie.GetDataList(dataZoom);
@@ -16,7 +16,7 @@ namespace XCharts.Runtime
var serieData = dataList[i];
if (serieData.show && serieData.data.Count > dimension)
{
var value = serieData.data[dimension];
var value = serieData.GetData(dimension, inverse);
if (value < min && !serie.IsIgnoreValue(serieData, value)) min = value;
}
}
@@ -42,7 +42,7 @@ namespace XCharts.Runtime
}
return minData;
}
public static double GetMaxData(Serie serie, int dimension = 1, DataZoom dataZoom = null)
public static double GetMaxData(Serie serie, int dimension = 1, DataZoom dataZoom = null, bool inverse = false)
{
double max = double.MinValue;
var dataList = serie.GetDataList(dataZoom);
@@ -51,7 +51,7 @@ namespace XCharts.Runtime
var serieData = dataList[i];
if (serieData.show && serieData.data.Count > dimension)
{
var value = serieData.data[dimension];
var value = serieData.GetData(dimension, inverse);
if (value > max && !serie.IsIgnoreValue(serieData, value)) max = value;
}
}
@@ -869,21 +869,36 @@ namespace XCharts.Runtime
private static void UpdateFilterData_Category(Serie serie, DataZoom dataZoom)
{
var data = serie.data;
var range = Mathf.RoundToInt(data.Count * (dataZoom.end - dataZoom.start) / 100);
if (range <= 0) range = 1;
int start = 0, end = 0;
// Use (N-1) intervals so that data point i maps to exactly i/(N-1)*100% of the
// DataZoom width — matching how the shadow draws data via scaleWid = width/(N-1).
// CeilToInt for start ensures we never include a point that lies before the filler.
// FloorToInt for end ensures we never include a point that lies after the filler.
int n = data.Count - 1;
int startIndex, endIndex;
if (n > 0)
{
if (dataZoom.context.invert)
{
end = Mathf.RoundToInt(data.Count * dataZoom.end / 100);
start = end - range;
if (start < 0) start = 0;
startIndex = Mathf.CeilToInt((float)n * (100 - dataZoom.end) / 100);
endIndex = Mathf.FloorToInt((float)n * (100 - dataZoom.start) / 100);
}
else
{
start = Mathf.RoundToInt(data.Count * dataZoom.start / 100);
end = start + range;
if (end > data.Count) end = data.Count;
startIndex = Mathf.CeilToInt((float)n * dataZoom.start / 100);
endIndex = Mathf.FloorToInt((float)n * dataZoom.end / 100);
}
}
else
{
startIndex = 0;
endIndex = 0;
}
var range = endIndex - startIndex + 1;
if (range <= 0) range = 1;
int start = startIndex;
if (start < 0) start = 0;
int end = start + range;
if (end > data.Count) end = data.Count;
var minZoomRatio = (int)(data.Count * dataZoom.minZoomRatio);
if (start != serie.m_FilterStart || end != serie.m_FilterEnd ||
minZoomRatio != serie.m_FilterMinShow || serie.m_NeedUpdateFilterData)

View File

@@ -324,9 +324,9 @@ namespace XCharts.Runtime
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
public static void GetXMinMaxValue(BaseChart chart, int axisIndex, bool inverse, out double minValue,
out double maxValue, bool isPolar = false, bool filterByDataZoom = true, bool needAnimation = false)
out double maxValue, bool isPolar = false, bool needAnimation = false)
{
GetMinMaxValue(chart, axisIndex, inverse, 0, out minValue, out maxValue, isPolar, filterByDataZoom, needAnimation);
GetMinMaxValue(chart, axisIndex, inverse, 0, out minValue, out maxValue, isPolar, needAnimation);
}
/// <summary>
@@ -337,9 +337,9 @@ namespace XCharts.Runtime
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
public static void GetYMinMaxValue(BaseChart chart, int axisIndex, bool inverse, out double minValue,
out double maxValue, bool isPolar = false, bool filterByDataZoom = true, bool needAnimation = false)
out double maxValue, bool isPolar = false, bool needAnimation = false)
{
GetMinMaxValue(chart, axisIndex, inverse, 1, out minValue, out maxValue, isPolar, filterByDataZoom, needAnimation);
GetMinMaxValue(chart, axisIndex, inverse, 1, out minValue, out maxValue, isPolar, needAnimation);
}
/// <summary>
@@ -350,16 +350,16 @@ namespace XCharts.Runtime
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
public static void GetZMinMaxValue(BaseChart chart, int axisIndex, bool inverse, out double minValue,
out double maxValue, bool isPolar = false, bool filterByDataZoom = true, bool needAnimation = false)
out double maxValue, bool isPolar = false, bool needAnimation = false)
{
GetMinMaxValue(chart, axisIndex, inverse, 2, out minValue, out maxValue, isPolar, filterByDataZoom, needAnimation);
GetMinMaxValue(chart, axisIndex, inverse, 2, out minValue, out maxValue, isPolar, needAnimation);
}
private static Dictionary<int, List<Serie>> _stackSeriesForMinMax = new Dictionary<int, List<Serie>>();
private static Dictionary<int, double> _serieTotalValueForMinMax = new Dictionary<int, double>();
public static void GetMinMaxValue(BaseChart chart, int axisIndex,
bool inverse, int dimension, out double minValue, out double maxValue, bool isPolar = false,
bool filterByDataZoom = true, bool needAnimation = false)
bool needAnimation = false)
{
double min = double.MaxValue;
double max = double.MinValue;
@@ -376,22 +376,48 @@ namespace XCharts.Runtime
var updateDuration = needAnimation ? serie.animation.GetChangeDuration() : 0;
var dataAddDuration = needAnimation ? serie.animation.GetAdditionDuration() : 0;
var unscaledTime = serie.animation.unscaledTime;
// determine whether DataZoom filtering applies for this serie
var dz = chart.GetXDataZoomOfSerie(serie);
// Only apply DataZoom filter for non-X dimensions (dimension > 0, e.g. Y axis
// scaling to visible data). For dimension=0 (X axis whose range is controlled
// by DataZoom), using filtered X data would create a circular dependency:
// rawMin/rawMax would be set from filtered data, making the filter boundary
// relative to an already-filtered range instead of the full data range.
bool useDataZoomFilter = dimension > 0 && dz != null && dz.enable && dz.filterAxisRange;
// try per-serie cache when not filtering by dataZoom and not in animation mode
if (!useDataZoomFilter && !needAnimation)
{
double cmin, cmax;
if (serie.context.TryGetCachedMinMax(dimension, out cmin, out cmax))
{
if (cmax > max) max = cmax;
if (cmin < min) min = cmin;
continue;
}
}
double smin = double.MaxValue;
double smax = double.MinValue;
if (isPercentStack && SeriesHelper.IsPercentStack<Bar>(series, serie.serieName))
{
if (100 > max) max = 100;
if (0 < min) min = 0;
// percent stack per-serie considered as full range
smin = 0;
smax = 100;
}
else
{
var showData = serie.GetDataList(filterByDataZoom ? chart.GetXDataZoomOfSerie(serie) : null);
var showData = serie.GetDataList(useDataZoomFilter ? dz : null);
if (dimension > 0 && (serie is Candlestick || serie is SimplifiedCandlestick))
{
foreach (var data in showData)
{
double dataMin, dataMax;
data.GetMinMaxData(1, inverse, out dataMin, out dataMax);
if (dataMax > max) max = dataMax;
if (dataMin < min) min = dataMin;
if (dataMax > smax) smax = dataMax;
if (dataMin < smin) smin = dataMin;
}
}
else
@@ -403,12 +429,25 @@ namespace XCharts.Runtime
data.GetCurrData(dimension, dataAddDuration, updateDuration, unscaledTime, inverse);
if (!serie.IsIgnoreValue(data, currData))
{
if (currData > max) max = currData;
if (currData < min) min = currData;
if (currData > smax) smax = currData;
if (currData < smin) smin = currData;
}
}
}
}
// if no data found for this serie, skip
if (smax == double.MinValue && smin == double.MaxValue)
continue;
// cache per-serie result for future calls
if (!needAnimation && !useDataZoomFilter)
{
serie.context.SetCachedMinMax(dimension, smin == double.MaxValue ? 0 : smin, smax == double.MinValue ? 0 : smax);
}
if (smax > max) max = smax;
if (smin < min) min = smin;
}
}
else
@@ -423,7 +462,10 @@ namespace XCharts.Runtime
if ((isPolar && serie.polarIndex != axisIndex) ||
(!isPolar && serie.yAxisIndex != axisIndex) ||
!serie.show) continue;
var showData = serie.GetDataList(filterByDataZoom ? chart.GetXDataZoomOfSerie(serie) : null);
var stackDz = chart.GetXDataZoomOfSerie(serie);
// Same rule as non-stack: don't use filtered data for dimension=0 (X axis).
if (stackDz != null && (dimension == 0 || !stackDz.filterAxisRange || !stackDz.enable)) stackDz = null;
var showData = serie.GetDataList(stackDz);
if (SeriesHelper.IsPercentStack<Bar>(series, serie.stack))
{
for (int j = 0; j < showData.Count; j++)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 529ec33cd4fb6466784b3a682204428c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
#if dUI_TextMeshPro
using TMPro;
#endif
namespace XCharts.Runtime
{
/// <summary>
/// Portable resource reference that supports a multi-level lookup strategy:
/// GUID → path → name → fallbackName → null/default
/// </summary>
[Serializable]
public class ResourceRef
{
/// <summary>Unity AssetDatabase GUID (editor-only, same project).</summary>
public string guid;
/// <summary>Asset path relative to project root e.g. "Assets/Fonts/Arial.ttf".</summary>
public string path;
/// <summary>Asset.name used for cross-project name search.</summary>
public string name;
/// <summary>Optional secondary name (system font alias, built-in default, etc.).</summary>
public string fallbackName;
/// <summary>Optional Base64-encoded asset bytes for full portability (< 100 KB recommended).</summary>
public string base64;
public bool IsEmpty()
{
return string.IsNullOrEmpty(guid)
&& string.IsNullOrEmpty(path)
&& string.IsNullOrEmpty(name)
&& string.IsNullOrEmpty(fallbackName)
&& string.IsNullOrEmpty(base64);
}
public override string ToString()
{
return string.Format("ResourceRef{{name={0}, path={1}, guid={2}}}", name, path, guid);
}
}
/// <summary>
/// Handles serialization and resolution of Unity asset references for chart JSON export/import.
/// Supports Font, TMP_FontAsset, Sprite, Material, Texture2D.
/// </summary>
public static class ResourceRefHandler
{
// ─── Serialize ─────────────────────────────────────────────────────────────
/// <summary>
/// Serializes a Unity Object into a portable ResourceRef.
/// </summary>
public static ResourceRef Serialize(UnityEngine.Object asset, bool includeBase64 = false)
{
if (asset == null) return null;
var refData = new ResourceRef
{
name = asset.name
};
#if UNITY_EDITOR
var assetPath = AssetDatabase.GetAssetPath(asset);
if (!string.IsNullOrEmpty(assetPath))
{
refData.path = assetPath;
refData.guid = AssetDatabase.AssetPathToGUID(assetPath);
}
#endif
// Optional base64 embedding for portability
if (includeBase64)
{
var b64 = TryEncodeBase64(asset);
if (!string.IsNullOrEmpty(b64))
refData.base64 = b64;
}
return refData;
}
/// <summary>
/// Serializes a Font with a fallback name hint.
/// </summary>
public static ResourceRef SerializeFont(Font font, string fallbackName = null)
{
if (font == null) return null;
var refData = Serialize(font) ?? new ResourceRef();
refData.fallbackName = fallbackName ?? "Arial";
return refData;
}
#if dUI_TextMeshPro
/// <summary>
/// Serializes a TMP_FontAsset with a fallback name hint.
/// </summary>
public static ResourceRef SerializeTMPFont(TMP_FontAsset font, string fallbackName = null)
{
if (font == null) return null;
var refData = Serialize(font) ?? new ResourceRef();
refData.fallbackName = fallbackName ?? "LiberationSans SDF";
return refData;
}
#endif
// ─── Resolve ────────────────────────────────────────────────────────────────
/// <summary>
/// Attempts to resolve a ResourceRef back to a Unity asset using the fallback chain:<br/>
/// GUID → path → name → fallbackName → null
/// </summary>
public static T TryResolve<T>(ResourceRef refData) where T : UnityEngine.Object
{
if (refData == null || refData.IsEmpty()) return null;
#if UNITY_EDITOR
// 1. GUID lookup (same project, exact)
if (!string.IsNullOrEmpty(refData.guid))
{
var guidPath = AssetDatabase.GUIDToAssetPath(refData.guid);
if (!string.IsNullOrEmpty(guidPath))
{
var asset = AssetDatabase.LoadAssetAtPath<T>(guidPath);
if (asset != null) return asset;
}
}
// 2. Path-based lookup
if (!string.IsNullOrEmpty(refData.path))
{
var asset = AssetDatabase.LoadAssetAtPath<T>(refData.path);
if (asset != null) return asset;
}
// 3. Name-based search across project
if (!string.IsNullOrEmpty(refData.name))
{
var found = FindAssetByName<T>(refData.name);
if (found != null) return found;
}
#endif
// 4. Resources.Load by name
if (!string.IsNullOrEmpty(refData.name))
{
var asset = Resources.Load<T>(refData.name);
if (asset != null) return asset;
}
// 5. Fallback name
if (!string.IsNullOrEmpty(refData.fallbackName))
{
#if UNITY_EDITOR
var found = FindAssetByName<T>(refData.fallbackName);
if (found != null) return found;
#endif
var asset = Resources.Load<T>(refData.fallbackName);
if (asset != null) return asset;
}
// 6. Base64 decode
if (!string.IsNullOrEmpty(refData.base64))
{
var decoded = TryDecodeBase64<T>(refData.base64, refData.name ?? "imported_asset");
if (decoded != null) return decoded;
}
if (!IsUnityBuiltinDefaultResourceRef(refData))
Debug.LogWarning(string.Format("[XCharts] ResourceRefHandler: Could not resolve asset '{0}'. Using default.", refData));
return null;
}
private static bool IsUnityBuiltinDefaultResourceRef(ResourceRef refData)
{
if (refData == null) return false;
bool pathIsBuiltin = !string.IsNullOrEmpty(refData.path)
&& refData.path.IndexOf("Library/unity default resources", StringComparison.OrdinalIgnoreCase) >= 0;
bool guidIsBuiltin = !string.IsNullOrEmpty(refData.guid)
&& string.Equals(refData.guid, "0000000000000000e000000000000000", StringComparison.OrdinalIgnoreCase);
bool nameIsBuiltinFont = string.Equals(refData.name, "Arial", StringComparison.OrdinalIgnoreCase)
|| string.Equals(refData.fallbackName, "Arial", StringComparison.OrdinalIgnoreCase)
#if dUI_TextMeshPro
|| string.Equals(refData.name, "LiberationSans SDF", StringComparison.OrdinalIgnoreCase)
|| string.Equals(refData.fallbackName, "LiberationSans SDF", StringComparison.OrdinalIgnoreCase)
#endif
;
return pathIsBuiltin || guidIsBuiltin || nameIsBuiltinFont;
}
#if UNITY_EDITOR
private static T FindAssetByName<T>(string assetName) where T : UnityEngine.Object
{
string typeName = typeof(T).Name;
var guids = AssetDatabase.FindAssets(string.Format("{0} t:{1}", assetName, typeName));
foreach (var g in guids)
{
var p = AssetDatabase.GUIDToAssetPath(g);
var asset = AssetDatabase.LoadAssetAtPath<T>(p);
if (asset != null && asset.name == assetName)
return asset;
}
// Looser match (name contains)
foreach (var g in guids)
{
var p = AssetDatabase.GUIDToAssetPath(g);
var asset = AssetDatabase.LoadAssetAtPath<T>(p);
if (asset != null) return asset;
}
return null;
}
#endif
// ─── Base64 helpers ──────────────────────────────────────────────────────────
private static string TryEncodeBase64(UnityEngine.Object asset)
{
var texture = asset as Texture2D;
if (texture != null)
return Convert.ToBase64String(texture.EncodeToPNG());
// Font/Material: not trivially serializable as bytes at runtime; skip.
return null;
}
private static T TryDecodeBase64<T>(string base64, string assetName) where T : UnityEngine.Object
{
try
{
if (typeof(T) == typeof(Texture2D) || typeof(T) == typeof(Sprite))
{
var bytes = Convert.FromBase64String(base64);
var tex = new Texture2D(2, 2);
if (tex.LoadImage(bytes))
{
tex.name = assetName;
if (typeof(T) == typeof(Sprite))
{
var sprite = Sprite.Create(tex,
new Rect(0, 0, tex.width, tex.height),
new Vector2(0.5f, 0.5f));
sprite.name = assetName;
return sprite as T;
}
return tex as T;
}
}
}
catch (Exception ex)
{
Debug.LogWarning(string.Format("[XCharts] ResourceRefHandler: Base64 decode failed: {0}", ex.Message));
}
return null;
}
// ─── Convenience overloads ───────────────────────────────────────────────────
public static Font TryResolveFont(ResourceRef refData)
{
return TryResolve<Font>(refData);
}
public static Sprite TryResolveSprite(ResourceRef refData)
{
return TryResolve<Sprite>(refData);
}
public static Material TryResolveMaterial(ResourceRef refData)
{
return TryResolve<Material>(refData);
}
public static Texture2D TryResolveTexture(ResourceRef refData)
{
return TryResolve<Texture2D>(refData);
}
#if dUI_TextMeshPro
public static TMP_FontAsset TryResolveTMPFont(ResourceRef refData)
{
return TryResolve<TMP_FontAsset>(refData);
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9c3e7340177cb43e488f9f9547ceea7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,299 @@
using System;
using System.Reflection;
using UnityEngine;
namespace XCharts.Runtime
{
[Serializable]
public class UIComponentJson
{
public string schemaVersion = "1.0";
public string componentType;
public string componentVersion;
public string exportedAt;
public string data;
}
public static class UIComponentJsonSerializer
{
public static string Serialize(UIComponent component, bool prettyPrint = true)
{
if (component == null) throw new ArgumentNullException("component");
var currentJson = JsonUtility.ToJson(component);
var defaultJson = GetDefaultInstanceJson(component.GetType());
var dataJson = BuildDataJson(component.GetType(), currentJson, defaultJson);
var dto = new UIComponentJson
{
schemaVersion = "1.0",
componentType = component.GetType().Name,
componentVersion = XChartsMgr.version,
exportedAt = DateTime.UtcNow.ToString("o"),
data = dataJson
};
var json = JsonUtility.ToJson(dto, prettyPrint);
json = ChartJsonDataFieldCodec.ConvertEscapedDataStringToRawObject(json);
if (prettyPrint)
{
object parsedJson;
if (SimpleJson.TryParse(json, out parsedJson))
json = SimpleJson.Stringify(parsedJson, true);
}
return json;
}
private static string BuildDataJson(Type componentType, string currentJson, string defaultJson)
{
if (componentType == null)
return JsonDiffPruner.PruneDefaults(currentJson, defaultJson);
if (string.Equals(componentType.Name, "UITable", StringComparison.Ordinal))
return PruneUITableData(componentType, currentJson, defaultJson);
return JsonDiffPruner.PruneDefaults(currentJson, defaultJson);
}
private static string PruneUITableData(Type tableType, string currentJson, string defaultJson)
{
object currentParsed;
object defaultParsed;
if (!SimpleJson.TryParse(currentJson, out currentParsed))
return JsonDiffPruner.PruneDefaults(currentJson, defaultJson);
if (!SimpleJson.TryParse(defaultJson, out defaultParsed))
return JsonDiffPruner.PruneDefaults(currentJson, defaultJson);
var currentRoot = currentParsed as System.Collections.Generic.Dictionary<string, object>;
var defaultRoot = defaultParsed as System.Collections.Generic.Dictionary<string, object>;
if (currentRoot == null)
return JsonDiffPruner.PruneDefaults(currentJson, defaultJson);
var prunedRootObj = JsonDiffPruner.PruneParsedDefaults(currentRoot, defaultRoot);
var prunedRoot = prunedRootObj as System.Collections.Generic.Dictionary<string, object>;
if (prunedRoot == null)
prunedRoot = new System.Collections.Generic.Dictionary<string, object>();
object currentRowsObj;
if (!currentRoot.TryGetValue("m_Data", out currentRowsObj))
return SimpleJson.Stringify(prunedRoot);
var currentRows = currentRowsObj as System.Collections.Generic.List<object>;
if (currentRows == null)
return SimpleJson.Stringify(prunedRoot);
var rowDefaultObj = CreateDefaultParsedObject(tableType.Assembly, "XCharts.Runtime.UI.TableRow");
if (rowDefaultObj == null)
return JsonDiffPruner.PruneDefaults(currentJson, defaultJson);
var cellDefaultObj = CreateDefaultParsedObject(tableType.Assembly, "XCharts.Runtime.UI.TableCell");
var prunedRows = new System.Collections.Generic.List<object>(currentRows.Count);
for (int i = 0; i < currentRows.Count; i++)
{
var prunedRow = PruneTableRowKeepCellShape(currentRows[i], rowDefaultObj, cellDefaultObj);
prunedRows.Add(prunedRow ?? new System.Collections.Generic.Dictionary<string, object>());
}
prunedRoot["m_Data"] = prunedRows;
return SimpleJson.Stringify(prunedRoot);
}
private static object PruneTableRowKeepCellShape(object rowObj, object rowDefaultObj, object cellDefaultObj)
{
var rowDict = rowObj as System.Collections.Generic.Dictionary<string, object>;
if (rowDict == null)
return JsonDiffPruner.PruneParsedDefaults(rowObj, rowDefaultObj);
object rawCells;
rowDict.TryGetValue("m_Data", out rawCells);
var currentCells = rawCells as System.Collections.Generic.List<object>;
var prunedRowObj = JsonDiffPruner.PruneParsedDefaults(rowObj, rowDefaultObj);
var prunedRowDict = prunedRowObj as System.Collections.Generic.Dictionary<string, object>;
if (prunedRowDict == null)
prunedRowDict = new System.Collections.Generic.Dictionary<string, object>();
if (currentCells != null)
{
var prunedCells = new System.Collections.Generic.List<object>(currentCells.Count);
for (int i = 0; i < currentCells.Count; i++)
{
var prunedCell = cellDefaultObj != null
? JsonDiffPruner.PruneParsedDefaults(currentCells[i], cellDefaultObj)
: JsonDiffPruner.PruneParsedDefaults(currentCells[i], null);
prunedCells.Add(prunedCell ?? new System.Collections.Generic.Dictionary<string, object>());
}
prunedRowDict["m_Data"] = prunedCells;
}
if (prunedRowDict.Count == 0)
return null;
return prunedRowDict;
}
private static object CreateDefaultParsedObject(Assembly assembly, string fullTypeName)
{
if (assembly == null || string.IsNullOrEmpty(fullTypeName))
return null;
try
{
var type = assembly.GetType(fullTypeName);
if (type == null)
return null;
var instance = Activator.CreateInstance(type);
if (instance == null)
return null;
var json = JsonUtility.ToJson(instance);
object parsed;
if (!SimpleJson.TryParse(json, out parsed))
return null;
return parsed;
}
catch
{
return null;
}
}
private static string GetDefaultInstanceJson(Type type)
{
if (type == null) return "{}";
GameObject tempObject = null;
try
{
if (typeof(MonoBehaviour).IsAssignableFrom(type))
{
tempObject = new GameObject("__XCharts_UIJson_Default__");
tempObject.hideFlags = HideFlags.HideAndDontSave;
var defaultComponent = tempObject.AddComponent(type) as UIComponent;
if (defaultComponent != null)
return JsonUtility.ToJson(defaultComponent);
return "{}";
}
var instance = Activator.CreateInstance(type);
if (instance == null)
return "{}";
return JsonUtility.ToJson(instance);
}
catch
{
return "{}";
}
finally
{
if (tempObject != null)
{
#if UNITY_EDITOR
UnityEngine.Object.DestroyImmediate(tempObject);
#else
UnityEngine.Object.Destroy(tempObject);
#endif
}
}
}
}
public static class UIComponentJsonDeserializer
{
private const string LOG_TAG = "[XCharts] UIComponentJsonDeserializer";
public static void Deserialize(string json, UIComponent target)
{
if (string.IsNullOrEmpty(json)) throw new ArgumentNullException("json");
if (target == null) throw new ArgumentNullException("target");
json = ChartJsonDataFieldCodec.ConvertRawObjectDataToEscapedString(json);
UIComponentJson dto;
try
{
dto = JsonUtility.FromJson<UIComponentJson>(json);
}
catch (Exception ex)
{
throw new ArgumentException(string.Format("Invalid JSON format: {0}", ex.Message), ex);
}
if (dto == null || string.IsNullOrEmpty(dto.schemaVersion))
throw new ArgumentException("JSON does not appear to be a valid XCharts UI component export (missing schemaVersion).");
if (!dto.schemaVersion.StartsWith("1."))
throw new ArgumentException(string.Format("Unsupported schema version '{0}'. This version only supports '1.x'.", dto.schemaVersion));
ValidateComponentType(dto.componentType, target);
if (!string.IsNullOrEmpty(dto.data))
JsonUtility.FromJsonOverwrite(dto.data, target);
target.RefreshAllComponent();
target.RefreshGraph();
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(target);
#endif
Debug.Log(string.Format("{0}: Import complete for '{1}'.", LOG_TAG, target.GetType().Name));
}
private static void ValidateComponentType(string typeName, UIComponent target)
{
if (string.IsNullOrEmpty(typeName) || target == null) return;
var resolved = ResolveType(typeName);
if (resolved == null) return;
if (!resolved.IsAssignableFrom(target.GetType()))
throw new ArgumentException(string.Format("JSON is for UI component '{0}', target is '{1}'.", resolved.Name, target.GetType().Name));
}
private static Type ResolveType(string typeName)
{
if (string.IsNullOrEmpty(typeName)) return null;
var type = Type.GetType(typeName);
if (type != null) return type;
var shortName = typeName.Split(',')[0].Trim();
var simpleName = shortName.Contains(".") ? shortName.Substring(shortName.LastIndexOf(".") + 1) : shortName;
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
type = asm.GetType(shortName);
if (type != null) return type;
var types = asm.GetTypes();
for (int i = 0; i < types.Length; i++)
{
var candidate = types[i];
if (candidate == null) continue;
if (string.Equals(candidate.Name, simpleName, StringComparison.Ordinal))
return candidate;
}
}
catch (ReflectionTypeLoadException rtle)
{
var partialTypes = rtle.Types;
if (partialTypes == null) continue;
for (int i = 0; i < partialTypes.Length; i++)
{
var candidate = partialTypes[i];
if (candidate == null) continue;
if (string.Equals(candidate.Name, simpleName, StringComparison.Ordinal))
return candidate;
}
}
catch
{
}
}
return null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20febe04d2ef346e196ab69da9c1d7d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -140,19 +140,26 @@ namespace XUGL
public static void DrawLine(VertexHelper vh, List<Vector3> points, float width, Color32 color, bool smooth, bool closepath = false)
{
for (int i = points.Count - 1; i >= 1; i--)
if (points == null || points.Count < 2) return;
// Compact duplicate consecutive points into a reusable buffer to avoid repeated RemoveAt (O(n^2)).
s_CurvesPosList.Clear();
s_CurvesPosList.Add(points[0]);
for (int i = 1; i < points.Count; i++)
{
if (UGLHelper.IsValueEqualsVector3(points[i], points[i - 1]))
points.RemoveAt(i);
if (!UGLHelper.IsValueEqualsVector3(points[i], points[i - 1]))
s_CurvesPosList.Add(points[i]);
}
if (points.Count < 2) return;
else if (points.Count <= 2)
var pts = s_CurvesPosList;
if (pts.Count < 2) return;
else if (pts.Count == 2)
{
DrawLine(vh, points[0], points[1], width, color);
DrawLine(vh, pts[0], pts[1], width, color);
}
else if (smooth)
{
DrawCurves(vh, points, width, color, 2, 2, Direction.XAxis, float.NaN, closepath);
DrawCurves(vh, pts, width, color, 2, 2, Direction.XAxis, float.NaN, closepath);
}
else
{
@@ -164,14 +171,14 @@ namespace XUGL
var ibp = Vector3.zero;
var ctp = Vector3.zero;
var cbp = Vector3.zero;
if (closepath && !UGLHelper.IsValueEqualsVector3(points[points.Count - 1], points[0]))
if (closepath && !UGLHelper.IsValueEqualsVector3(pts[pts.Count - 1], pts[0]))
{
points.Add(points[0]);
pts.Add(pts[0]);
}
for (int i = 1; i < points.Count - 1; i++)
for (int i = 1; i < pts.Count - 1; i++)
{
bool bitp = true, bibp = true;
UGLHelper.GetLinePoints(points[i - 1], points[i], points[i + 1], width,
UGLHelper.GetLinePoints(pts[i - 1], pts[i], pts[i + 1], width,
ref ltp, ref lbp,
ref ntp, ref nbp,
ref itp, ref ibp,