Redefining Broadway ticketing: From static charts to interactive zones

Understanding Broadway ticket sales at Headout

Broadway theaters are immensely popular in the New York region, offering live entertainment shows throughout the year. Each theater has unique seating arrangements, with different selling zones, pricing, and availability. At Headout, we sell these theater tickets and present seating charts in image format to help users visualise their options before purchasing. The screenshot below illustrates how we present these options to users, on the left side there are zones and on the right side there is a seating chart.

Zones & seating Chart

The Challenge: A confusing purchase experience

Theater seating arrangements are similar to cinema halls, where users expect to select individual seats. Imagine booking a movie ticket, but instead of picking a specific seat, you're told to just choose a “zone.” No seat map. No view preview. Just a vague label like “Mezzanine Center R-S” as we see in Screenshot 1. That’s the default Broadway experience—and it’s where most users drop while booking.

Adding to the complexity, these zones frequently change as suppliers adjust them either by splitting, merging or disabling zones to optimize business. To make matters more significant, each ticket costs $150 on an average, and from a user’s perspective, they want to carefully assess all available zones and make informed decision based on pricing and view from the zone to ensure the best possible experience.

At Headout, we already facilitates zone-based bookings for other products by manually creating seating charts, mapping zones with sections in image and assist users as their zones don’t change often. Below is an example of non-broadway product showing fully interactive zones where "Bronze" is mapped to highlighted section in seating chart

Interactive seating chart

This isn’t possible with the Broadway seating chart as the zones change very frequently. The user experience is not ideal, and as expected, most users dropped off during section selection.

Solution: Turning theater layout into seating charts

In Screenshot 1: Zones and seating chart, zones are labeled based on their directional orientation and row number within a section. For instance, a zone like "Front Orchestra" suggests a front-rows area in the Orchestra, whereas "Left Orchestra Row A" points to a left area of Row A of Orchestra section. This naming convention helps users quickly associate the ones with seating areas

While this process could be replicated programmatically, It’s hard to do because the seating chart is an SVG image and programmatically demarcating these zones with precise boundaries is challenging.

Then came the realization: we already had the theater layouts. Since these are popular theaters/shows, their seating arrangements are publicly available. We don't use it for selecting seats during booking as our suppliers don’t support it. But we can utilize this layout to form a new seating chart as demarcating can be easily done based on number of seats. Below is a screenshot of the theater layout, corresponding to the seating chart shown in Screenshot 1.

Theater layout

This was our 'Eureka' moment where we realised it’s possible to map zone names in the theater layout like we have it for other products as shown in Screenshot 2. The missing link? Understanding how we naturally interpret these zones—and translating that intuition into algorithms. Let’s see how we did this.

P.S - What we’re about to share wasn’t meticulously planned from day one—it’s the outcome of trial, error, and heavy reverse engineering over time.Consider it a story in evolution, not a blueprint.

Step 1: Decoding the messy section names

With the layout in place, our next challenge was interpreting the zone labels—often a mix of human-friendly descriptions and machine-unfriendly ambiguity. Let's consider the fourth zone in sequential order in Screenshot 1.

Mezzanine Center H-K, Far side C-D, Side G

Its a combination of various sub-zones and there’s a lot of contextual inference in the sentence which can be broken down into three distinct sub-zones for better clarity.

  • Mezzanine Center (Rows H-K)
  • Mezzanine Far Sides (Rows C-D)
  • Mezzanine Sides (Row G)

Imagine the frustration a user would experience while trying to decipher these section names. Clearly, traditional algorithms can not directly parse this information. So we have written an AI prompt that just works


Prompt: `Given a list of seating section labels for a Broadway show, return a list that splits 
all the different seating sections with a '/' in the same string. 

- Ensure the input and output list sizes are equal.  
- Standardize section names (e.g., mezz → Mezzanine, orch → Orchestra).  
- Standardize directional names (e.g., fr → Front).  
- Each split section must retain a parent section name (Mezzanine, Orchestra, Balcony, etc.).  
- Do not allow sections with only a direction (e.g., 'Front' should be 'Front Orchestra').  
- Do not provide explanations or conversational text. The output should only be the expected list.`

