1330 lines
54 KiB
C#
1330 lines
54 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using EasyChart;
|
|
|
|
namespace EasyChart.Layers
|
|
{
|
|
public class BarSeriesRenderer : BaseSeriesRenderer
|
|
{
|
|
private struct BarHit
|
|
{
|
|
public int SerieIndex;
|
|
public int PointIndex;
|
|
public float Dist;
|
|
}
|
|
|
|
private BarHit? _hover;
|
|
private readonly HashSet<long> _categoryHover = new HashSet<long>();
|
|
private readonly HashSet<long> _categoryHoverTmp = new HashSet<long>();
|
|
private int _categoryHoverIndex = int.MinValue;
|
|
|
|
private static long GetHoverKey(int serieIndex, int pointIndex)
|
|
{
|
|
return ((long)serieIndex << 32) ^ (uint)pointIndex;
|
|
}
|
|
|
|
private void UpdateCategoryHover(int index, bool isCategoryAxis)
|
|
{
|
|
if (!isCategoryAxis)
|
|
{
|
|
if (_categoryHover.Count > 0 || _categoryHoverIndex != int.MinValue)
|
|
{
|
|
_categoryHover.Clear();
|
|
_categoryHoverIndex = int.MinValue;
|
|
MarkDirtyRepaint();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (_categoryHoverIndex == index) return;
|
|
|
|
_categoryHoverIndex = index;
|
|
_categoryHoverTmp.Clear();
|
|
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
for (int si = 0; si < Data.Series.Count; si++)
|
|
{
|
|
var serie = Data.Series[si];
|
|
if (serie == null || !serie.visible) continue;
|
|
if (serie.type != SerieType.Bar) continue;
|
|
if (serie.settings is not BarSettings settings) continue;
|
|
if (settings.hover == null || !settings.hover.enabled) continue;
|
|
|
|
var points = serie.seriesData;
|
|
if (points == null) continue;
|
|
|
|
for (int pi = 0; pi < points.Count; pi++)
|
|
{
|
|
var p = points[pi];
|
|
if (p == null) continue;
|
|
if (!Mathf.Approximately(p.x, index)) continue;
|
|
_categoryHoverTmp.Add(GetHoverKey(si, pi));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!_categoryHover.SetEquals(_categoryHoverTmp))
|
|
{
|
|
_categoryHover.Clear();
|
|
foreach (var k in _categoryHoverTmp) _categoryHover.Add(k);
|
|
MarkDirtyRepaint();
|
|
}
|
|
}
|
|
|
|
private int GetSerieIndex(Serie target)
|
|
{
|
|
if (Data == null || Data.Series == null || target == null) return -1;
|
|
for (int i = 0; i < Data.Series.Count; i++)
|
|
{
|
|
if (ReferenceEquals(Data.Series[i], target)) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private float GetPickRadius(BarSettings settings)
|
|
{
|
|
if (settings == null || settings.hover == null || !settings.hover.enabled) return 0f;
|
|
return Mathf.Max(0f, settings.hover.pickRadius);
|
|
}
|
|
|
|
private BarHit? FindHoverHit(TooltipContext context)
|
|
{
|
|
if (Data == null || Data.Series == null) return null;
|
|
|
|
float width = context.Width;
|
|
float height = context.Height;
|
|
if (width <= 0f || height <= 0f) return null;
|
|
|
|
var groups = GetBarGroups(Data.Series);
|
|
if (groups == null || groups.Count == 0) return null;
|
|
|
|
float baseBarWidth;
|
|
float barGap;
|
|
float categoryGap;
|
|
TryGetLayoutSettings(out baseBarWidth, out barGap, out categoryGap);
|
|
|
|
int totalGroups = groups.Count;
|
|
float layoutBarWidth = ComputeEffectiveBarWidth(baseBarWidth, width, totalGroups);
|
|
|
|
float categoryPad = 0f;
|
|
if (categoryGap > 0f && width > 0f && _xMax > _xMin)
|
|
{
|
|
float denom = _xMax - _xMin;
|
|
if (TryGetXAxisCategoryInfo(out _, out int count, out bool cellCenter) && cellCenter && count > 0)
|
|
{
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
denom = span;
|
|
}
|
|
|
|
float step = denom > 0f ? (width / denom) : width;
|
|
categoryPad = step * categoryGap * 0.5f;
|
|
}
|
|
|
|
float totalGroupWidthForCategory = totalGroups * layoutBarWidth + (totalGroups - 1) * barGap;
|
|
float edgePad = Mathf.Max(categoryPad, totalGroupWidthForCategory * 0.5f);
|
|
edgePad = Mathf.Min(edgePad, width * 0.5f);
|
|
|
|
var stackPos = new Dictionary<int, float>();
|
|
var stackNeg = new Dictionary<int, float>();
|
|
|
|
BarHit? best = null;
|
|
Vector2 mousePos = context.LocalPos;
|
|
|
|
for (int g = 0; g < groups.Count; g++)
|
|
{
|
|
var group = groups[g];
|
|
if (group == null || group.Count == 0) continue;
|
|
|
|
float totalGroupWidth = totalGroups * layoutBarWidth + (totalGroups - 1) * barGap;
|
|
float startOffset = -totalGroupWidth / 2f + layoutBarWidth / 2f;
|
|
float currentOffset = startOffset + g * (layoutBarWidth + barGap);
|
|
|
|
bool isStackedGroup = false;
|
|
if (group.Count > 0 && group[0].settings is BarSettings groupSettings)
|
|
{
|
|
isStackedGroup = groupSettings.stacked;
|
|
}
|
|
|
|
stackPos.Clear();
|
|
stackNeg.Clear();
|
|
|
|
for (int sgi = 0; sgi < group.Count; sgi++)
|
|
{
|
|
var serie = group[sgi];
|
|
if (serie == null || !serie.visible || serie.type != SerieType.Bar) continue;
|
|
if (serie.settings is not BarSettings settings) continue;
|
|
|
|
float pickRadius = GetPickRadius(settings);
|
|
if (pickRadius <= 0f) continue;
|
|
|
|
int serieIndex = GetSerieIndex(serie);
|
|
if (serieIndex < 0) continue;
|
|
|
|
var points = serie.seriesData;
|
|
if (points == null || points.Count == 0) continue;
|
|
|
|
for (int pi = 0; pi < points.Count; pi++)
|
|
{
|
|
var point = points[pi];
|
|
if (point == null) continue;
|
|
|
|
int xIndex = Mathf.RoundToInt(point.x);
|
|
float bottomVal = 0f;
|
|
if (isStackedGroup)
|
|
{
|
|
if (point.value >= 0)
|
|
{
|
|
if (stackPos.TryGetValue(xIndex, out float v)) bottomVal = v;
|
|
}
|
|
else
|
|
{
|
|
if (stackNeg.TryGetValue(xIndex, out float v)) bottomVal = v;
|
|
}
|
|
}
|
|
|
|
float animatedY = point.value * _animationProgress;
|
|
float topVal = bottomVal + animatedY;
|
|
|
|
if (isStackedGroup)
|
|
{
|
|
if (point.value >= 0) stackPos[xIndex] = topVal;
|
|
else stackNeg[xIndex] = topVal;
|
|
}
|
|
|
|
float pixelYBottom = GetPixelPos(new Vector2(point.x, bottomVal), width, height).y;
|
|
float pixelYTop = GetPixelPos(new Vector2(point.x, topVal), width, height).y;
|
|
|
|
float px = GetCategoryPixelX(point.x, width);
|
|
if (!IsXAxisCategoryWindowingActive() && !(TryGetXAxisCategoryInfo(out _, out _, out bool cellCenter) && cellCenter) && width > 0f && edgePad > 0f)
|
|
{
|
|
float ratio = Mathf.Clamp01(px / width);
|
|
px = Mathf.Lerp(edgePad, width - edgePad, ratio);
|
|
}
|
|
|
|
float actualBarWidth = settings.barWidth;
|
|
float x = px + currentOffset - actualBarWidth / 2f;
|
|
float drawY = pixelYTop;
|
|
float drawH = pixelYBottom - pixelYTop;
|
|
if (point.value < 0)
|
|
{
|
|
drawY = pixelYBottom;
|
|
drawH = pixelYTop - pixelYBottom;
|
|
}
|
|
|
|
if (drawH <= 0.001f) continue;
|
|
|
|
var rect = new Rect(x, drawY, actualBarWidth, drawH);
|
|
var pickRect = new Rect(rect.xMin - pickRadius, rect.yMin - pickRadius, rect.width + pickRadius * 2f, rect.height + pickRadius * 2f);
|
|
if (!pickRect.Contains(mousePos)) continue;
|
|
|
|
float dist = Vector2.Distance(mousePos, rect.center);
|
|
if (best == null || dist < best.Value.Dist)
|
|
{
|
|
best = new BarHit
|
|
{
|
|
SerieIndex = serieIndex,
|
|
PointIndex = pi,
|
|
Dist = dist
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
public override void ClearHover()
|
|
{
|
|
if (_hover != null)
|
|
{
|
|
_hover = null;
|
|
}
|
|
|
|
if (_categoryHover.Count > 0 || _categoryHoverIndex != int.MinValue)
|
|
{
|
|
_categoryHover.Clear();
|
|
_categoryHoverIndex = int.MinValue;
|
|
}
|
|
|
|
MarkDirtyRepaint();
|
|
}
|
|
|
|
private List<List<Serie>> GetBarGroups(List<Serie> series)
|
|
{
|
|
var groups = new List<List<Serie>>();
|
|
var stackGroups = new Dictionary<string, List<Serie>>();
|
|
var stackGroupOrder = new List<string>();
|
|
|
|
foreach (var s in series)
|
|
{
|
|
if (!s.visible || s.type != SerieType.Bar) continue;
|
|
if (s.settings is BarSettings settings)
|
|
{
|
|
if (settings.stacked)
|
|
{
|
|
string key = string.IsNullOrEmpty(settings.stackGroup) ? "__default__" : settings.stackGroup;
|
|
if (!stackGroups.TryGetValue(key, out var list))
|
|
{
|
|
list = new List<Serie>();
|
|
stackGroups[key] = list;
|
|
stackGroupOrder.Add(key);
|
|
}
|
|
list.Add(s);
|
|
}
|
|
else
|
|
{
|
|
groups.Add(new List<Serie> { s });
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < stackGroupOrder.Count; i++)
|
|
{
|
|
var key = stackGroupOrder[i];
|
|
if (stackGroups.TryGetValue(key, out var list) && list != null && list.Count > 0)
|
|
{
|
|
groups.Add(list);
|
|
}
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
private bool TryGetLayoutSettings(out float barWidth, out float barGap, out float categoryGap)
|
|
{
|
|
barWidth = 10f;
|
|
barGap = 0f;
|
|
categoryGap = 0f;
|
|
|
|
if (Data == null || Data.Series == null) return false;
|
|
for (int i = 0; i < Data.Series.Count; i++)
|
|
{
|
|
var s = Data.Series[i];
|
|
if (!s.visible || s.type != SerieType.Bar) continue;
|
|
if (s.settings is BarSettings bs)
|
|
{
|
|
barWidth = bs.barWidth;
|
|
barGap = bs.barGap;
|
|
categoryGap = bs.categoryGap;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private float ComputeEffectiveBarWidth(float baseBarWidth, float plotWidth, int groupCount)
|
|
{
|
|
if (plotWidth <= 0f || groupCount <= 0) return baseBarWidth;
|
|
|
|
// Get visible category count
|
|
int visibleCount = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (visibleCount < 1) visibleCount = 1;
|
|
|
|
// Calculate category slot width
|
|
float categorySlotWidth = plotWidth / visibleCount;
|
|
|
|
// Reserve some padding (e.g., 20% of slot for gaps)
|
|
float availableWidth = categorySlotWidth * 0.8f;
|
|
|
|
// Divide among groups
|
|
float maxBarWidth = availableWidth / groupCount;
|
|
|
|
// Use the smaller of configured width and calculated max
|
|
return Mathf.Min(baseBarWidth, maxBarWidth);
|
|
}
|
|
|
|
private bool TryGetXAxisCategoryInfo(out AxisConfig axis, out int labelCount, out bool cellCenter)
|
|
{
|
|
axis = null;
|
|
labelCount = 0;
|
|
cellCenter = false;
|
|
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
axis = GetAxisConfig(xAxisId);
|
|
if (axis == null || axis.axisType != AxisType.Category) return false;
|
|
if (axis.labels == null || axis.labels.Count <= 0) return false;
|
|
|
|
labelCount = axis.labels.Count;
|
|
cellCenter = axis.labelPlacement == CategoryLabelPlacement.CellCenter;
|
|
return true;
|
|
}
|
|
|
|
private bool IsXAxisCategoryIndexVisible(float categoryIndex)
|
|
{
|
|
if (TryGetXAxisCategoryInfo(out _, out int count, out _) && count > 0)
|
|
{
|
|
float xVal = categoryIndex;
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
int preloadExtra = count > span ? 1 : 0;
|
|
|
|
float xMax = _xMax + preloadExtra;
|
|
bool wraps = xMax >= count;
|
|
if (wraps && xVal < _xMin)
|
|
{
|
|
xVal += count;
|
|
}
|
|
|
|
return xVal >= (_xMin - 0.0001f) && xVal <= (xMax + 0.0001f);
|
|
}
|
|
|
|
return categoryIndex >= (_xMin - 0.0001f) && categoryIndex <= (_xMax + 0.0001f);
|
|
}
|
|
|
|
private bool IsXAxisCategoryWindowingActive()
|
|
{
|
|
if (TryGetXAxisCategoryInfo(out _, out int count, out _) && count > 0)
|
|
{
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
return count > span;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool IsFirstPreloadedCategoryIndex(float categoryIndex)
|
|
{
|
|
if (!TryGetXAxisCategoryInfo(out _, out int count, out _) || count <= 0) return false;
|
|
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
if (count <= span) return false;
|
|
|
|
float xVal = categoryIndex;
|
|
|
|
// Handle wrap-around
|
|
bool wraps = _xMax >= count - 1;
|
|
if (wraps && xVal < _xMin)
|
|
{
|
|
xVal += count;
|
|
}
|
|
|
|
// First visible item (at _xMin) is the one about to scroll out
|
|
return Mathf.Abs(xVal - _xMin) < 0.0001f;
|
|
}
|
|
|
|
private bool IsLastPreloadedCategoryIndex(float categoryIndex)
|
|
{
|
|
if (!TryGetXAxisCategoryInfo(out _, out int count, out _) || count <= 0) return false;
|
|
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
if (count <= span) return false;
|
|
|
|
float xVal = categoryIndex;
|
|
|
|
// Handle wrap-around
|
|
bool wraps = _xMax >= count - 1;
|
|
if (wraps && xVal < _xMin)
|
|
{
|
|
xVal += count;
|
|
}
|
|
|
|
// Last preloaded item (at _xMax + 1) is the one about to scroll in
|
|
float lastPreloadedIndex = _xMax + 1;
|
|
if (wraps && lastPreloadedIndex >= count)
|
|
{
|
|
lastPreloadedIndex = lastPreloadedIndex % count;
|
|
if (xVal >= count) xVal = xVal % count;
|
|
}
|
|
return Mathf.Abs(xVal - lastPreloadedIndex) < 0.0001f || Mathf.Abs(categoryIndex - (_xMax + 1)) < 0.0001f;
|
|
}
|
|
|
|
private bool IsEdgeCategoryIndexForLabel(float categoryIndex)
|
|
{
|
|
// When windowing is active, skip labels for the first and last visible items
|
|
if (!IsXAxisCategoryWindowingActive()) return false;
|
|
return IsLastPreloadedCategoryIndex(categoryIndex);
|
|
}
|
|
|
|
private bool IsFirstVisibleCategoryIndexForScaleOut(float categoryIndex)
|
|
{
|
|
// Check if this is the first visible item (will become edge after scroll)
|
|
// Note: Edge items are already skipped, so this checks the first non-edge item
|
|
if (!TryGetXAxisCategoryInfo(out _, out int count, out _) || count <= 0) return false;
|
|
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
if (count <= span) return false;
|
|
|
|
float xVal = categoryIndex;
|
|
|
|
// Handle wrap-around
|
|
bool wraps = _xMax >= count - 1;
|
|
if (wraps && xVal < _xMin)
|
|
{
|
|
xVal += count;
|
|
}
|
|
|
|
// First visible non-edge item (at _xMin + 1) is the one that will become edge after scroll
|
|
return Mathf.Abs(xVal - (_xMin + 1)) < 0.0001f;
|
|
}
|
|
|
|
private float GetCategoryPixelX(float categoryIndex, float width)
|
|
{
|
|
if (width <= 0f) return 0f;
|
|
|
|
if (TryGetXAxisCategoryInfo(out _, out int count, out bool cellCenter) && count > 0)
|
|
{
|
|
float xVal = categoryIndex;
|
|
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
|
|
// Handle wrap-around for circular category scrolling
|
|
bool windowingActive = count > span;
|
|
bool wraps = _xMax >= count - 1 && windowingActive;
|
|
if (wraps && xVal < _xMin)
|
|
{
|
|
xVal += count;
|
|
}
|
|
|
|
float t;
|
|
if (cellCenter)
|
|
{
|
|
// Use visibleCount (span) for interval calculation
|
|
// Preloaded items will extend beyond the visible area
|
|
// t = (i + 0.5f) / visibleCount where i = xVal - _xMin
|
|
t = (xVal - _xMin + 0.5f) / span;
|
|
if (float.IsNaN(t)) t = 0f;
|
|
return t * width; // Don't clamp - preloaded items extend beyond
|
|
}
|
|
else
|
|
{
|
|
// Tick mode: use span for interval calculation
|
|
// When windowing is active, use span directly (no edge padding)
|
|
// to match AxisLayer's label positioning
|
|
if (windowingActive)
|
|
{
|
|
// Match AxisLayer: t = i / (visibleCount - 1) where i = xVal - _xMin
|
|
t = span > 1 ? (xVal - _xMin) / (span - 1) : 0.5f;
|
|
if (float.IsNaN(t)) t = 0f;
|
|
return t * width;
|
|
}
|
|
else
|
|
{
|
|
// No windowing: use edge padding for better visual appearance
|
|
float baseBarWidth, barGap, categoryGap;
|
|
TryGetLayoutSettings(out baseBarWidth, out barGap, out categoryGap);
|
|
|
|
var groups = GetBarGroups(Data?.Series);
|
|
int totalGroups = groups != null ? groups.Count : 1;
|
|
float barWidth = ComputeEffectiveBarWidth(baseBarWidth, width, totalGroups);
|
|
|
|
float totalBarGroupWidth = totalGroups * barWidth + (totalGroups - 1) * barGap;
|
|
float edgePadding = totalBarGroupWidth * 0.5f + 10f;
|
|
edgePadding = Mathf.Min(edgePadding, width * 0.4f);
|
|
|
|
float availableWidth = width - 2f * edgePadding;
|
|
t = span > 1 ? (xVal - _xMin) / (span - 1) : 0.5f;
|
|
if (float.IsNaN(t)) t = 0f;
|
|
|
|
return edgePadding + t * availableWidth;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Mathf.Approximately(_xMax, _xMin)) return 0f;
|
|
float xRatio = (categoryIndex - _xMin) / (_xMax - _xMin);
|
|
if (float.IsNaN(xRatio)) xRatio = 0f;
|
|
return xRatio * width;
|
|
}
|
|
|
|
private int GetCategoryIndexFromPixelX(float pixelX, float width)
|
|
{
|
|
if (width <= 0f) return 0;
|
|
float t = Mathf.Clamp01(pixelX / width);
|
|
|
|
if (TryGetXAxisCategoryInfo(out _, out int count, out bool cellCenter) && count > 0 && cellCenter)
|
|
{
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
float idxF = (t * span) - 0.5f + _xMin;
|
|
int idx = Mathf.RoundToInt(idxF);
|
|
|
|
// Handle wrap-around
|
|
bool wraps = _xMax >= count - 1 && count > span;
|
|
if (wraps)
|
|
{
|
|
idx = ((idx % count) + count) % count;
|
|
}
|
|
return idx;
|
|
}
|
|
|
|
int span2 = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span2 < 1) span2 = 1;
|
|
float dataX = t * (span2 - 1) + _xMin;
|
|
int iX = Mathf.RoundToInt(dataX);
|
|
if (TryGetXAxisCategoryInfo(out _, out int count2, out _) && count2 > 0)
|
|
{
|
|
bool wraps = _xMax >= count2 - 1 && count2 > span2;
|
|
if (wraps)
|
|
{
|
|
iX = ((iX % count2) + count2) % count2;
|
|
}
|
|
}
|
|
return iX;
|
|
}
|
|
|
|
public override void UpdateLabels()
|
|
{
|
|
if (!TryGetChartAndLabelController(out var chart, out var labelController)) return;
|
|
GetSmoothScrollOffsets(chart, out float scrollOffsetX, out float scrollOffsetY);
|
|
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
float w = contentRect.width;
|
|
float h = contentRect.height;
|
|
if (w <= 0 || h <= 0) return;
|
|
|
|
var groups = GetBarGroups(Data.Series);
|
|
if (groups.Count == 0) return;
|
|
|
|
AxisId xAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
var xAxis = GetAxisConfig(xAxisId);
|
|
bool xIsCategory = xAxis != null && xAxis.axisType == AxisType.Category && xAxis.labels != null && xAxis.labels.Count > 0;
|
|
var xLabels = xIsCategory ? xAxis.labels : null;
|
|
|
|
AxisId yAxisId = (Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft;
|
|
var yAxis = GetAxisConfig(yAxisId);
|
|
|
|
var stackPos = new Dictionary<int, float>();
|
|
var stackNeg = new Dictionary<int, float>();
|
|
|
|
float baseBarWidth, barGap, categoryGap;
|
|
TryGetLayoutSettings(out baseBarWidth, out barGap, out categoryGap);
|
|
|
|
int totalGroups = groups.Count;
|
|
float barWidth = ComputeEffectiveBarWidth(baseBarWidth, w, totalGroups);
|
|
|
|
float categoryPad = 0f;
|
|
if (categoryGap > 0f && w > 0f && _xMax > _xMin)
|
|
{
|
|
float denom = _xMax - _xMin;
|
|
if (TryGetXAxisCategoryInfo(out _, out int count, out bool cellCenter) && cellCenter && count > 0)
|
|
{
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
denom = span;
|
|
}
|
|
float step = denom > 0f ? (w / denom) : w;
|
|
categoryPad = step * categoryGap * 0.5f;
|
|
}
|
|
|
|
float totalGroupWidthForCategory = totalGroups * barWidth + (totalGroups - 1) * barGap;
|
|
float edgePad = Mathf.Max(categoryPad, totalGroupWidthForCategory * 0.5f);
|
|
edgePad = Mathf.Min(edgePad, w * 0.5f);
|
|
|
|
for (int g = 0; g < groups.Count; g++)
|
|
{
|
|
var group = groups[g];
|
|
if (group == null || group.Count == 0) continue;
|
|
|
|
float totalGroupWidth = totalGroups * barWidth + (totalGroups - 1) * barGap;
|
|
float startOffset = -totalGroupWidth / 2f + barWidth / 2f;
|
|
float currentOffset = startOffset + g * (barWidth + barGap);
|
|
|
|
bool isStackedGroup = group.Count > 0 && group[0].settings is BarSettings gs && gs.stacked;
|
|
|
|
stackPos.Clear();
|
|
stackNeg.Clear();
|
|
|
|
for (int sgi = 0; sgi < group.Count; sgi++)
|
|
{
|
|
var serie = group[sgi];
|
|
if (serie == null || !serie.visible || serie.type != SerieType.Bar) continue;
|
|
|
|
int serieIndex = GetSerieIndex(serie);
|
|
if (serieIndex < 0) continue;
|
|
if (serie.settings is not BarSettings settings) continue;
|
|
|
|
var points = serie.seriesData;
|
|
if (points == null) continue;
|
|
|
|
if (!serie.labelSettings.enabled)
|
|
{
|
|
if (isStackedGroup) AccumulateStackHeight(serie, stackPos, stackNeg);
|
|
continue;
|
|
}
|
|
|
|
int dpPlaces = Mathf.Clamp(serie.labelSettings != null ? serie.labelSettings.decimalPlaces : 2, 0, 8);
|
|
bool showName = serie.labelSettings != null && serie.labelSettings.showName;
|
|
Vector2 extraOffset = serie.labelSettings != null ? serie.labelSettings.offset : Vector2.zero;
|
|
int fontSizeOverride = serie.labelSettings != null ? serie.labelSettings.fontSize : 0;
|
|
Color colorOverride = serie.labelSettings != null ? serie.labelSettings.color : Color.clear;
|
|
|
|
for (int pi = 0; pi < points.Count; pi++)
|
|
{
|
|
var point = points[pi];
|
|
if (point == null) continue;
|
|
if (!IsXAxisCategoryIndexVisible(point.x)) continue;
|
|
// Skip labels for edge items when windowing is active
|
|
if (IsEdgeCategoryIndexForLabel(point.x)) continue;
|
|
|
|
int xIndex = Mathf.RoundToInt(point.x);
|
|
|
|
float bottomVal = 0f;
|
|
if (isStackedGroup)
|
|
{
|
|
if (point.value >= 0)
|
|
{
|
|
if (stackPos.TryGetValue(xIndex, out float v)) bottomVal = v;
|
|
}
|
|
else
|
|
{
|
|
if (stackNeg.TryGetValue(xIndex, out float v)) bottomVal = v;
|
|
}
|
|
}
|
|
|
|
float topVal = bottomVal + point.value;
|
|
if (isStackedGroup)
|
|
{
|
|
if (point.value >= 0) stackPos[xIndex] = topVal;
|
|
else stackNeg[xIndex] = topVal;
|
|
}
|
|
|
|
float pixelYTop = GetPixelPos(new Vector2(point.x, topVal), w, h).y;
|
|
float pixelYBottom = GetPixelPos(new Vector2(point.x, bottomVal), w, h).y;
|
|
|
|
float px = GetCategoryPixelX(point.x, w);
|
|
if (!IsXAxisCategoryWindowingActive() && !(TryGetXAxisCategoryInfo(out _, out _, out bool cellCenter) && cellCenter) && w > 0f && edgePad > 0f)
|
|
{
|
|
float ratio = Mathf.Clamp01(px / w);
|
|
px = Mathf.Lerp(edgePad, w - edgePad, ratio);
|
|
}
|
|
|
|
float x = px + currentOffset;
|
|
float y;
|
|
|
|
// Calculate label Y position based on LabelPosition
|
|
if (serie.labelSettings.position == LabelPosition.Center)
|
|
{
|
|
// Center of bar
|
|
y = (pixelYTop + pixelYBottom) / 2f;
|
|
}
|
|
else if (serie.labelSettings.position == LabelPosition.Inside)
|
|
{
|
|
// Inside bar, offset from top edge
|
|
y = point.value >= 0 ? pixelYTop + 15 : pixelYTop - 15;
|
|
}
|
|
else
|
|
{
|
|
// Outside: above bar for positive, below for negative
|
|
y = point.value >= 0 ? pixelYTop - 10 : pixelYTop + 10;
|
|
}
|
|
|
|
Vector2 anchor = new Vector2(x, y);
|
|
if (!IsAnchorVisibleInPlot(anchor, w, h, scrollOffsetX, scrollOffsetY)) continue;
|
|
|
|
string text = FormatAxisValue(point.value, yAxis, dpPlaces);
|
|
if (showName)
|
|
{
|
|
string name = (xLabels != null && xIndex < xLabels.Count && xIndex >= 0) ? xLabels[xIndex] : serie.name;
|
|
text = $"{name}\n{text}";
|
|
}
|
|
|
|
string pointId = GetStableKeyForPoint(xIsCategory, point.x, point.id, pi);
|
|
string labelKey = $"bar:{serieIndex}:{pointId}";
|
|
var bg = serie.labelSettings.background;
|
|
var bgColor = bg != null ? bg.color : default;
|
|
var bgTex = bg != null ? bg.texture : null;
|
|
var desc = BuildSeriesLabelDesc(
|
|
labelKey,
|
|
text,
|
|
anchor,
|
|
new Vector2(extraOffset.x, extraOffset.y),
|
|
clipToPlot: true,
|
|
fontSizeOverride,
|
|
colorOverride,
|
|
ChartLabelAnchor.Center,
|
|
bgColor,
|
|
bgTex);
|
|
|
|
labelController.Submit(desc);
|
|
|
|
// Animate scale out for the first visible item (will become edge after scroll)
|
|
if (IsXAxisCategoryWindowingActive() && IsFirstPreloadedCategoryIndex(point.x))
|
|
{
|
|
labelController.RequestScaleOut(labelKey);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override bool GetTooltip(TooltipContext context, List<TooltipItem> items, ref Vector2? cursorPosition, ref string categoryLabel)
|
|
{
|
|
if (Data == null || Data.Series == null) return false;
|
|
|
|
var hoverHit = FindHoverHit(context);
|
|
bool hoverChanged = false;
|
|
if (hoverHit.HasValue)
|
|
{
|
|
if (!_hover.HasValue || _hover.Value.SerieIndex != hoverHit.Value.SerieIndex || _hover.Value.PointIndex != hoverHit.Value.PointIndex)
|
|
{
|
|
_hover = hoverHit;
|
|
hoverChanged = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_hover != null)
|
|
{
|
|
_hover = null;
|
|
hoverChanged = true;
|
|
}
|
|
}
|
|
|
|
if (hoverChanged) MarkDirtyRepaint();
|
|
|
|
float width = context.Width;
|
|
float areaX = context.LocalPos.x;
|
|
|
|
if (width <= 0) return false;
|
|
|
|
// Calculate current index based on X position
|
|
float ratio = Mathf.Clamp01(areaX / width);
|
|
int index = GetCategoryIndexFromPixelX(areaX, width);
|
|
|
|
List<string> labels = null;
|
|
if (Data != null && Data.Axes != null)
|
|
{
|
|
AxisId xAxisId = (Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
for (int i = 0; i < Data.Axes.Count; i++)
|
|
{
|
|
var a = Data.Axes[i];
|
|
if (a != null && a.id == xAxisId && a.axisType == AxisType.Category)
|
|
{
|
|
labels = a.labels;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate index
|
|
bool isCategoryAxis = labels != null && labels.Count > 0;
|
|
if (index < 0 || (isCategoryAxis && index >= labels.Count))
|
|
{
|
|
UpdateCategoryHover(int.MinValue, false);
|
|
return false;
|
|
}
|
|
|
|
UpdateCategoryHover(index, isCategoryAxis);
|
|
|
|
// Set shared cursor position (snap to X)
|
|
if (cursorPosition == null)
|
|
{
|
|
float snapPixelX = GetCategoryPixelX(index, width);
|
|
cursorPosition = new Vector2(snapPixelX, 0);
|
|
}
|
|
|
|
// Set Category Label if not already set
|
|
if (string.IsNullOrEmpty(categoryLabel))
|
|
{
|
|
categoryLabel = (isCategoryAxis && index < labels.Count && index >= 0)
|
|
? labels[index]
|
|
: FormatIntCached(index);
|
|
}
|
|
|
|
bool hit = false;
|
|
|
|
// Find values for this index
|
|
foreach (var serie in Data.Series)
|
|
{
|
|
if (!serie.visible) continue;
|
|
if (serie.type != SerieType.Bar) continue;
|
|
|
|
// Find point with approx X
|
|
bool found = false;
|
|
Vector2 val = Vector2.zero;
|
|
|
|
if (serie.seriesData == null) continue;
|
|
foreach(var p in serie.seriesData)
|
|
{
|
|
if (p == null) continue;
|
|
if (Mathf.Approximately(p.x, index))
|
|
{
|
|
val = new Vector2(p.x, p.value);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (found && serie.settings is BarSettings settings)
|
|
{
|
|
Color color = ResolveFillColor(settings.textureFill, Color.white);
|
|
items.Add(new TooltipItem
|
|
{
|
|
Name = serie.name,
|
|
Value = FormatAxisValue(
|
|
val.y,
|
|
GetAxisConfig((Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft),
|
|
Mathf.Clamp(serie.labelSettings != null ? serie.labelSettings.decimalPlaces : 2, 0, 8)
|
|
),
|
|
Color = color
|
|
});
|
|
hit = true;
|
|
}
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
private void AccumulateStackHeight(Serie serie, Dictionary<int, float> stackPos, Dictionary<int, float> stackNeg)
|
|
{
|
|
if (serie.seriesData == null) return;
|
|
foreach (var p in serie.seriesData)
|
|
{
|
|
if (p == null) continue;
|
|
if (!IsXAxisCategoryIndexVisible(p.x)) continue;
|
|
int xIndex = Mathf.RoundToInt(p.x);
|
|
if (p.value >= 0)
|
|
{
|
|
if (!stackPos.ContainsKey(xIndex)) stackPos[xIndex] = 0;
|
|
stackPos[xIndex] += p.value;
|
|
}
|
|
else
|
|
{
|
|
if (!stackNeg.ContainsKey(xIndex)) stackNeg[xIndex] = 0;
|
|
stackNeg[xIndex] += p.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CreateLabelsForSerie(Serie serie, float width, float height, float offset, bool isStacked, Dictionary<int, float> stackPos, Dictionary<int, float> stackNeg, float edgePad)
|
|
{
|
|
var points = serie.seriesData;
|
|
if (points == null) return;
|
|
float zeroPixelY = GetPixelPos(new Vector2(_xMin, 0), width, height).y;
|
|
|
|
List<string> labels = null;
|
|
if (Data != null && Data.Axes != null)
|
|
{
|
|
AxisId xAxisId = (Data.Cartesian != null) ? Data.Cartesian.xAxisId : AxisId.XBottom;
|
|
for (int i = 0; i < Data.Axes.Count; i++)
|
|
{
|
|
var a = Data.Axes[i];
|
|
if (a != null && a.id == xAxisId && a.axisType == AxisType.Category)
|
|
{
|
|
labels = a.labels;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < points.Count; i++)
|
|
{
|
|
var point = points[i];
|
|
if (point == null) continue;
|
|
if (!IsXAxisCategoryIndexVisible(point.x)) continue;
|
|
// Skip labels for edge items when windowing is active
|
|
if (IsEdgeCategoryIndexForLabel(point.x)) continue;
|
|
int xIndex = Mathf.RoundToInt(point.x);
|
|
|
|
float bottomVal = 0;
|
|
if (isStacked)
|
|
{
|
|
if (point.value >= 0)
|
|
{
|
|
if (stackPos.TryGetValue(xIndex, out float v)) bottomVal = v;
|
|
}
|
|
else
|
|
{
|
|
if (stackNeg.TryGetValue(xIndex, out float v)) bottomVal = v;
|
|
}
|
|
}
|
|
|
|
float topVal = bottomVal + point.value;
|
|
|
|
// Update Stack (Accumulate)
|
|
if (isStacked)
|
|
{
|
|
if (point.value >= 0) stackPos[xIndex] = topVal;
|
|
else stackNeg[xIndex] = topVal;
|
|
}
|
|
|
|
// Calculate Position
|
|
// We want label at the "Tip" of the bar segment.
|
|
// Tip is 'topVal'.
|
|
|
|
// Get pixel positions
|
|
// We need Top and Bottom of the segment to center vertically if needed.
|
|
// topVal -> Top Y
|
|
// bottomVal -> Bottom Y
|
|
|
|
float pixelYTop = GetPixelPos(new Vector2(point.x, topVal), width, height).y;
|
|
float pixelYBottom = GetPixelPos(new Vector2(point.x, bottomVal), width, height).y;
|
|
|
|
float px = GetCategoryPixelX(point.x, width);
|
|
if (!IsXAxisCategoryWindowingActive() && !(TryGetXAxisCategoryInfo(out _, out _, out bool cellCenter) && cellCenter) && width > 0f && edgePad > 0f)
|
|
{
|
|
float ratio = Mathf.Clamp01(px / width);
|
|
px = Mathf.Lerp(edgePad, width - edgePad, ratio);
|
|
}
|
|
float x = px + offset;
|
|
float y = pixelYTop; // Default Outside/Top
|
|
|
|
if (serie.labelSettings.position == LabelPosition.Inside)
|
|
{
|
|
// Slightly inside the top
|
|
y = pixelYTop + 15; // Move down (Y increases down)
|
|
if (point.value < 0) y = pixelYTop - 15; // Move up if negative bar?
|
|
|
|
// Actually:
|
|
// Positive Bar: Top is pixelYTop (smaller Y). Bottom is pixelYBottom (larger Y).
|
|
// Inside Top: pixelYTop + padding.
|
|
|
|
// Negative Bar: Top is pixelYTop (larger Y? No).
|
|
// If point.y < 0: topVal < bottomVal.
|
|
// pixelYTop > pixelYBottom.
|
|
// Visual Top of bar is pixelYBottom?
|
|
// Wait, negative bar extends DOWN from 0.
|
|
// 0 is at pixelYBottom. End is at pixelYTop.
|
|
// Visual "End" is pixelYTop.
|
|
// So "Inside" means slightly towards 0 (upwards, smaller Y).
|
|
|
|
if (point.value >= 0) y = pixelYTop + 15;
|
|
else y = pixelYTop - 15;
|
|
}
|
|
else if (serie.labelSettings.position == LabelPosition.Center)
|
|
{
|
|
// Center of the segment
|
|
y = (pixelYTop + pixelYBottom) / 2f;
|
|
}
|
|
else // Outside
|
|
{
|
|
y = pixelYTop;
|
|
// Adjust margin later
|
|
}
|
|
|
|
int dpPlaces = Mathf.Clamp(serie.labelSettings != null ? serie.labelSettings.decimalPlaces : 2, 0, 8);
|
|
string text = FormatAxisValue(
|
|
point.value,
|
|
GetAxisConfig((Data != null && Data.Cartesian != null) ? Data.Cartesian.yAxisId : AxisId.YLeft),
|
|
dpPlaces
|
|
);
|
|
if (serie.labelSettings.showName)
|
|
{
|
|
string name = (labels != null && xIndex < labels.Count && xIndex >= 0) ? labels[xIndex] : serie.name;
|
|
text = $"{name}\n{text}";
|
|
}
|
|
|
|
// Alignment adjustments based on position
|
|
float baseMarginTop;
|
|
if (serie.labelSettings.position == LabelPosition.Center || serie.labelSettings.position == LabelPosition.Inside)
|
|
{
|
|
// Center align text vertically around y
|
|
baseMarginTop = -7; // Half line height approx
|
|
}
|
|
else
|
|
{
|
|
// Outside
|
|
baseMarginTop = point.value < 0 ? 5 : -20;
|
|
}
|
|
|
|
DrawSerieLabel(serie, text, new Vector2(x, y), -10, baseMarginTop);
|
|
//_labelContainer.Add(lbl); // Handled by GetLabel
|
|
}
|
|
}
|
|
|
|
protected override void OnGenerateVisualContent(MeshGenerationContext context)
|
|
{
|
|
if (Data == null || Data.Series == null) return;
|
|
|
|
var width = contentRect.width;
|
|
var height = contentRect.height;
|
|
if (width <= 0 || height <= 0) return;
|
|
|
|
var painter = context.painter2D;
|
|
|
|
var groups = GetBarGroups(Data.Series);
|
|
if (groups.Count == 0) return;
|
|
|
|
// Stacking Dictionaries
|
|
var stackPos = new Dictionary<int, float>();
|
|
var stackNeg = new Dictionary<int, float>();
|
|
|
|
float baseBarWidth;
|
|
float barGap;
|
|
float categoryGap;
|
|
TryGetLayoutSettings(out baseBarWidth, out barGap, out categoryGap);
|
|
|
|
int totalGroups = groups.Count;
|
|
|
|
// Compute effective bar width based on visible category count
|
|
float barWidth = ComputeEffectiveBarWidth(baseBarWidth, width, totalGroups);
|
|
|
|
float categoryPad = 0f;
|
|
if (categoryGap > 0f && width > 0f && _xMax > _xMin)
|
|
{
|
|
float denom = _xMax - _xMin;
|
|
if (TryGetXAxisCategoryInfo(out _, out int count, out bool cellCenter) && cellCenter && count > 0)
|
|
{
|
|
int span = Mathf.RoundToInt(_xMax - _xMin + 1);
|
|
if (span < 1) span = 1;
|
|
denom = span;
|
|
}
|
|
|
|
float step = denom > 0f ? (width / denom) : width;
|
|
categoryPad = step * categoryGap * 0.5f;
|
|
}
|
|
|
|
float totalGroupWidthForCategory = totalGroups * barWidth + (totalGroups - 1) * barGap;
|
|
float edgePad = Mathf.Max(categoryPad, totalGroupWidthForCategory * 0.5f);
|
|
edgePad = Mathf.Min(edgePad, width * 0.5f);
|
|
|
|
for (int g = 0; g < groups.Count; g++)
|
|
{
|
|
var group = groups[g];
|
|
if (group.Count == 0) continue;
|
|
|
|
float totalGroupWidth = totalGroups * barWidth + (totalGroups - 1) * barGap;
|
|
float startOffset = -totalGroupWidth / 2f + barWidth / 2f;
|
|
float currentOffset = startOffset + g * (barWidth + barGap);
|
|
|
|
bool isStackedGroup = false;
|
|
if (group.Count > 0 && group[0].settings is BarSettings gs) isStackedGroup = gs.stacked;
|
|
|
|
stackPos.Clear();
|
|
stackNeg.Clear();
|
|
|
|
for (int i = 0; i < group.Count; i++)
|
|
{
|
|
var serie = group[i];
|
|
bool isLastInStackGroup = isStackedGroup && i == group.Count - 1;
|
|
DrawBarSerie(context, painter, serie, width, height, currentOffset, isStackedGroup, isLastInStackGroup, stackPos, stackNeg, edgePad, barWidth);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawBarSerie(MeshGenerationContext context, Painter2D painter, Serie serie, float width, float height, float offset, bool isStacked, bool isLastInStackGroup, Dictionary<int, float> stackPos, Dictionary<int, float> stackNeg, float edgePad, float effectiveBarWidth)
|
|
{
|
|
if (!(serie.settings is BarSettings settings)) return;
|
|
|
|
int serieIndex = GetSerieIndex(serie);
|
|
var points = serie.seriesData;
|
|
if (points == null) return;
|
|
|
|
float zeroY = GetPixelPos(new Vector2(_xMin, 0), width, height).y;
|
|
float baseBarWidth = effectiveBarWidth;
|
|
|
|
float cornerRadius = Mathf.Max(0f, settings.cornerRadius);
|
|
int cornerSegments = Mathf.Clamp(settings.cornerSegments, 1, 16);
|
|
bool canRound = cornerRadius > 0.01f;
|
|
|
|
var roundedPoints = canRound ? new List<Vector2>(32) : null;
|
|
|
|
for (int pi = 0; pi < points.Count; pi++)
|
|
{
|
|
var point = points[pi];
|
|
if (point == null) continue;
|
|
if (!IsXAxisCategoryIndexVisible(point.x)) continue;
|
|
|
|
int xIndex = Mathf.RoundToInt(point.x);
|
|
float bottomVal = 0f;
|
|
if (isStacked)
|
|
{
|
|
if (point.value >= 0)
|
|
{
|
|
if (stackPos.TryGetValue(xIndex, out float v)) bottomVal = v;
|
|
}
|
|
else
|
|
{
|
|
if (stackNeg.TryGetValue(xIndex, out float v)) bottomVal = v;
|
|
}
|
|
}
|
|
|
|
float animatedY = point.value * _animationProgress;
|
|
float topVal = bottomVal + animatedY;
|
|
|
|
if (isStacked)
|
|
{
|
|
if (point.value >= 0) stackPos[xIndex] = topVal;
|
|
else stackNeg[xIndex] = topVal;
|
|
}
|
|
|
|
float pixelYBottom = GetPixelPos(new Vector2(point.x, bottomVal), width, height).y;
|
|
float pixelYTop = GetPixelPos(new Vector2(point.x, topVal), width, height).y;
|
|
|
|
float px = GetCategoryPixelX(point.x, width);
|
|
if (!IsXAxisCategoryWindowingActive() && !(TryGetXAxisCategoryInfo(out _, out _, out bool cellCenter) && cellCenter) && width > 0f && edgePad > 0f)
|
|
{
|
|
float ratio = Mathf.Clamp01(px / width);
|
|
px = Mathf.Lerp(edgePad, width - edgePad, ratio);
|
|
}
|
|
|
|
float drawY = pixelYTop;
|
|
float drawH = pixelYBottom - pixelYTop;
|
|
if (point.value < 0)
|
|
{
|
|
drawY = pixelYBottom;
|
|
drawH = pixelYTop - pixelYBottom;
|
|
}
|
|
|
|
if (Mathf.Approximately(drawH, 0f))
|
|
continue;
|
|
|
|
bool isHovered = false;
|
|
if (settings.hover != null && settings.hover.enabled)
|
|
{
|
|
if (_hover.HasValue && _hover.Value.SerieIndex == serieIndex && _hover.Value.PointIndex == pi)
|
|
{
|
|
isHovered = true;
|
|
}
|
|
else if (_categoryHover.Count > 0 && _categoryHover.Contains(GetHoverKey(serieIndex, pi)))
|
|
{
|
|
isHovered = true;
|
|
}
|
|
}
|
|
|
|
float localBarWidth = baseBarWidth;
|
|
var barFill = settings.textureFill;
|
|
UnpackTextureFill(barFill, Color.white, out var barTex, out var tiling, out var uvOffset, out var barTint);
|
|
|
|
Texture2D hoverTex = null;
|
|
Vector2 hoverTiling = Vector2.one;
|
|
Vector2 hoverUvOffset = Vector2.zero;
|
|
Color hoverOverlayTint = Color.clear;
|
|
|
|
if (isHovered)
|
|
{
|
|
float scale = Mathf.Max(0f, settings.hover.scale);
|
|
if (!Mathf.Approximately(scale, 0f) && !Mathf.Approximately(scale, 1f))
|
|
{
|
|
localBarWidth *= scale;
|
|
}
|
|
|
|
barTint = Color.Lerp(barTint, Color.white, 0.35f);
|
|
var hoverFill = settings.hover.textureFill;
|
|
TryResolveTextureFill(hoverFill, Color.clear, true, out hoverTex, out hoverTiling, out hoverUvOffset, out hoverOverlayTint);
|
|
}
|
|
|
|
float centerX = px + offset;
|
|
float x = centerX - localBarWidth / 2f;
|
|
var rect = new Rect(x, drawY, localBarWidth, drawH);
|
|
|
|
bool roundThisSegment = canRound && (!isStacked || isLastInStackGroup);
|
|
bool isPositive = point.value >= 0;
|
|
bool roundTop = roundThisSegment && isPositive;
|
|
bool roundBottom = roundThisSegment && !isPositive;
|
|
|
|
float r = roundThisSegment ? cornerRadius : 0f;
|
|
if (r > 0f)
|
|
{
|
|
r = Mathf.Min(r, Mathf.Min(rect.width, rect.height) * 0.5f);
|
|
}
|
|
|
|
var backgroundSettings = settings.background;
|
|
if (backgroundSettings != null && backgroundSettings.show)
|
|
{
|
|
bool drawBackground = true;
|
|
if (isStacked)
|
|
{
|
|
drawBackground = Mathf.Approximately(bottomVal, 0f);
|
|
}
|
|
|
|
if (drawBackground)
|
|
{
|
|
var bgFill = backgroundSettings.textureFill;
|
|
UnpackTextureFill(bgFill, out var bgTex, out var bgTiling, out var bgOffset, out var bgTint);
|
|
if (bgTex != null)
|
|
{
|
|
DrawTexturedQuad(context, new Rect(x, 0, localBarWidth, height), bgTex, bgTiling, bgOffset, bgTint, true);
|
|
}
|
|
else
|
|
{
|
|
painter.fillColor = bgTint;
|
|
painter.BeginPath();
|
|
painter.MoveTo(new Vector2(x, 0));
|
|
painter.LineTo(new Vector2(x + localBarWidth, 0));
|
|
painter.LineTo(new Vector2(x + localBarWidth, height));
|
|
painter.LineTo(new Vector2(x, height));
|
|
painter.ClosePath();
|
|
painter.Fill();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (barTex != null)
|
|
{
|
|
Color tint = barTint;
|
|
|
|
if (r > 0f)
|
|
{
|
|
if (roundedPoints == null) roundedPoints = new List<Vector2>(32);
|
|
RoundedRectUtils.BuildRoundedRectPoints(roundedPoints, rect, r, cornerSegments, roundTop, roundTop, roundBottom, roundBottom);
|
|
DrawTexturedFan(context, roundedPoints, rect, barTex, tiling, uvOffset, tint, true);
|
|
}
|
|
else
|
|
{
|
|
DrawTexturedQuad(context, rect, barTex, tiling, uvOffset, tint, true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
painter.fillColor = barTint;
|
|
if (r > 0f)
|
|
{
|
|
RoundedRectUtils.BeginRoundedRectPath(painter, rect, r, cornerSegments, roundTop, roundTop, roundBottom, roundBottom);
|
|
}
|
|
else
|
|
{
|
|
painter.BeginPath();
|
|
painter.MoveTo(new Vector2(rect.xMin, rect.yMin));
|
|
painter.LineTo(new Vector2(rect.xMax, rect.yMin));
|
|
painter.LineTo(new Vector2(rect.xMax, rect.yMax));
|
|
painter.LineTo(new Vector2(rect.xMin, rect.yMax));
|
|
painter.ClosePath();
|
|
}
|
|
painter.Fill();
|
|
}
|
|
|
|
if (hoverTex != null && hoverOverlayTint.a > 0f)
|
|
{
|
|
if (r > 0f)
|
|
{
|
|
if (roundedPoints == null) roundedPoints = new List<Vector2>(32);
|
|
RoundedRectUtils.BuildRoundedRectPoints(roundedPoints, rect, r, cornerSegments, roundTop, roundTop, roundBottom, roundBottom);
|
|
DrawTexturedFan(context, roundedPoints, rect, hoverTex, hoverTiling, hoverUvOffset, hoverOverlayTint, true);
|
|
}
|
|
else
|
|
{
|
|
DrawTexturedQuad(context, rect, hoverTex, hoverTiling, hoverUvOffset, hoverOverlayTint, true);
|
|
}
|
|
}
|
|
|
|
var borderSettings = settings.border;
|
|
float borderWidth = borderSettings != null ? borderSettings.width : 0f;
|
|
Color borderColor = borderSettings != null ? borderSettings.color : new Color(0, 0, 0, 0);
|
|
|
|
if (borderWidth > 0f && borderColor.a > 0f)
|
|
{
|
|
painter.strokeColor = borderColor;
|
|
painter.lineWidth = borderWidth;
|
|
|
|
if (barTex != null)
|
|
{
|
|
if (r > 0f) RoundedRectUtils.BeginRoundedRectPath(painter, rect, r, cornerSegments, roundTop, roundTop, roundBottom, roundBottom);
|
|
else
|
|
{
|
|
painter.BeginPath();
|
|
painter.MoveTo(new Vector2(rect.xMin, rect.yMin));
|
|
painter.LineTo(new Vector2(rect.xMax, rect.yMin));
|
|
painter.LineTo(new Vector2(rect.xMax, rect.yMax));
|
|
painter.LineTo(new Vector2(rect.xMin, rect.yMax));
|
|
painter.ClosePath();
|
|
}
|
|
}
|
|
|
|
painter.Stroke();
|
|
}
|
|
|
|
_ = zeroY;
|
|
}
|
|
}
|
|
}
|
|
}
|