Overview

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

The charting system provides five concrete series painters, each responsible for rendering a distinct chart type. All painters implement the internal IChartSeriesPainter interface and are resolved at runtime via the SeriesPainterFactory static factory class. Each painter receives coordinate conversion delegates (toX / toY) that map data-space values to pixel-space positions within the computed plot rectangle. Series rendering is configured through the SeriesRenderOptions data class, which carries all style, layout, and behavior flags.

ChartTypePainter ClassRendering Style
BarBarSeriesPainterVertical bars with configurable width/padding, stacked mode, data labels, negative value support
LineLineSeriesPainterPolyline segments with optional cubic Bezier smoothing, configurable point markers (Circle/Square/Diamond/Triangle)
PiePieSeriesPainterProportional slices with optional donut mode (InnerRadius > 0), exploded slice on hover, pie-slice-shaped hit areas
AreaAreaSeriesPainterFilled polygon below line to baseline, semi-transparent fill, stacked accumulation mode
BubbleBubbleSeriesPainterProportional circles at XY coordinates, circle area (not radius) proportional to Size value

SeriesPainterFactory

The static SeriesPainterFactory class maps ChartType enum values to concrete painter instances. LineSeriesPainter serves as the default fallback for any unrecognized or unspecified chart type.

internal static class SeriesPainterFactory
{
    public static IChartSeriesPainter Get(ChartType type)
    {
        switch (type)
        {
            case ChartType.Bar:    return new BarSeriesPainter();
            case ChartType.Line:   return new LineSeriesPainter();
            case ChartType.Pie:    return new PieSeriesPainter();
            case ChartType.Area:   return new AreaSeriesPainter();
            case ChartType.Bubble: return new BubbleSeriesPainter();
            default:               return new LineSeriesPainter();
        }
    }
}

The factory is called once during chart initialization in BeepChart.cs and the resolved painter instance is cached for the lifetime of the chart control. If ChartType is changed at runtime, the painter is re-resolved and Initialize() is called again.

IChartSeriesPainter Contract

Every series painter implements this internal interface. The two-method contract separates drawing from hit-test area registration:

internal interface IChartSeriesPainter
{
    void Initialize(BaseControl owner);

    void DrawSeries(Graphics g, Rectangle plotRect,
        List<ChartDataSeries> data,
        Func<ChartDataPoint, object> toX,
        Func<ChartDataPoint, object> toY,
        float xMin, float xMax, float yMin, float yMax,
        List<Color> palette, Color axisColor, Color textColor,
        SeriesRenderOptions options);

    void UpdateHitAreas(BaseControl owner, Rectangle plotRect,
        List<ChartDataSeries> data,
        Func<ChartDataPoint, PointF> toScreen,
        Action<string, Rectangle> notifyAreaHit);
}
MethodParametersPurpose
InitializeBaseControl ownerOne-time setup when painter is first resolved. Stores owner reference for theme/color access.
DrawSeriesGraphics g, Rectangle plotRect, List<ChartDataSeries> data, toX/toY delegates, float xMin/xMax/yMin/yMax, palette, axisColor, textColor, SeriesRenderOptionsRenders all data series onto the graphics surface within the specified plot rectangle. The toX and toY delegates convert each data point's X/Y values from string/object to a numeric pixel coordinate in the range [plotRect.Left..plotRect.Right] and [plotRect.Top..plotRect.Bottom].
UpdateHitAreasBaseControl owner, Rectangle plotRect, List<ChartDataSeries> data, toScreen delegate, notifyAreaHit callbackRegisters per-data-point hit-test rectangles (or polygons for pie) via the notifyAreaHit(string key, Rectangle bounds) callback. These are used by ChartInputHelper for mouse hover, click, and trackball interaction.

Coordinate Conversion

The toX and toY delegates are closures created in BeepChart.Drawing.cs that encapsulate the axis type detection and value conversion logic:

Func<ChartDataPoint, object> toX = point =>
    plotRect.Left + (BeepChartDataHelper.ConvertXValue(chart, point) - xMin)
        / (xMax - xMin) * plotRect.Width;

