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:
Make the API request
Render the pack SVG
Create the list of boxes & items
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)
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.
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).
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.
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:
Determines which specific polygon
elements in the SVG represent the item the user is hovering over.
Determines which figure
is the parent of the targeted box.
Checks if we are activating or deactivating the box
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
.
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!
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.