Snippet 1: AI Prompt


Input: ["Mezzanine Center H-K, Far side C-D, Side G"]
Output: ["Mezzanine Center Rows H-K/Mezzanine Far Sides Rows C-D/Mezzanine Sides Row G"]

Snippet 2: Standardised zone names

With this prompt we are able to define independent sub zones for each of the zone as we see in Snippet 2. To further standardize zones, we created a directional keyword map (see Snippet 3) which will later help us to keyword comparison.

section_list = {
    "left":       ["sides", "side", "left"],
    "right":      ["right", "side", "sides"],
    "center":     ["center"],
    "top":        ["front", "top", "first"],
    "bottom":     ["rear", "last", "back", "bottom"],
    "mid":        ["mid", "middle"],
    "left_left":   ["far", "extreme"],
    "right_right": ["far", "extreme"],
    "left_right":   ["near"],
    "right_left":   ["near"],
    "centercenter": ["median"]
}

Snippet 3: Keywords map


Using this directional map and applying logical transformations, we standardised the data into what we started calling "directional tokens."

Mezzanine Center Rows H-K -> { horizontal: ["center"], rows: "H-K" }
Mezzanine Far Sides Rows C-D -> { horizontal: ["left_left", "right_right"], rows: "C-D" }
Mezzanine Sides Row G -> { horizontal: ["left", "right"], rows: "C-D" }

Snippet 4: Directional tokens of zones

These are the directional tokens that we are able to generate for each zone. That's it! We'll revisit this later, but for now, we've sorted out the zone names. Now, let's shift our focus back to the theater’s seating chart.

Step 2: Identifying physical clusters in Broadway theaters

From our analysis, we noticed a pattern:

  • Each theater layout is divided into sections (like Orchestra or Mezzanine)
  • Each section contains physical clusters of seats

You can see this clearly in Screenshot 4—a dissected and labeled version of the original layout.

Section and clusters

In this particular layout, we have two sections Orchestra and Mezzanine and the Orchestra section, is physically divided into 3 clusters. So our job is to find exiting sections, clusters and its seats. Fortunately section classification is already there in theater layout through SVG attribute markers, as it is an SVG-based image. We ran the popular clustering algorithm DBSCAN, to find the existing clusters for each section.This was essential because most zone names incorporate directional references (e.g., left, right, center for horizontal splits). We needed a method to classify clusters accordingly, as they naturally align with these divisions in most cases.

While clusters could be manually labeled within the SVG, this would require reprocessing each SVG whenever a new theater or layout is introduced. Since we could programatically find the clusters, there was no need for a manual approach.

Below is the output of clusters we generated for the Orchestra section of the theater layout.

{
    "section": {
        "id": "Orchestra",
        "clusters": [
            {
                "id": 101,
                "seats": [
                    "Row E": ["E1", "E2", "E3", "E4"],
                    "Row F": ["F1", "F2",..... "F6"],
                    ....
                    ....
                    "Row T": ["T1", "T2",.... "T11"] 
                ]
            }, {
                "id": 102,
                "seats": [
                    "Row C": ["C1", "C2",......, "C12"],  
                    "Row D": ["D1", "D2",......, "D14"]
                     ....
                     ....
                     "Row W": ["T12", "T13",.... "T26"]  
                ]
            }, {
                "id": 102,
                "seats": [
                    "Row E": ["E20", "E21","E22", "E23"],  
                    "Row F": ["F22", "F23",......, "F27"]
                     ....
                     ....
                     "Row T": ["T27", "T28",.... "T38"]  
                ]
            }
        ]
    }
}

Snippet 5: Section and physical clusters

