Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions canvas/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Plugin Module: canvas

The canvas panel renders weathermap-style network diagrams. Nodes and edges can be colored by
query-bound thresholds, labeled with metric values, and (for edges) scaled in thickness by value.

The panel has two modes, selected in the panel's **Settings** editor under **Items**:

- **Manual** — draw the graph by hand: place nodes, connect them with edges, and add background
shapes or images. Each node/edge can bind to a query to drive its color and label.
- **From data** — generate the graph live from query results. Use this for dynamic topologies
(for example, frequently changing service or host relationships) where drawing by hand is not
feasible.

### Data-driven mode

In **From data** mode the panel derives nodes and edges from your queries on every refresh and
lays them out automatically — no manual positioning. Graph structure is inferred from the labels on
the returned time series, and each series' **last value** drives color (via the panel thresholds),
label interpolation, and edge thickness.

Configuration:

- **Node query** — the query whose series define the nodes.
- **Node id label** — the series label that identifies a node (e.g. `instance`). One node is created
per distinct label value.
- **Node label template** — optional caption, using `{{label_name}}` / `{{value}}` placeholders.
Defaults to the node id label.
- **Edge query** — the query whose series define the edges (leave as *None* for a nodes-only graph).
- **Edge source label** / **Edge target label** — series labels whose values name the edge's source
and target nodes. Edges whose endpoints are not among the generated nodes are dropped.
- **Edge label template** — optional edge caption (e.g. `{{value}}`).
- **Bidirectional edges** — render edges with arrows in both directions.

Node colors come from the **Node color** thresholds configured for the panel, and **Node size**
thresholds can scale a node's size by its value (relative to the configurable default node size).
Edges use the **Edge color** thresholds when set and otherwise fall back to the node thresholds, so
edges can carry their own color scheme independent of the nodes. Edge thickness is set per
breakpoint under **Edge thickness**, which shares the same thresholds as the edge color (the edge
thresholds when set, otherwise the node thresholds). Layout is deterministic: a given topology
always settles to the same positions, so nodes stay put as their metric values change — they only
move when nodes or edges are added or removed.

### How to install