Func<ChartDataPoint, object> toY = point =>
    plotRect.Bottom - (BeepChartDataHelper.ConvertYValue(chart, point) - yMin)
        / (yMax - yMin) * plotRect.Height;

BarSeriesPainter

Renders vertical bars for each data point in the series. Key features:

FeatureDescription
BarWidth / BarPaddingControl bar thickness as a fraction of the column width (default BarWidth=0.8) and the gap between adjacent bar groups (default BarPadding=0.2). For single-series charts, bars are centered in each X column. For multi-series, bars are grouped side-by-side within each X column.
Stacked ModeWhen SeriesRenderOptions.Stacked = true, bars from multiple series accumulate vertically within each X column. Each series draws on top of the previous one's rendered height. The bottom series starts at the baseline (yMin). Useful for part-to-whole compositions.
Data LabelsWhen SeriesRenderOptions.ShowDataLabels = true, the Y value of each bar is drawn as text centered above the bar in the chart's text color. For stacked mode, labels appear at the top of each segment.
Negative ValuesBars with negative Y values extend downward from the baseline. The baseline is y=0 when the range spans both positive and negative, otherwise it's the yMin of the plot rectangle.
Multi-Series GroupingFor N series, each X category is divided into N sub-columns. The group width = BarWidth * columnWidth, and sub-bar width = groupWidth / N. Colors cycle through the palette.
// BarSeriesPainter core loop (simplified):
int seriesCount = data.Count;
float colWidth = plotRect.Width / (float)(xMax - xMin + 1);
float groupWidth = colWidth * options.BarWidth;
float barWidth = groupWidth / seriesCount;

foreach (var series in data)
{
    int seriesIdx = data.IndexOf(series);
    Color color = palette[seriesIdx % palette.Count];
    float prevY = plotRect.Bottom; // baseline for stacked

    foreach (var point in series.DataPoints)
    {
        float xVal = Convert.ToSingle(toX(point));
        float yVal = Convert.ToSingle(toY(point));
        float x = xVal - groupWidth / 2f + barWidth * seriesIdx;

        float barHeight = options.Stacked
            ? prevY - yVal
            : plotRect.Bottom - yVal;

        if (barHeight < 0) // negative values
        {
            y = plotRect.Bottom;
            barHeight = plotRect.Bottom - yVal;
        }

        g.FillRectangle(new SolidBrush(color), x, yVal, barWidth, barHeight);

        if (options.Stacked) prevY -= barHeight;
    }
}

LineSeriesPainter

Renders connected line segments for each series' data points. Key features:

FeatureDescription
Polyline SegmentsEach series is drawn as a sequence of connected line segments using Graphics.DrawLines(). The line width is derived from the theme or set to 2px by default. A semi-transparent fill below the line can be enabled via FillOpacity.
Cubic Bezier SmoothingWhen SeriesRenderOptions.SmoothLines = true, the painter computes cubic Bezier control points between each pair of consecutive data points and renders using Graphics.DrawBeziers(). This produces a smooth curve through all data points. Control points are computed using the Catmull-Rom to Bezier conversion formula with a tension of 0.5.
Point MarkersWhen SeriesRenderOptions.ShowMarkers = true (default), each data point is decorated with a small shape. Available shapes via the MarkerStyle enum: Circle (default) — filled ellipse, Square — filled rectangle, Diamond — rotated square (45°), Triangle — equilateral triangle pointing up. Marker size is controlled by MarkerSize (default 6px radius).
Multi-SeriesEach series is drawn with its own color from the palette. Series are rendered in order (first series = first color). Lines may overlap; legend toggling is used to declutter.
// Bezier smoothing calculation (Catmull-Rom to Bezier):
private PointF[] GetBezierControlPoints(PointF p0, PointF p1, PointF p2, PointF p3)
{
    float tension = 0.5f;
    PointF cp1 = new PointF(
        p1.X + (p2.X - p0.X) / (6f / tension),
        p1.Y + (p2.Y - p0.Y) / (6f / tension));
    PointF cp2 = new PointF(
        p2.X - (p3.X - p1.X) / (6f / tension),
        p2.Y - (p3.Y - p1.Y) / (6f / tension));
    return new PointF[] { p1, cp1, cp2, p2 };
}

