Overview

Namespace: TheTechIdea.Beep.Winform.Controls.Charts
Assembly: TheTechIdea.Beep.Winform.Controls.dll

The chart control provides two static helper classes to manage viewport navigation and rendering performance. BeepChartViewportHelper (internal) handles interactive zoom/pan via mouse wheel drag and keyboard, auto-scale computation, and viewport limit enforcement. BeepChartPerformanceHelper (public) provides point culling, vertex decimation, grid density optimization, label recommendation, and rendering cost estimation for large datasets exceeding thousands of data points. Together, they enable smooth interaction with datasets of 100,000+ points on modest hardware.

The viewport is defined by four properties: ViewportXMin, ViewportXMax, ViewportYMin, ViewportYMax. These represent the visible data range and can diverge from the full data range when the user zooms or pans. Series painters receive the viewport range (not the full data range) through their xMin/xMax/yMin/yMax parameters, so only visible data is drawn.

Viewport Properties

PropertyTypeDefaultDescription
ViewportXMinfloatAutoLeft edge of the visible X-axis data range. Set by AutoScaleViewport() or user zoom/pan.
ViewportXMaxfloatAutoRight edge of the visible X-axis data range.
ViewportYMinfloatAutoBottom edge of the visible Y-axis data range.
ViewportYMaxfloatAutoTop edge of the visible Y-axis data range.
CanResetViewportbooltrueWhen true, double-clicking the chart or pressing the Home key calls ResetViewport(), which delegates to AutoScaleViewport() and fires ViewportChanged.
EnableMouseWheelZoomboolfalseEnables Ctrl+MouseWheel zoom in/out. Zooming is always centered on the mouse cursor position for intuitive navigation.
MouseWheelZoomStepPercentfloat10fPercentage of the current viewport range to zoom per wheel step. 10 = zoom 10% of current range in or out. Higher values = faster zoom.
EnableMouseDragPanboolfalseEnables click-and-drag panning of the viewport. The mouse cursor "grabs" the chart and moves the visible range proportional to the drag distance. Right-click cancels a pan in progress.
MouseDragPanFactorfloat1.0fMultiplier for drag-to-pan sensitivity. 1.0 = 1:1 cursor-to-data mapping. 0.5 = half speed (drag 100px moves data 50px). 2.0 = double speed.
EnableKeyboardViewportNavigationboolfalseEnables arrow keys for panning and +/- for zooming. Requires the chart to have focus.
KeyboardPanStepPercentfloat10fPercentage of the current viewport range to pan per arrow key press. 10 = move 10% per keypress.

Auto-Scale Logic

BeepChartViewportHelper provides two primary methods for viewport range management:

AutoScaleViewport

Scans all data points across all series to compute the full data range, then sets the viewport to that range plus 10% padding on each axis. This ensures no data point touches the edge of the plot area.

internal static void AutoScaleViewport(BeepChart chart)
{
    float dataXMin = float.MaxValue, dataXMax = float.MinValue;
    float dataYMin = float.MaxValue, dataYMax = float.MinValue;

    foreach (var series in chart.DataSeries)
    {
        foreach (var point in series.DataPoints)
        {
            float x = BeepChartDataHelper.ConvertXValue(chart, point);
            float y = BeepChartDataHelper.ConvertYValue(chart, point);
            dataXMin = Math.Min(dataXMin, x);
            dataXMax = Math.Max(dataXMax, x);
            dataYMin = Math.Min(dataYMin, y);
            dataYMax = Math.Max(dataYMax, y);
        }
    }

    float xPadding = (dataXMax - dataXMin) * 0.1f;
    float yPadding = (dataYMax - dataYMin) * 0.1f;

    // Handle degenerate ranges (single point or identical values)
    if (Math.Abs(dataXMax - dataXMin) < float.Epsilon)
        { xPadding = 1f; dataXMin -= 0.5f; dataXMax += 0.5f; }
    if (Math.Abs(dataYMax - dataYMin) < float.Epsilon)
        { yPadding = dataYMax * 0.1f; }

    chart.ViewportXMin = dataXMin - xPadding;
    chart.ViewportXMax = dataXMax + xPadding;
    chart.ViewportYMin = dataYMin - yPadding;
    chart.ViewportYMax = dataYMax + yPadding;
}

