picjs

picjs Guide

This guide takes you from first diagram to advanced features. Each section builds on the previous.

Getting Started

Browser Setup

Add picjs to your HTML page.

<script src="https://cdn.jsdelivr.net/npm/@strike48/picjs@latest/dist/picjs.umd.js"></script>

Or install as a package:

$ npm i @strike48/picjs

Or self-host by copying picjs.umd.js from the dist folder.

Automatic Code Block Processing

picjs can automatically find and render diagram code blocks, similar to Mermaid:

<pre><code class="language-picjs">
box "Hello"
arrow
box "World"
</code></pre>

<script>
  // Process all picjs code blocks on page load
  picjs.processCodeBlocks();
</script>

The selector defaults to code.picjs, pre > code.language-picjs. Pass a custom selector if needed:

picjs.processCodeBlocks('pre.diagram > code');

Direct API

For programmatic use:

const result = picjs.picjs('box "Hello"; arrow; box "World"');

console.log(result.svg);      // SVG markup string
console.log(result.width);    // Width in pixels
console.log(result.height);   // Height in pixels
console.log(result.isError);  // true if parsing failed

Options:

picjs.picjs(source, {
  cssClass: 'my-diagram',    // Add CSS class to SVG element
  darkMode: true,            // Invert colors for dark backgrounds
  plaintextErrors: true      // Return errors as plain text, not HTML
});

Tutorial

1. Hello World

The simplest diagram:

box "Hello, World!"
box "Hello, World!"

This creates a single box with text centered inside.

2. Multiple Objects

Chain objects with semicolons or newlines:

box "Hello,"
box "World!"
box "Hello," box "World!"

By default, objects flow to the right. Each new object appears to the right of the previous one.

3. Shapes

picjs provides these shape primitives:

Block shapes (have width, height, can contain text):

define show { [ down; $1 $2; $3 ]; move right 50% } down [ right show(box, "a box", "box \"a box\"") show(circle, "a circle", "circle \"a circle\"") show(ellipse, "a ellipse", "ellipse \"a ellipse\"") show(oval, "a oval", "oval \"a oval\"") ] move down .3 [ right show(cylinder, "a cylinder", "cylinder \"a cylinder\"") show(diamond, "a diamond", "diamond \"a diamond\"") show(file, "a file", "file \"a file\"") ]

Line shapes (connect points):

S: box wid 2 ht 2 invis
line from S.nw to S.ne
spline color red from S.ne to \
       S.ne+(.5,-.5) to \
       S.se - (.5,-.5) to \
       S.se
arrow color green from S.se to S.sw
arc color blue from S.sw to S.nw
line dotted from S.n to S.s color pink
line thin dashed from S.sw to S.ne
S: box wid 2 ht 2 invis line from S.nw to S.ne spline color red from S.ne to \ S.ne+(.5,-.5) to \ S.se - (.5,-.5) to \ S.se arrow color green from S.se to S.sw arc color blue from S.sw to S.nw line dotted from S.n to S.s color pink line thin dashed from S.sw to S.ne

Other:

4. Connecting with Lines

Use arrow and line to connect shapes:

box "A"
arrow
box "B"
arrow
box "C"
box "A" arrow box "B" arrow box "C"

Arrow variants:

5. Direction

The default direction is right. Change it with direction statements:

right
box "A"
arrow
box "B"
down
arrow
box "C"
left
arrow
box "D"
up
arrow
right box "A" arrow box "B" down arrow box "C" left arrow box "D" up arrow

Directions: right, down, left, up

6. Positioning

Relative positioning with compass points

Every object has named points:

S: box wid 1.4 ht 1.4 fill 0xf0f0f0 color lightgrey thin

dot at S.s color green
text ".s  .south"  ".bot  .bottom" with .n at last dot.s

dot at S.c  same
text ".c  .center"  with .t at last dot.s

dot at S.n same
text ".n  .north" ".t  .top" with .s at last dot.n

dot at S.ne same
text ".ne" with .sw at last dot.ne

dot at S.e same
text ".e" ".east" ".right" with .w at last dot.e

dot at S.se same
text ".se" with .nw at last dot.se

dot at S.sw same
text ".sw" with .ne at last dot.sw

dot at S.w same
text ".w" ".west" ".left" with .e at last dot.w