// Marker rendering dispatch:
void DrawMarker(Graphics g, PointF pt, MarkerStyle style, int size, Color color)
{
    int half = size / 2;
    switch (style)
    {
        case MarkerStyle.Circle:
            g.FillEllipse(brush, pt.X - half, pt.Y - half, size, size); break;
        case MarkerStyle.Square:
            g.FillRectangle(brush, pt.X - half, pt.Y - half, size, size); break;
        case MarkerStyle.Diamond:
            PointF[] diamond = { new(pt.X, pt.Y-half), new(pt.X+half, pt.Y),
                new(pt.X, pt.Y+half), new(pt.X-half, pt.Y) };
            g.FillPolygon(brush, diamond); break;
        case MarkerStyle.Triangle:
            PointF[] tri = { new(pt.X, pt.Y-half), new(pt.X+half, pt.Y+half),
                new(pt.X-half, pt.Y+half) };
            g.FillPolygon(brush, tri); break;
    }
}

PieSeriesPainter

Renders proportional pie or donut slices. Key features:

FeatureDescription
Proportional SlicesEach data point's angle is proportional to its Y value relative to the series total: angle = (pointY / totalY) * 360°. Slices are drawn counterclockwise starting from 12 o'clock. Each slice uses its series palette color.
Donut ModeWhen InnerRadius > 0, the center of the pie is hollow, creating a donut chart. InnerRadius is specified as a float (e.g. 0.4 = 40% of the pie radius). The donut hole is rendered as a filled circle in the chart background color to simulate transparency.
Exploded SliceOn mouse hover (detected via hit-test), the hovered slice is drawn with an offset of ~10px radially outward from the center, creating an "exploded" visual effect. The original position of surrounding slices remains unchanged.
Sliver ProtectionExtremely small slices (angle < 1°) are merged into a combined "Other" slice to prevent rendering artifacts. The threshold is configurable.
Data LabelsWhen ShowDataLabels = true, percentage values are drawn near the midpoint of each slice's arc. Labels use the text color and are positioned radially at 70% of the slice radius.
// Pie slice rendering (simplified):
float sweepAngle = (pointY / totalY) * 360f;
float midAngle = startAngle + sweepAngle / 2f;
float labelRad = (pieRadius * 0.7f);

g.FillPie(brush, pieRect, startAngle, sweepAngle);
g.DrawPie(pen, pieRect, startAngle, sweepAngle);

if (options.ShowDataLabels)
{
    float pct = (pointY / totalY) * 100f;
    float labelX = center.X + (float)Math.Cos(ToRadians(midAngle)) * labelRad;
    float labelY = center.Y + (float)Math.Sin(ToRadians(midAngle)) * labelRad;
    g.DrawString($"{pct:F1}%", font, textBrush, labelX, labelY, sf);
}

// Donut hole:
if (options.InnerRadius > 0)
{
    float holeSize = pieRadius * options.InnerRadius;
    RectangleF holeRect = new(center.X - holeSize, center.Y - holeSize, holeSize*2, holeSize*2);
    g.FillEllipse(bgBrush, holeRect);
}

AreaSeriesPainter

Renders filled polygon areas below connected line segments. Key features:

FeatureDescription
Filled PolygonFor each series, the painter constructs a polygon from the data points (top edge) plus the baseline (bottom edge), forming a closed shape. This polygon is filled with the series color at FillOpacity transparency and outlined with the line color at full opacity.
Semi-Transparent FillControlled via FillOpacity (default 0.3 = 30%). A value of 0 produces no fill (line-only), while 1.0 produces a fully opaque fill. Internally, Color.FromArgb((int)(255*fillOpacity), seriesColor) is used.
Stacked ModeWhen Stacked = true, areas accumulate vertically. Series 0 is filled from pixelZeroLine (bottom of plot rect) to its Y values. Series 1 is filled from Series 0's Y values to Series 1's Y values. This creates a stacked area chart similar to a stream graph layout. The baseline remains the bottom of the plot rectangle for the bottom series.
Smooth LinesWhen SmoothLines = true, the top edge uses Bezier curves (like LineSeriesPainter), and the fill polygon boundary follows the smoothed path.
// Area fill construction (simplified):
List<PointF> polygonPoints = new();
foreach (var point in series.DataPoints)
    polygonPoints.Add(new PointF(Single(toX(point)), Single(toY(point))));

