using System; using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using EasyChart; namespace EasyChart.Editor { public partial class EasyChartLibraryWindow { private const string JsonPanelCollapsedPrefsKey = "EasyChart.LibraryWindow.JsonPanelCollapsed"; private bool _jsonPanelCollapsed; private bool _jsonExampleDirtyByUser; private enum JsonDatasMode { Values, Standard, Full } private static readonly List JsonDatasModeChoices = new List { "Values", "Standard", "Full" }; private JsonDatasMode _jsonDatasMode = JsonDatasMode.Standard; private PopupField _jsonDatasModeDropdown; private struct JsonExampleModeOption { public string label; public JsonExampleMode mode; public JsonExampleModeOption(string label, JsonExampleMode mode) { this.label = label; this.mode = mode; } } private static readonly List JsonExampleModeOptions = new List { new JsonExampleModeOption("Lite", JsonExampleMode.Lite_Index), new JsonExampleModeOption("Standard / ID", JsonExampleMode.Lite_ID), new JsonExampleModeOption("Standard / Default", JsonExampleMode.Standard), new JsonExampleModeOption("Standard / With Axes", JsonExampleMode.Standard_Axis), new JsonExampleModeOption("Full", JsonExampleMode.Full) }; private static readonly List JsonExampleModeChoices = new List { "Lite", "Standard / ID", "Standard / Default", "Standard / With Axes", "Full" }; private void BuildInjectionJsonPanel(VisualElement leftPanel) { if (leftPanel == null) return; _jsonPanelCollapsed = EditorPrefs.GetBool(JsonPanelCollapsedPrefsKey, false); _jsonExampleContainer = new VisualElement(); void ApplyJsonPanelHeight() { _jsonExampleContainer.style.height = _jsonPanelCollapsed ? 200 : 520; } ApplyJsonPanelHeight(); _jsonExampleContainer.style.flexShrink = 0; _jsonExampleContainer.style.flexDirection = FlexDirection.Column; _jsonExampleContainer.style.minHeight = 0; _jsonExampleContainer.style.borderTopWidth = 1; _jsonExampleContainer.style.borderTopColor = new Color(0.15f, 0.15f, 0.15f); _jsonExampleContainer.style.backgroundColor = new Color(0.20f, 0.20f, 0.20f); _jsonExampleContainer.style.paddingLeft = 6; _jsonExampleContainer.style.paddingRight = 6; _jsonExampleContainer.style.paddingTop = 6; _jsonExampleContainer.style.paddingBottom = 6; var jsonHeaderRow = new VisualElement(); jsonHeaderRow.style.flexDirection = FlexDirection.Row; jsonHeaderRow.style.alignItems = Align.Center; jsonHeaderRow.style.paddingLeft = 10; jsonHeaderRow.style.paddingRight = 10; jsonHeaderRow.style.paddingTop = 5; jsonHeaderRow.style.paddingBottom = 5; jsonHeaderRow.style.backgroundColor = new Color(0.25f, 0.25f, 0.25f); var jsonHeader = new Label("JSON Injection"); jsonHeader.style.unityFontStyleAndWeight = FontStyle.Bold; jsonHeaderRow.Add(jsonHeader); var jsonHeaderSpacer = new VisualElement(); jsonHeaderSpacer.style.flexGrow = 1; jsonHeaderRow.Add(jsonHeaderSpacer); EnsureSharedIconsLoaded(); Button jsonCollapseBtn = null; jsonCollapseBtn = new Button(() => { _jsonPanelCollapsed = !_jsonPanelCollapsed; EditorPrefs.SetBool(JsonPanelCollapsedPrefsKey, _jsonPanelCollapsed); jsonCollapseBtn.text = _jsonPanelCollapsed ? "Max" : "Min"; ApplyJsonPanelHeight(); }); jsonCollapseBtn.text = _jsonPanelCollapsed ? "Max" : "Min"; jsonCollapseBtn.tooltip = "Toggle panel height"; jsonCollapseBtn.style.height = 18; jsonCollapseBtn.style.minWidth = 44; jsonCollapseBtn.style.marginLeft = 6; jsonHeaderRow.Add(jsonCollapseBtn); _jsonApplyToChartButton = CreateClickableIconImage(_applyToChartIcon, "ApplyToChart", () => ApplyInjectionJsonToSelectedProfile()); _jsonApplyToChartButton.style.marginLeft = 6; _jsonApplyToChartButton.style.marginBottom = 0; jsonHeaderRow.Add(_jsonApplyToChartButton); _jsonExampleContainer.Add(jsonHeaderRow); var jsonBtnRow = new VisualElement(); jsonBtnRow.style.flexDirection = FlexDirection.Row; jsonBtnRow.style.marginTop = 6; jsonBtnRow.style.paddingRight = 6; jsonBtnRow.style.flexShrink = 0; jsonBtnRow.style.flexWrap = Wrap.Wrap; void UpdateApiToggleIcon() { if (_jsonApiToggleButton == null) return; _jsonApiToggleButton.image = _jsonUseApiEnvelope ? _apiOnIcon : _apiOffIcon; } _jsonApiToggleButton = CreateClickableIconImage(_apiOffIcon, "API Envelope", () => { _jsonUseApiEnvelope = !_jsonUseApiEnvelope; UpdateApiToggleIcon(); _jsonExampleDirtyByUser = false; UpdateInjectionJsonExample(forceOverwrite: true); }); _jsonApiToggleButton.style.marginBottom = 4; jsonBtnRow.Add(_jsonApiToggleButton); UpdateApiToggleIcon(); _jsonModeDropdown = new PopupField(JsonExampleModeChoices, 0); _jsonModeDropdown.tooltip = "Feed Mode"; _jsonModeDropdown.style.width = 150; _jsonModeDropdown.style.marginLeft = 6; _jsonModeDropdown.style.marginBottom = 4; _jsonModeDropdown.RegisterValueChangedCallback(evt => { _jsonExampleMode = JsonExampleModeFromLabel(evt.newValue); _jsonExampleDirtyByUser = false; UpdateInjectionJsonExample(forceOverwrite: true); }); InjectPopupIcon(_jsonModeDropdown, _feedIcon, "feed-icon"); jsonBtnRow.Add(_jsonModeDropdown); _jsonDatasModeDropdown = new PopupField(JsonDatasModeChoices, 1); _jsonDatasModeDropdown.tooltip = "Datas Format"; _jsonDatasModeDropdown.style.width = 90; _jsonDatasModeDropdown.style.marginLeft = 6; _jsonDatasModeDropdown.style.marginBottom = 4; _jsonDatasModeDropdown.RegisterValueChangedCallback(evt => { _jsonDatasMode = JsonDatasModeFromLabel(evt.newValue); _jsonExampleDirtyByUser = false; UpdateInjectionJsonExample(forceOverwrite: true); }); InjectPopupIcon(_jsonDatasModeDropdown, _dataIcon, "data-icon"); jsonBtnRow.Add(_jsonDatasModeDropdown); var jsonBtnSpacer = new VisualElement(); jsonBtnSpacer.style.flexGrow = 1; jsonBtnRow.Add(jsonBtnSpacer); _jsonCopyButton = CreateClickableIconImage(_copyIcon, "Copy", () => { if (_jsonExampleField == null) return; GUIUtility.systemCopyBuffer = _jsonExampleField.value; }); _jsonCopyButton.style.marginLeft = 6; _jsonCopyButton.style.marginBottom = 4; jsonBtnRow.Add(_jsonCopyButton); _jsonExampleContainer.Add(jsonBtnRow); _jsonExampleScroll = new ScrollView(ScrollViewMode.Vertical); _jsonExampleScroll.style.flexGrow = 1; _jsonExampleScroll.style.flexShrink = 1; _jsonExampleScroll.style.flexBasis = 0; _jsonExampleScroll.style.minHeight = 0; _jsonExampleScroll.style.paddingLeft = 2; _jsonExampleScroll.style.paddingRight = 2; _jsonExampleScroll.style.paddingTop = 2; _jsonExampleScroll.style.paddingBottom = 2; _jsonExampleScroll.style.marginBottom = 0; _jsonExampleScroll.verticalScrollerVisibility = ScrollerVisibility.AlwaysVisible; _jsonExampleContainer.Add(_jsonExampleScroll); _jsonExampleScroll.contentContainer.style.flexGrow = 0; _jsonExampleScroll.contentContainer.style.flexShrink = 0; _jsonExampleScroll.contentContainer.style.minHeight = 0; _jsonExampleField = new TextField(); _jsonExampleField.multiline = true; _jsonExampleField.isReadOnly = false; _jsonExampleField.style.flexGrow = 0; _jsonExampleField.style.flexShrink = 0; _jsonExampleField.style.minHeight = 0; _jsonExampleField.style.whiteSpace = WhiteSpace.Normal; _jsonExampleField.style.unityTextAlign = TextAnchor.UpperLeft; _jsonExampleField.RegisterValueChangedCallback(_ => { _jsonExampleDirtyByUser = true; }); _jsonExampleScroll.Add(_jsonExampleField); leftPanel.Add(_jsonExampleContainer); UpdateJsonModeDropdown(); _jsonExampleDirtyByUser = false; UpdateInjectionJsonExample(forceOverwrite: true); } private static void InjectPopupIcon(PopupField popup, Texture2D icon, string iconElementName) { if (popup == null) return; if (icon == null) return; var input = popup.Q(className: "unity-base-popup-field__input"); if (input == null) return; if (!string.IsNullOrEmpty(iconElementName) && input.Q(iconElementName) != null) return; input.style.position = Position.Relative; input.style.paddingLeft = 22; var img = new Image(); img.name = iconElementName; img.image = icon; img.scaleMode = ScaleMode.ScaleToFit; img.style.position = Position.Absolute; img.style.left = 4; img.style.top = 2; img.style.width = 16; img.style.height = 16; input.Add(img); } private void UpdateInjectionJsonExample(bool forceOverwrite = false) { if (_jsonExampleField == null) return; if (!forceOverwrite && _jsonExampleDirtyByUser) return; if (_selectedProfile == null) { _jsonExampleField.value = string.Empty; return; } if (_selectedProfile.EnsureRuntimeData()) { EditorUtility.SetDirty(_selectedProfile); } _jsonChartId = _selectedProfile.chartId; var json = BuildInjectionJsonForSelectedProfile(_selectedProfile, _jsonChartId, _jsonExampleMode, _jsonDatasMode); _jsonExampleField.value = _jsonUseApiEnvelope ? WrapAsApiResponse(json) : json; _jsonExampleDirtyByUser = false; } private void ApplyInjectionJsonToSelectedProfile() { if (_selectedProfile == null) return; if (_jsonExampleField == null) return; if (_serializedProfile != null && _serializedProfile.hasModifiedProperties) { _serializedProfile.ApplyModifiedProperties(); } string json = _jsonExampleField.value; if (string.IsNullOrWhiteSpace(json)) return; if (!TryDeserializeFeed(json, out var feed)) { Debug.LogError("[EasyChartLibraryWindow] ApplyToChart failed: invalid JSON or unsupported format."); return; } bool allowMetaOverwrite = _jsonExampleMode == JsonExampleMode.Full; bool changed = ApplyFeedToProfile(_selectedProfile, feed, allowMetaOverwrite); if (changed) { EditorUtility.SetDirty(_selectedProfile); AssetDatabase.SaveAssets(); _serializedProfile?.Update(); ScheduleRefreshSeriesList(); ScheduleUpdatePreview(); _jsonExampleDirtyByUser = false; } } private static bool TryDeserializeFeed(string json, out ChartFeed feed) { feed = null; if (TryExtractWrappedDataJson(json, out var dataJson) && !string.IsNullOrEmpty(dataJson)) { json = dataJson; } if (ShouldPreferFlexibleFeedParser(json)) { if (TryDeserializeFeedFlexibleNewtonsoft(json, out feed)) { return true; } // Prevent falling back to typed deserializers when datas is in tuple/values form. // Those deserializers can silently produce default DataFeed entries, which then // overwrites chart data (e.g. heatmap points become [0,0,0]). return false; } try { var jsonConvertType = Type.GetType("Newtonsoft.Json.JsonConvert, Newtonsoft.Json") ?? Type.GetType("Newtonsoft.Json.JsonConvert, Unity.Newtonsoft.Json"); if (jsonConvertType != null) { var deserialize = jsonConvertType.GetMethod("DeserializeObject", new[] { typeof(string), typeof(Type) }); if (deserialize != null) { var obj = deserialize.Invoke(null, new object[] { json, typeof(ChartFeed) }); feed = obj as ChartFeed; return feed != null; } } } catch { } try { json = NormalizeJsonForUnityJsonUtility(json); feed = JsonUtility.FromJson(json); return feed != null; } catch { // Continue to flexible parse below. } return TryDeserializeFeedFlexibleNewtonsoft(json, out feed); } private static string WrapAsApiResponse(string payloadJson) { if (string.IsNullOrWhiteSpace(payloadJson)) return string.Empty; var payload = payloadJson.Trim(); if (payload.StartsWith("{", StringComparison.Ordinal) && payload.IndexOf("\"code\"", StringComparison.Ordinal) >= 0) { return payloadJson; } if (payload.Contains("\n")) { payload = IndentLines(payload, 2); return "{\n \"code\": 200,\n \"message\": \"success\",\n \"data\":\n" + payload + "\n}"; } return "{\n \"code\": 200,\n \"message\": \"success\",\n \"data\": " + payload + "\n}"; } private static string IndentLines(string text, int spaces) { if (string.IsNullOrEmpty(text)) return text; string pad = new string(' ', Mathf.Max(0, spaces)); var lines = text.Replace("\r\n", "\n").Split('\n'); for (int i = 0; i < lines.Length; i++) { lines[i] = pad + lines[i]; } return string.Join("\n", lines); } private static bool TryExtractWrappedDataJson(string json, out string dataJson) { dataJson = null; if (string.IsNullOrEmpty(json)) return false; int dataKey = json.IndexOf("\"data\"", StringComparison.Ordinal); if (dataKey < 0) return false; int colon = json.IndexOf(':', dataKey + 6); if (colon < 0) return false; int start = -1; for (int i = colon + 1; i < json.Length; i++) { char c = json[i]; if (char.IsWhiteSpace(c)) continue; if (c == '{' || c == '[') { start = i; break; } return false; } if (start < 0) return false; char open = json[start]; char close = open == '{' ? '}' : ']'; bool inString = false; bool escaped = false; int depth = 0; for (int i = start; i < json.Length; i++) { char c = json[i]; if (inString) { if (escaped) { escaped = false; continue; } if (c == '\\') { escaped = true; continue; } if (c == '"') { inString = false; } continue; } if (c == '"') { inString = true; continue; } if (c == open) depth++; else if (c == close) { depth--; if (depth == 0) { dataJson = json.Substring(start, i - start + 1); return true; } } } return false; } private static bool ShouldPreferFlexibleFeedParser(string json) { if (string.IsNullOrEmpty(json)) return false; int idx = 0; while (idx < json.Length) { idx = json.IndexOf("\"datas\"", idx, StringComparison.Ordinal); if (idx < 0) return false; int colon = json.IndexOf(':', idx + 6); if (colon < 0) return false; int openBracket = -1; for (int i = colon + 1; i < json.Length; i++) { char c = json[i]; if (char.IsWhiteSpace(c)) continue; if (c != '[') break; openBracket = i; break; } if (openBracket < 0) { idx = colon + 1; continue; } int j = openBracket + 1; while (j < json.Length && char.IsWhiteSpace(json[j])) j++; if (j >= json.Length) return false; char first = json[j]; if (first == '{' || first == ']') { idx = j + 1; continue; } return true; } return false; } private static bool TryDeserializeFeedFlexibleNewtonsoft(string json, out ChartFeed feed) { feed = null; try { var jObjectType = Type.GetType("Newtonsoft.Json.Linq.JObject, Newtonsoft.Json") ?? Type.GetType("Newtonsoft.Json.Linq.JObject, Unity.Newtonsoft.Json"); var jArrayType = Type.GetType("Newtonsoft.Json.Linq.JArray, Newtonsoft.Json") ?? Type.GetType("Newtonsoft.Json.Linq.JArray, Unity.Newtonsoft.Json"); if (jObjectType == null || jArrayType == null) return false; var parse = jObjectType.GetMethod("Parse", new[] { typeof(string) }); if (parse == null) return false; var root = parse.Invoke(null, new object[] { json }); if (root == null) return false; feed = new ChartFeed(); var itemProp = jObjectType.GetProperty("Item", new[] { typeof(string) }); if (itemProp == null) return false; string ReadString(object obj, string key) { if (obj == null) return null; var token = itemProp.GetValue(obj, new object[] { key }); if (token == null) return null; var s = token.ToString(); if (string.IsNullOrEmpty(s)) return null; if (s.Length >= 2 && s[0] == '"' && s[s.Length - 1] == '"') s = s.Substring(1, s.Length - 2); return s; } float ReadFloat(object obj, string key, float defaultValue = 0f) { var s = ReadString(obj, key); if (string.IsNullOrEmpty(s)) return defaultValue; if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v)) return v; return defaultValue; } bool ReadBool(object obj, string key, bool defaultValue = false) { var s = ReadString(obj, key); if (string.IsNullOrEmpty(s)) return defaultValue; if (bool.TryParse(s, out var v)) return v; return defaultValue; } object GetToken(object obj, string key) { if (obj == null) return null; return itemProp.GetValue(obj, new object[] { key }); } var rootChartId = ReadString(root, "chartId"); var rootChartName = ReadString(root, "chartName"); feed.chartId = rootChartId; feed.chartName = rootChartName; // Axes var axesToken = GetToken(root, "axes"); if (axesToken != null && jArrayType.IsInstanceOfType(axesToken)) { var axes = new List(); foreach (var axisObj in (System.Collections.IEnumerable)axesToken) { if (axisObj == null) continue; var axisIdStr = ReadString(axisObj, "axisId"); if (!Enum.TryParse(axisIdStr, ignoreCase: true, out AxisId axisId)) continue; var labelsToken = GetToken(axisObj, "labels"); string[] labels = null; if (labelsToken != null && jArrayType.IsInstanceOfType(labelsToken)) { var list = new List(); foreach (var l in (System.Collections.IEnumerable)labelsToken) { if (l == null) continue; var s = l.ToString(); if (s.Length >= 2 && s[0] == '"' && s[s.Length - 1] == '"') s = s.Substring(1, s.Length - 2); list.Add(s); } labels = list.ToArray(); } axes.Add(new AxisFeed { axisId = axisId, labels = labels }); } feed.axes = axes.Count > 0 ? axes.ToArray() : null; } // Series var seriesToken = GetToken(root, "series"); if (seriesToken != null && jArrayType.IsInstanceOfType(seriesToken)) { var series = new List(); foreach (var serieObj in (System.Collections.IEnumerable)seriesToken) { if (serieObj == null) continue; var sf = new SerieFeed(); sf.serieId = ReadString(serieObj, "serieId"); sf.name = ReadString(serieObj, "name"); var typeStr = ReadString(serieObj, "type"); if (!string.IsNullOrEmpty(typeStr) && int.TryParse(typeStr, out var tInt)) { sf.type = (SerieType)tInt; } else if (!string.IsNullOrEmpty(typeStr) && Enum.TryParse(typeStr, ignoreCase: true, out SerieType tEnum)) { sf.type = tEnum; } var datasToken = GetToken(serieObj, "datas"); if (datasToken != null && jArrayType.IsInstanceOfType(datasToken)) { var datas = new List(); object first = null; foreach (var tmp in (System.Collections.IEnumerable)datasToken) { first = tmp; break; } // Values mode: datas: [1,2,3] if (first != null) { var firstText = first.ToString(); bool firstIsObject = firstText.StartsWith("{"); bool firstIsArray = firstText.StartsWith("["); if (!firstIsObject && !firstIsArray) { int idx = 0; foreach (var vToken in (System.Collections.IEnumerable)datasToken) { if (vToken == null) { idx++; continue; } var vs = vToken.ToString(); if (!float.TryParse(vs, NumberStyles.Float, CultureInfo.InvariantCulture, out var vv)) vv = 0f; datas.Add(new DataFeed { x = idx, y = 0f, value = vv }); idx++; } } else if (firstIsArray) { // Tuple mode: datas: [[x,value], [x,value]] or [[x,y,value], ...] foreach (var tupleToken in (System.Collections.IEnumerable)datasToken) { if (tupleToken == null) continue; var tupleText = tupleToken.ToString(); if (string.IsNullOrEmpty(tupleText)) continue; tupleText = tupleText.Trim(); if (tupleText.Length < 2) continue; if (!tupleText.StartsWith("[") || !tupleText.EndsWith("]")) continue; tupleText = tupleText.Substring(1, tupleText.Length - 2); var parts = tupleText.Split(','); if (parts.Length < 2) continue; float tx = 0f; float ty = 0f; float tv = 0f; float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out tx); if (parts.Length == 2) { float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out tv); } else { float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out ty); float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out tv); } datas.Add(new DataFeed { x = tx, y = ty, value = tv }); } } else { // Standard/Full object mode foreach (var dpObj in (System.Collections.IEnumerable)datasToken) { if (dpObj == null) continue; var df = new DataFeed(); df.id = ReadString(dpObj, "id"); df.x = ReadFloat(dpObj, "x", 0f); df.y = ReadFloat(dpObj, "y", 0f); df.z = ReadFloat(dpObj, "z", 0f); df.value = ReadFloat(dpObj, "value", 0f); df.name = ReadString(dpObj, "name"); df.useColor = ReadBool(dpObj, "useColor", false); if (df.useColor) { var colorToken = GetToken(dpObj, "color"); if (colorToken != null) { df.color = new Color( ReadFloat(colorToken, "r", 1f), ReadFloat(colorToken, "g", 1f), ReadFloat(colorToken, "b", 1f), ReadFloat(colorToken, "a", 1f)); } } datas.Add(df); } } } sf.datas = datas.Count > 0 ? datas.ToArray() : null; } series.Add(sf); } feed.series = series.Count > 0 ? series.ToArray() : null; } return feed != null; } catch { feed = null; return false; } } private static string NormalizeJsonForUnityJsonUtility(string json) { if (string.IsNullOrEmpty(json)) return json; // Unity JsonUtility cannot parse enum values from strings. // Convert: "type": "Line" -> "type": 0 json = Regex.Replace(json, "\\\"type\\\"\\s*:\\s*\\\"([^\\\"]+)\\\"", match => { var label = match.Groups[1].Value; if (Enum.TryParse(label, ignoreCase: true, out SerieType parsed)) { return "\"type\": " + ((int)parsed).ToString(); } return match.Value; }); // Convert: "axisId": "XBottom" -> "axisId": 0 json = Regex.Replace(json, "\\\"axisId\\\"\\s*:\\s*\\\"([^\\\"]+)\\\"", match => { var label = match.Groups[1].Value; if (Enum.TryParse(label, ignoreCase: true, out AxisId parsed)) { return "\"axisId\": " + ((int)parsed).ToString(); } return match.Value; }); return json; } private static string ReplaceSerieTypeNumbersWithNames(string json) { if (string.IsNullOrEmpty(json)) return json; // Convert: "type": 0 -> "type": "Line" return Regex.Replace(json, "\\\"type\\\"\\s*:\\s*(\\d+)", match => { if (!int.TryParse(match.Groups[1].Value, out var intValue)) return match.Value; var name = ((SerieType)intValue).ToString(); return "\"type\": \"" + name + "\""; }); } private static bool ApplyFeedToProfile(ChartProfile profile, ChartFeed feed, bool allowMetaOverwrite) { if (profile == null || feed == null) return false; bool changed = false; if (profile.EnsureRuntimeData()) changed = true; if (allowMetaOverwrite) { if (!string.IsNullOrEmpty(feed.chartId) && !string.Equals(profile.chartId, feed.chartId, StringComparison.Ordinal)) { profile.chartId = feed.chartId; changed = true; } if (!string.IsNullOrEmpty(feed.chartName) && !string.Equals(profile.chartName, feed.chartName, StringComparison.Ordinal)) { profile.chartName = feed.chartName; changed = true; } } if (feed.axes != null && feed.axes.Length > 0) { if (profile.axes == null) { profile.axes = new List(); changed = true; } for (int i = 0; i < feed.axes.Length; i++) { var af = feed.axes[i]; if (af == null) continue; AxisConfig axis = null; for (int ai = 0; ai < profile.axes.Count; ai++) { var a = profile.axes[ai]; if (a != null && a.id == af.axisId) { axis = a; break; } } if (axis == null) { axis = new AxisConfig { id = af.axisId }; profile.axes.Add(axis); changed = true; } if (af.labels != null) { axis.axisType = AxisType.Category; if (axis.labels == null) { axis.labels = new List(); changed = true; } axis.labels.Clear(); axis.labels.AddRange(af.labels); changed = true; } } } if (feed.series != null && feed.series.Length > 0) { if (profile.series == null) { profile.series = new List(); changed = true; } for (int i = 0; i < feed.series.Length; i++) { var sf = feed.series[i]; if (sf == null) continue; bool indexMode = string.IsNullOrEmpty(sf.serieId) && string.IsNullOrEmpty(sf.name); Serie serie = null; if (!string.IsNullOrEmpty(sf.serieId)) { for (int si = 0; si < profile.series.Count; si++) { var s = profile.series[si]; if (s != null && string.Equals(s.id, sf.serieId, StringComparison.Ordinal)) { serie = s; break; } } } if (serie == null && !string.IsNullOrEmpty(sf.name)) { for (int si = 0; si < profile.series.Count; si++) { var s = profile.series[si]; if (s != null && string.Equals(s.name, sf.name, StringComparison.Ordinal)) { serie = s; break; } } } if (serie == null && indexMode && i >= 0 && i < profile.series.Count) { serie = profile.series[i]; } if (serie == null) { if (!allowMetaOverwrite && !indexMode) continue; serie = new Serie { name = string.IsNullOrEmpty(sf.name) ? $"Series {i + 1}" : sf.name, type = sf.type, visible = true, seriesData = new List() }; if (!string.IsNullOrEmpty(sf.serieId)) serie.id = sf.serieId; serie.EnsureIntegrity(); profile.series.Add(serie); changed = true; } if (allowMetaOverwrite && !indexMode) { if (!string.IsNullOrEmpty(sf.serieId) && string.IsNullOrEmpty(serie.id)) { serie.id = sf.serieId; changed = true; } if (!string.IsNullOrEmpty(sf.name) && !string.Equals(serie.name, sf.name, StringComparison.Ordinal)) { serie.name = sf.name; changed = true; } if (serie.type != sf.type) { serie.SetType(sf.type); changed = true; } } if (serie.seriesData == null) { serie.seriesData = new List(); changed = true; } if (sf.datas != null) { EnsureSeriesDataCount(serie.seriesData, sf.datas.Length); for (int pi = 0; pi < sf.datas.Length; pi++) { var df = sf.datas[pi]; if (df == null) continue; var p = serie.seriesData[pi] ?? (serie.seriesData[pi] = new SeriesData()); if (!string.IsNullOrEmpty(df.id)) p.id = df.id; p.x = df.x; p.y = df.y; p.z = df.z; p.value = df.value; if (!string.IsNullOrEmpty(df.name)) p.name = df.name; p.useColor = df.useColor; if (df.useColor) p.color = df.color; } changed = true; } if (serie.EnsureIntegrity()) changed = true; } } if (profile.EnsureRuntimeData()) changed = true; return changed; } private static void EnsureSeriesDataCount(List list, int count) { if (list == null) return; while (list.Count < count) list.Add(new SeriesData()); if (list.Count > count) list.RemoveRange(count, list.Count - count); } private void UpdateJsonModeDropdown() { if (_jsonModeDropdown == null) return; var label = JsonExampleModeToLabel(_jsonExampleMode); _jsonModeDropdown.SetValueWithoutNotify(label); } private static string JsonExampleModeToLabel(JsonExampleMode mode) { for (int i = 0; i < JsonExampleModeOptions.Count; i++) { if (JsonExampleModeOptions[i].mode == mode) return JsonExampleModeOptions[i].label; } return JsonExampleModeOptions != null && JsonExampleModeOptions.Count > 0 ? JsonExampleModeOptions[0].label : "Lite_Index"; } private static JsonExampleMode JsonExampleModeFromLabel(string label) { if (string.IsNullOrEmpty(label)) return JsonExampleMode.Lite_Index; for (int i = 0; i < JsonExampleModeOptions.Count; i++) { if (string.Equals(JsonExampleModeOptions[i].label, label, StringComparison.OrdinalIgnoreCase)) { return JsonExampleModeOptions[i].mode; } } return JsonExampleMode.Lite_Index; } private static string BuildInjectionJsonForSelectedProfile(ChartProfile profile, string chartId, JsonExampleMode mode) { return BuildInjectionJsonForSelectedProfile(profile, chartId, mode, JsonDatasMode.Standard); } private static string BuildInjectionJsonForSelectedProfile(ChartProfile profile, string chartId, JsonExampleMode mode, JsonDatasMode datasMode) { if (profile == null) return string.Empty; if (mode == JsonExampleMode.Lite_Index) return BuildLiteIndexInjectionJson(profile, datasMode); if (mode == JsonExampleMode.Lite_Name) return BuildStandardInjectionJson(profile, datasMode); if (mode == JsonExampleMode.Lite_ID) return BuildLiteIdInjectionJson(profile, chartId, datasMode); if (mode == JsonExampleMode.Standard_Axis) return BuildStandardAxisInjectionJson(profile, datasMode); if (mode == JsonExampleMode.Standard) return BuildStandardInjectionJson(profile, datasMode); if (mode == JsonExampleMode.Full) return BuildFullInjectionJson(profile, chartId, datasMode); return string.Empty; } private static string BuildFullInjectionJson(ChartProfile profile, string chartId, JsonDatasMode datasMode) { if (profile == null) return string.Empty; string resolvedChartId = string.IsNullOrWhiteSpace(chartId) ? profile.chartId : chartId; string resolvedChartName = string.IsNullOrWhiteSpace(profile.chartName) ? profile.name : profile.chartName; var sb = new System.Text.StringBuilder(8192); sb.Append("{\n"); sb.Append(" \"chartId\": \"").Append(JsonEscape(resolvedChartId)).Append("\","); sb.Append("\n \"chartName\": \"").Append(JsonEscape(resolvedChartName)).Append("\""); // Axes (Category only) if (profile.axes != null) { var axisList = new List(); for (int i = 0; i < profile.axes.Count; i++) { var a = profile.axes[i]; if (a == null) continue; if (a.axisType != AxisType.Category) continue; if (a.labels == null || a.labels.Count == 0) continue; axisList.Add(a); } if (axisList.Count > 0) { sb.Append(",\n \"axes\": [\n"); for (int i = 0; i < axisList.Count; i++) { var a = axisList[i]; if (i > 0) sb.Append(",\n"); sb.Append(" {\n"); sb.Append(" \"axisId\": \"").Append(JsonEscape(a.id.ToString())).Append("\",\n"); sb.Append(" \"labels\": ["); for (int li = 0; li < a.labels.Count; li++) { if (li > 0) sb.Append(", "); sb.Append("\"").Append(JsonEscape(a.labels[li])).Append("\""); } sb.Append("]\n"); sb.Append(" }"); } sb.Append("\n ]"); } } if (profile.series != null && profile.series.Count > 0) { sb.Append(",\n \"series\": [\n"); for (int i = 0; i < profile.series.Count; i++) { var s = profile.series[i]; if (s == null) continue; if (i > 0) sb.Append(",\n"); sb.Append(" {\n"); sb.Append(" \"serieId\": \"").Append(JsonEscape(s.id)).Append("\",\n"); sb.Append(" \"name\": \"").Append(JsonEscape(s.name)).Append("\",\n"); sb.Append(" \"type\": \"").Append(JsonEscape(s.type.ToString())).Append("\""); int dataCount = s.seriesData != null ? s.seriesData.Count : 0; if (dataCount > 0) { bool isHeatmap = s.type == SerieType.Heatmap; if (datasMode == JsonDatasMode.Values) { sb.Append(",\n \"datas\": ["); } else { sb.Append(",\n \"datas\": [\n"); } for (int pi = 0; pi < dataCount; pi++) { var dp = s.seriesData[pi]; if (pi > 0) { if (datasMode == JsonDatasMode.Values) sb.Append(", "); else sb.Append(",\n"); } float x = dp != null ? dp.x : 0f; float y = dp != null ? dp.y : 0f; float v = dp != null ? dp.value : 0f; if (datasMode == JsonDatasMode.Values && !isHeatmap) { sb.Append(v.ToString("R", CultureInfo.InvariantCulture)); } else if (datasMode == JsonDatasMode.Values && isHeatmap) { sb.Append("[").Append(x.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append("]"); } else { sb.Append(" { \"x\": ").Append(x.ToString("R", CultureInfo.InvariantCulture)); if (isHeatmap) sb.Append(", \"y\": ").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"value\": ").Append(v.ToString("R", CultureInfo.InvariantCulture)); if (dp != null) { if (!string.IsNullOrEmpty(dp.id)) sb.Append(", \"id\": \"").Append(JsonEscape(dp.id)).Append("\""); if (!string.IsNullOrEmpty(dp.name)) sb.Append(", \"name\": \"").Append(JsonEscape(dp.name)).Append("\""); if (dp.useColor) { sb.Append(", \"useColor\": true"); sb.Append(", \"color\": { \"r\": ").Append(dp.color.r.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"g\": ").Append(dp.color.g.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"b\": ").Append(dp.color.b.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"a\": ").Append(dp.color.a.ToString("R", CultureInfo.InvariantCulture)); sb.Append(" }"); } } sb.Append(" }"); } } if (datasMode == JsonDatasMode.Values) sb.Append("]"); else sb.Append("\n ]"); } sb.Append("\n }"); } sb.Append("\n ]"); } sb.Append("\n}"); return sb.ToString(); } private static string BuildLiteIndexInjectionJson(ChartProfile profile, JsonDatasMode datasMode) { if (profile == null) return string.Empty; string resolvedChartName = string.IsNullOrWhiteSpace(profile.chartName) ? profile.name : profile.chartName; var sb = new System.Text.StringBuilder(2048); sb.Append("{\n"); sb.Append(" \"chartName\": \"").Append(JsonEscape(resolvedChartName)).Append("\""); bool hasSeries = profile.series != null && profile.series.Count > 0; if (hasSeries) { sb.Append(",\n \"series\": [\n"); for (int i = 0; i < profile.series.Count; i++) { var s = profile.series[i]; if (i > 0) sb.Append(",\n"); sb.Append(" {\n"); int dataCount = s != null && s.seriesData != null ? s.seriesData.Count : 0; if (dataCount > 0) { bool isHeatmap = s != null && s.type == SerieType.Heatmap; if (datasMode == JsonDatasMode.Values) { sb.Append(" \"datas\": ["); } else { sb.Append(" \"datas\": [\n"); } for (int pi = 0; pi < dataCount; pi++) { var dp = s.seriesData[pi]; if (pi > 0) { if (datasMode == JsonDatasMode.Values) sb.Append(", "); else sb.Append(",\n"); } float x = isHeatmap ? (dp != null ? dp.x : 0f) : pi; float y = dp != null ? dp.y : 0f; float v = dp != null ? dp.value : 0f; if (datasMode == JsonDatasMode.Values && !isHeatmap) { sb.Append(v.ToString("R", CultureInfo.InvariantCulture)); } else if (datasMode == JsonDatasMode.Values && isHeatmap) { sb.Append("[").Append(x.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append("]"); } else { sb.Append(" { \"x\": ").Append(x.ToString("R", CultureInfo.InvariantCulture)); if (isHeatmap) sb.Append(", \"y\": ").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"value\": ").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append(" }"); } } if (datasMode == JsonDatasMode.Values) sb.Append("]\n"); else sb.Append("\n ]\n"); } sb.Append(" }"); } sb.Append("\n ]"); } sb.Append("\n}"); return sb.ToString(); } private static string BuildStandardInjectionJson(ChartProfile profile, JsonDatasMode datasMode) { if (profile == null) return string.Empty; string resolvedChartName = string.IsNullOrWhiteSpace(profile.chartName) ? profile.name : profile.chartName; var sb = new System.Text.StringBuilder(4096); sb.Append("{\n"); sb.Append(" \"chartName\": \"").Append(JsonEscape(resolvedChartName)).Append("\""); if (profile.series != null && profile.series.Count > 0) { sb.Append(",\n \"series\": [\n"); for (int i = 0; i < profile.series.Count; i++) { var s = profile.series[i]; if (i > 0) sb.Append(",\n"); sb.Append(" {\n"); sb.Append(" \"name\": \"").Append(JsonEscape(s != null ? s.name : string.Empty)).Append("\""); int dataCount = s != null && s.seriesData != null ? s.seriesData.Count : 0; if (dataCount > 0) { bool isHeatmap = s.type == SerieType.Heatmap; if (datasMode == JsonDatasMode.Values) { sb.Append(",\n \"datas\": ["); } else { sb.Append(",\n \"datas\": [\n"); } for (int pi = 0; pi < dataCount; pi++) { var dp = s.seriesData[pi]; if (pi > 0) { if (datasMode == JsonDatasMode.Values) sb.Append(", "); else sb.Append(",\n"); } float x = dp != null ? dp.x : 0f; float y = dp != null ? dp.y : 0f; float v = dp != null ? dp.value : 0f; if (datasMode == JsonDatasMode.Values && !isHeatmap) { sb.Append(v.ToString("R", CultureInfo.InvariantCulture)); } else if (datasMode == JsonDatasMode.Values && isHeatmap) { sb.Append("[").Append(x.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append("]"); } else { sb.Append(" { \"x\": ").Append(x.ToString("R", CultureInfo.InvariantCulture)); if (isHeatmap) sb.Append(", \"y\": ").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"value\": ").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append(" }"); } } if (datasMode == JsonDatasMode.Values) sb.Append("]"); else sb.Append("\n ]"); } sb.Append("\n }"); } sb.Append("\n ]"); } sb.Append("\n}"); return sb.ToString(); } private static string BuildLiteNameInjectionJson(ChartProfile profile, JsonDatasMode datasMode) { if (profile == null) return string.Empty; string resolvedChartName = string.IsNullOrWhiteSpace(profile.chartName) ? profile.name : profile.chartName; var sb = new System.Text.StringBuilder(2048); sb.Append("{\n"); sb.Append(" \"chartName\": \"").Append(JsonEscape(resolvedChartName)).Append("\""); bool hasSeries = profile.series != null && profile.series.Count > 0; if (hasSeries) { sb.Append(",\n \"series\": [\n"); for (int i = 0; i < profile.series.Count; i++) { var s = profile.series[i]; if (i > 0) sb.Append(",\n"); sb.Append(" {\n"); sb.Append(" \"name\": \"").Append(JsonEscape(s != null ? s.name : string.Empty)).Append("\""); int dataCount = s != null && s.seriesData != null ? s.seriesData.Count : 0; if (dataCount > 0) { bool isHeatmap = s != null && s.type == SerieType.Heatmap; if (datasMode == JsonDatasMode.Values) { sb.Append(",\n \"datas\": ["); } else { sb.Append(",\n \"datas\": [\n"); } for (int pi = 0; pi < dataCount; pi++) { var dp = s.seriesData[pi]; if (pi > 0) { if (datasMode == JsonDatasMode.Values) sb.Append(", "); else sb.Append(",\n"); } float x = isHeatmap ? (dp != null ? dp.x : 0f) : pi; float y = dp != null ? dp.y : 0f; float v = dp != null ? dp.value : 0f; if (datasMode == JsonDatasMode.Values && !isHeatmap) { sb.Append(v.ToString("R", CultureInfo.InvariantCulture)); } else if (datasMode == JsonDatasMode.Values && isHeatmap) { sb.Append("[").Append(x.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append("]"); } else { sb.Append(" { \"x\": ").Append(x.ToString("R", CultureInfo.InvariantCulture)); if (isHeatmap) sb.Append(", \"y\": ").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"value\": ").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append(" }"); } } if (datasMode == JsonDatasMode.Values) sb.Append("]\n"); else sb.Append("\n ]\n"); } sb.Append(" }"); } sb.Append("\n ]"); } sb.Append("\n}"); return sb.ToString(); } private static string BuildLiteIdInjectionJson(ChartProfile profile, string chartId, JsonDatasMode datasMode) { if (profile == null) return string.Empty; string resolvedChartId = string.IsNullOrWhiteSpace(chartId) ? profile.chartId : chartId; var sb = new System.Text.StringBuilder(2048); sb.Append("{\n"); sb.Append(" \"chartId\": \"").Append(JsonEscape(resolvedChartId)).Append("\""); bool hasSeries = profile.series != null && profile.series.Count > 0; if (hasSeries) { sb.Append(",\n \"series\": [\n"); for (int i = 0; i < profile.series.Count; i++) { var s = profile.series[i]; if (i > 0) sb.Append(",\n"); sb.Append(" {\n"); sb.Append(" \"serieId\": \"").Append(JsonEscape(s != null ? s.id : string.Empty)).Append("\""); int dataCount = s != null && s.seriesData != null ? s.seriesData.Count : 0; if (dataCount > 0) { bool isHeatmap = s != null && s.type == SerieType.Heatmap; if (datasMode == JsonDatasMode.Values) { sb.Append(",\n \"datas\": ["); } else { sb.Append(",\n \"datas\": [\n"); } for (int pi = 0; pi < dataCount; pi++) { var dp = s.seriesData[pi]; if (pi > 0) { if (datasMode == JsonDatasMode.Values) sb.Append(", "); else sb.Append(",\n"); } float x = isHeatmap ? (dp != null ? dp.x : 0f) : pi; float y = dp != null ? dp.y : 0f; float v = dp != null ? dp.value : 0f; if (datasMode == JsonDatasMode.Values) { if (isHeatmap) { sb.Append("[").Append(x.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append("]"); } else { sb.Append(v.ToString("R", CultureInfo.InvariantCulture)); } } else { sb.Append(" { \"x\": ").Append(x.ToString("R", CultureInfo.InvariantCulture)); if (isHeatmap) sb.Append(", \"y\": ").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"value\": ").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append(" }"); } } if (datasMode == JsonDatasMode.Values) sb.Append("]"); else sb.Append("\n ]"); } sb.Append("\n }"); } sb.Append("\n ]"); } sb.Append("\n}"); return sb.ToString(); } private static string BuildStandardAxisInjectionJson(ChartProfile profile, JsonDatasMode datasMode) { if (profile == null) return string.Empty; string resolvedChartName = string.IsNullOrWhiteSpace(profile.chartName) ? profile.name : profile.chartName; var sb = new System.Text.StringBuilder(4096); sb.Append("{\n"); sb.Append(" \"chartName\": \"").Append(JsonEscape(resolvedChartName)).Append("\""); // Axes (Category only) if (profile.axes != null) { var axisList = new List(); for (int i = 0; i < profile.axes.Count; i++) { var a = profile.axes[i]; if (a == null) continue; if (a.axisType != AxisType.Category) continue; if (a.labels == null || a.labels.Count == 0) continue; axisList.Add(a); } if (axisList.Count > 0) { sb.Append(",\n \"axes\": [\n"); for (int i = 0; i < axisList.Count; i++) { var a = axisList[i]; if (i > 0) sb.Append(",\n"); sb.Append(" {\n"); sb.Append(" \"axisId\": \"").Append(JsonEscape(a.id.ToString())).Append("\",\n"); sb.Append(" \"labels\": ["); for (int li = 0; li < a.labels.Count; li++) { if (li > 0) sb.Append(", "); sb.Append("\"").Append(JsonEscape(a.labels[li])).Append("\""); } sb.Append("]\n"); sb.Append(" }"); } sb.Append("\n ]"); } } // Series points (name aligned) if (profile.series != null && profile.series.Count > 0) { sb.Append(",\n \"series\": [\n"); for (int i = 0; i < profile.series.Count; i++) { var s = profile.series[i]; if (i > 0) sb.Append(",\n"); sb.Append(" {\n"); sb.Append(" \"name\": \"").Append(JsonEscape(s != null ? s.name : string.Empty)).Append("\""); int dataCount = s != null && s.seriesData != null ? s.seriesData.Count : 0; if (dataCount > 0) { bool isHeatmap = s.type == SerieType.Heatmap; if (datasMode == JsonDatasMode.Values) { sb.Append(",\n \"datas\": ["); } else { sb.Append(",\n \"datas\": [\n"); } for (int pi = 0; pi < dataCount; pi++) { var dp = s.seriesData[pi]; if (pi > 0) { if (datasMode == JsonDatasMode.Values) sb.Append(", "); else sb.Append(",\n"); } float x = dp != null ? dp.x : 0f; float y = dp != null ? dp.y : 0f; float v = dp != null ? dp.value : 0f; if (datasMode == JsonDatasMode.Values && !isHeatmap) { sb.Append(v.ToString("R", CultureInfo.InvariantCulture)); } else if (datasMode == JsonDatasMode.Values && isHeatmap) { sb.Append("[").Append(x.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(",").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append("]"); } else { sb.Append(" { \"x\": ").Append(x.ToString("R", CultureInfo.InvariantCulture)); if (isHeatmap) sb.Append(", \"y\": ").Append(y.ToString("R", CultureInfo.InvariantCulture)); sb.Append(", \"value\": ").Append(v.ToString("R", CultureInfo.InvariantCulture)); sb.Append(" }"); } } if (datasMode == JsonDatasMode.Values) sb.Append("]"); else sb.Append("\n ]"); } sb.Append("\n }"); } sb.Append("\n ]"); } sb.Append("\n}"); return sb.ToString(); } private static string JsonEscape(string s) { if (string.IsNullOrEmpty(s)) return string.Empty; return s .Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\r", "\\r") .Replace("\n", "\\n") .Replace("\t", "\\t"); } private static string FloatArrayToJson(float[] values) { if (values == null || values.Length == 0) return "[]"; var sb = new System.Text.StringBuilder(values.Length * 6); sb.Append("["); for (int i = 0; i < values.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(values[i].ToString("R", CultureInfo.InvariantCulture)); } sb.Append("]"); return sb.ToString(); } private static JsonDatasMode JsonDatasModeFromLabel(string label) { if (string.IsNullOrEmpty(label)) return JsonDatasMode.Standard; if (string.Equals(label, "Values", StringComparison.OrdinalIgnoreCase)) return JsonDatasMode.Values; if (string.Equals(label, "Full", StringComparison.OrdinalIgnoreCase)) return JsonDatasMode.Full; return JsonDatasMode.Standard; } } }