EnforceViewportLimits

Called after every user interaction (zoom, pan) to clamp values and prevent inverted ranges (XMin >= XMax). If the minimum exceeds the maximum due to over-zooming, the range is collapsed to a single-point span.

internal static void EnforceViewportLimits(BeepChart chart)
{
    if (chart.ViewportXMin > chart.ViewportXMax)
    {
        float center = (chart.ViewportXMin + chart.ViewportXMax) / 2f;
        chart.ViewportXMin = center - 0.5f;
        chart.ViewportXMax = center + 0.5f;
    }
    if (chart.ViewportYMin > chart.ViewportYMax)
    {
        float center = (chart.ViewportYMin + chart.ViewportYMax) / 2f;
        chart.ViewportYMin = center - 0.5f;
        chart.ViewportYMax = center + 0.5f;
    }
}

The 10% padding ensures data points never sit exactly on the axis lines, which is important for bar charts (bars would otherwise be clipped at the edge) and bubble charts (circles would be cut off). The padding is applied symmetrically on both sides of the range.

Real-Time Streaming

The streaming subsystem enables live data feeds (sensor readings, stock prices, log tails) to update the chart incrementally without full repaints. Configuration and API:

Property / MethodTypeDefaultDescription
EnableRealTimeStreamingboolfalseMaster toggle. When enabled, a System.Windows.Forms.Timer with interval StreamRenderThrottleMs is started. The timer callback batches pending data and triggers an incremental render.
StreamRenderThrottleMsint50Milliseconds between batch renders. Lower = more responsive but higher CPU. 50ms = ~20 FPS max for the chart area.
MaxStreamPointsPerSeriesint1000Rolling window size in points. When exceeded, oldest points are removed via a circular buffer to prevent unbounded memory growth.
AppendDataPoint(int seriesIndex, ChartDataPoint, bool flush)Method—Adds a single data point to the specified series' streaming buffer. If flush=true, triggers an immediate render batch.
AppendDataPoints(int seriesIndex, IEnumerable, bool flush)Method—Adds multiple data points at once. Useful for importing a batch of historical data before live streaming begins.
FlushStreamingRender()Method—Manually triggers a batch render of all pending buffered points. Called automatically by the throttle timer.

Streaming Events

EventEventArgsDescription
StreamingDataAppendedChartStreamingDataAppendedEventArgsFired when new data points are appended to the buffer. Properties: SeriesIndex, NewPointCount, TotalBufferedPoints.
IncrementalRenderCompletedChartIncrementalRenderCompletedEventArgsFired after each batch render completes. Properties: RenderTimeMs, PointsRendered, SeriesRendered.
// Internal streaming buffer structure (per series):
class StreamingBuffer
{
    Queue<ChartDataPoint> pendingPoints = new();
    List<ChartDataPoint> activePoints = new(); // visible window

    void Append(ChartDataPoint point)
    {
        pendingPoints.Enqueue(point);
    }

    List<ChartDataPoint> Flush(int maxPoints)
    {
        while (pendingPoints.Count > 0)
        {
            activePoints.Add(pendingPoints.Dequeue());
            while (activePoints.Count > maxPoints)
                activePoints.RemoveAt(0);
        }
        return new List<ChartDataPoint>(activePoints);
    }
}

Point Culling

Point culling reduces the number of points sent to the painter by filtering out those that fall outside the current viewport. This is critical for large datasets where 99% of points may be off-screen.

