The Block component is the fundamental building block for rendering graph nodes in the visualization system. This document describes the Block component's structure, behavior, events, and how to extend it with custom implementations.
The Block component extends the GraphComponent class and provides the base functionality for rendering and interacting with graph blocks (nodes).
classDiagram
GraphComponent <|-- Block
Block <|-- CustomBlock
Block -- BlockState
Block -- BlockController
Block -- GraphLayer
class GraphComponent {
+props
+state
+context
+iterate()
+render()
}
class Block {
+static IS: string
+isBlock: boolean
+connectedState: BlockState
+blockController: BlockController
+$viewState: Signal
+getGeometry()
+updatePosition()
+handleEvent()
+getAnchorPosition()
+renderBody()
+renderSchematicView()
+renderDetailedView()
}
class BlockState {
+$state: Signal
+$anchors: Signal
+$geometry: Signal
+updateXY()
+setViewComponent()
}
class BlockController {
+handleEvent()
+onDragStart()
+onDragUpdate()
+onDragEnd()
}
The basic data structure for a block:
type TBlock<T extends Record<string, unknown> = {}> = {
id: TBlockId; // Unique identifier
is: string; // Block type identifier
x: number; // X coordinate
y: number; // Y coordinate
group?: string; // Optional group identifier
width: number; // Width of the block
height: number; // Height of the block
selected: boolean; // Selection state
label: string; // Display name
anchors: TAnchor[]; // Connection points
settings?: TBlockSettings; // Additional settings
meta?: T; // Custom metadata
};type TBlockProps = {
id: TBlockId; // ID of the block to render
font: string; // Font to use for text rendering
label: string; // Label of the block to render
};The Block component maintains several internal state properties:
connectedState: Connection to the block's data in the storeshouldRenderText: Controls text renderingshouldRenderHtml: Controls HTML overlay renderingraised: Indicates if the block is raised (higher z-index)hidden: Controls visibility of the block
The Block component follows the standard component lifecycle, with additional specific behaviors:
-
Initialization:
- Constructor subscribes to the block's data in the store
- Sets up event listeners for drag events
-
Rendering Pipeline:
willRender()- Prepares the block for renderingrender()- Main rendering method, delegates to view-specific methods based on camera scalerenderMinimalisticBlock()- Renders a basic representation of the blockrenderSchematicView()- Renders the block in schematic (simplified) viewrenderDetailedView()- Renders the block in detailed view
-
Children Management:
updateChildren()- Creates anchor components for the block
-
State Management:
stateChanged()- Handles changes to the block's stateupdatePosition()- Updates the block's position
The Block component uses a scale-dependent rendering system that displays different levels of detail based on the camera zoom level. This is controlled in the render() method:
protected render() {
const scaleLevel = this.context.graph.cameraService.getCameraBlockScaleLevel();
switch (scaleLevel) {
case ECameraScaleLevel.Minimalistic:
this.renderMinimalisticBlock(this.context.ctx);
break;
case ECameraScaleLevel.Schematic:
this.renderSchematicView(this.context.ctx);
break;
case ECameraScaleLevel.Detailed:
this.renderDetailedView(this.context.ctx);
break;
}
}The zoom levels are determined by the SCALES array in block constants configuration:
-
Minimalistic View (zoom < first threshold):
- Used when blocks are viewed from far away
- Renders only basic block shapes without text
- Optimized for performance when many blocks are visible
-
Schematic View (between first and third threshold):
- Standard view for normal interaction
- Shows block name and basic structure
- Balances detail and performance
-
Detailed View (zoom >= third threshold):
- Full detail view for close-up inspection
- Shows all block information and details
- Used when working with specific blocks
You can configure these thresholds in the graph constants configuration:
// Configure zoom level thresholds in graph settings
graph.setConstants({
block: {
// Array of three numbers defining zoom thresholds:
// [minimalistic_threshold, schematic_threshold, detailed_threshold]
SCALES: [
0.125, // Below this - minimalistic view
0.225, // Between first and third - schematic view
0.7 // Above this - detailed view
]
}
});These thresholds control when the block switches between different rendering modes. You can adjust them based on your needs:
- Lower values will make the view mode switch at lower zoom levels (when zoomed out more)
- Higher values will make the view mode switch at higher zoom levels (when zoomed in more)
For example, if you want blocks to stay in detailed view longer when zooming out, you could lower the third threshold:
graph.setConstants({
block: {
SCALES: [0.125, 0.225, 0.5] // Detailed view stays until zoom = 0.5
}
});The Block component has three primary rendering methods:
- renderMinimalisticBlock(ctx): Renders a basic representation of the block, used when the block is far away from the viewer.
- renderSchematicView(ctx): Renders a simplified view of the block, including the block label.
- renderDetailedView(ctx): Renders a detailed view of the block, including any additional information.
The Block component handles several events:
sequenceDiagram
participant User
participant Block
participant BlockController
participant Store
User->>Block: Mouse Down
Block->>BlockController: onDragStart()
BlockController->>Block: Emit "block-drag-start"
User->>Block: Mouse Move
Block->>BlockController: onDragUpdate()
BlockController->>Block: Calculate next position
Block->>Block: applyNextPosition()
Block->>Store: Update position
Block->>Block: Emit "block-drag"
User->>Block: Mouse Up
Block->>BlockController: onDragEnd()
BlockController->>Block: Emit "block-drag-end"
The Block component defines these custom events:
- block-drag-start - Fired when a block drag operation begins
- block-drag - Fired continuously during block dragging
- block-drag-end - Fired when a block drag operation ends
Anchors are connection points on blocks that allow creating connections between blocks.
The Block component provides methods to manage anchors:
getAnchorPosition()- Gets the position of an anchorgetConnectionPoint()- Gets a connection point for in/out directionsrenderAnchor()- Renders an anchor at a specific position
The Block component provides several methods that can be overridden to customize appearance:
- renderStroke(color) - Renders the block's border
- renderBody(ctx) - Renders the block's main body
- renderSchematicView(ctx) - Renders the simplified view
- renderDetailedView(ctx) - Renders the detailed view
- renderText(text, ctx, options) - Renders text within the block
To create a custom block type, extend the Block class and override the rendering methods:
class CustomBlock extends Block {
// Override rendering methods to customize appearance
protected renderBody(ctx: CanvasRenderingContext2D) {
// Custom rendering code
ctx.fillStyle = "blue";
ctx.fillRect(this.state.x, this.state.y, this.state.width, this.state.height);
// Call the base implementation if needed
// super.renderBody(ctx);
}
// Override behavior methods to customize interactions
protected onDragStart(event: MouseEvent) {
// Custom drag start handling
console.log("Custom drag start");
// Call the base implementation
super.onDragStart(event);
}
}To use custom block implementations in a graph, you need to register them in the graph configuration:
import { Block, EAnchorType } from "@gravity-ui/graph";
// Define a custom block type
class MySpecialBlock extends Block {
protected renderSchematicView(ctx: CanvasRenderingContext2D) {
// Custom rendering logic
ctx.fillStyle = "#FFD700"; // Gold color
ctx.beginPath();
this.roundRect(ctx, this.state.x, this.state.y, this.state.width, this.state.height, [10]);
ctx.fill();
// Render text
if (this.shouldRenderText) {
ctx.fillStyle = "#000";
this.renderText(this.state.label);
}
}
// Polyfill for roundRect
private roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number[] | number) {
const radii = typeof radius === 'number' ? [radius, radius, radius, radius] : radius;
ctx.moveTo(x + radii[0], y);
ctx.lineTo(x + width - radii[1], y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radii[1]);
ctx.lineTo(x + width, y + height - radii[2]);
ctx.quadraticCurveTo(x + width, y + height, x + width - radii[2], y + height);
ctx.lineTo(x + radii[3], y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radii[3]);
ctx.lineTo(x, y + radii[0]);
ctx.quadraticCurveTo(x, y, x + radii[0], y);
ctx.closePath();
}
}
// Define a unique identifier for this block type
const SPECIAL_BLOCK_TYPE = "special-block";
// Create graph configuration with custom block
const graphConfig: TGraphConfig = {
blocks: [
{
id: "block1",
is: SPECIAL_BLOCK_TYPE, // Use the custom block type
x: 100,
y: 200,
width: 180,
height: 120,
label: "My Special Block",
selected: false,
anchors: [
// Define anchors for the block
{ id: "input1", type: EAnchorType.INPUT, point: [0, 0.5] },
{ id: "output1", type: EAnchorType.OUTPUT, point: [1, 0.5] }
]
}
],
settings: {
// Register the custom block implementation
blockComponents: {
[SPECIAL_BLOCK_TYPE]: MySpecialBlock
}
}
};
// Create the graph with the custom configuration
const graph = new Graph(graphConfig, rootElement);Here's a complete example of creating and using a custom block:
import { Group } from "@/lib/Component";
import { Block, CanvasBlock, TGraphConfig, EAnchorType } from "@gravity-ui/graph";
// 1. Define a custom block class
class RoundedBlock extends CanvasBlock {
// Override the stroke rendering
protected renderStroke(color: string) {
this.context.ctx.lineWidth = Math.round(3 / this.context.camera.getCameraScale());
this.context.ctx.strokeStyle = color;
this.roundRect(this.context.ctx, this.state.x, this.state.y, this.state.width, this.state.height, [15]);
this.context.ctx.stroke();
}
// Override the schematic view rendering
public override renderSchematicView() {
const ctx = this.context.ctx;
// Fill with custom color
ctx.fillStyle = "#E6F7FF";
ctx.beginPath();
this.roundRect(ctx, this.state.x, this.state.y, this.state.width, this.state.height, [15]);
ctx.fill();
// Stroke
ctx.lineWidth = 2;
ctx.strokeStyle = "#0077CC";
ctx.beginPath();
this.roundRect(ctx, this.state.x, this.state.y, this.state.width, this.state.height, [15]);
ctx.stroke();
// Render text
if (this.shouldRenderText) {
ctx.fillStyle = "#003366";
ctx.textAlign = "center";
this.renderText(this.state.label);
}
// Render selection highlight
if (this.state.selected) {
this.renderStroke(this.context.colors.block.selectedBorder);
}
}
// Polyfill for roundRect
private roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number[] | number) {
const radii = typeof radius === 'number' ? [radius, radius, radius, radius] : radius;
ctx.moveTo(x + radii[0], y);
ctx.lineTo(x + width - radii[1], y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radii[1]);
ctx.lineTo(x + width, y + height - radii[2]);
ctx.quadraticCurveTo(x + width, y + height, x + width - radii[2], y + height);
ctx.lineTo(x + radii[3], y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radii[3]);
ctx.lineTo(x, y + radii[0]);
ctx.quadraticCurveTo(x, y, x + radii[0], y);
ctx.closePath();
}
}
// 2. Define a unique type for the block
const ROUNDED_BLOCK_TYPE = "rounded-block";
// 3. Create graph configuration that uses the custom block
export const customBlockGraphConfig: TGraphConfig = {
configurationName: "custom-blocks-example",
blocks: [
{
id: "block1",
is: ROUNDED_BLOCK_TYPE,
x: 100,
y: 100,
width: 200,
height: 150,
label: "Rounded Block 1",
selected: false,
anchors: [
{ id: "in1", type: EAnchorType.INPUT, point: [0, 0.3] },
{ id: "in2", type: EAnchorType.INPUT, point: [0, 0.7] },
{ id: "out", type: EAnchorType.OUTPUT, point: [1, 0.5] }
]
},
{
id: "block2",
is: ROUNDED_BLOCK_TYPE,
x: 400,
y: 200,
width: 200,
height: 150,
label: "Rounded Block 2",
selected: false,
anchors: [
{ id: "in", type: EAnchorType.INPUT, point: [0, 0.5] },
{ id: "out1", type: EAnchorType.OUTPUT, point: [1, 0.3] },
{ id: "out2", type: EAnchorType.OUTPUT, point: [1, 0.7] }
]
}
],
connections: [
{
id: "conn1",
from: { block: "block1", anchor: "out" },
to: { block: "block2", anchor: "in" }
}
],
settings: {
blockComponents: {
// Register the custom block implementation
[ROUNDED_BLOCK_TYPE]: RoundedBlock
}
}
};
// 4. Create the graph with the custom blocks
const graph = new Graph(customBlockGraphConfig, document.getElementById("graph-container"));When creating custom block implementations, follow these best practices:
-
Respect Camera Scale:
// Scale line widths based on camera zoom ctx.lineWidth = Math.round(2 / this.context.camera.getCameraScale());
-
Use Theme Colors:
// Use colors from graph theme ctx.fillStyle = this.context.colors.block.background; ctx.strokeStyle = this.context.colors.block.border;
-
Handle Selection State:
if (this.state.selected) { // Custom selection visualization this.renderStroke(this.context.colors.block.selectedBorder); }
-
Optimize Rendering:
// Only render text when needed if (this.shouldRenderText && this.context.camera.getCameraScale() > 0.5) { this.renderText(this.state.label); }
-
Don't Transform Coordinates:
// Incorrect - don't transform coordinates manually // const worldX = originalX * transform.scale + transform.x; // Correct - use original coordinates ctx.fillRect(this.state.x, this.state.y, this.state.width, this.state.height);
-
Consider Performance:
// Optimize for performance with complex custom blocks protected willRender() { // Only update complex calculations when needed if (this.needsRecalculation) { this.calculateGeometry(); this.needsRecalculation = false; } }
-
Support Different View Modes:
// Implement both schematic and detailed views public renderSchematicView(ctx: CanvasRenderingContext2D) { // Simplified rendering for schematic view } public renderDetailedView(ctx: CanvasRenderingContext2D) { // More detailed rendering for close-up view }
The Block component provides lifecycle methods to respond to changes in state and props. By overriding these methods, custom blocks can optimize rendering and perform specific actions when data changes.
class OptimizedBlock extends Block {
protected stateChanged(nextState: TBlock): void {
// Check what changed in the state
if (nextState.selected !== this.state.selected) {
// Handle selection state change
console.log(`Block ${this.state.id} selection changed to ${nextState.selected}`);
// Raise block z-index when selected
this.zIndex = nextState.selected ?
this.context.constants.block.DEFAULT_Z_INDEX + 1 :
this.context.constants.block.DEFAULT_Z_INDEX;
}
if (nextState.label !== this.state.label) {
// Handle label change
console.log(`Block label changed from "${this.state.label}" to "${nextState.label}"`);
// Only trigger a render if text-related properties change
this.shouldRender = true;
} else if (nextState.x !== this.state.x || nextState.y !== this.state.y) {
// Handle position change
console.log(`Block position changed to (${nextState.x}, ${nextState.y})`);
// Update hit box for interaction
// this.updateHitBox(nextState); // TODO: Implement updateHitBox
} else {
// Skip render for other property changes
this.shouldRender = false;
}
// Always call parent method after your custom logic
super.stateChanged(nextState);
}class OptimizedBlock extends Block {
// Override shouldRender to customize rendering behavior
protected shouldRender(nextState: TBlock, nextProps: TBlockProps): boolean {
// Check if relevant properties have changed
if (nextState.x !== this.state.x || nextState.y !== this.state.y) {
// Position changed, need to render
return true;
}
if (nextProps.font !== this.props.font) {
// Font changed, need to render
return true;
}
// No relevant changes, skip rendering
return false;
}
}For complex blocks with many properties, you can use advanced state diffing techniques to optimize rendering performance. This involves comparing the current and next state to identify only the properties that have changed and trigger a render only when necessary.
import { isEqual } from 'lodash';
class OptimizedBlock extends Block {
protected stateChanged(nextState: TBlock): void {
// Compare relevant properties using lodash's isEqual
if (!isEqual(nextState.style, this.state.style)) {
// Style changed, need to render
this.shouldRender = true;
} else if (!isEqual(nextState.data, this.state.data)) {
// Data changed, need to render
this.shouldRender = true;
} else {
// No relevant changes, skip rendering
this.shouldRender = false;
}
// Always call parent method after your custom logic
super.stateChanged(nextState);
}
}By implementing these techniques, you can create custom block implementations that are both flexible and performant.