// Close the polygon at the baseline
if (options.Stacked)
    for (int i = series.DataPoints.Count - 1; i >= 0; i--)
        polygonPoints.Add(new PointF(Single(toX(series.DataPoints[i])), baseY));

g.FillPolygon(new SolidBrush(Color.FromArgb(fillAlpha, color)), polygonPoints.ToArray());
g.DrawLines(pen, polygonPoints.Take(dataPoints.Count).ToArray());

BubbleSeriesPainter

Renders proportional circles at XY coordinates. Key features:

FeatureDescription
Proportional CirclesEach data point is rendered as a filled circle at its (X, Y) position. The circle area (not radius) is proportional to the data point's Size value (Y-dimension). This ensures visual accuracy — a value of 200 produces a circle with twice the area of a value of 100. Circle area = Math.PI * sizeScale * yValue, so radius = Math.Sqrt(yValue * sizeScale / Math.PI).
Size ScalingThe sizeScale is computed automatically based on the plot rectangle dimensions and the data range: sizeScale = (plotRect.Width * plotRect.Height) / (maxY * 20), ensuring bubbles are visible but not overwhelming.
Opacity & BorderBubbles are rendered with a semi-transparent fill (default alpha 180) and a solid 2px border in the same color. This allows overlapping bubbles to remain visible.
Hit TestingHit test is a circular distance check: (mouse.X - center.X)^2 + (mouse.Y - center.Y)^2 <= radius^2. The hit rectangle registered is the bounding box of the circle.
// Bubble rendering (simplified):
float maxSize = data.SelectMany(s => s.DataPoints).Max(p => float.Parse(p.Y));
float sizeScale = (plotRect.Width * plotRect.Height) / (maxSize * 20f);

foreach (var series in data)
{
    Color color = palette[data.IndexOf(series) % palette.Count];
    using var fillBrush = new SolidBrush(Color.FromArgb(180, color));
    using var pen = new Pen(color, 2f);

    foreach (var point in series.DataPoints)
    {
        float cx = Convert.ToSingle(toX(point));
        float cy = Convert.ToSingle(toY(point));
        float val = float.Parse(point.Y);
        float radius = (float)Math.Sqrt(val * sizeScale / Math.PI);
        float r = Math.Max(radius, 3f);

        g.FillEllipse(fillBrush, cx - r, cy - r, r * 2f, r * 2f);
        g.DrawEllipse(pen, cx - r, cy - r, r * 2f, r * 2f);
    }
}

SeriesRenderOptions Reference

The SeriesRenderOptions data class encapsulates all configurable rendering parameters for series painters. Each painter reads the subset of options relevant to its chart type and ignores others (e.g., BarSeriesPainter uses BarWidth and BarPadding but ignores MarkerShape).

PropertyTypeDefaultUsed ByEffect
BarWidthfloat0.8BarFraction of column width occupied by bar group (0.1-1.0). Lower values create thinner bars with more gap.
BarPaddingfloat0.2BarFraction of column width used as gap between bar groups. Total column usage = BarWidth + BarPadding.
ShowDataLabelsboolfalseBar, PieDisplay Y values (or percentages for pie) as text labels on or near each visual element.
SmoothLinesboolfalseLine, AreaApply cubic Bezier interpolation between consecutive points for a smooth curve instead of straight line segments.
ShowMarkersbooltrueLineDraw a shape marker at each data point on the line.
MarkerShapeMarkerStyleCircleLineShape drawn at each data point: Circle, Square, Diamond, or Triangle.
MarkerSizeint6LineRadius (or half-size) of markers in pixels. Affects both visual size and hit-test area.
FillOpacityfloat0.3AreaTransparency of the area fill (0.0 = fully transparent / line-only, 1.0 = fully opaque).
InnerRadiusfloat0PieHole size for donut mode. 0 = full pie, 0.4 = donut with 40% radius hole, 0.8 = thin ring.
StackedboolfalseBar, AreaAccumulate bars or areas vertically across series instead of drawing them side-by-side (bar) or independently (area).

