Skip to content

Commit b07acaa

Browse files
committed
fix: per-series coloring in multi-series braille line graphs
Render each series independently with its own pixel grid and color instead of sharing one grid with last-writer-wins color map. Braille patterns are OR'd where series overlap in the same cell.
1 parent ac8da9f commit b07acaa

1 file changed

Lines changed: 148 additions & 61 deletions

File tree

SharpConsoleUI/Controls/LineGraphControl/LineGraphControl.Braille.cs

Lines changed: 148 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,28 @@ private void PaintBraille(CharacterBuffer buffer, int graphX, int graphY,
3030
if (pixelWidth <= 0 || pixelHeight <= 0)
3131
return;
3232

33+
double range = globalMax - globalMin;
34+
35+
// For single series, use the fast path (original logic)
36+
if (seriesSnapshots.Count <= 1)
37+
{
38+
PaintBrailleSingleSeries(buffer, graphX, graphY, graphWidth, graphHeight,
39+
pixelWidth, pixelHeight, clipRect, bgColor, seriesSnapshots, globalMin, range);
40+
return;
41+
}
42+
43+
// Multi-series: render each series into its own pixel grid,
44+
// then composite into buffer with per-series colors.
45+
PaintBrailleMultiSeries(buffer, graphX, graphY, graphWidth, graphHeight,
46+
pixelWidth, pixelHeight, clipRect, bgColor, seriesSnapshots, globalMin, range);
47+
}
48+
49+
private void PaintBrailleSingleSeries(CharacterBuffer buffer, int graphX, int graphY,
50+
int graphWidth, int graphHeight, int pixelWidth, int pixelHeight,
51+
LayoutRect clipRect, Color bgColor,
52+
List<(LineGraphSeries series, List<double> data)> seriesSnapshots,
53+
double globalMin, double range)
54+
{
3355
// Allocate or reuse pixel grid
3456
if (_pixelGridCache == null ||
3557
_pixelGridCache.GetLength(0) != pixelHeight ||
@@ -42,60 +64,61 @@ private void PaintBraille(CharacterBuffer buffer, int graphX, int graphY,
4264
Array.Clear(_pixelGridCache);
4365
}
4466

45-
double range = globalMax - globalMin;
67+
Color seriesColor = Color.Cyan1;
68+
ColorGradient? gradient = null;
4669

47-
// Track which series contributes to each cell column for coloring
48-
// Last series rendered wins per cell
49-
var seriesColorMap = new int[graphWidth];
50-
Array.Fill(seriesColorMap, -1);
70+
if (seriesSnapshots.Count == 1)
71+
{
72+
var (series, data) = seriesSnapshots[0];
73+
seriesColor = series.LineColor;
74+
gradient = series.Gradient;
75+
if (data.Count > 0)
76+
DrawSeriesPixels(_pixelGridCache, data, pixelWidth, pixelHeight, globalMin, range);
77+
}
5178

52-
// Draw all series into pixel grid
53-
for (int si = 0; si < seriesSnapshots.Count; si++)
79+
for (int row = 0; row < graphHeight; row++)
5480
{
55-
var (series, data) = seriesSnapshots[si];
56-
if (data.Count == 0)
81+
int paintY = graphY + row;
82+
if (paintY < clipRect.Y || paintY >= clipRect.Bottom)
5783
continue;
5884

59-
if (data.Count == 1)
85+
for (int col = 0; col < graphWidth; col++)
6086
{
61-
// Single point: draw a dot at the mapped position
62-
int py = pixelHeight - 1 - (int)((data[0] - globalMin) / range * (pixelHeight - 1));
63-
py = Math.Clamp(py, 0, pixelHeight - 1);
64-
int px = pixelWidth / 2;
65-
if (px >= 0 && px < pixelWidth)
87+
int paintX = graphX + col;
88+
if (paintX < clipRect.X || paintX >= clipRect.Right)
89+
continue;
90+
91+
char brailleChar = BrailleHelpers.GetBrailleChar(_pixelGridCache, col, row);
92+
93+
if (brailleChar == BrailleHelpers.BrailleEmpty)
6694
{
67-
_pixelGridCache[py, px] = true;
68-
seriesColorMap[px / BrailleHelpers.DotsPerCellWidth] = si;
95+
buffer.SetNarrowCell(paintX, paintY, brailleChar, EmptyCellColor, bgColor);
6996
}
70-
continue;
71-
}
72-
73-
// Draw line segments between consecutive points
74-
for (int i = 0; i < data.Count - 1; i++)
75-
{
76-
int x1 = (int)(i * (pixelWidth - 1.0) / Math.Max(data.Count - 1, 1));
77-
int y1 = pixelHeight - 1 - (int)((data[i] - globalMin) / range * (pixelHeight - 1));
78-
int x2 = (int)((i + 1) * (pixelWidth - 1.0) / Math.Max(data.Count - 1, 1));
79-
int y2 = pixelHeight - 1 - (int)((data[i + 1] - globalMin) / range * (pixelHeight - 1));
80-
81-
x1 = Math.Clamp(x1, 0, pixelWidth - 1);
82-
y1 = Math.Clamp(y1, 0, pixelHeight - 1);
83-
x2 = Math.Clamp(x2, 0, pixelWidth - 1);
84-
y2 = Math.Clamp(y2, 0, pixelHeight - 1);
85-
86-
BrailleHelpers.DrawLinePixels(_pixelGridCache, x1, y1, x2, y2);
87-
88-
// Mark cell columns touched by this segment for coloring
89-
int cellStart = Math.Min(x1, x2) / BrailleHelpers.DotsPerCellWidth;
90-
int cellEnd = Math.Max(x1, x2) / BrailleHelpers.DotsPerCellWidth;
91-
for (int c = cellStart; c <= cellEnd && c < graphWidth; c++)
97+
else
9298
{
93-
seriesColorMap[c] = si;
99+
Color cellColor;
100+
if (gradient != null)
101+
{
102+
double position = graphWidth > 1 ? (double)col / (graphWidth - 1) : 0.5;
103+
cellColor = gradient.Interpolate(position);
104+
}
105+
else
106+
{
107+
cellColor = seriesColor;
108+
}
109+
buffer.SetNarrowCell(paintX, paintY, brailleChar, cellColor, bgColor);
94110
}
95111
}
96112
}
113+
}
97114

98-
// Convert pixel grid to braille characters and write to buffer
115+
private void PaintBrailleMultiSeries(CharacterBuffer buffer, int graphX, int graphY,
116+
int graphWidth, int graphHeight, int pixelWidth, int pixelHeight,
117+
LayoutRect clipRect, Color bgColor,
118+
List<(LineGraphSeries series, List<double> data)> seriesSnapshots,
119+
double globalMin, double range)
120+
{
121+
// First pass: paint empty braille background for all cells
99122
for (int row = 0; row < graphHeight; row++)
100123
{
101124
int paintY = graphY + row;
@@ -108,38 +131,102 @@ private void PaintBraille(CharacterBuffer buffer, int graphX, int graphY,
108131
if (paintX < clipRect.X || paintX >= clipRect.Right)
109132
continue;
110133

111-
char brailleChar = BrailleHelpers.GetBrailleChar(_pixelGridCache, col, row);
134+
buffer.SetNarrowCell(paintX, paintY, BrailleHelpers.BrailleEmpty, EmptyCellColor, bgColor);
135+
}
136+
}
112137

113-
if (brailleChar == BrailleHelpers.BrailleEmpty)
114-
{
115-
buffer.SetNarrowCell(paintX, paintY, brailleChar, EmptyCellColor, bgColor);
116-
}
117-
else
138+
// Render each series independently with its own pixel grid and color
139+
var pixelGrid = new bool[pixelHeight, pixelWidth];
140+
141+
for (int si = 0; si < seriesSnapshots.Count; si++)
142+
{
143+
var (series, data) = seriesSnapshots[si];
144+
if (data.Count == 0)
145+
continue;
146+
147+
Array.Clear(pixelGrid);
148+
DrawSeriesPixels(pixelGrid, data, pixelWidth, pixelHeight, globalMin, range);
149+
150+
// Write this series' braille chars to buffer with its own color
151+
for (int row = 0; row < graphHeight; row++)
152+
{
153+
int paintY = graphY + row;
154+
if (paintY < clipRect.Y || paintY >= clipRect.Bottom)
155+
continue;
156+
157+
for (int col = 0; col < graphWidth; col++)
118158
{
159+
int paintX = graphX + col;
160+
if (paintX < clipRect.X || paintX >= clipRect.Right)
161+
continue;
162+
163+
char brailleChar = BrailleHelpers.GetBrailleChar(pixelGrid, col, row);
164+
if (brailleChar == BrailleHelpers.BrailleEmpty)
165+
continue;
166+
119167
Color cellColor;
120-
int seriesIndex = seriesColorMap[col];
121-
if (seriesIndex >= 0 && seriesIndex < seriesSnapshots.Count)
168+
if (series.Gradient != null)
122169
{
123-
var (series, _) = seriesSnapshots[seriesIndex];
124-
if (series.Gradient != null)
125-
{
126-
double position = graphWidth > 1 ? (double)col / (graphWidth - 1) : 0.5;
127-
cellColor = series.Gradient.Interpolate(position);
128-
}
129-
else
130-
{
131-
cellColor = series.LineColor;
132-
}
170+
double position = graphWidth > 1 ? (double)col / (graphWidth - 1) : 0.5;
171+
cellColor = series.Gradient.Interpolate(position);
133172
}
134173
else
135174
{
136-
cellColor = Color.Cyan1;
175+
cellColor = series.LineColor;
137176
}
138177

139-
buffer.SetNarrowCell(paintX, paintY, brailleChar, cellColor, bgColor);
178+
// Merge with existing braille content in this cell
179+
var existing = buffer.GetCell(paintX, paintY);
180+
int existingBraille = existing.Character.Value;
181+
int newBraille = brailleChar;
182+
183+
// If the existing cell is a braille character, OR the patterns together
184+
if (existingBraille >= 0x2800 && existingBraille <= 0x28FF)
185+
{
186+
int merged = existingBraille | newBraille;
187+
buffer.SetNarrowCell(paintX, paintY, (char)merged, cellColor, bgColor);
188+
}
189+
else
190+
{
191+
buffer.SetNarrowCell(paintX, paintY, brailleChar, cellColor, bgColor);
192+
}
140193
}
141194
}
142195
}
143196
}
197+
198+
/// <summary>
199+
/// Draw a series' data points into a pixel grid using Bresenham line segments.
200+
/// </summary>
201+
private static void DrawSeriesPixels(bool[,] grid, List<double> data,
202+
int pixelWidth, int pixelHeight, double globalMin, double range)
203+
{
204+
if (data.Count == 1)
205+
{
206+
int py = range > 0
207+
? pixelHeight - 1 - (int)((data[0] - globalMin) / range * (pixelHeight - 1))
208+
: pixelHeight / 2;
209+
py = Math.Clamp(py, 0, pixelHeight - 1);
210+
int px = pixelWidth / 2;
211+
if (px >= 0 && px < pixelWidth)
212+
grid[py, px] = true;
213+
return;
214+
}
215+
216+
for (int i = 0; i < data.Count - 1; i++)
217+
{
218+
int x1 = (int)(i * (pixelWidth - 1.0) / Math.Max(data.Count - 1, 1));
219+
int y1 = pixelHeight - 1 - (int)((data[i] - globalMin) / range * (pixelHeight - 1));
220+
int x2 = (int)((i + 1) * (pixelWidth - 1.0) / Math.Max(data.Count - 1, 1));
221+
int y2 = pixelHeight - 1 - (int)((data[i + 1] - globalMin) / range * (pixelHeight - 1));
222+
223+
x1 = Math.Clamp(x1, 0, pixelWidth - 1);
224+
y1 = Math.Clamp(y1, 0, pixelHeight - 1);
225+
x2 = Math.Clamp(x2, 0, pixelWidth - 1);
226+
y2 = Math.Clamp(y2, 0, pixelHeight - 1);
227+
228+
BrailleHelpers.DrawLinePixels(grid, x1, y1, x2, y2);
229+
}
230+
}
144231
}
145232
}

0 commit comments

Comments
 (0)