PropertyDescription
EnablePointCullingbool, default false. When true, CullPointsOutsideViewport() is called before each paint cycle. Only points within the viewport range are passed to the series painter.
CullPointsOutsideViewportFilters a list to points where pointX >= xMin && pointX <= xMax. Critically, the first and last points of the series are always retained, even if they fall outside the viewport. This prevents line segments from abruptly starting/stopping at the viewport edge — the line painter can extend from the last off-screen point into the visible region.
public static List<ChartDataPoint> CullPointsOutsideViewport(
    List<ChartDataPoint> points,
    Func<ChartDataPoint, float> toXValue,
    float xMin, float xMax)
{
    if (!ShouldOptimizePerformance(points.Count, 1000))
        return new List<ChartDataPoint>(points); // pass-through for small sets

    var result = new List<ChartDataPoint> { points.First() };

    for (int i = 1; i < points.Count - 1; i++)
    {
        float x = toXValue(points[i]);
        if (x >= xMin && x <= xMax)
            result.Add(points[i]);
    }

    result.Add(points.Last());
    return result;
}

Vertex Decimation

Vertex decimation reduces the number of rendered vertices to a target count while preserving the visual shape of the data.

Property / MethodDescription
EnableVertexSimplificationbool, default false. When true, decimation is applied before rendering. The last data point is always preserved.
DecimatePoints(points, factor)Takes every Nth point from the list. factor=5 keeps every 5th point (80% reduction). Always includes the last point. Returns a new list.
EstimateDecimationFactor(count, target=200)Computes the factor needed to reduce a dataset to approximately the target point count. factor = Math.Max(1, count / target). For 10,000 points with target=200, returns factor=50.
SimplificationTolerancefloat property tied to the chart. Controls decimation aggressiveness. Higher = more points removed. Used in conjunction with EstimateDecimationFactor.
public static List<ChartDataPoint> DecimatePoints(
    List<ChartDataPoint> points, int factor)
{
    if (factor <= 1) return new List<ChartDataPoint>(points);

    var result = new List<ChartDataPoint>(points.Count / factor + 2);
    for (int i = 0; i < points.Count; i += factor)
        result.Add(points[i]);

    // Always include the last point to preserve the right boundary
    if (result.Last() != points.Last())
        result.Add(points.Last());

    return result;
}

public static int EstimateDecimationFactor(int count, int target = 200)
{
    if (count <= target) return 1;
    return Math.Max(1, count / target);
}

Performance Methods Table

BeepChartPerformanceHelper is public and static, meaning it can be used from user code for custom optimization pipelines:

MethodReturnsDescription
CullPointsOutsideViewport
(List<ChartDataPoint>, Func<ChartDataPoint,float>, float xMin, float xMax)
List<ChartDataPoint>Filters points to visible viewport range. Always preserves first and last points. Pass-through for datasets under 1,000 points.
DecimatePoints
(List<ChartDataPoint>, int factor)
List<ChartDataPoint>Reduces points by keeping every Nth. Always preserves last point. factor=1 returns original list copy.
EstimateDecimationFactor
(int count, int target = 200)
intComputes factor = Ceil(count / target). Returns 1 if count <= target. The 200-point target is chosen because most screens can render 200 data points without perceivable difference at common chart sizes.
OptimizeGridDensity
(int dataPointCount, float zoomLevel)
intRecommends grid line skip interval. At zoom < 1x (zoomed out), grid lines are skipped. Returns 1 (all grid lines) for small datasets, up to 10 (every 10th) for 100K+ points with wide zoom.
RecommendLabelInterval
(int axisLengthPixels, float dataRangePercent)
intRecommends tick label skip interval to prevent label overcrowding. Based on available pixel width and the percentage of total data range visible. Shorter visible ranges (zoomed in) get more labels.
EstimateRenderingCost
(int pointCount, int pixelWidth)
floatReturns 0.0-1.0 cost estimate based on points-per-pixel ratio. cost = Math.Min(1f, pointCount / (pixelWidth * 2f)). A value > 0.5 triggers the PerformanceOptimized event suggesting optimization is desirable.
ShouldOptimizePerformance
(int pointCount, int threshold = 10000)
boolReturns true when point count exceeds the threshold. Default threshold is 10,000 points. This is the gatekeeper method checked before applying culling or decimation.
// Integration in BeepChart.Drawing.cs paint cycle:
if (EnablePerformanceMode || EnablePointCulling || EnableVertexSimplification)
{
    bool shouldOpt = BeepChartPerformanceHelper.ShouldOptimizePerformance(
        GetTotalPointCount(), LargeDatasetThreshold);

    if (shouldOpt && EnablePointCulling)
    {
        optimizedPoints = BeepChartPerformanceHelper.CullPointsOutsideViewport(
            allPoints, p => ConvertXValue(p), ViewportXMin, ViewportXMax);
    }

    if (shouldOpt && EnableVertexSimplification)
    {
        int factor = BeepChartPerformanceHelper.EstimateDecimationFactor(
            optimizedPoints?.Count ?? allPoints.Count);
        optimizedPoints = BeepChartPerformanceHelper.DecimatePoints(
            optimizedPoints ?? allPoints, factor);
    }

    OnPerformanceOptimized(beforeCount, optimizedPoints?.Count ?? allPoints.Count);
    OnLabelOptimizationApplied(optimizedCount, factor);
}