Additional internal fields: SeriesColor (nullable override color), EnableAnimation (future), AnimationDurationMs (future, default 400).

Examples

Pie Donut Chart

var chart = new BeepChart();
chart.ChartType = ChartType.Pie;
chart.ChartTitle = "Market Share";

chart.DataSeries.Add(new ChartDataSeries
{
    SeriesName = "Browser Share",
    DataPoints = new List<ChartDataPoint>
    {
        new() { X = "Chrome", Y = "65" },
        new() { X = "Edge", Y = "12" },
        new() { X = "Firefox", Y = "8" },
        new() { X = "Safari", Y = "10" },
        new() { X = "Other", Y = "5" }
    }
});

// Enable donut mode with 50% inner radius
chart.InnerRadius = 0.5f;
chart.ShowDataLabels = true;

Stacked Bar Chart

chart.ChartType = ChartType.Bar;
chart.Stacked = true;
chart.BarWidth = 0.7f;
chart.BarPadding = 0.3f;
chart.ChartTitle = "Quarterly Revenue by Product";

chart.DataSeries.Clear();
chart.DataSeries.Add(new ChartDataSeries
{
    SeriesName = "Product A",
    DataPoints = new List<ChartDataPoint>
    {
        new() { X = "Q1", Y = "1200" },
        new() { X = "Q2", Y = "1400" },
        new() { X = "Q3", Y = "1100" },
        new() { X = "Q4", Y = "1600" }
    }
});
chart.DataSeries.Add(new ChartDataSeries
{
    SeriesName = "Product B",
    DataPoints = new List<ChartDataPoint>
    {
        new() { X = "Q1", Y = "800" },
        new() { X = "Q2", Y = "700" },
        new() { X = "Q3", Y = "900" },
        new() { X = "Q4", Y = "600" }
    }
});
chart.DataSeries.Add(new ChartDataSeries
{
    SeriesName = "Product C",
    DataPoints = new List<ChartDataPoint>
    {
        new() { X = "Q1", Y = "500" },
        new() { X = "Q2", Y = "600" },
        new() { X = "Q3", Y = "400" },
        new() { X = "Q4", Y = "700" }
    }
});

Smooth Line Chart with Markers

chart.ChartType = ChartType.Line;
chart.SmoothLines = true;
chart.ShowMarkers = true;
chart.MarkerShape = MarkerStyle.Diamond;
chart.MarkerSize = 8;
chart.ChartTitle = "Temperature Trend";

chart.DataSeries.Clear();
chart.DataSeries.Add(new ChartDataSeries
{
    SeriesName = "Sensor A",
    DataPoints = new List<ChartDataPoint>
    {
        new() { X = "Jan", Y = "22" },
        new() { X = "Feb", Y = "24" },
        new() { X = "Mar", Y = "28" },
        new() { X = "Apr", Y = "32" },
        new() { X = "May", Y = "35" },
        new() { X = "Jun", Y = "38" }
    }
});

// Custom draw hook for additional styling
chart.CustomDrawSeries += (s, e) =>
{
    if (e.SeriesIndex == 0)
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
};

Programmatic Painter Selection

// Direct factory usage (internal, for testing/extension):
IChartSeriesPainter painter = SeriesPainterFactory.Get(ChartType.Bubble);
painter.Initialize(chart);
painter.DrawSeries(g, plotRect, chart.DataSeries,
    toX, toY, xMin, xMax, yMin, yMax,
    palette, axisColor, textColor, options);
painter.UpdateHitAreas(chart, plotRect, chart.DataSeries,
    toScreen, (key, rect) => chart.HitAreas[key] = rect);