Creating seating plans

Welcome to the documentation of pretix' seating plan editor. Currently, we don't have a point-and-click editor, and manually writing out the JSON format that pretix uses to import seating plans would be so much tedious work that it's virtually impossible.

Instead, we created an easy way to write JavaScript code that generates the JSON structure programatically. Using the control flow tools a programing language gives you, it's easy to quickly model large plans, because you can loop and re-use lots of things. Our web-based editor gives you instant feedback on the result, which shortens the feedback loop and allows you to iterate very quickly. However, this approach requires you to have some basic JavaScript knowledge to proceed.

All code examples in this documentation are interactive and you are welcome to play around. Note, though, that all changes will not be persisted and everything will be lost when you close or reload this page. If you build something pretty, make sure to store the source code somewhere else!

If you work in the actual editor, your current content will be stored to your browser's local storage. Therefore, you don't need to worry about losing your input when you close your browser.

The editor

The editor shows a screen divided into four sections.

The top-left section is your code editor. It's the only part where you can actually modify things and where you input your code.

The top-right section is the live preview of your plan. You can press the Ctrl key and scroll to zoom in and you can move around by dragging the plan with your mouse.

The bottom-left section is the live preview of the JSON representation of the plan. This is what you'll need to copy when you want to import the plan into pretix. However, we always recommend also storing the code, in case you want to modify the plan later.

The bottom-right section shows the error console as well as some options. You can upload a picture that will be used as a background (only in the editor, not in the actual plan) and change the transparency of both the background and your plan. If your JavaScript code does not run correctly or if the resulting plan violates the specification, you will see error messages here.

First steps

To create a plan with our editor, at the very minimum you need to create a Plan object, assign it a size and return it:

const plan = Frontrow.plan('Sample');
plan.size = {width: 600, height: 600};
plan.getPlan();

Every plan is constructed from a hierarchy of objects. The top-level Plan object currently supports the following sub-objects:

All coordinates are in virtual units and relative to the position of the parent object, e.g. the coordinates of a seat are relative to the coordinates of the row. The origin of the plan itself is in the top-left corner.

Object definitions

The following tables show you the currently implemented properties an object can have.

Category

Property Description
name A human-readable name of the category
color A color code used to draw seats of this category.

Zone

Property Description
name A human-readable name of the zone
position An object of the format {x: …, y: …} with the position of the zone relative to the plan canvas.
rows A list of Row objects
zone_id A list of Area objects

Row

Property Description
row_number A human-readable row number
row_label Human-readable name for the row. May include %s as a placeholder for the row_number value. Not used for rendering the plan, but for describing the seats in text. E.g. „Row %s“.
seat_label Human-readable name for seats in this row. May include %s as a placeholder for the seat_number value. Not used for rendering the plan, but for describing the seats in text. E.g. „Seat %s“.
position An object of the format {x: …, y: …} with the position of the row relative to the zone position.
seats A list of Seat objects
row_number_position Whether to auto-render row numbers visibly. Can be "start", "end", "both", or null.

Seat

Property Description
seat_guid A machine-readable seat ID that is unique in the whole plan
seat_number A human-readable seat number
position An object of the format {x: …, y: …} with the position of the seat relative to the row position.
category The name of a valid Category object
radius The size of the seat (defaults to 10)
start_direction An optional hint for the seat allocation optimizer. For example, if you would like the optimizer to fill up your row from the first seat to the last, set this to > on the first seat. For filling up in the other direction, set this to < on the last seat. You can also set multiple markers, and you can set markers such as <> e.g. on a middle seat. The optimizer will always move seat selections towards the closest marker.

Area

Property Description
color A background color
border_color A border color
position An object of the format {x: …, y: …} with the position of the area relative to the zone.
rotation Rotation angle (around the point specified in position) in degrees clockwise.
shape One of the strings polygon, rectangle, ellipse, circle, or text.
rectangle Object with rectangle options (width and height)
circle Object with circle options (radius)
ellipse Object with ellipse options (radius.x and radius.y)
polygon Object with polygon options (points[i].x and points[i].y)
text Object with text options (text, size, color, position.x, and position.y)