dot at S.nw same
text ".nw" with .se at last dot.nw
S: box wid 1.4 ht 1.4 fill 0xf0f0f0 color lightgrey thin dot at S.s color green text ".s .south" ".bot .bottom" with .n at last dot.s dot at S.c same text ".c .center" with .t at last dot.s dot at S.n same text ".n .north" ".t .top" with .s at last dot.n dot at S.ne same text ".ne" with .sw at last dot.ne dot at S.e same text ".e" ".east" ".right" with .w at last dot.e dot at S.se same text ".se" with .nw at last dot.se dot at S.sw same text ".sw" with .ne at last dot.sw dot at S.w same text ".w" ".west" ".left" with .e at last dot.w dot at S.nw same text ".nw" with .se at last dot.nw

Lines also have .start and .end.

Reference these points to connect objects precisely:

A: box "A"
B: box "B" at 1 right of A
arrow from A.e to B.w
A: box "A" B: box "B" at 1 right of A arrow from A.e to B.w

Absolute positioning

Use at with coordinates:

box "Origin" at 0,0
box "Right" at 1,0
box "Up" at 0,1
box "Origin" at 0,0 box "Right" at 1,0 box "Up" at 0,1

Relative to other objects

A: box "A"
box "B" at 1 right of A
box "C" at 0.5 below A
box "D" at 1 ne of A
A: box "A" box "B" at 1 right of A box "C" at 0.5 below A box "D" at 1 ne of A

7. Labels

Give objects names (labels) for later reference:

Start: box "Start"
arrow
Process: box "Process"
arrow from Process.s down
End: box "End"
arrow from Start.s to End.w
Start: box "Start" arrow Process: box "Process" arrow from Process.s down End: box "End" arrow from Start.s to End.w

Labels must start with an uppercase letter.

8. Sizing

Set dimensions with width/wid, height/ht, radius/rad:

box "Wide" width 2
box "Tall" height 1.5
box "Rounded" rad 0.2
circle "Big" radius 0.5
box "Wide" width 2 box "Tall" height 1.5 box "Rounded" rad 0.2 circle "Big" radius 0.5

Use percentages to scale relative to defaults:

box "150% wide" width 150%
arrow right 200%
box "Normal"
box "150% wide" width 150% arrow right 200% box "Normal"

The fit keyword sizes a shape to fit its text:

box "Short" fit
box "A much longer label" fit
box "Short" fit box "A much longer label" fit

9. Colors and Styling

Colors

box "Red outline" color red
box "Blue fill" fill blue
box "Both" color white fill darkgreen
box "Red outline" color red box "Blue fill" fill blue box "Both" color white fill darkgreen

148 named colors are supported (HTML/CSS color names).

You can also create colors programmatically:

box "RGB" fill rgb(255, 128, 0)
box "HSL" fill hsl(210, 80, 60)
box "OKLCH" fill oklch(70, 0.15, 150)
box "RGB" fill rgb(255, 128, 0) box "HSL" fill hsl(210, 80, 60) box "OKLCH" fill oklch(70, 0.15, 150)

Line styles

line "solid" right
line "dashed" dashed right
line "dotted" dotted right
box dashed
box dotted
line "solid" right line "dashed" dashed right line "dotted" dotted right box dashed box dotted

Thickness

line thick right 1
line thin right 1
line thickness 0.05 right 1
line thick right 1 line thin right 1 line thickness 0.05 right 1

10. Text Formatting

Position

box "above" above "center" "below" below
box "above" above "center" "below" below

Alignment

box "left" ljust "center" "right" rjust fit
box "left" ljust "center" "right" rjust fit

Style

box "bold" bold
box "italic" italic
box "mono" mono
box "big" big
box "small" small
box "bold" bold box "italic" italic box "mono" mono box "big" big box "small" small

Combine them:

box "Bold Italic" bold italic fit
box "Bold Italic" bold italic fit

Command Line

