Building a Pack Legend with the Paccurate API

Iso Box
December 28th, 2022 API

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

  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)

Creating the API Request

As covered here, 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.

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:

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:

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.

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.

[
    {
        "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.

// 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.

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 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 :

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.

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:

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):

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:

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.

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:

// 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>
  <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!

Demo Example

You can also view the example on codepen.

Thanks for reading this far, and if you have any questions please reach out to product@paccurate.io.

Ready to get started?

Whether you're a small business or a large enterprise, we'll show you how to pack more efficiently.