Working with rows

Creating rows

To create a simple block of seats, you could just create a few rows and a few seats in a loop:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});

for (let rowindex of Frontrow.range(10)) {

  let row = floor.addRow({
    row_number: (rowindex + 1).toString(),
    position: {
      x: 25, y: 25 * rowindex + 25
    }
  });

  for (let seatindex of Frontrow.range(15)) {
    row.addSeat({
      seat_number: (seatindex + 1).toString(),
      seat_guid: rowindex + '-' + seatindex,
      position: {
        x: seatindex * 25, y: 0
      },
      category: 'Seat'
    })
  }

}

plan.getPlan();

As you can see, you can manually set the properties of the seats, such as the seat number. This allows you to easily implement even complex numbering logic.

After you created a row, there are a couple of utility functions to make it easier for you to model real-life seating plans.

Rotating a row

You can use the utility function Row.rotate(degrees, [originx, originy]) to rotate a row by a specified number of degrees clock-wise.

By default, the seats are rotated around the origin of the row. If you want to rotate around a different point, you can pass the originx and originy parameters with coordinates relative to the origin of the row.

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')
const floor = plan.addZone({name: 'Ground floor', position: {x: 0, y: 0}});

let row = floor.addRow({
  row_number: "1",
  position: { x: 25, y: 25 }
});

for (let seatindex of Frontrow.range(15)) {
 row.addSeat({
   seat_number: (seatindex + 1).toString(),
   seat_guid: '1-' + seatindex,
   position: { x: seatindex * 25, y: 0 },
    category: 'Seat'
 })
}
row.rotate(30);
plan.getPlan();

Rotating all seats in a zone

Rotating a full block of seats can still be quite hard, since you need to ensure that you rotate all rows around the same point. To make this easier, we added the function Zone.rotateSeats(degrees, [originx, originy]) that rotates all seats within a zone around a point specified relative to the origin of the zone. Note that this only rotates seats, not areas contained in the zone.

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});

for (let rowindex of Frontrow.range(5)) {

  let row = floor.addRow({
    row_number: (rowindex + 1).toString(),
    position: {
      x: 110, y: 25 * rowindex + 30
    }
  });

  for (let seatindex of Frontrow.range(10)) {
    row.addSeat({
      seat_number: (seatindex + 1).toString(),
      seat_guid: rowindex + '-' + seatindex,
      position: {
        x: seatindex * 25, y: 0
      },
      category: 'Seat'
    })
  }

}
floor.rotateSeats(20)

plan.getPlan();

Rotating all areas in a zone

Zone.rotateAreas(degrees, [originx, originy]) rotates all areas within a zone around a point specified relative to the origin of the zone.

Creating seats along an ellipsis

If you have ellipsoidal seats, you can create a regular row and then call the function Row.curveEquispaced(yradius, [xradius, [cutleft, [cutright]]]) to re-shape the row along an ellipsis. The only parameter you need to give is yradius, the radius of the ellipsis in y direction:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});

for (let rowindex of Frontrow.range(8)) {
  let row = floor.addRow({
    row_number: (rowindex + 1).toString(),
    position: {
      x: 25, y: 25 * rowindex + 25
    }
  });

  for (let seatindex of Frontrow.range(15)) {
    row.addSeat({
      seat_number: (seatindex + 1).toString(),
      seat_guid: rowindex + '-' + seatindex,
      position: {
        x: seatindex * 25, y: 0
      },
      category: 'Seat'
    })
  }

  row.curveEquispaced(-30)
}

plan.getPlan();

The radius in x direction (xradius) is taken automatically from the distance between the two outmost seats in row before the modification. However, you may need to specify it manually if you have rows with different numbers of seats but still want the same shape.

By default, the seats will be aligned with equal spacing starting at the outmost pint on the left side until the outmost point on the right side. By specifying the cutleft and cutright parameters, you can move the position of the first or last seat by e.g. cutleft / (number_of_seats + cutleft + cutright). This allows you to further adjust the shape to your desired shape and allows to have partial rows within a set of ellipsized rows.

Note that using cutleft and cutright will make your row narrower in total. Here's a visualization of the geometrical definition:

xradius yradius cutleft

The following example shows how you can combine xradius, cutleft, and cutright to create an ellipse-shaped block with a different number of seats per row.

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});

for (let rowindex of Frontrow.range(8)) {
  let row = floor.addRow({
    row_number: (rowindex + 1).toString(),
    position: {
      x: 25, y: 25 * rowindex + 25
    }
  });

  for (let seatindex of Frontrow.range(15 - rowindex)) {
    row.addSeat({
      seat_number: (seatindex + 1).toString(),
      seat_guid: rowindex + '-' + seatindex,
      position: {
        x: seatindex * 25, y: 0
      },
      category: 'Seat'
    })
  }

  row.curveEquispaced(-30, 175, rowindex, 1)
}

plan.getPlan();

Creating seats along a circle line

You can use the function Row.curveCircular(radius, [centerx, centery, [angle_start, angle_end]]) to re-shape the row along a circle line. If you only pass the parameter radius, the program will automatically search for the circle that passes through the seats that are the most apart and has a certain radius. You can flip the sign of the radius to specify which of the two possible circles should be used. Note that the radius needs to be at least half of the distance between those two seats (otherwise that will be used as the radius).

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});

for (let rowindex of Frontrow.range(8)) {
  let row = floor.addRow({
    row_number: (rowindex + 1).toString(),
    position: {
      x: 25, y: 25 * rowindex + 25
    }
  });

  for (let seatindex of Frontrow.range(15)) {
    row.addSeat({
      seat_number: (seatindex + 1).toString(),
      seat_guid: rowindex + '-' + seatindex,
      position: {
        x: seatindex * 25, y: 0
      },
      category: 'Seat'
    })
  }

  row.curveCircular(290)
}

plan.getPlan();

You can also specify the optional parameters cx and cy to specify the center of the circle yourself instead of heaving it computed. With angle_start and angle_end, you can modify the interval of angles (defaults to the angles corresponding to the positons of the two seats which are most apart – usually 0° and 180° for a horizontal row). This way, you can create circus-style setups with full or partial circles of different radii:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});

for (let rowindex of Frontrow.range(5)) {
  let row = floor.addRow({
    row_number: (rowindex + 1).toString(),
    position: {
      x: 190, y: 25
    }
  });

  for (let seatindex of Frontrow.range(5 + rowindex * 2)) {
    row.addSeat({
      seat_number: (seatindex + 1).toString(),
      seat_guid: rowindex + '-' + seatindex,
      position: {
        x:0, y: 0
      },
      category: 'Seat'
    })
  }

  row.curveCircular(60 + rowindex * 25, 0, 0, 30, 140)
}

plan.getPlan();

Drawing areas

Areas represent shapes that are drawn on the canvas. You can use them to represent the stage, or a general admission area, or whatever else is on the plan. Every area consists of two sub-objects: One shape and one text object. The area itself is positioned relative to the zone and the options of the shape depend on the type of area. The text object is always relative to the position of the area and anchored in the middle of the text, e.g. the text center will be horizontally and vertically aligned to the position you give. If you want to create an area without a text object, you need to just use the empty string.

Rectangles

You can draw a rectangular area with the Zone.addRectangle({config}) function. The position of the area specifies the top-left corner of the rectangle and the height and width options specify the size. Example:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 25, y: 25}
});
floor.addRectangle({
  position: {x: 25, y: 25},
  color: "#990000",
  border_color: "#0000ff",
  width: 200,
  height: 100,
  rotation: 0,
  text: {
    color: "#ffffff",
    text: "Hello world!",
    size: 22,
    position: {x: 100, y: 50}
  }
})
plan.getPlan();
    

Circle

You can draw a circular area with the Zone.addCircle({config}) function. The position of the area specifies the center of the circle and the radius option specifies the size. Example:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 100, y: 100}
});
floor.addCircle({
  position: {x: 100, y: 40},
  color: "#990000",
  border_color: "#0000ff",
  radius: 100,
  rotation: 0,
  text: {
    color: "#ffffff",
    text: "Hello world!",
    position: {x: 0, y: 0}
  }
})
plan.getPlan();
    