This plugin requires react and react-dom 18
Expand Down
22 changes: 22 additions & 0 deletions canvas/schemas/panels/canvas/canvas.cue
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,31 @@ kind: "Canvas"
spec: close({
legend?: #legend
thresholds?: common.#thresholds
edgeThresholds?: common.#thresholds
format?: common.#format
querySettings?: #querySettings
edgeDefaultStrokeWidth?: number & >0
edgeThresholdWidths?: [...#edgeThresholdStep]
nodeThresholdSizes?: [...#nodeThresholdStep]
nodeDefaultSize?: number & >0
backgrounds?: [...#background]
nodes?: [...#node]
edges?: [...#edge]
graphGeneration?: #graphGeneration
})

#graphGeneration: {
enabled?: bool
nodeQueryIndex: int & >=0
nodeIdLabel: string
nodeLabelTemplate?: string
edgeQueryIndex?: int & >=0
edgeSourceLabel?: string
edgeTargetLabel?: string
edgeLabelTemplate?: string
bidirectional?: bool
}

#legend: {
position: "bottom" | "right"
}
Expand Down Expand Up @@ -75,6 +91,7 @@ spec: close({
queryIndex?: int & >=0
colorMode?: "threshold" | "fixed"
color?: =~"^#(?:[0-9a-fA-F]{3}){1,2}$"
sizeMode?: "fixed" | "threshold"
}

#edge: {
Expand All @@ -101,3 +118,8 @@ spec: close({
value: number
strokeWidth: number & >0
}

#nodeThresholdStep: {
value: number
scale: number & >0
}
96 changes: 87 additions & 9 deletions canvas/sdk/go/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ const (
ThresholdThicknessMode ThicknessMode = "threshold"
)

type SizeMode string

const (
FixedSizeMode SizeMode = "fixed"
ThresholdSizeMode SizeMode = "threshold"
)

type ImageFit string

const (
Expand Down Expand Up @@ -94,7 +101,8 @@ type NodeSpec struct {
QueryIndex *uint `json:"queryIndex,omitempty" yaml:"queryIndex,omitempty"`
ColorMode ColorMode `json:"colorMode,omitempty" yaml:"colorMode,omitempty"`
// Color is a hex color string (e.g. "#ff0000").
Color string `json:"color,omitempty" yaml:"color,omitempty"`
Color string `json:"color,omitempty" yaml:"color,omitempty"`
SizeMode SizeMode `json:"sizeMode,omitempty" yaml:"sizeMode,omitempty"`
}

type EdgeSpec struct {
Expand Down Expand Up @@ -132,21 +140,46 @@ type EdgeThresholdStep struct {
StrokeWidth float64 `json:"strokeWidth" yaml:"strokeWidth"`
}

type NodeThresholdStep struct {
Value float64 `json:"value" yaml:"value"`
// Scale multiplies the node's base width/height.
Scale float64 `json:"scale" yaml:"scale"`
}

type QueryColorSettings struct {
QueryIndex uint `json:"queryIndex" yaml:"queryIndex"`
ColorMode ColorMode `json:"colorMode" yaml:"colorMode"`
ColorValue string `json:"colorValue" yaml:"colorValue"`
}

// GraphGenerationSpec configures the data-driven graph mode. When Enabled, the panel derives nodes
// and edges live from query results and lays them out automatically; graph structure is inferred
// from series labels.
type GraphGenerationSpec struct {
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
NodeQueryIndex uint `json:"nodeQueryIndex" yaml:"nodeQueryIndex"`
NodeIDLabel string `json:"nodeIdLabel" yaml:"nodeIdLabel"`
NodeLabelTemplate string `json:"nodeLabelTemplate,omitempty" yaml:"nodeLabelTemplate,omitempty"`
EdgeQueryIndex *uint `json:"edgeQueryIndex,omitempty" yaml:"edgeQueryIndex,omitempty"`
EdgeSourceLabel string `json:"edgeSourceLabel,omitempty" yaml:"edgeSourceLabel,omitempty"`
EdgeTargetLabel string `json:"edgeTargetLabel,omitempty" yaml:"edgeTargetLabel,omitempty"`
EdgeLabelTemplate string `json:"edgeLabelTemplate,omitempty" yaml:"edgeLabelTemplate,omitempty"`
Bidirectional bool `json:"bidirectional,omitempty" yaml:"bidirectional,omitempty"`
}

type PluginSpec struct {
Thresholds *common.Thresholds `json:"thresholds,omitempty" yaml:"thresholds,omitempty"`
Format *common.Format `json:"format,omitempty" yaml:"format,omitempty"`
EdgeThresholdWidths []EdgeThresholdStep `json:"edgeThresholdWidths,omitempty" yaml:"edgeThresholdWidths,omitempty"`
EdgeDefaultStrokeWidth *float64 `json:"edgeDefaultStrokeWidth,omitempty" yaml:"edgeDefaultStrokeWidth,omitempty"`
QuerySettings []QueryColorSettings `json:"querySettings,omitempty" yaml:"querySettings,omitempty"`
Backgrounds []BackgroundSpec `json:"backgrounds,omitempty" yaml:"backgrounds,omitempty"`
Nodes []NodeSpec `json:"nodes,omitempty" yaml:"nodes,omitempty"`
Edges []EdgeSpec `json:"edges,omitempty" yaml:"edges,omitempty"`
Thresholds *common.Thresholds `json:"thresholds,omitempty" yaml:"thresholds,omitempty"`
EdgeThresholds *common.Thresholds `json:"edgeThresholds,omitempty" yaml:"edgeThresholds,omitempty"`
Format *common.Format `json:"format,omitempty" yaml:"format,omitempty"`
EdgeThresholdWidths []EdgeThresholdStep `json:"edgeThresholdWidths,omitempty" yaml:"edgeThresholdWidths,omitempty"`
EdgeDefaultStrokeWidth *float64 `json:"edgeDefaultStrokeWidth,omitempty" yaml:"edgeDefaultStrokeWidth,omitempty"`
NodeThresholdSizes []NodeThresholdStep `json:"nodeThresholdSizes,omitempty" yaml:"nodeThresholdSizes,omitempty"`
NodeDefaultSize *float64 `json:"nodeDefaultSize,omitempty" yaml:"nodeDefaultSize,omitempty"`
QuerySettings []QueryColorSettings `json:"querySettings,omitempty" yaml:"querySettings,omitempty"`
Backgrounds []BackgroundSpec `json:"backgrounds,omitempty" yaml:"backgrounds,omitempty"`
Nodes []NodeSpec `json:"nodes,omitempty" yaml:"nodes,omitempty"`
Edges []EdgeSpec `json:"edges,omitempty" yaml:"edges,omitempty"`
GraphGeneration *GraphGenerationSpec `json:"graphGeneration,omitempty" yaml:"graphGeneration,omitempty"`
}

type Option func(plugin *Builder) error
Expand Down Expand Up @@ -180,3 +213,48 @@ func Chart(options ...Option) panel.Option {
return nil
}
}

// WithGraphGeneration enables the data-driven graph mode, deriving nodes and edges from query
// results instead of a manually authored graph.
func WithGraphGeneration(spec GraphGenerationSpec) Option {
return func(builder *Builder) error {
builder.GraphGeneration = &spec
return nil
}
}

// WithThresholds sets the color scheme applied to nodes.
func WithThresholds(thresholds common.Thresholds) Option {
return func(builder *Builder) error {
builder.Thresholds = &thresholds
return nil
}
}

// WithEdgeThresholds sets a color scheme applied to edges. When unset, edges fall back to the node
// thresholds configured with WithThresholds.
func WithEdgeThresholds(thresholds common.Thresholds) Option {
return func(builder *Builder) error {
builder.EdgeThresholds = &thresholds
return nil
}
}

// WithNodeThresholdSizes scales nodes by their bound value. Each step's Scale multiplies the node's
// base width/height when the value reaches the step's Value. Only applies to nodes whose SizeMode is
// "threshold" (generated nodes set this automatically).
func WithNodeThresholdSizes(steps ...NodeThresholdStep) Option {
return func(builder *Builder) error {
builder.NodeThresholdSizes = steps
return nil
}
}

// WithNodeDefaultSize sets the base width/height (px) for generated nodes, which the node-size
// thresholds scale. Defaults to 48 when unset.
func WithNodeDefaultSize(size float64) Option {
return func(builder *Builder) error {
builder.NodeDefaultSize = &size
return nil
}
}
Loading