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
| Property | Type | Meaning |
|---|---|---|
AllowPointAdd | bool | Left click adds points in plot area. |
AllowPointDrag | bool | Left drag moves points. |
AllowPointDelete | bool | Deletion is allowed. |
DeletePointOnDragOutsideControl | bool | Dragging outside can delete interior point. |
EnableDefaultPointInteraction | bool | Enable built-in add/drag behaviour. |
EnableDefaultPointEditor | bool | Enable built-in right-click editor dialogue. |
LockEndpointXOnDrag | bool | Endpoints keep x fixed while dragging. |
LockEndpointXOnEdit | bool | Endpoints keep x fixed in editor dialogue. |
ClampInteriorXForNonEndpoints | bool | Interior x stays inside open range (0,1). |
PointHitRadiusPx | float | Hit testing radius for points. |
3. Visual properties
| Property | Type | Meaning |
|---|---|---|
ShowCrosshair | bool | Draw crosshair in plot. |
PlotBackgroundColour | Color | Plot area fill. |
AxisColour | Color | Axis rectangle/tick colour. |
GridColour | Color | Grid line colour. |
TickTextColour | Color | Ruler text colour. |
CrosshairColour | Color | Crosshair colour. |
XLabelTickCount | int? | 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;
PointsChanged: emitted for each change (including drag movement).PointsEditCommitted: emitted on commit boundaries (drag end, editor OK, delete complete).MouseReadout: emitted with converted x/y values while hovering plot area.
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
TransferCurveEditorControl(dynamic processing)EnvelopeEditorControl(envelope effect)PanExpandCurveEditorControl(pan/expand + stereo rotate)
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.
- Designer rendering is for layout/bounds visibility, not real plugin state.
- Real axis ranges, points, units, and callbacks are all configured at runtime.
- Always place the graph control into a host panel and use
Dock=Fill.
// 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)
11. Ruler placement and what is supported
| Ruler side | Built-in support | Control flag |
|---|---|---|
| Left Y ruler | Yes | drawLeftYAxisLabels |
| Bottom X ruler | Yes | drawBottomXAxisLabels |
| Right Y ruler | Unit marker only by default | yUnitText draw position |
| Top X ruler | No dedicated built-in label strip | Custom 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)
- X tick text is horizontally centred at each tick.
- Y tick text is right-aligned to the left ruler edge.
xUnitTextis drawn at bottom-left of the plot area.yUnitTextis drawn near top-right outside the plot area.- Right-side Y numeric labels are not built in; provide custom draw in a derived control if needed.
- If
drawBottomXAxisLabels=false, X grid may still draw; only label text is suppressed. - If
drawLeftYAxisLabels=false, Y grid may still draw; only label text is suppressed.
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
XLabelTickCount = null: usesxSteppositions for labels.XLabelTickCount = 0: auto-fit by measuring\"000000\"width and fitting labels across plot width.XLabelTickCount > 0: fixed count, evenly spaced from start to end.
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);