Ellipse

You can draw a ellipse area with the Zone.addEllipse({config}) function. The position of the area specifies the center of the ellipse and the radius option specifies the size. Example:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 100, y: 100}
});
floor.addEllipse({
  position: {x: 100, y: 40},
  color: "#990000",
  border_color: "#0000ff",
  radius: {x: 100, y: 50},
  rotation: 0,
  text: {
    color: "#ffffff",
    text: "Hello world!",
    position: {x: 0, y: 0}
  }
})
plan.getPlan();
    

Polygon

You can draw a polygon area with the Zone.addPolygon({config}) function. You can define the nodes of the polygon with the points option relative to the position of the area. Example:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 20, y: 20}
});
floor.addPolygon({
  position: {x: 20, y: 20},
  color: "#990000",
  border_color: "#0000ff",
  rotation: 0,
  points: [
    {x: 10, y: 40},
    {x: 300, y: 10},
    {x: 250, y: 180},
    {x: 220, y: 140},
    {x: 30, y: 190},
  ],
  text: {
    color: "#ffffff",
    text: "Hello world!",
    position: {x: 140, y: 90}
  }
})
plan.getPlan();
    

Text

You can draw a text-only area with the Zone.addText({config}) function.

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 20, y: 20}
});
floor.addText({
  position: {x: 180, y: 120},
  rotation: 0,
  text: {
    color: "#000000",
    text: "Hello world!",
    position: {x: 0, y: 0}
  }
})
plan.getPlan();
    

Utilities

The following utility functions are available for you to use.

block()

The function Frontrow.block(zone, config) allows to quickly create a large number of seats within a block. The first parameter needs to be a Zone object, and the second parameter needs to be an object of options. All of the options are optional, and the supported options are given in the following table. The seats in the block will be assigned a "row index" (ri, starting at 1) and a "seat index" (si, starting at 1) within the row. Many of the options expect a functional callback that will be called with the row/seat index and may return an appropriate value.

Option Description Default
rows Number of rows 5
seats Number of seats per row 10
orientation Orientation of rows, either "horizontal" or "vertical". "horizontal"
x Position within the area 0
y Position within the area 0
gridx Spacing between seats within a row. Tip: Use negative numbers for right-to-left counting of seats. 25
gridy Spacing between adjacent rows. Tip: Use negative numbers for bottom-to-top counting counting of rows. 25
radius A function of (ri, si) that sets the radius of said seat (defaults to 10). ((ri, si) => 10)
category A function of (ri, si) that assigns a category to a seat. ((ri, si) => "?")
row_number A function of (ri) that assigns a human-readable number to a row. ((ri) => ri)
seat_number A function of (ri, si) that assigns a human-readable number to a seat. ((ri, si) => ri)
skip A function of (ri, si) that will prevent the seat from rendering when it returns true. ((ri, si) => false)
xoffset A function of (ri, si) that returns an offset in x-direction for rendering the seat. ((ri, si) => 0)
yoffset A function of (ri, si) that returns an offset in y-direction for rendering the seat. ((ri, si) => 0)
start_direction A function of (ri, si) that optionally returns a start direction flag (see seat object documentation). ((ri, si) => null)
seat_guid A function of (ri, si) that optionally returns a unique guid for each seat. ((ri, si) => null)
label A boolean controlling whether a label with the area name should be rendered below the block.
Can also be a string, that is being output instead of the default zone.name (which is used for seat-GUID and should NOT change when seating plan is in active use).
Object with parameters controlling the label, all of which are optional.
{ size: 20, color: "#666", rotation: 90, text: "Label", offset: {x:0,y:0}, position: {x:0,y:0} }
position is relative to the zone’s position. offset is relative to the text-label’s position; offset is usually used to fine-tune the centered auto position of the label.
false
showrn A value controlling whether a row number should be rendered next to the rows. The allowed values are true, false, "left", "right", "start", and "end". true
alignrn A value controlling whether row numbers should be aligned with the actual ends of the rows (taking skip and offsets into account). false
count_skipped A value controlling whether skipped seats should be subtracted from the input to seat_number. false

