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:
Category: A type of seat-
Zone: A part of the seating plan, e.g. a room area-
Row: A collection of seats that belong togetherSeat: An individual seat that will be drawn
Area: A shape (e.g. a rectangle, polygon or circle) that represents a special location, like the stage.
-
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:
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.