Once clusters are identified for each section, the next step is to determine their directional orientation horizontally and vertically relative to the overall layout. For this, we compute the centroid of each cluster and additionally determine the centroid of the overall cluster group. We then sort the clusters, determine the number of clusters along each axis/direction, and assign labels based on the following rules.

  • Fewer Clusters in a section (single or at most two clusters):
    • Fewer clusters along X-axis (horizontal split): Since all rows and columns are stored in sorted order, we traverse each row of all the clusters, count the total columns, and divide each row into three equal parts left, center, right accordingly. Physical division of clusters didn’t help as number of clusters is less than number of divisions.
    • Fewer clusters along Y-axis (vertical split): Similarly, since rows are in sorted order, we divide them into three parts and mark them as vertical divisions(top, mid, bottom).
  • Multiple Clusters in a section (more than two):
    • We classify all clusters based on their relative position to the centroid in each direction: top, mid, bottom for vertical direction and left, center, right for horizontal direction. If there is an even number of clusters, we designate two as the middle ones. Fortunately, in our use cases multiple clusters appear in only one direction at a time, simplifying the need to accomodate other test cases.

This process restructures the data as shown in the code snippet below. Essentially, we transition from cluster-based partitions to direction-based partitions.

{
    "section": {
        "id": "Orchestra",
        "clusters": [
            {
                "id": "left",
                "seats": [
                     "seats": [
                    "Row E": ["E1", "E2", "E3", "E4"],
                    "Row F": ["F1", "F2",..... "F6"],
                    ....
                    ....
                    "Row T": ["T1", "T2",.... "T11"] 
                ]
            }, {
                "id": "center",
                "seats": [
                    "Row C": ["C1", "C2",......, "C12"],  
                    "Row D": ["D1", "D2",......, "D14"]
                     ....
                     ....
                     "Row W": ["W1", "W2",.... "W16"]  
                ]
            }, {
                "id": "right",
                .....
            },
            {
                "id": "top",
                "seats": [
                    "Row C": ["C1", "C2",......, "C12"],  
                    "Row D": ["D1", "D2",......, "D14"],
                    "Row E": ["E1", "E2",......., "E23"]
                     ....
                     ....
                     "Row W": ["W1", "W2",.... "W16"]  
                ]
            },
            ..so on...
        ]
    }
}

Snippet 6: Cluster classification

Once we have seats divided into three parts for each section and direction, we further need to split into two. This is relatively easy, we split each partition into two equal halves since the seats are already sorted in both horizontal and vertical coordinates. Below is the snippet on how we are trying to split the data

{
    "section": {
        "id": "Orchestra",
        "clusters": [
            {
                "id": "left_left",
                "seats": [
                    "Row E": ["E1", "E2"],
                    "Row F": ["F1", "F2","F3"],
                    ....
                    ....
                    "Row T": ["T1", "T2",.... "T5"] 
                ]
            },{
                "id": "left_right",
                "seats": [
                    "Row E": ["E3", "E4"],
                    "Row F": ["F4", "F5","F6"],
                    ....
                    ....
                    "Row T": ["T6", "T7",.... "T11"] 
                ]
            },
            {
                "id": "top_top",
                "seats": [
                    "Row C": ["C1", "C2",......, "C12"],  
                    "Row D": ["D1", "D2",......, "D14"],
                    "Row E": ["E1", "E2",......., "E23"]
                ]
            },
        ]
    }
}

(Snippet 7: Cluster classification and division)

Step 3: Identify seats and zones based on common directional tokens

For each available zone/sub-zone we have extracted the directional tokens earlier. Now we will use that directional tokens and seat level directional tokens generated in the last step to find seats for each available zone based on the following rules

A zone contains only horizontal or vertical tokens → Directly retrieve the relevant seats from the classification data based on directional token. If row filters are provided, apply them and return the list of matching seats. Ex -

Mezzanine Center Rows H-K -> { horizontal: ["center"], rows: "H-K" }

We pick all the seats where section is Mezzanine and horizontal identifier is center and then filter rows between H-K

A zone has no directional token → Check if any specific rows are mentioned; if not, iterate through all section seats in the classified data and compile a list of the corresponding seats. This is the case where section is not divided or divided only based on rows. Ex zone - Orchestra row A -D

A zone contains both horizontal and vertical tokens → Extract seats separately from both horizontal and vertical classifications, then identify common seats in both. If row filters are provided, apply them as well

Next, we assign a priority value to each zone based on the depth of its labels. Zones with more detailed directional splits are given higher priority Ex - Mezzanine Far Sides will get a higher priority than Mezzanine sides. This is to ensure whenever there is an overlap on z-index they are displayed first on z index when generating the final SVG layout. This ordering makes sure each of the zone is interactive in final SVG even if it’s subset or overlapping of other zone.Below is the code snippet on how we are calculating the value.

priority_value = max(  
    sum(len(s) for s.split(
    '_') in vertical_split + horizontal_split),  
    priority_value  
)

This is the sample output we will get from this above step after transformations

[
    {
        "zone": "Mezzanine Center H-K, Far Side C-D, Side G",
        "value": [
            {
                "cx": 292.0,
                "cy": 769.0
            },
            {
                "cx": 344.0,
                "cy": 770.0
            }
        ], "priority": 4
    }
]
// here cx, cy are the x and y coordinates of seats

Snippet 8: Zone seats with priority

Step 4: From data to visual representation: Building the final SVG

Clustering seat data for accurate zone mapping

With seats categorized, we now needed to visualize this data. Since zones are often physically disconnected, we applied DBSCAN again to group seats into contiguous areas before mapping them onto the SVG layout. Ex - For the zone “Mezzanine Center H-K, Far side C-D, Side G” , these are the different clusters

Clustering zone seats

Creating shapes using concave hulls

The next challenge was linking the identified points to construct a shape for different clusters. After extensive trial and error, we adopted a combination of Delaunay Triangulation and Unary Union. The process begins with Delaunay Triangulation, which creates a triangle mesh ensuring no point falls inside another triangle’s circumcircle. We then extract all edges, store them in a set, and calculate their lengths. Edges exceeding a specific threshold (defined as alpha times the mean length) are filtered out as outliers. The remaining edges are transformed into LineString objects, unified, and polygonized to form the final shape.

This method is often referred to as the indirect concave hull algorithm. While we won’t delve into the intricate mathematical details, our implementation primarily relied on iterative experimentation on available implementations on web to achieve most accurate boundaries of theater layout and zones.

Finally, we’ll need to repeat both of the above steps using all seats in the theater—regardless of their zone assignments—to generate the base shape of the entire theater. In the next step, we’ll explore why this base shape is important. Here’s what the base shape looks like:

Base theater shape

Overlaying seating zones on the final map

We encapsulated all previously generated shapes of different zones, taking priority into account on the base shape. Base shape was important as some seats might not come under any zone or available zones and just encapsulating these zones could distort the overall shape of the theater. We then added section labels and other minor details to the SVG which becomes our new interactive seating chart with zone mappings. This is how a new interactive seating chart looks like

Interactive seating chart & zones

What’s next? Fine-tuning for accuracy

With our solution we drastically improved the user experience, but with one key assumption that for directional labels we divided section into equal parts or used physical clusters. Before this our users might also be doing the same but an interesting question remains: Do our suppliers think about zones in the same way? May be not !! They might be optimising it for their business needs

In case I didn't mention it earlier, users can view their actual seats after reserving a zone and before finalizing their booking on Headout. We started collecting this data to match our predicted zones and directions—if they match, great; if not, we continue refining our dataset to enhance accuracy over time. We’re currently collecting data and don’t yet have accuracy metrics to share.

It's important to note that we cannot rely solely on this data, as it won't cover the majority of seats, and available zones frequently change. Therefore, maintaining the initial clustering was essential.

Redefining Broadway ticketing: A new beginning

Our solution not only transformed the way Broadway tickets are sold but also significantly enhanced the user experience. By leveraging structured thinking, clustering algorithms, and generative AI, we bridged the gap between static seating charts and interactive zone selection.

This approach resulted in a 19% increase in users completing their zone selections and placing order and a similar 8% rise on mobile web, proving that thoughtful UX improvements can drive real business impact. More importantly, it empowered users with better visibility, ensuring they feel confident in their purchasing decisions.

But this is just the beginning. As Broadway theaters evolve and suppliers refine their zoning strategies, we will continue iterating on our models, optimising for accuracy, and pushing the boundaries of data-driven UX design. Because at the heart of it all, ticketing should be as seamless and immersive as the Broadway experience itself.

Dive into more stories