Usage example:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});

Frontrow.block(floor, {
  x: 25,
  y: 0,
  rows: 6,
  seats: 11,
  gridx: 25,
  gridy: 25,
  category: (ri, si) => "Seat",
  row_number: (ri) => 6 - ri,
  seat_number: (ri, si) => 12 - si,
  skip: (ri, si) => (si > ri* 2),
  xoffset: (ri, si) => (ri * 5),
  yoffset: (ri, si) => (ri * 5),
  label: true,
  showrn: true,
  alignrn: true,
})

plan.getPlan();

range()

The function Frontrow.range(size, startAt = 0) returns an array with size numbers starting at startAt. Usage example:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});
let row = floor.addRow({
  row_number: "1",
  position: {
    x: 25, y: 25
  }
});
for (let seatindex of Frontrow.range(15)) {
  row.addSeat({
    seat_number: (seatindex + 1).toString(),
    seat_guid: 'ß-' + seatindex,
    position: {
      x: seatindex * 25, y: 0
    },
    category: 'Seat'
  })
}

plan.getPlan();

table()

The function Frontrow.table(zone, number, config) allows to quickly draw a round table. The first parameter needs to be a Zone object, the second parameter needs to be the table number, and the third parameter needs to be an object of options. All of the options are optional, and the supported options are given in the following table. The seats at the table will be assigned a "seat index" (si, starting at 1). Some of the options expect a functional callback that will be called with the row/seat index and may return an appropriate value.

Option Description Default
seats Number of seats at the table 6
x Position within the area 0
y Position within the area 0
r0 Radius of the table 18
r1 Radius of the circle of seats 32
category A function of si that assigns a category to a seat ((si) => "?")
seat_number A function of si that assigns a human-readable number to a seat ((si) => si)
skip A function of si that will prevent the seat from rendering when it returns true ((si) => false)
angle The position of the first seat on the circle (in degrees) ((ri, si) => 0)

Usage example:

const plan = Frontrow.plan('Sample');
plan.size = {width: 400, height: 280};
plan.addCategory('Seat', '#7f4a91')

const floor = plan.addZone({
  name: 'Ground floor',
  position: {x: 0, y: 0}
});

Frontrow.table(floor, "1", {
  x: 50,
  y: 50,
  seats: 6,
  angle: 45,
  r0: 18,
  r1: 32,
  category: (si) => "Seat",
  seat_number: (si) => 12 - si,
})
Frontrow.table(floor, "2", {
  x: 150,
  y: 50,
  seats: 6,
  skip: (si) => (si == 2),
  angle: 45,
  r0: 18,
  r1: 32,
  category: (si) => "Seat",
  seat_number: (si) => 12 - si,
})
Frontrow.table(floor, "3", {
  x: 250,
  y: 80,
  seats: 6,
  skip: (si) => (si >= 5),
  angle: 190,
  r0: 26,
  r1: 44,
  category: (si) => "Seat",
  seat_number: (si) => 12 - si,
})

plan.getPlan();

Editor tricks

Shortcuts

Key Action
Ctrl-F / Cmd-F Start searching
Ctrl-G / Cmd-G Find next
Shift-Ctrl-G / Shift-Cmd-G Find previous
Shift-Ctrl-F / Cmd-Option-F Replace
Shift-Ctrl-R / Shift-Cmd-Option-F Replace all
Alt-G Jump to line

Number manipulation

Whenever your cursor is inside a number value, you can hold down Alt (sometimes also labeled Option) and use your up/down keys to in-/decrease the number by 1. If you want to move it by 10, hold down Alt+Shift. This works with negative numbers as well as expressions/calculations.

Position manipulation

Your code will likely be full of object declarations like {x: 3, y: 15}, because this is how all positions inside the plan are defined.

Whenever your cursor is inside an object with properties x or y, you can hold down Ctrl and use your arrow keys to move the position in any direction by 1 pixel. If you want to move it by 10 pixels, hold down Ctrl+Shift, if you want to move it by 100 pixels, hold down Ctrl+Alt+Shift, and if you want to move it by 0.1 pixels, hold down Ctrl+Alt instead.