From 2688d93f17f22274122487c6d3cbc13a149f3aa5 Mon Sep 17 00:00:00 2001 From: monitor1394 Date: Thu, 28 May 2026 09:14:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96`DataZoom`=E7=9A=84`Marquee`?= =?UTF-8?q?=E6=A1=86=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Documentation~/zh/changelog.md | 2 +- Runtime/Component/Axis/AxisContext.cs | 35 ++++++--- Runtime/Component/DataZoom/DataZoomHandler.cs | 73 ++++++++++++++++--- Runtime/Serie/SerieHelper.cs | 35 ++++++--- Runtime/Serie/SeriesHelper.cs | 10 ++- 5 files changed, 121 insertions(+), 34 deletions(-) diff --git a/Documentation~/zh/changelog.md b/Documentation~/zh/changelog.md index ff9bad75..8795461a 100644 --- a/Documentation~/zh/changelog.md +++ b/Documentation~/zh/changelog.md @@ -81,7 +81,7 @@ slug: /changelog ## master -* (2026.05.23) 增加`DataZoom`的`filterAxisRange`设置坐标轴的范围计算是否受`DataZoom`的影响 +* (2026.05.25) 增加`DataZoom`的`filterAxisRange`设置坐标轴的范围计算是否受`DataZoom`的影响 * (2026.05.23) 优化`DataZoom`的`Marquee`框选功能 * (2026.05.23) 修复`DataZoom`内绘制的折线图可能会超出范围的问题 * (2026.05.23) 修复`Axis`的`inverse`没能正确反转的问题 diff --git a/Runtime/Component/Axis/AxisContext.cs b/Runtime/Component/Axis/AxisContext.cs index 89df8e66..370c8bc8 100644 --- a/Runtime/Component/Axis/AxisContext.cs +++ b/Runtime/Component/Axis/AxisContext.cs @@ -103,22 +103,35 @@ namespace XCharts.Runtime internal void UpdateFilterData(List data, DataZoom dataZoom) { int start = 0, end = 0; - var range = Mathf.RoundToInt(data.Count * (dataZoom.end - dataZoom.start) / 100); - if (range <= 0) - range = 1; - - if (dataZoom.context.invert) + // 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) { - end = Mathf.RoundToInt(data.Count * dataZoom.end / 100); - start = end - range; - if (start < 0) start = 0; + if (dataZoom.context.invert) + { + startIndex = Mathf.CeilToInt((float)n * (100 - dataZoom.end) / 100); + endIndex = Mathf.FloorToInt((float)n * (100 - dataZoom.start) / 100); + } + else + { + startIndex = Mathf.CeilToInt((float)n * dataZoom.start / 100); + endIndex = Mathf.FloorToInt((float)n * dataZoom.end / 100); + } } else { - start = Mathf.RoundToInt(data.Count * dataZoom.start / 100); - end = start + range; - if (end > data.Count) end = data.Count; + 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 || diff --git a/Runtime/Component/DataZoom/DataZoomHandler.cs b/Runtime/Component/DataZoom/DataZoomHandler.cs index d31eeb18..e98db669 100644 --- a/Runtime/Component/DataZoom/DataZoomHandler.cs +++ b/Runtime/Component/DataZoom/DataZoomHandler.cs @@ -228,9 +228,18 @@ namespace XCharts.Runtime start = end; end = temp; } - UpdateDataZoomRange(dataZoom, start, end, grid); - chart.OnDataZoomRangeChanged(dataZoom); - chart.RefreshChart(); + 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(); @@ -336,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); } } @@ -417,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(dataZoom.xAxisIndexs[0]); + isInverse = xAxis != null && xAxis.inverse; + } + else if (dataZoom.orient == Orient.Vertical && dataZoom.yAxisIndexs.Count > 0) + { + var yAxis = chart.GetChartComponent(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); } @@ -453,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; } diff --git a/Runtime/Serie/SerieHelper.cs b/Runtime/Serie/SerieHelper.cs index b19a781d..e24e137d 100644 --- a/Runtime/Serie/SerieHelper.cs +++ b/Runtime/Serie/SerieHelper.cs @@ -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; - if (dataZoom.context.invert) + // 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) { - end = Mathf.RoundToInt(data.Count * dataZoom.end / 100); - start = end - range; - if (start < 0) start = 0; + if (dataZoom.context.invert) + { + startIndex = Mathf.CeilToInt((float)n * (100 - dataZoom.end) / 100); + endIndex = Mathf.FloorToInt((float)n * (100 - dataZoom.start) / 100); + } + else + { + startIndex = Mathf.CeilToInt((float)n * dataZoom.start / 100); + endIndex = Mathf.FloorToInt((float)n * dataZoom.end / 100); + } } else { - start = Mathf.RoundToInt(data.Count * dataZoom.start / 100); - end = start + range; - if (end > data.Count) end = data.Count; + 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) diff --git a/Runtime/Serie/SeriesHelper.cs b/Runtime/Serie/SeriesHelper.cs index 22a628f1..9c80e535 100644 --- a/Runtime/Serie/SeriesHelper.cs +++ b/Runtime/Serie/SeriesHelper.cs @@ -379,7 +379,12 @@ namespace XCharts.Runtime // determine whether DataZoom filtering applies for this serie var dz = chart.GetXDataZoomOfSerie(serie); - bool useDataZoomFilter = dz != null && dz.enable && dz.filterAxisRange; + // 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) @@ -476,7 +481,8 @@ namespace XCharts.Runtime (!isPolar && serie.yAxisIndex != axisIndex) || !serie.show) continue; var stackDz = chart.GetXDataZoomOfSerie(serie); - if (stackDz != null && (!stackDz.filterAxisRange || !stackDz.enable)) stackDz = null; + // 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(series, serie.stack)) {