The script in bib/picjs will take a markdown file containing picjs code blocks and generate SVG files (by default in a _diagrams directory. It then moved the original code block into an HTML comment and embed the corresponding diagram.

If the diagram in the comment is subsequently changed, the tool will regenerate the corresponding diagram.

READMEs

The picjs tool was written to allow diagrams to be added to GitHub READMEs, which don't allow scipts to be run.

Just run npm cli README.md, add _diagrams to your repo, and check in.

Alternatively, have a look at this projects .github/workflows for an action that does this for you on commit.

Advanced Topics

How picjs Works

picjs is a single-pass layout engine:

  1. Parse statements left to right
  2. Build objects with default positions
  3. Resolve constraints (at, from, to, with)
  4. Render to SVG

The key insight: each object's position is determined by constraints relative to previous objects. This is why order matters.

The Position Model

Current position: picjs tracks a "current position" that advances as objects are created.

Default placement: New objects appear at the current position, offset in the current direction.

Explicit placement: Use at to override default placement.

Connection points: from and to connect lines to specific points on objects.

Sublists (Grouping)

Group objects with [ ... ]:

[
  box "A"
  arrow
  box "B"
]
arrow
[
  down
  box "C"
  box "D"
]
[ box "A" arrow box "B" ] arrow [ down box "C" box "D" ]

A sublist acts as a single object with its own bounding box. Use it to:

Variables

Store values in variables (lowercase names or starting with $ or @):

$gap = 0.5
box "A"
move right $gap
box "B"
move right $gap
box "C"
$gap = 0.5 box "A" move right $gap box "B" move right $gap box "C"

Assignment operators: =, +=, -=, *=, /=

Mathematical constants

Use $pi and $2pi for trigonometric calculations:

for angle from 0 to $2pi step 0.3 do {
    dot at (0.8*cos(angle), 0.8*sin(angle))
}
for angle from 0 to $2pi step 0.3 do { dot at (0.8*cos(angle), 0.8*sin(angle)) }

For working with degrees instead of radians, use d2r() and r2d():

for deg from 0 to 360 step 30 do {
    dot at (0.8*cos(d2r(deg)), 0.8*sin(d2r(deg)))
}
for deg from 0 to 360 step 30 do { dot at (0.8*cos(d2r(deg)), 0.8*sin(d2r(deg))) }

Built-in variables

Built-in variables control defaults:

boxwid = 1.5
boxht = 0.75
box "Wider and shorter"
boxwid = 1.5 boxht = 0.75 box "Wider and shorter"

String Interpolation

Embed expressions in strings with ${...}:

$n = 42
box "Value: ${$n}"

A: box "A"
box "Width: ${A.wid}"
$n = 42 box "Value: ${$n}" A: box "A" box "Width: ${A.wid}"

Macros

Define reusable patterns:

define roundbox { box rad 0.15 $1 }

roundbox("First")
arrow
roundbox("Second")
arrow
roundbox("Third")
define roundbox { box rad 0.15 $1 } roundbox("First") arrow roundbox("Second") arrow roundbox("Third")

The $1, $2, etc. are positional parameters.

For Loops

for i from 1 to 3 do {
  box "Box ${i}"
  arrow
}
for i from 1 to 3 do { box "Box ${i}" arrow }

With step:

for i from 5 to 10 step 2 do {
  box "${i}"
  move
}
for i from 5 to 10 step 2 do { box "${i}" move }

Chop

The chop keyword shortens lines so they don't overlap shapes:

A: circle "A"
B: circle "B" at 2 right of A
arrow from A to B chop
A: circle "A" B: circle "B" at 2 right of A arrow from A to B chop

Without chop, the arrow would extend into the circles.

The same Keyword

Copy attributes from a previous object:

box "Template" color blue fill lightblue rad 0.1 fit
box "Copy 1" same
box "Copy 2" same
box "Template" color blue fill lightblue rad 0.1 fit box "Copy 1" same box "Copy 2" same

Or reference a specific object:

A: box "A" color red
B: box "B" color blue
box "Like A" same as A
A: box "A" color red B: box "B" color blue box "Like A" same as A

Invisible Objects

Use invis or invisible to create spacing without visible output:

box "Visible"
box invis
box "Also visible"
box "Visible" box invis box "Also visible"

Useful for alignment and layout control.


Tips

  1. Start simple: Get basic layout working before adding styling
  2. Use labels: Name important objects for easier reference
  3. Use fit: Let picjs size boxes to fit text
  4. Use variables: Store repeated values
  5. Use sublists: Group related objects
  6. Test incrementally: Add one thing at a time

Next Steps