Skip to content

Commit a1667d3

Browse files
manarutoswharden
andauthored
DataStreamer: add area fill support (ScottPlot#5023)
* add area fill support to DataStreamer - Add FillY properties matching Scatter class API (FillY, FillYValue, FillYAboveColor, FillYBelowColor) - Implement two-tone fill rendering for areas above/below baseline - Add GetSegments() method to IDataStreamerView interface - Update Wipe and Scroll renderers to expose segment structure - Handle partial buffers correctly during initial streaming BREAKING CHANGE: IDataStreamerView interface now requires GetSegments() method implementation * Update CHANGELOG.md --------- Co-authored-by: Scott W Harden <swharden@gmail.com>
1 parent 84c53fc commit a1667d3

5 files changed

Lines changed: 142 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ _Not yet on NuGet..._
2222
* Labels: Added styling options for underline with customizable thickness and offset (#4893) @manaruto
2323
* Legend: Added the ability to customize default marker shape for legend items (#5005, #5006) @aespitia
2424
* Pie: Added a `Radius` property (instead of forcing `1.0`) and improved rendering and SVG export (#5020) @CoderPM2011 @aespitia
25+
* Data Streamer: Added `FillY` and related properties so streaming plots support filled ares (#4948, #5023) @manaruto @Stephanowicz
2526

2627
## ScottPlot 5.0.55
2728
_Published on [NuGet](https://www.nuget.org/profiles/ScottPlot) on 2025-03-22_

src/ScottPlot5/ScottPlot5/DataViews/IDataStreamerView.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ public interface IDataStreamerView
99
{
1010
DataStreamer Streamer { get; }
1111
void Render(RenderPack rp);
12+
IReadOnlyList<Pixel[]> GetSegments(RenderPack rp);
1213
}

src/ScottPlot5/ScottPlot5/DataViews/Scroll.cs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,55 @@ public Scroll(DataStreamer streamer, bool newOnRight)
1616

1717
public void Render(RenderPack rp)
1818
{
19-
int dataLength = Streamer.Data.Length;
20-
int dataNextIndex = Streamer.Data.NextIndex;
21-
int oldPointCount = dataLength - dataNextIndex;
19+
using SKPaint paint = new();
20+
foreach (Pixel[] seg in GetSegments(rp))
21+
{
22+
Drawing.DrawLines(rp.Canvas, paint, seg, Streamer.LineStyle);
23+
Drawing.DrawMarkers(rp.Canvas, paint, seg, Streamer.MarkerStyle);
24+
}
25+
}
2226

23-
Pixel[] points = new Pixel[dataLength];
27+
public IReadOnlyList<Pixel[]> GetSegments(RenderPack rp)
28+
{
29+
int cap = Streamer.Data.Length;
30+
int samples = Math.Min(Streamer.Data.CountTotal, cap);
31+
if (samples < 2)
32+
return Array.Empty<Pixel[]>();
33+
34+
int nextIdx = Streamer.Data.NextIndex;
35+
bool wrapped = samples == cap;
36+
int oldCnt = wrapped ? cap - nextIdx : 0;
2437

25-
for (int i = 0; i < dataLength; i++)
38+
Span<double> vals = stackalloc double[samples];
39+
int dst = 0;
40+
41+
if (wrapped && oldCnt > 0)
2642
{
27-
bool isNewPoint = i < oldPointCount;
28-
int sourceIndex = isNewPoint ? dataNextIndex + i : i - oldPointCount;
29-
int targetIndex = NewOnRight ? i : dataLength - 1 - i;
30-
points[targetIndex] = new(
31-
x: Streamer.Axes.GetPixelX(targetIndex * Streamer.Data.SamplePeriod + Streamer.Data.OffsetX),
32-
y: Streamer.Axes.GetPixelY(Streamer.Data.Data[sourceIndex] + Streamer.Data.OffsetY));
43+
for (int i = 0; i < oldCnt; i++, dst++)
44+
vals[dst] = Streamer.Data.Data[nextIdx + i];
3345
}
3446

35-
using SKPaint paint = new();
36-
Drawing.DrawLines(rp.Canvas, paint, points, Streamer.LineStyle);
37-
Drawing.DrawMarkers(rp.Canvas, paint, points, Streamer.MarkerStyle);
47+
for (int i = 0; i < nextIdx && dst < samples; i++, dst++)
48+
vals[dst] = Streamer.Data.Data[i];
49+
50+
int xShift = NewOnRight ? cap - samples : 0;
51+
52+
Pixel[] px = new Pixel[samples];
53+
for (int screenX = 0; screenX < samples; screenX++)
54+
{
55+
int valIdx = NewOnRight
56+
? screenX
57+
: samples - 1 - screenX;
58+
59+
int plotIdx = screenX + xShift;
60+
double xVal = plotIdx * Streamer.Data.SamplePeriod + Streamer.Data.OffsetX;
61+
double yVal = vals[valIdx] + Streamer.Data.OffsetY;
62+
63+
px[screenX] = new Pixel(
64+
Streamer.Axes.GetPixelX(xVal),
65+
Streamer.Axes.GetPixelY(yVal));
66+
}
67+
68+
return new[] { px };
3869
}
3970
}

src/ScottPlot5/ScottPlot5/DataViews/Wipe.cs

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ public Wipe(DataStreamer streamer, bool wipeRight)
1717
}
1818

1919
public void Render(RenderPack rp)
20+
{
21+
using SKPaint paint = new();
22+
foreach (Pixel[] seg in GetSegments(rp))
23+
{
24+
Drawing.DrawLines(rp.Canvas, paint, seg, Streamer.LineStyle);
25+
Drawing.DrawMarkers(rp.Canvas, paint, seg, Streamer.MarkerStyle);
26+
}
27+
}
28+
29+
public IReadOnlyList<Pixel[]> GetSegments(RenderPack rp)
2030
{
2131
int newestCount = Streamer.Data.NextIndex;
2232
int oldestCount = Streamer.Data.Data.Length - newestCount;
@@ -31,36 +41,49 @@ public void Render(RenderPack rp)
3141
double xPos = i * Streamer.Data.SamplePeriod + Streamer.Data.OffsetX;
3242
float x = Streamer.Axes.GetPixelX(WipeRight ? xPos : xMax - xPos);
3343
float y = Streamer.Axes.GetPixelY(Streamer.Data.Data[i] + Streamer.Data.OffsetY);
34-
newest[i] = new(x, y);
44+
newest[i] = new Pixel(x, y);
3545
}
3646

3747
for (int i = 0; i < oldest.Length; i++)
3848
{
3949
double xPos = (i + newestCount) * Streamer.Data.SamplePeriod + Streamer.Data.OffsetX;
4050
float x = Streamer.Axes.GetPixelX(WipeRight ? xPos : xMax - xPos);
4151
float y = Streamer.Axes.GetPixelY(Streamer.Data.Data[i + newestCount] + Streamer.Data.OffsetY);
42-
oldest[i] = new(x, y);
52+
oldest[i] = new Pixel(x, y);
4353
}
4454

4555
if (BlankFraction > 0)
4656
{
47-
int blankPoints = (int)(BlankFraction * Streamer.Data.Length);
57+
int blank = (int)(BlankFraction * Streamer.Data.Length);
4858

49-
if (blankPoints <= oldest.Length)
50-
{
51-
oldest = oldest.Skip(blankPoints).ToArray();
52-
}
59+
if (blank <= oldest.Length)
60+
oldest = oldest.Skip(blank).ToArray();
5361
else
5462
{
55-
oldest = [];
56-
newest = newest.Skip(blankPoints - oldest.Length).ToArray();
63+
newest = newest.Skip(blank - oldest.Length).ToArray();
64+
oldest = Array.Empty<Pixel>();
5765
}
5866
}
5967

60-
using SKPaint paint = new();
61-
Drawing.DrawLines(rp.Canvas, paint, oldest, Streamer.LineStyle);
62-
Drawing.DrawLines(rp.Canvas, paint, newest, Streamer.LineStyle);
63-
Drawing.DrawMarkers(rp.Canvas, paint, oldest, Streamer.MarkerStyle);
64-
Drawing.DrawMarkers(rp.Canvas, paint, newest, Streamer.MarkerStyle);
68+
if (!WipeRight)
69+
{
70+
System.Array.Reverse(newest);
71+
System.Array.Reverse(oldest);
72+
}
73+
74+
var list = new List<Pixel[]>();
75+
76+
if (WipeRight)
77+
{
78+
if (oldest.Length > 1) list.Add(oldest);
79+
if (newest.Length > 1) list.Add(newest);
80+
}
81+
else
82+
{
83+
if (newest.Length > 1) list.Add(newest);
84+
if (oldest.Length > 1) list.Add(oldest);
85+
}
86+
87+
return list;
6588
}
6689
}

src/ScottPlot5/ScottPlot5/Plottables/DataStreamer.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using ScottPlot.AxisLimitManagers;
22
using ScottPlot.DataSources;
3+
using ScottPlot.DataViews;
34

45
namespace ScottPlot.Plottables;
56

@@ -25,6 +26,11 @@ public class DataStreamer : IPlottable, IManagesAxisLimits, IHasLine, IHasLegend
2526
public Color MarkerLineColor { get => MarkerStyle.LineColor; set => MarkerStyle.LineColor = value; }
2627
public Color MarkerColor { get => MarkerStyle.MarkerColor; set => MarkerStyle.MarkerColor = value; }
2728
public float MarkerLineWidth { get => MarkerStyle.LineWidth; set => MarkerStyle.LineWidth = value; }
29+
public bool FillY { get; set; } = false;
30+
public double FillYValue { get; set; } = 0;
31+
public Color FillYAboveColor { get; set; } = Colors.Blue.WithAlpha(.2);
32+
public Color FillYBelowColor { get; set; } = Colors.Blue.WithAlpha(.2);
33+
public Color FillYColor { get => FillYAboveColor; set { FillYAboveColor = value; FillYBelowColor = value; } }
2834

2935
public Color Color
3036
{
@@ -184,7 +190,59 @@ public void UpdateAxisLimits(Plot plot)
184190

185191
public virtual void Render(RenderPack rp)
186192
{
193+
if (Renderer is not IDataStreamerView viewWithSegs)
194+
{
195+
Renderer.Render(rp);
196+
return;
197+
}
198+
199+
var segs = viewWithSegs.GetSegments(rp);
200+
if (segs.Count == 0)
201+
return;
202+
203+
if (FillY)
204+
{
205+
float yBasePx = Axes.YAxis.GetPixel(FillYValue + Data.OffsetY, rp.DataRect);
206+
207+
using SKPaint paint = new();
208+
foreach (Pixel[] seg in segs)
209+
{
210+
using SKPath path = new();
211+
path.MoveTo(seg[0].X, seg[0].Y);
212+
for (int i = 1; i < seg.Length; i++)
213+
path.LineTo(seg[i].X, seg[i].Y);
214+
215+
PixelRect rect = new(seg);
216+
using SKPath fill = new(path);
217+
fill.LineTo(rect.Right, yBasePx);
218+
fill.LineTo(rect.Left, yBasePx);
219+
220+
// above
221+
if (yBasePx > rect.Top)
222+
{
223+
PixelRect clip = new(rp.DataRect.Left, rp.DataRect.Right, rp.DataRect.Top, yBasePx);
224+
FillStyle fs = new() { IsVisible = true, Color = FillYAboveColor };
225+
rp.CanvasState.Save();
226+
rp.CanvasState.Clip(clip);
227+
Drawing.FillPath(rp.Canvas, paint, fill, fs, clip);
228+
rp.CanvasState.Restore();
229+
}
230+
231+
// below
232+
if (yBasePx < rect.Bottom)
233+
{
234+
PixelRect clip = new(rp.DataRect.Left, rp.DataRect.Right, yBasePx, rp.DataRect.Bottom);
235+
FillStyle fs = new() { IsVisible = true, Color = FillYBelowColor };
236+
rp.CanvasState.Save();
237+
rp.CanvasState.Clip(clip);
238+
Drawing.FillPath(rp.Canvas, paint, fill, fs, clip);
239+
rp.CanvasState.Restore();
240+
}
241+
}
242+
}
243+
187244
Renderer.Render(rp);
245+
188246
Data.CountTotalOnLastRender = Data.CountTotal;
189247
}
190248
}

0 commit comments

Comments
 (0)