Skip to main content

Technical Brief

Pintora builds a simplified tool chain for diagrammers from DSL-parsing to diagram-drawing through sensible layering and abstraction.

Workflow and data

Pintora's workflow and data are shown below.

 Input Text
|
| () IDiagramParser
v
DiagramIR
|
| () IDiagramArtist
v
GraphicsIR
|
| () IRenderer
v
Output

IR stands for Intermediate Representation and represents the different phases of the processing.

  • DiagramIR represents the logical data for a specific diagram type, relevant to the textual DSL of the diagram
  • GraphicsIR is the visual repesentation format provided by Pintora

IDiagramParser and DiagramIR

The role of the IDiagramParser is to convert the textual DSL of the diagram into logical data, preparing the ground for the subsequent construction of the visual elements.

For example, here is the logical data that corresponds to Pintora's built-in Entity Relationship Diagram.

export type ErDiagramIR = {
entities: Record<string, Entity>
relationships: Relationship[]
}

export type Attribute = {
attributeType: string
attributeName: string
attributeKey?: string
}

export type Entity = {
attributes: Attribute[]
}

export type Relationship = {
entityA: string
roleA: string
entityB: string
relSpec: RelSpec
}

export type RelSpec = {
cardA: Cardinality
cardB: Cardinality
relType: Identification
}

Any parser tool and technique can be used to implement this process, from line-by-line parsing of regular expressions to programs generated by various parser generators, as long as they can be run in a JS environment.

Pintora's built-in diagrams use nearley.js to generate context-independent syntax parsers that are easy to use, based on an improved Earley algorithm, it has decent performance (though perhaps relatively slow - in worst case - of the mainstream solutions, but perfectly adequate for small text diagram DSLs) and a pretty small runtime. Diagram authors can choose between an efficient parser generator solution such as jison / PEG.js, or a handwritten parser.

IDiagramArtist and GraphicsIR

IDiagramArtist converts diagram logic data into visual description data GraphicsIR, which provides input to the IRenderer for different platforms later.

The main parts of GraphicsIR are

  • rootMark, which must be a Group type mark, is the root element of the diagram, and all other elements are its children
  • width and height that describe the overall width and height of the diagram
  • an optional bgColor for the diagram's background color
export type Mark = Group | Rect | Circle | Ellipse | Text | Line | PolyLine | Polygon | Marker | Path | GSymbol

export interface GraphicsIR {
mark: Mark
width: number
height: number
bgColor?: string
}

Pintora abstracts visual elements into different types of marks. A collection of attributes attrs is used to describe the characteristics of the marks, some (e.g. x and y) are common attributes, while each type of mark has its specific attributes (e.g. path for the Path mark).

In addition to attrs, there are also special fields on the tags that describe other behaviors. For example, matrix for describing visual transformations, or children specific to Group.

export interface IMark {
attrs?: MarkAttrs
class?: string
/** for transform */
matrix?: Matrix | number[]
}

export interface Group extends IMark {
type: 'group'
children: Mark[]
}

export interface Circle extends IMark {
type: 'circle'
attrs: MarkAttrs & {
x: number
y: number
r: number
}
}

/**
* Common mark attrs, borrowed from @antv/g
*/
export type MarkAttrs = {
x?: number
y?: number
/** radius of circle */
r?: number
/** stroke color */
stroke?: ColorType
/** fill color */
fill?: ColorType
opacity?: number
lineWidth?: number
...
}

You can find the full GraphicsIR definition in pintora's source code.

Pintora's rendering layer currently uses antv/g and can output both canvas and svg formats. So GraphicsIR is currently defined in much the same way as antv/g, and you will also find many terms similar to SVG definitions.

To build a complete visual representation of a diagram, the artist needs to do several things, including generating various markers, specifying colors, calculating layout-related data, etc., so the amount of code is usually the largest part of the diagram implementation.

IDiagram and diagramRegistry

IDiagram is a fully defined interface to a diagram. After an object that implements this interface registers itself into the diagram collection diagramRegistry, Pintora can recognize and process the input text of the diagram description and turn it into a specific image output.

export interface IDiagram<D = any, Config = any> {
/**
* A pattern used to detect if the input text should be handled by this diagram.
* @example /^\s*sequenceDiagram/
*/
pattern: RegExp
parser: IDiagramParser<D>
artist: IDiagramArtist<D, Config>
configKey?: string
clear(): void
}

export type ParseContext = {
preContent?: string
}

/**
* Parse input text to DiagramIR
*/
export interface IDiagramParser<D> {
parse(text: string, context?: ParseContext): D
}

/**
* Convert DiagramIR to GraphicsIR
*/
export interface IDiagramArtist<D, Config = any> {
draw(diagramIR: D, config?: Config): GraphicsIR
}

To register a new type of diagram:

import { IDiagram } from '@pintora/core'
import pintora from '@pintora/standalone'

const diagramDefinition: IDiagram = { ... }

pintora.diagramRegistry.registerDiagram(diagramDefinition)

Some other details

Text layout

Pintora uses canvas.measureText to calculate the layout parameters for text, and uses jsdom and its underlying dependency node-canvas to do this on the Node.js side.

Layout libraries

For some diagram types, it is not easy to compute layouts that satisfy the logical properties of the diagram, but are also readable and aesthetically pleasing. Inspired by the Mermaid.js' implementation, Pintora maintains a fork of dagrejs/dagre - @pintora/dagre.