---
title: Building a Pack Legend with the Paccurate API
slug: docs/interactive-legend
description: Learn how to create a  box visualization with Paccurate's API. This guide shows you how to build a visual legend that highlights packed items on hover using JavaScript, SVG, and CSS.
icon: 🏗️
docTags: 
createdAt: 2025-06-05T20:06:28.122Z
---

![](https://api.archbee.com/api/optimize/r7zS7wPAexQ28nYzh-Eq7-0dEv7OM5yMds0tWx6VqhX-20250804-181317.png "Legend & Pack Image")

Making a pack request to Paccurate is straightforward: give us some items and some cartons to put them into, and we will cartonize those items. How customers consume the pack response is up to them: save the box assignment back to their ERP system, capture the estimated cost and use it to make a shipping decision, or even render an image of the items in their assigned boxes.

In this post, we’ll be covering how to display a packed carton with an interactive legend of the items contained in the carton. This example will use JavaScript, but the concepts should be applicable to other programming languages.

At a high level, creating the legend takes 4 steps:

1. Make the API request
2. Render the pack [SVG](https://developer.mozilla.org/en-US/docs/Web/SVG)
3. Create the list of boxes & items
4. Add event listeners to the list

Let’s get started. All you need to make this work is an html file (call it legend.html), javascript file (call it legend.js), a text editor (notepad will do!), and a web browser (If you’re feeling impatient, the assembled js and html are available at the end of the article).

:::hint{type="info"}
Check out [Embedding Images with Inspector](docId\:eBOXAKDX2-7aY_4vALq6e) to learn how to integrate pack imagery into your client system with much lower code!
:::

## Creating the API Request

As covered [here](https://paccurate.io/posts/getting-started-with-paccurate-anatomy-of-a-pack-request), the API accepts a JSON body with item and carton data. For more information on how to construct the body of the request, please read Anatomy of a Pack request. To keep these code samples readable, we’ll be using the variable `config` to represent your pack request in JSON.

In order to communicate with the API, we need the browser to make an HTTP request. This can be handled using the `fetch()` API (available in most modern browsers), a third-party library such as `axios` or the long-supported `XMLHttpRequest` API which is available in all browsers. We’ll be using the latter approach for this example.

```javascript
const config = {/* your JSON body data */}
document.addEventListener('DOMContentLoaded', function() {
  const request = new XMLHttpRequest();
  const method = 'POST';
  const url = 'https://api.paccurate.io';
  const packObj = config;
  request.open(method, url, true);
  request.setRequestHeader('Content-Type', 'application/json');
  request.onreadystatechange = function(){
    if(request.readyState === XMLHttpRequest.DONE) {
      var status = request.status;
      if (status === 0 || (status >= 200 && status < 400)) {
        // The request has been completed successfully
        try{
          let packResponse = JSON.parse(request.responseText);
          writeLegend(packResponse);
        }catch(e){
          console.error(e);
        }
      } else {
        console.log(status);
      }
    }
  }
  request.send(JSON.stringify(packObj));
});
```

In the above snippet, we are doing a few things.

- First, we’re adding a listener to the webpage to make the request when it has finished loading. On the `DOMContentLoaded` browser event, we then setup the function to call Paccurate. First we create a new `XMLHttpRequest` object, called `request`.
- Then we configure the request — we set the method to POST, the target URL to our API, and make a reference to the request configuration. The `request.open` starts the HTTP request.
- Once the request is opened, we set the `Content-Type` to `application/json` so the request knows to pass it to our API correctly. We then assign a state change listener to handle the response to the request.
- When the API responds successfully, the `JSON.parse` takes the string of the response and turns it into JSON data that we can use to hand off to `writeLegend` which builds the markup.
- With all of the above setup, `request.send` tells the `XMLHttpRequest` to send the data to our API.

## Building the HTML

In the code sample above, there is a call to function called `writeLegend` that accepts the response data from the Paccurate API. Within that function, we start to assemble the markup for the legend. Let’s explore the function below:

```javascript
const writeLegend = (json)=> {
  const packData = json;
  const target = document.querySelector('#pack-list');
  const svgs = packData.svgs;
  const boxes = packData.boxes;
  boxes.forEach((box, index)=>{
    // create a layout for each box
    target.appendChild(generateMarkup(svgs[index], box))
  })
  // setup listeners
  addLegendListeners();
}
```

The `writeLegend` function takes the argument `json` which is the parsed JSON of the API response (referenced as `packData`). In the third line of the function, we create a variable called `target` which is a reference to the DOM node in the HTML where the legend will be placed on the page. The variables `svgs` and `boxes` represent the arrays of svg markup and box data that the API has returned.

With the `boxes` array defined, we loop through each box using `forEach` — the `target` HTML is passed a string of HTML generated by the `generateMarkup` function (which we’ll get to next!). This function takes two arguments: the first is an svg with the same index as the current box in the loop, the second is the box data from the current box in the loop.

After the markup has been generated from the loop, it’s time to add the hover functionality by calling `addLegendListeners` (more on that later).

## Extracting the item and SVG data

The `generateMarkup` function calls a two other helper functions, `parsedItems` and `keyItems` — all three are in the following snippet:

```javascript
const parsedItems = (arr)=>{
  const skus = {}
  arr.forEach((element) => {
    element = element.item
    const id = element.name ? element.name : element.refId
    if (typeof skus[id] === 'undefined') {
      // push item into sku list
      skus[id] = {
        refId: element.refId,
        name: element.name,
        weight: element.weight,
        dimensions: element.dimensions ? [element.dimensions.x, element.dimensions.y, element.dimensions.z] : [1, 1, 1],
        boxItems: [{ id: element.uniqueId, index: element.index, color: element.color }]
      }
    } else {
      skus[id].boxItems.push({ id: element.uniqueId, index: element.index, color: element.color })
    }
  })
  const flattened = Object.keys(skus).map((element) => { return skus[element] })
  return flattened
}
const keyItems = (box, index)=>{
  const markup = `<tr>
    <td>
      <ul style="width:300px; list-style-type:none; margin:0; padding:0;" class="legend">
        ${box.boxItems.map((item)=> { 
          return `<li data-box-index="${index}" data-volume-index="${item.index}" style="width:20px; height:20px; margin:0 5px 5px 0; float:left; background-color:${item.color}"></li>`
        }).join('')}
      </ul>
    </td>
    <td>${box.name || box.refId}</td>
    <td>${box.dimensions.join(',')}</td> 
    <td>${box.weight}</td>
    <td>${box.boxItems.length}</td>
    </tr>`
  return markup
}
const generateMarkup = (svg, box)=>{
  // compress total item list into a list of skus with count, dimensions, weight, and name
  const parsed = parsedItems(box.box.items)
  // box Id is important if an order has multiple boxes to be packed -- the SVG uses this id as a parent to target the inner boxes
  const boxId = box.box.id
  // create wrapper for svg and legend
  let layout = document.createElement('div')
  let svgWrap = document.createElement('div')
  let itemKey = document.createElement('table')
  itemKey.innerHTML = `<tr>
      <th>item</th>
      <th>name/id</th>
      <th>dims</th>
      <th>weight</th>
      <th>qty</th>
    </tr>
    ${parsed.map((item)=>{return keyItems(item, boxId)}).join('')}
  `
  svgWrap.innerHTML = svg

  // add elements to wrapper
  layout.appendChild(svgWrap)
  layout.appendChild(itemKey)
  return layout
}
```

There are two arguments provided to the `generateMarkup` function — the first is a string of SVG markup for the box, the second is a box object that contains item data for the items packed within the box.

The items array within each box contains the placement data for each item individually, but in order to render the legend, we need to group the items by their name (or refId). The `parsedItems` function does just this.

```javascript
const parsedItems = (arr) => {
  const skus = {};
  arr.forEach((element) => {
    element = element.item;
    const id = element.name ? element.name : element.refId;
    if (typeof skus[id] === "undefined") {
      // push item into sku list
      skus[id] = {
        refId: element.refId,
        name: element.name,
        weight: element.weight,
        dimensions: element.dimensions
          ? [element.dimensions.x, element.dimensions.y, element.dimensions.z]
          : [1, 1, 1],
        boxItems: [
          { id: element.uniqueId, index: element.index, color: element.color },
        ],
      };
    } else {
      skus[id].boxItems.push({
        id: element.uniqueId,
        index: element.index,
        color: element.color,
      });
    }
  });
  const flattened = Object.keys(skus).map((element) => {
    return skus[element];
  });
  console.info(flattened);
  return flattened;
};
```

It accepts the array of items for each box, and returns an array of objects, one for each type of item in the box, with item data and a list of instances of the item in the box.

```javascript
[
    {
        "refId": 1,
        "name": "larger box",
        "weight": 10,
        "dimensions": [
            3,
            6,
            9
        ],
        "boxItems": [
            {
                "id": "1-0",
                "index": 1,
                "color": "indigo"
            },
            {
                "id": "1-1",
                "index": 2,
                "color": "indigo"
            }
        ]
    },
    {
        "refId": 0,
        "name": "smaller box",
        "weight": 1,
        "dimensions": [
            1,
            2,
            1
        ],
        "boxItems": [
            {
                "id": "0-2",
                "index": 3,
                "color": "darkorange"
            },
            {
                "id": "0-3",
                "index": 4,
                "color": "darkorange"
            },
            {
                "id": "0-4",
                "index": 5,
                "color": "darkorange"
            }
        ]
    }
]
```

In the example above, a box with 5 items packed in it (3 of type A, 2 of type B) returns an array of 2 objects: one with the data for item A and an array of 3 instances of the item, a second with data for item B and an array of 2 instances of the item. These nested arrays will drive the color-keys that activate the hover state of the SVG.

Back to generating markup — once we’ve gotten our item data extracted to the `parsed` variable, it’s time to set up a few wrappers to contain the SVG data and legend table.

```javascript
// create wrapper for svg and legend
  let layout = document.createElement("div");
  let svgWrap = document.createElement("div");
  let itemKey = document.createElement("table"); 
```

The variable `layout` is a parent div that will hold the `svgWrap` div, as well as the item information which will be contained inside the `itemKey` `<table>` element. With these initialized, we can loop through `parsed` to populate the `itemKey` table.

```javascript
itemKey.innerHTML = `<tr>
      <th>item</th>
      <th>name/id</th>
      <th>dims</th>
      <th>weight</th>
      <th>qty</th>
    </tr>
    ${parsed
      .map((item) => {
        return keyItems(item, boxId);
      })
      .join("")}
  `;
```

The legend table is comprised of one row of headers and additional rows for each item type. We’re using [template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) above to write the headers and then mapping each item in the `parsed` array to return a row of HTML. Let’s look at how `keyItems` builds the HTML for each item in `parsed` :

```javascript
const keyItems = (box, index) => {
  const markup = `<tr>
    <td>
      <ul style="width:300px; list-style-type:none; margin:0; padding:0;" class="legend">
        ${box.boxItems
          .map((item) => {
            return `<li data-box-index="${index}" data-volume-index="${item.index}" style="width:20px; height:20px; margin:0 5px 5px 0; float:left; background-color:${item.color}"></li>`;
          })
          .join("")}
      </ul>
    </td>
    <td>${box.name || box.refId}</td>
    <td>${box.dimensions.join(",")}</td> 
    <td>${box.weight}</td>
    <td>${box.boxItems.length}</td>
    </tr>`;
  return markup;
};
```

Once again, we’re utilizing template literals and `map()` to render a representative list of each instance of the item. Each item in the `boxItems` array has a color and an index, which are written into the `style` and `data-volume-index` attributes of a `li` respectively. The function also accepts an `index` argument which represents the index of the box where the items have been packed. The box’s `index` is passed to the `data-box-index` attribute of the `li` , and is used in conjunction with `data-volume-index` to target the appropriate SVG on the page when hovering over the legend.

Additionally, we’re filling out the item name, dimensions, weight and quantity (derived from the length of `boxItems` array). With all of this HTML written in the `markup` variable, the function returns `markup` as a string.

```javascript
const generateMarkup = (svg, boxes) => {
  ...
  svgWrap.innerHTML = svg;

  // add elements to wrapper
  layout.appendChild(svgWrap);
  layout.appendChild(itemKey);
  return layout;
};
```

As we get to the end of the `generateMarkup` method, all that’s left to do is insert the `svg` markup into the `svgWrap` element, append it to the `layout` wrapper, and then do the same with the `itemKey` table which has been fully populated courtesy of the `keyItems` function.

Let’s take a look back at the `writeLegend` function:

```javascript
const writeLegend = (json) => {
  const packData = json;
  const target = document.querySelector("#pack-list");
  const svgs = packData.svgs;
  const boxes = packData.boxes;
  boxes.forEach((box, index) => {
    // create a layout for each box
    target.appendChild(generateMarkup(svgs[index], box));
  });
  // setup listeners
  addLegendListeners();
};
```

Now that we’ve created the layouts and legends for each box in the response, it’s time to add the hover functionality.

## Adding Event Listeners

One of the powerful features of the SVG format is that it is easily addressable via CSS and JavaScript. This means we can change colors, strokes, and opacities via both languages as well. For this example, we’ll be using a little of both. We’ll let CSS rules handle color & opacity, and use JavaScript to simply add or remove rules.

First, setup these rules in a `<style>` tag on your HTML page (or in an external stylesheet):

```css
polygon{
  transform: translateY(0);
  position: relative;
  transition:transform .3s, fill .3s, fill-opacity .3s, stroke-opacity .3s;
}
line.volume-line {
  stroke: #666;
  stroke-dasharray: 2,1;
  stroke-width: 1;
}
polygon.volume-line {
  stroke: #666;
}
.x-ray polygon{
  fill-opacity: .1;
  stroke-opacity: .2;
}
.x-ray polygon.active{
  fill-opacity:1 !important;
  stroke-opacity:1 !important;
}
ul.legend li{ cursor:pointer}
table{ max-width:100%;}
figure{
  width: 400px;
}
```

The above styles set some defaults for the `line` and `polygon` elements contained in our returned SVG — they establish a stroke color for the item graphics, as well as a width and dash array for the outer box lines. Additionally, there are overrides for these rules — `.x-ray polygon` and `.x-ray polygon.active`. These rules apply when a user hovers over an item in the legend.

Back in `writeLegend`, there was a reference to a function called `addLegendListeners` which we will look at here:

```javascript
const addLegendListeners = () => {
  document.querySelectorAll("ul.legend li").forEach((element) => {
    element.addEventListener("mouseenter", (e) => {
      const box = e.target.getAttribute("data-box-index");
      const item = e.target.getAttribute("data-volume-index");
      activateBox(box, item, true);
    });
    element.addEventListener("mouseleave", (e) => {
      const box = e.target.getAttribute("data-box-index");
      const item = e.target.getAttribute("data-volume-index");
      activateBox(box, item, false);
    });
  });
};
```

This function attaches two mouse events for all of the `li` elements within the legend. On `mouseenter` , we want to highlight the corresponding item within the SVG. In order to do that, we grab the `data-box-index` and `data-volume-index` attributes from the `li` and pass them along to the `activateBox` method. On `mouseleave` , we want to deactivate that box, so we make the same call to `activateBox` but with `false` as the third argument, which tells the function we are deactivating the item.

```javascript
const activateBox = (boxId, itemId, toggle) => {
  const elems = document.querySelectorAll(
    `figure[data-box-index="${boxId}"] polygon[data-volume-index="${itemId}"]`
  );
  const parent = document.querySelector(`figure[data-box-index="${boxId}"]`);
  if (toggle) {
    // x-ray class is defined in html styles; this can be updated to use inline styles, etc
    parent.classList.add("x-ray");
    elems.forEach((item) => {
      item.classList.add("active");
    });
  } else {
    parent.classList.remove("x-ray");
    elems.forEach((item) => {
      item.classList.remove("active");
    });
  }
}
```

The function `activateBox` does 4 things:

1. Determines which specific `polygon` elements in the SVG represent the item the user is hovering over.
2. Determines which `figure` is the parent of the targeted box.
3. Checks if we are activating or deactivating the box
4. Applies the class `x-ray` to the parent `figure` and the class `active` to the specific polygons (or removes these classes if we are deactivating the box).

With the listeners set up, when a user hovers over a key item, it will apply `x-ray` to the figure, causing all of the polygons inside to have the `fill-opacity` of `.1` , making them almost totally transparent. At the same time, the polygons that represent that key item will have the class `active` appled, and thus their `fill-opacity` set to `1` — recall above when we wrote the rule for `.x-ray polygon.active` , we set the fill and stroke opacities to 1, along with an `!important` flag to override the rule setting everything else to `.1`.

## Putting it all together

We’ve covered all of the pieces of JavaScript (and a little CSS!) necessary to make a pack request to the Paccurate API, and build an interactive legend based on the response. Let’s bundle all of the functions into their appropriate files. Your `legend.js` file should look like this:

```javascript
// DEMO CONFIG USED IN XML REQUEST AT BOTTOM
const config = {
  itemSets: [
    {
      refId: 0,
      color: "darkorange",
      weight: 1,
      name: "smaller box",
      dimensions: {
        x: 1,
        y: 2,
        z: 1,
      },
      quantity: 3,
    },
    {
      refId: 1,
      color: "indigo",
      weight: 10,
      name: "larger box",
      dimensions: {
        x: 6,
        y: 9,
        z: 3,
      },
      quantity: 2,
    },
  ],
  boxTypeSets: ["usps", "fedex"]
};

const parsedItems = (arr) => {
  const skus = {};
  arr.forEach((element) => {
    element = element.item;
    const id = element.name ? element.name : element.refId;
    if (typeof skus[id] === "undefined") {
      // push item into sku list
      skus[id] = {
        refId: element.refId,
        name: element.name,
        weight: element.weight,
        dimensions: element.dimensions
          ? [element.dimensions.x, element.dimensions.y, element.dimensions.z]
          : [1, 1, 1],
        boxItems: [
          { id: element.uniqueId, index: element.index, color: element.color },
        ],
      };
    } else {
      skus[id].boxItems.push({
        id: element.uniqueId,
        index: element.index,
        color: element.color,
      });
    }
  });
  const flattened = Object.keys(skus).map((element) => {
    return skus[element];
  });
  console.info(flattened);
  return flattened;
};
const keyItems = (box, index) => {
  const markup = `<tr>
    <td>
      <ul style="width:300px; list-style-type:none; margin:0; padding:0;" class="legend">
        ${box.boxItems
          .map((item) => {
            return `<li data-box-index="${index}" data-volume-index="${item.index}" style="width:20px; height:20px; margin:0 5px 5px 0; float:left; background-color:${item.color}"></li>`;
          })
          .join("")}
      </ul>
    </td>
    <td>${box.name || box.refId}</td>
    <td>${box.dimensions.join(",")}</td> 
    <td>${box.weight}</td>
    <td>${box.boxItems.length}</td>
    </tr>`;
  return markup;
};
const generateMarkup = (svg, boxes) => {
  // compress total item list into a list of skus with count, dimensions, weight, and name
  const parsed = parsedItems(boxes.box.items);
  // box Id is important if an order has multiple boxes to be packed -- the SVG uses this id as a parent to target the inner boxes
  const boxId = boxes.box.id;
  // create wrapper for svg and legend
  let layout = document.createElement("div");
  let svgWrap = document.createElement("div");
  let itemKey = document.createElement("table");
  itemKey.innerHTML = `<tr>
      <th>item</th>
      <th>name/id</th>
      <th>dims</th>
      <th>weight</th>
      <th>qty</th>
    </tr>
    ${parsed
      .map((item) => {
        return keyItems(item, boxId);
      })
      .join("")}
  `;
  svgWrap.innerHTML = svg;

  // add elements to wrapper
  layout.appendChild(svgWrap);
  layout.appendChild(itemKey);
  return layout;
};
const activateBox = (boxId, itemId, toggle) => {
  const elems = document.querySelectorAll(
    `figure[data-box-index="${boxId}"] polygon[data-volume-index="${itemId}"]`
  );
  const parent = document.querySelector(`figure[data-box-index="${boxId}"]`);
  if (toggle) {
    // x-ray class is defined in html styles; this can be updated to use inline styles, etc
    parent.classList.add("x-ray");
    elems.forEach((item) => {
      item.classList.add("active");
    });
  } else {
    parent.classList.remove("x-ray");
    elems.forEach((item) => {
      item.classList.remove("active");
    });
  }
};
const addLegendListeners = () => {
  document.querySelectorAll("ul.legend li").forEach((element) => {
    element.addEventListener("mouseenter", (e) => {
      const box = e.target.getAttribute("data-box-index");
      const item = e.target.getAttribute("data-volume-index");
      activateBox(box, item, true);
    });
    element.addEventListener("mouseleave", (e) => {
      const box = e.target.getAttribute("data-box-index");
      const item = e.target.getAttribute("data-volume-index");
      activateBox(box, item, false);
    });
  });
};
const writeLegend = (json) => {
  const packData = json;
  const target = document.querySelector("#pack-list");
  const svgs = packData.svgs;
  const boxes = packData.boxes;
  boxes.forEach((box, index) => {
    // create a layout for each box
    target.appendChild(generateMarkup(svgs[index], box));
  });
  // setup listeners
  addLegendListeners();
};

document.addEventListener("DOMContentLoaded", function () {
  const request = new XMLHttpRequest();
  const method = "POST";
  const url = "<https://api.paccurate.io/>";
  const packObj = config;
  request.open(method, url, true);
  request.setRequestHeader("Content-Type", "application/json");
  request.onreadystatechange = function () {
    if (request.readyState === XMLHttpRequest.DONE) {
      var status = request.status;
      if (status === 0 || (status >= 200 && status < 400)) {
        // The request has been completed successfully
        try {
          let packResponse = JSON.parse(request.responseText);
          writeLegend(packResponse);
        } catch (e) {
          console.error(e);
        }
      } else {
        console.log(status);
      }
    }
  };
  request.send(JSON.stringify(packObj));
}); 
```

With the js assembled, let’s update our `legend.html` file with a reference to this script, include the CSS we discussed earlier, and make sure our target div is included on the page:

```html
<html>
  <head>
    <style type="text/css">
      body{
        font-family:Arial, Helvetica, sans-serif;
      }
      polygon{
        transform: translateY(0);
        position: relative;
        transition:transform .3s, fill .3s, fill-opacity .3s, stroke-opacity .3s;
      }
      th{
        text-transform:uppercase;
        text-align:left;
        color:#666;
        letter-spacing: 1px;
        font-size:.8rem;
        font-weight: normal;3
      }
      .x-ray polygon{
        fill-opacity: .1;
        stroke-opacity: .2;
      }
      .x-ray polygon.active{
        fill-opacity:1 !important;
        stroke-opacity:1 !important;
      }
      line.volume-line {
        stroke: #666;
        stroke-dasharray: 2,1;
        stroke-width: 1;
      }
      polygon.volume-line {
        stroke: #666;
      }
      ul.legend li{ cursor:pointer}
      table{ max-width:100%;}
      figure{
        width: 400px;
      }
    </style>
  </head>
  <body>
    <div id="pack-list"></div>
    <script src="legend.js"></script>
  </body>
</html>
```

Open up `legend.html` in a web browser, and you’ll see a working legend!

[https://codepen.io/paccurate/pen/jOpqeBz](https://codepen.io/paccurate/pen/jOpqeBz)

![Demo Example](https://a.storyblok.com/f/76719/547x404/d06be917b9/example-pack.png)

You can also [view the example on codepen](https://codepen.io/paccurate/pen/jOpqeBz).

Thanks for reading this far, and if you have any questions please reach out to [product@paccurate.io](mailto\:product@paccurate.io).