Performance Events

EventEventArgsFires When
PerformanceOptimizedChartPerformanceOptimizedEventArgs
(int BeforePointCount, int AfterPointCount, float CostEstimate, OptimizationType Type)
After culling/decimation is applied. Provides before/after counts so callers can log optimization effectiveness.
LabelOptimizationAppliedChartLabelOptimizationEventArgs
(int OriginalLabelCount, int OptimizedLabelCount, float SkipFactor)
When tick label density optimization skips labels to prevent overlap. Reports reduction ratios.
ViewportChangedChartViewportChangedEventArgs
(float XMin, float XMax, float YMin, float YMax, ViewportChangeSource Source)
After any viewport modification (zoom, pan, auto-scale, reset). Source enum: UserInteraction, Programmatic, AutoScale, Reset.
IncrementalRenderCompletedChartIncrementalRenderCompletedEventArgs
(long RenderTimeMs, int PointsRendered, int SeriesRendered)
After a streaming batch render completes. Useful for monitoring streaming performance in real-time dashboards.

Visual Presets Table

ApplyVisualPreset(ChartVisualPreset) applies a pre-configured set of property values optimized for different use cases. This method is defined in BeepChart.Methods.cs:

PresetSettings AppliedUse Case
DashboardShowLegend=false, ShowTitle=false, compact fonts (8pt), ShowGrid=false, thin margins (24px), auto-scale, simplified axis labels, SurfaceStyle=CardEmbedded chart in a dashboard tile (200-300px). Minimal chrome to maximize data ink.
AnalyticalShowLegend=true, ShowTitle=true, standard fonts (10pt), ShowGrid=true, 48px margins, EnableMouseWheelZoom=true, EnableMouseDragPan=true, EnableTrackball=true, EnablePointSelection=true, EnablePerformanceMode=trueFull-featured exploratory chart (500+ px). All interactions enabled for data analysis.
HighContrastThick pen widths (3px), larger markers (10px), bold fonts, high-contrast palette (black/white/primary), large legend squares, disabled transparency, SurfaceStyle=OutlineAccessibility-focused chart for users with visual impairments. WCAG AA compliant contrast ratios.
PrintBlack-and-white palette, thick axis lines (2px), dashed fill patterns, disabled grid lines, white background forced, SurfaceStyle=Classic, Font=Arial 10ptChart destined for monochrome printing or PDF export. Eliminates color dependency.
PresentationLarge fonts (14pt title, 11pt labels), thick lines (3px), bold markers (10px), ShowDataLabels=true (if applicable), high-contrast palette, SurfaceStyle=Card, accent color highlight, ShowGrid=falseChart shown in a presentation or on a large display (TV/projector). Maximizes readability at distance.

Examples

Large Dataset Optimization

var chart = new BeepChart();
chart.ChartType = ChartType.Line;
chart.EnablePerformanceMode = true;
chart.LargeDatasetThreshold = 5000;  // optimize above 5K points
chart.EnablePointCulling = true;
chart.EnableVertexSimplification = true;
chart.SimplificationTolerance = 0.5f;

