mirror of
https://github.com/XCharts-Team/XCharts.git
synced 2026-05-28 20:28:46 +00:00
优化DataZoom的Marquee框选功能
This commit is contained in:
@@ -81,7 +81,7 @@ slug: /changelog
|
|||||||
|
|
||||||
## master
|
## master
|
||||||
|
|
||||||
* (2026.05.23) 增加`DataZoom`的`filterAxisRange`设置坐标轴的范围计算是否受`DataZoom`的影响
|
* (2026.05.25) 增加`DataZoom`的`filterAxisRange`设置坐标轴的范围计算是否受`DataZoom`的影响
|
||||||
* (2026.05.23) 优化`DataZoom`的`Marquee`框选功能
|
* (2026.05.23) 优化`DataZoom`的`Marquee`框选功能
|
||||||
* (2026.05.23) 修复`DataZoom`内绘制的折线图可能会超出范围的问题
|
* (2026.05.23) 修复`DataZoom`内绘制的折线图可能会超出范围的问题
|
||||||
* (2026.05.23) 修复`Axis`的`inverse`没能正确反转的问题
|
* (2026.05.23) 修复`Axis`的`inverse`没能正确反转的问题
|
||||||
|
|||||||
@@ -103,22 +103,35 @@ namespace XCharts.Runtime
|
|||||||
internal void UpdateFilterData(List<string> data, DataZoom dataZoom)
|
internal void UpdateFilterData(List<string> data, DataZoom dataZoom)
|
||||||
{
|
{
|
||||||
int start = 0, end = 0;
|
int start = 0, end = 0;
|
||||||
var range = Mathf.RoundToInt(data.Count * (dataZoom.end - dataZoom.start) / 100);
|
// Use (N-1) intervals to match shadow drawing (scaleWid = width/(N-1)).
|
||||||
if (range <= 0)
|
// CeilToInt for start, FloorToInt for end, so filter aligns exactly with filler.
|
||||||
range = 1;
|
int n = data.Count - 1;
|
||||||
|
int startIndex, endIndex;
|
||||||
if (dataZoom.context.invert)
|
if (n > 0)
|
||||||
{
|
{
|
||||||
end = Mathf.RoundToInt(data.Count * dataZoom.end / 100);
|
if (dataZoom.context.invert)
|
||||||
start = end - range;
|
{
|
||||||
if (start < 0) start = 0;
|
startIndex = Mathf.CeilToInt((float)n * (100 - dataZoom.end) / 100);
|
||||||
|
endIndex = Mathf.FloorToInt((float)n * (100 - dataZoom.start) / 100);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
startIndex = Mathf.CeilToInt((float)n * dataZoom.start / 100);
|
||||||
|
endIndex = Mathf.FloorToInt((float)n * dataZoom.end / 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
start = Mathf.RoundToInt(data.Count * dataZoom.start / 100);
|
startIndex = 0;
|
||||||
end = start + range;
|
endIndex = 0;
|
||||||
if (end > data.Count) end = data.Count;
|
|
||||||
}
|
}
|
||||||
|
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);
|
var minZoomRatio = (int)(data.Count * dataZoom.minZoomRatio);
|
||||||
if (start != filterStart ||
|
if (start != filterStart ||
|
||||||
|
|||||||
@@ -228,9 +228,18 @@ namespace XCharts.Runtime
|
|||||||
start = end;
|
start = end;
|
||||||
end = temp;
|
end = temp;
|
||||||
}
|
}
|
||||||
UpdateDataZoomRange(dataZoom, start, end, grid);
|
if (start < 0) start = 0;
|
||||||
chart.OnDataZoomRangeChanged(dataZoom);
|
if (end > 100) end = 100;
|
||||||
chart.RefreshChart();
|
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.context.marqueeRect = Rect.zero;
|
||||||
dataZoom.SetVerticesDirty();
|
dataZoom.SetVerticesDirty();
|
||||||
@@ -336,7 +345,7 @@ namespace XCharts.Runtime
|
|||||||
if ((dataZoom.supportInside && dataZoom.supportInsideScroll && grid.Contains(pos)) ||
|
if ((dataZoom.supportInside && dataZoom.supportInsideScroll && grid.Contains(pos)) ||
|
||||||
dataZoom.IsInZoom(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);
|
if (grid == null) grid = chart.GetGridOfDataZoom(dataZoom);
|
||||||
var range = dataZoom.orient == Orient.Horizonal ? grid.context.width : grid.context.height;
|
var range = dataZoom.orient == Orient.Horizonal ? grid.context.width : grid.context.height;
|
||||||
var deltaPercent = Mathf.Abs(delta / range * 100);
|
var deltaPercent = Mathf.Abs(delta / range * 100);
|
||||||
float start, end;
|
float start, end;
|
||||||
|
|
||||||
|
// Calculate the anchor ratio within the current [start, end] range based on mouse position.
|
||||||
|
// This ensures the data point under the cursor stays fixed while zooming.
|
||||||
|
float centerRatio = 0.5f;
|
||||||
|
if (mousePos.HasValue)
|
||||||
|
{
|
||||||
|
float mousePercent;
|
||||||
|
if (dataZoom.orient == Orient.Horizonal)
|
||||||
|
mousePercent = grid.context.width > 0
|
||||||
|
? (mousePos.Value.x - grid.context.x) / grid.context.width * 100
|
||||||
|
: 50f;
|
||||||
|
else
|
||||||
|
mousePercent = grid.context.height > 0
|
||||||
|
? (mousePos.Value.y - grid.context.y) / grid.context.height * 100
|
||||||
|
: 50f;
|
||||||
|
|
||||||
|
var currentRange = dataZoom.end - dataZoom.start;
|
||||||
|
if (currentRange > 0)
|
||||||
|
{
|
||||||
|
// mousePercent is always grid-relative (0=left edge, 100=right edge).
|
||||||
|
// When DataZoom shows [start, end], the grid spans exactly that window,
|
||||||
|
// so the anchor fraction is simply mousePercent/100.
|
||||||
|
// For inverse axes the data runs right→left, so flip the fraction.
|
||||||
|
bool isInverse = false;
|
||||||
|
if (dataZoom.orient == Orient.Horizonal && dataZoom.xAxisIndexs.Count > 0)
|
||||||
|
{
|
||||||
|
var xAxis = chart.GetChartComponent<XAxis>(dataZoom.xAxisIndexs[0]);
|
||||||
|
isInverse = xAxis != null && xAxis.inverse;
|
||||||
|
}
|
||||||
|
else if (dataZoom.orient == Orient.Vertical && dataZoom.yAxisIndexs.Count > 0)
|
||||||
|
{
|
||||||
|
var yAxis = chart.GetChartComponent<YAxis>(dataZoom.yAxisIndexs[0]);
|
||||||
|
isInverse = yAxis != null && yAxis.inverse;
|
||||||
|
}
|
||||||
|
centerRatio = isInverse
|
||||||
|
? 1f - mousePercent / 100f
|
||||||
|
: mousePercent / 100f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (delta > 0)
|
if (delta > 0)
|
||||||
{
|
{
|
||||||
if (dataZoom.end <= dataZoom.start) return;
|
if (dataZoom.end <= dataZoom.start) return;
|
||||||
start = dataZoom.start + deltaPercent;
|
start = dataZoom.start + deltaPercent * centerRatio;
|
||||||
end = dataZoom.end - deltaPercent;
|
end = dataZoom.end - deltaPercent * (1 - centerRatio);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
start = dataZoom.start - deltaPercent;
|
start = dataZoom.start - deltaPercent * centerRatio;
|
||||||
end = dataZoom.end + deltaPercent;
|
end = dataZoom.end + deltaPercent * (1 - centerRatio);
|
||||||
}
|
}
|
||||||
UpdateDataZoomRange(dataZoom, start, end, grid);
|
UpdateDataZoomRange(dataZoom, start, end, grid);
|
||||||
}
|
}
|
||||||
@@ -453,7 +502,11 @@ namespace XCharts.Runtime
|
|||||||
if(grid == null) grid = chart.GetGridOfDataZoom(dataZoom);
|
if(grid == null) grid = chart.GetGridOfDataZoom(dataZoom);
|
||||||
var range = dataZoom.orient == Orient.Horizonal ? grid.context.width : grid.context.height;
|
var range = dataZoom.orient == Orient.Horizonal ? grid.context.width : grid.context.height;
|
||||||
var minRange = dataZoom.minZoomRatio * range;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -869,21 +869,36 @@ namespace XCharts.Runtime
|
|||||||
private static void UpdateFilterData_Category(Serie serie, DataZoom dataZoom)
|
private static void UpdateFilterData_Category(Serie serie, DataZoom dataZoom)
|
||||||
{
|
{
|
||||||
var data = serie.data;
|
var data = serie.data;
|
||||||
var range = Mathf.RoundToInt(data.Count * (dataZoom.end - dataZoom.start) / 100);
|
// Use (N-1) intervals so that data point i maps to exactly i/(N-1)*100% of the
|
||||||
if (range <= 0) range = 1;
|
// DataZoom width — matching how the shadow draws data via scaleWid = width/(N-1).
|
||||||
int start = 0, end = 0;
|
// CeilToInt for start ensures we never include a point that lies before the filler.
|
||||||
if (dataZoom.context.invert)
|
// 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);
|
if (dataZoom.context.invert)
|
||||||
start = end - range;
|
{
|
||||||
if (start < 0) start = 0;
|
startIndex = Mathf.CeilToInt((float)n * (100 - dataZoom.end) / 100);
|
||||||
|
endIndex = Mathf.FloorToInt((float)n * (100 - dataZoom.start) / 100);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
startIndex = Mathf.CeilToInt((float)n * dataZoom.start / 100);
|
||||||
|
endIndex = Mathf.FloorToInt((float)n * dataZoom.end / 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
start = Mathf.RoundToInt(data.Count * dataZoom.start / 100);
|
startIndex = 0;
|
||||||
end = start + range;
|
endIndex = 0;
|
||||||
if (end > data.Count) end = data.Count;
|
|
||||||
}
|
}
|
||||||
|
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);
|
var minZoomRatio = (int)(data.Count * dataZoom.minZoomRatio);
|
||||||
if (start != serie.m_FilterStart || end != serie.m_FilterEnd ||
|
if (start != serie.m_FilterStart || end != serie.m_FilterEnd ||
|
||||||
minZoomRatio != serie.m_FilterMinShow || serie.m_NeedUpdateFilterData)
|
minZoomRatio != serie.m_FilterMinShow || serie.m_NeedUpdateFilterData)
|
||||||
|
|||||||
@@ -379,7 +379,12 @@ namespace XCharts.Runtime
|
|||||||
|
|
||||||
// determine whether DataZoom filtering applies for this serie
|
// determine whether DataZoom filtering applies for this serie
|
||||||
var dz = chart.GetXDataZoomOfSerie(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
|
// try per-serie cache when not filtering by dataZoom and not in animation mode
|
||||||
if (!useDataZoomFilter && !needAnimation)
|
if (!useDataZoomFilter && !needAnimation)
|
||||||
@@ -476,7 +481,8 @@ namespace XCharts.Runtime
|
|||||||
(!isPolar && serie.yAxisIndex != axisIndex) ||
|
(!isPolar && serie.yAxisIndex != axisIndex) ||
|
||||||
!serie.show) continue;
|
!serie.show) continue;
|
||||||
var stackDz = chart.GetXDataZoomOfSerie(serie);
|
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);
|
var showData = serie.GetDataList(stackDz);
|
||||||
if (SeriesHelper.IsPercentStack<Bar>(series, serie.stack))
|
if (SeriesHelper.IsPercentStack<Bar>(series, serie.stack))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user