Drawing tools

Drawing tools

Overview & anchors

Interactive chart annotations — lines, shapes, Fibonacci level sets, patterns — built on a small base class that owns selection, movement and styling, and on bar-index-independent anchors that survive scrolling, live updates and replays.

What a drawing tool is

A drawing tool is one placed annotation on a chart — a trend line, a ray, a horizontal line, a rectangle, a Fibonacci retracement, an Elliott-wave count. Each placed instance is a live object that stores its own anchors and style, paints itself every frame, and answers "did the user click me?". The contract is IDrawingTool in the TradeStrike.Pipeline.Drawing namespace; the chart's drawing layer owns the list of tools and drives their lifecycle.

Why it exists. The host gives you the projection maths (time/price↔pixel), the placement state machine, selection, the properties dialog, persistence and toolbar discovery. You supply only the geometry of your shape. That separation is what lets a two-anchor trend line be roughly twenty lines of code.

When to build one. Reach for a drawing tool whenever you want a user-placed, user-draggable annotation tied to points on the price timeline — as opposed to an indicator, which computes a series from bar data. If the thing follows the cursor during placement and survives a workspace save, it is a drawing tool.

The drawing-tool model

You subclass DrawingToolBase and tag it with [DrawingToolMetadata]. The base class owns the anchor list, the process-unique Id, selection state (IsSelected), the placed flag (IsPlaced), the standard Color / LineStyle / LineWidthPx properties, and the default Move. It reads ToolName from the metadata attribute. That leaves your subclass with exactly two abstract methods to implement: OnRender (paint the tool) and HitTest (is this pixel close enough to count as a hit?).

IDrawingTool.csusing System;
using System.Collections.Generic;
using TradeStrike.Pipeline.Plots;

namespace TradeStrike.Pipeline.Drawing;

public interface IDrawingTool
{
    Guid Id { get; }
    string ToolName { get; }
    string PanelId { get; set; }
    bool IsSelected { get; set; }
    bool IsPlaced { get; set; }

    int AnchorCount { get; }
    int MaxAnchorCount { get; }
    DrawingPlacementMode PlacementMode { get; }
    bool CanAddAnchor { get; }
    IReadOnlyList<DrawingAnchor> Anchors { get; }

    ChartColor Color { get; set; }
    double LineWidthPx { get; set; }
    LineStyle LineStyle { get; set; }

    void SetAnchor(int index, DrawingAnchor anchor);
    void AddAnchor(DrawingAnchor anchor);
    bool RemoveLastAnchor();
    void Move(TimeSpan dTime, double dPrice);

    void OnRender(IDrawingToolRenderContext ctx);
    bool HitTest(double pixelX, double pixelY, IDrawingToolRenderContext ctx, double tolerancePx = 6);
}

DrawingToolBase has two construction modes. The fixed-anchor constructor takes an anchor count and pre-allocates that many slots, so SetAnchor works on every index from the start; its placement mode is FixedAnchors. The variable-anchor constructor takes a placement mode (and optionally a max count and an initial count) for open-ended or paint tools.

DrawingToolBase.cspublic abstract class DrawingToolBase : IDrawingTool
{
    // Fixed-anchor tools (line, ray, rectangle, triangle, arrow):
    protected DrawingToolBase(int anchorCount);

    // Variable-anchor tools (path, polyline, brush, highlighter):
    protected DrawingToolBase(
        DrawingPlacementMode mode,
        int maxAnchorCount = int.MaxValue,
        int initialAnchorCount = 0);

    public IReadOnlyList<DrawingAnchor> Anchors { get; }
    public ChartColor Color { get; set; }          // already an editable property
    public LineStyle LineStyle { get; set; }        // Solid / Dashed / Dotted
    public double LineWidthPx { get; set; }         // throws if <= 0
    public int MinAnchorsForFinalize { get; protected set; } = 2;

    public abstract void OnRender(IDrawingToolRenderContext ctx);
    public abstract bool HitTest(double px, double py, IDrawingToolRenderContext ctx, double tol = 6);
    public virtual void Move(TimeSpan dTime, double dPrice);   // default: shift all anchors
}
Defaults that already work. The base sets Color to DodgerBlue (new ChartColor(0x1E, 0x90, 0xFF)), LineStyle to Solid and LineWidthPx to 1.0, and annotates all three with [DrawingToolProperty] so every tool surfaces those editors with no extra work.

Anchors

An anchor is a (Time, Price) pair on the bar series' canonical timeline. It is a readonly record struct and is deliberately bar-index independent: a tool stores when and at what price it lives, never which bar number. That is what lets a tool survive scrolling, live bar updates, replays and even bar-type changes (range / Renko / tick) — time and price do not move when the bar grid does.

Two factory helpers cover the degenerate cases: a horizontal line cares only about price, a vertical line only about time. The convention is to store the unused coordinate as a sentinel (DateTime.MinValue or double.NaN) and never read it.

DrawingAnchor.csnamespace TradeStrike.Pipeline.Drawing;

public readonly record struct DrawingAnchor(DateTime Time, double Price)
{
    public static DrawingAnchor PriceOnly(double price) => new(DateTime.MinValue, price);
    public static DrawingAnchor TimeOnly(DateTime time) => new(time, double.NaN);
}

The tool's Anchors list always returns AnchorCount entries. For fixed-count tools the slots are pre-allocated (zero-valued until set) and you write them with SetAnchor(index, anchor). For variable-count tools the list starts empty (or at the initial count) and grows via AddAnchor; RemoveLastAnchor pops the most recent point.

Placement modes

How the host gathers anchors during placement is driven by DrawingPlacementMode, which your constructor choice sets. The chart's interaction state machine dispatches on it.

DrawingPlacementMode Placement gesture Used by
FixedAnchors Click once per anchor; placement completes when AnchorCount clicks land. Line, ray, rectangle, triangle, arrow.
OpenEnded Click to append a point; double-click or Escape finalizes (must reach MinAnchorsForFinalize). Path, polyline.
DragToPaint Mouse-down + drag samples points while held; mouse-up finalizes. Brush, highlighter.

The fixed-anchor constructor always selects FixedAnchors; passing that mode to the variable-anchor constructor throws, because fixed tools must pre-allocate their slots. Variable tools default MinAnchorsForFinalize to 2 — a single point is not a meaningful shape.

Panels

Every tool carries a PanelId. The constant DrawingToolPanelIds.Main ("main") is the main candle panel and is the default; any other value matches a subpanel (typically a panel-hint indicator's PanelId). The chart's render and hit-test passes filter tools by this so each subpanel keeps its own drawings in its own price domain.

This overview is the entry point. The pages below cover each part of building a tool, in the order you will usually need them.

Page What it covers
The render context The IDrawingToolRenderContext that does time/price↔pixel work.
Your first tool A complete two-anchor trend line and the metadata fields.
Editable properties Exposing extra settings with [DrawingToolProperty].
Constrained movement Overriding Move to restrict dragging.
Fibonacci levels Editable FibLevelSet level sets.
Geometry & hit-testing The DrawingGeometry distance helpers.
Discovery & persistence How tools are found via the catalog and listed in the toolbar.