// Generate 100,000 points
var points = new List<ChartDataPoint>();
var rng = new Random(42);
for (int i = 0; i < 100000; i++)
{
    float x = i * 0.1f;
    float y = (float)(Math.Sin(x * 0.1) * 100 + rng.NextDouble() * 10);
    points.Add(new ChartDataPoint { X = x.ToString("F1"), Y = y.ToString("F2") });
}

chart.DataSeries.Add(new ChartDataSeries
{
    SeriesName = "Signal",
    DataPoints = points
});

chart.AutoScaleViewport();

// Monitor optimization
chart.PerformanceOptimized += (s, e) =>
    Console.WriteLine($"Optimized: {e.BeforePointCount} -> {e.AfterPointCount} points");

// Get rendering cost estimate
float cost = BeepChartPerformanceHelper.EstimateRenderingCost(
    chart.GetTotalPointCount(), chart.Width);
if (cost > 0.5f)
    chart.EnablePointCulling = true;

Interactive Zoom/Pan Setup

chart.EnableMouseWheelZoom = true;
chart.MouseWheelZoomStepPercent = 15f;
chart.EnableMouseDragPan = true;
chart.MouseDragPanFactor = 1.0f;
chart.EnableKeyboardViewportNavigation = true;
chart.KeyboardPanStepPercent = 10f;
chart.CanResetViewport = true;

chart.ViewportChanged += (s, e) =>
{
    Console.WriteLine($"Viewport: X[{e.XMin:F1}-{e.XMax:F1}] Y[{e.YMin:F1}-{e.YMax:F1}]");
    if (e.Source == ViewportChangeSource.UserInteraction)
        UpdateDependentCharts();
};

// Reset on double-click (handled internally via CanResetViewport)
// Ctrl+Wheel zooms centered on cursor
// Arrow keys pan 10% per press
// Home resets to auto-scale

Real-Time Streaming Setup

chart.ChartType = ChartType.Line;
chart.EnableRealTimeStreaming = true;
chart.StreamRenderThrottleMs = 50;   // 20 FPS max
chart.MaxStreamPointsPerSeries = 500; // rolling 500-point window
chart.ChartTitle = "Live Sensor Data";

chart.DataSeries.Add(new ChartDataSeries
{
    SeriesName = "Temperature",
    DataPoints = new List<ChartDataPoint>()
});

chart.AutoScaleViewport();

// Streaming data source (e.g., Timer or background thread)
var timer = new System.Timers.Timer(100); // 10 Hz data source
timer.Elapsed += (s, e) =>
{
    float value = ReadSensor();
    chart.BeginInvoke(new Action(() =>
    {
        chart.AppendDataPoint(0, new ChartDataPoint
        {
            X = DateTime.Now.ToString("HH:mm:ss.fff"),
            Y = value.ToString("F1")
        }, flush: false); // batch every 50ms via throttle
    }));
};
timer.Start();

chart.StreamingDataAppended += (s, e) =>
    Console.WriteLine($"{e.NewPointCount} new, {e.TotalBufferedPoints} buffered");

chart.IncrementalRenderCompleted += (s, e) =>
    Debug.WriteLine($"Batch render: {e.RenderTimeMs}ms, {e.PointsRendered}pts");

Apply Visual Presets

// Dashboard mode for a small embedded chart
chart.ApplyVisualPreset(ChartVisualPreset.Dashboard);

// Analytical mode for exploratory data analysis
chart.ApplyVisualPreset(ChartVisualPreset.Analytical);

// High-contrast for accessibility
chart.ApplyVisualPreset(ChartVisualPreset.HighContrast);

// Print-ready chart
chart.ApplyVisualPreset(ChartVisualPreset.Print);
using var bmp = new Bitmap(chart.Width, chart.Height);
chart.DrawToBitmap(bmp, chart.ClientRectangle);
bmp.Save("chart_print.png", ImageFormat.Png);

// Presentation mode for large display
chart.ApplyVisualPreset(ChartVisualPreset.Presentation);
chart.Font = new Font("Segoe UI", 12f);