Compare commits

..

9 Commits

24 changed files with 2873 additions and 36 deletions

View File

@@ -81,6 +81,17 @@ slug: /changelog
## master
* (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

@@ -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

@@ -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

@@ -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

@@ -255,22 +255,47 @@ namespace XCharts.Runtime
if (dataZoom.IsInZoom(localPos) &&
!dataZoom.IsInSelectedZoom(localPos))
{
var pointerX = localPos.x;
var selectWidth = grid.context.width * (dataZoom.end - dataZoom.start) / 100;
var startX = pointerX - selectWidth / 2;
var endX = pointerX + selectWidth / 2;
if (startX < grid.context.x)
var start = dataZoom.start;
var end = dataZoom.end;
switch (dataZoom.orient)
{
startX = grid.context.x;
endX = grid.context.x + selectWidth;
case Orient.Horizonal:
var pointerX = localPos.x;
var selectWidth = dataZoom.context.width * (dataZoom.end - dataZoom.start) / 100;
var startX = pointerX - selectWidth / 2;
var endX = pointerX + selectWidth / 2;
if (startX < dataZoom.context.x)
{
startX = dataZoom.context.x;
endX = dataZoom.context.x + selectWidth;
}
else if (endX > dataZoom.context.x + dataZoom.context.width)
{
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;
}
else if (endX > grid.context.x + grid.context.width)
{
endX = grid.context.x + grid.context.width;
startX = grid.context.x + grid.context.width - selectWidth;
}
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);
}
}

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

@@ -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

@@ -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

@@ -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;
}
}
}

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: