Graphing API (Reusable Curve Engine)

1. Base control

GraphEditorControlBase is the shared graph engine. Plugins should treat it as a black box for add/move/edit/delete point interactions and rendering helpers.

2. Interaction properties

PropertyTypeMeaning
AllowPointAddboolLeft click adds points in plot area.
AllowPointDragboolLeft drag moves points.
AllowPointDeleteboolDeletion is allowed.
DeletePointOnDragOutsideControlboolDragging outside can delete interior point.
EnableDefaultPointInteractionboolEnable built-in add/drag behaviour.
EnableDefaultPointEditorboolEnable built-in right-click editor dialogue.
LockEndpointXOnDragboolEndpoints keep x fixed while dragging.
LockEndpointXOnEditboolEndpoints keep x fixed in editor dialogue.
ClampInteriorXForNonEndpointsboolInterior x stays inside open range (0,1).
PointHitRadiusPxfloatHit testing radius for points.

3. Visual properties

PropertyTypeMeaning
ShowCrosshairboolDraw crosshair in plot.
PlotBackgroundColourColorPlot area fill.
AxisColourColorAxis rectangle/tick colour.
GridColourColorGrid line colour.
TickTextColourColorRuler text colour.
CrosshairColourColorCrosshair colour.
XLabelTickCountint?null use xStep labels, 0 auto-fit labels to width, >0 fixed count.

4. Point editor properties

public string PointEditXLabel { get; set; } = "Input Signal Level";
public string PointEditYLabel { get; set; } = "Output Signal Level";
public int PointEditXDecimalPlaces { get; set; } = 3;
public int PointEditYDecimalPlaces { get; set; } = 3;

These control the built-in right-click edit dialogue labels and precision.

5. Events

public event EventHandler? PointsChanged;
public event EventHandler? PointsEditCommitted;
public event EventHandler<GraphMouseReadoutEventArgs>? MouseReadout;
Do heavy buffer processing on PointsEditCommitted, not on PointsChanged.

6. Public helper function

public void BindReadoutLabel(Label? label, Func<float, float, string>? formatter = null, string emptyText = "")

If provided, the graph updates this label automatically and also continues to raise MouseReadout.

7. Protected function set (for derived controls)

// data
protected int AddPoint(GraphPoint point)
protected bool RemovePointAt(int index)
protected bool UpdatePointAt(int index, GraphPoint point)
protected GraphPoint GetPointAt(int index)
protected void SetPoints(IEnumerable<GraphPoint> points, bool ensureEndpoints = false)
protected IEnumerable<(GraphPoint Point, int Index)> EnumeratePointsOrdered()

// conversion/mapping
protected float NormalizedXToScreen(float x)
protected float NormalizedYToScreen(float y)
protected float ScreenToNormalizedX(float x)
protected float ScreenToNormalizedY(float y)
protected float ValueToX(float value, float min, float max)
protected float ValueToY(float value, float min, float max)
protected float XToValue(float x, float min, float max)
protected float YToValue(float y, float min, float max)

// drawing
protected RectangleF BuildPlotRect(float leftMargin, float topMargin, float rightMargin, float bottomMargin, float minWidth = 50f, float minHeight = 50f)
protected void DrawGridAndAxes(Graphics g, float xMin, float xMax, float xStep, float yMin, float yMax, float yStep, Func<float, string>? xTickFormatter, Func<float, string>? yTickFormatter, string? xUnitText = null, string? yUnitText = null, bool drawLeftYAxisLabels = true, bool drawBottomXAxisLabels = true, int? xLabelTickCount = null)
protected void DrawCrosshair(Graphics g)
protected void DrawGraphPolylineAndPoints(Graphics g, Pen linePen, Brush pointBrush, Pen pointBorder, float pointSize = 6f)

// point editor
protected void ShowPointContextMenu(Point clientLocation, PointEditRequest request, Action<PointEditResult> onApply, Action? onDelete = null)

8. Derived controls in current project

Pattern: each derived control raises plugin-level events by overriding RaisePointsChanged and RaisePointsEditCommitted.

9. Recommended integration pattern

_graph = new PanExpandCurveEditorControl();
_graph.BindReadoutLabel(_pointInfoLabel, (x, y) => $"{x:0.###} sec -> {y:0.##} %", "");
_graph.PointsChanged += (_, _) => UpdateUiOnly();
_graph.PointsEditCommitted += (_, _) => RebuildPreviewBuffer();

// apply
var points = _graph.GetPoints();
ApplyPointsToBuffer(points);

9.1 Design-time behaviour (important)

Graph controls render a placeholder preview only in WinForms Designer.

// typical runtime wiring
_graph.Dock = DockStyle.Fill;
_graph.SetPoints(runtimePoints);
_graph.BindReadoutLabel(_pointInfoLabel, (x, y) => $"{x:0.###} sec -> {y:0.##} %", "");

10. Scale and value mapping (exact behaviour)

The graph stores points in normalised space (x,y in [0..1]), while rulers and labels use value-space.

// value -> normalised
xNorm = (xValue - xMin) / (xMax - xMin)
yNorm = (yValue - yMin) / (yMax - yMin)

// normalised -> value
xValue = xMin + xNorm * (xMax - xMin)
yValue = yMin + yNorm * (yMax - yMin)

Use these helper functions from the base class when implementing derived controls:

ValueToX(value, min, max)
ValueToY(value, min, max)
XToValue(xPx, min, max)
YToValue(yPx, min, max)
Rule: keep plugin state in value-space if meaningful for DSP, and convert only at graph boundary.

11. Ruler placement and what is supported

Ruler sideBuilt-in supportControl flag
Left Y rulerYesdrawLeftYAxisLabels
Bottom X rulerYesdrawBottomXAxisLabels
Right Y rulerUnit marker only by defaultyUnitText draw position
Top X rulerNo dedicated built-in label stripCustom draw in derived control

Configured through:

DrawGridAndAxes(
  g,
  xMin, xMax, xStep,
  yMin, yMax, yStep,
  xTickFormatter, yTickFormatter,
  xUnitText, yUnitText,
  drawLeftYAxisLabels: true/false,
  drawBottomXAxisLabels: true/false,
  xLabelTickCount: XLabelTickCount)

11.1 Ruler/text draw rules (exact)

12. Controlling what ruler text displays

Use formatter functions to display text labels instead of raw numeric values.

// numeric dB labels
yTickFormatter: y => y.ToString(\"0\")

// named labels
yTickFormatter: y => y switch
{
    <= -60f => \"Low\",
    <= -20f => \"Mid\",
    _ => \"High\"
};

// time labels
xTickFormatter: x => $\"{x:0.###}\"

Units are independent from label text:

xUnitText: \"sec\"
yUnitText: \"%\"

12.1 Passing label text instead of raw values

Use formatters to convert numeric axis values into semantic labels.

// Left/Center/Right ruler labels for pan graph
yTickFormatter: y => y switch
{
    > 50f  => "Left",
    < -50f => "Right",
    _      => "Center"
};

For sparse named labels, return empty string for unlabeled ticks.

yTickFormatter: y => Math.Abs(y) < 0.001f ? "Center" : "";

13. Tick density rules

Practical usage:

// force 10 labels
_graph.XLabelTickCount = 10;

// let graph decide based on available width
_graph.XLabelTickCount = 0;

14. Readout text: built-in and custom

Built-in readout from graph:

_graph.BindReadoutLabel(_infoLabel);

Custom readout format:

_graph.BindReadoutLabel(
  _infoLabel,
  (x, y) => $\"{x:0.###} sec -> {y:0.##} %\",
  \"\");

You can also subscribe directly:

_graph.MouseReadout += (_, e) =>
{
    if (!e.HasReadout) return;
    _infoLabel.Text = $\"x={e.XValue:0.###}, y={e.YValue:0.###}\";
};

15. Implementation recipes (copy-paste)

15.1 Percent X / dB Y graph

DrawGridAndAxes(
  g,
  xMin: 0f, xMax: 100f, xStep: 10f,
  yMin: -96f, yMax: 0f, yStep: 12f,
  xTickFormatter: x => x.ToString(\"0\"),
  yTickFormatter: y => y.ToString(\"0\"),
  xUnitText: \"%\",
  yUnitText: \"dB\",
  drawLeftYAxisLabels: true,
  drawBottomXAxisLabels: true,
  xLabelTickCount: XLabelTickCount);

15.2 Time X / percentage Y graph

DrawGridAndAxes(
  g,
  xMin: rangeStartSec,
  xMax: rangeEndSec,
  xStep: 1f,
  yMin: 0f,
  yMax: 100f,
  yStep: 10f,
  xTickFormatter: sec => sec.ToString(\"0.###\"),
  yTickFormatter: amp => amp.ToString(\"0\"),
  xUnitText: \"sec\",
  yUnitText: \"%\",
  drawLeftYAxisLabels: true,
  drawBottomXAxisLabels: true,
  xLabelTickCount: 0);

Back to top