Plain JavaScript
With a bundler
npm install @crosswalkjs/canvas @crosswalkjs/core
Here's a whole page, running. Edit any file and the preview follows; Reset puts it back:
main.js
import { createMapper } from "@crosswalkjs/canvas";
import { createTransform } from "@crosswalkjs/core";
import startingMap from "./order.map.json";
import sampleOrder from "./sample-order.json";
const output = document.getElementById("output");
function show(map) {
try {
output.textContent = JSON.stringify(createTransform(map)(sampleOrder), null, 2);
} catch (error) {
output.textContent = error.message;
}
}
createMapper(document.getElementById("mapper"), { document: startingMap, onChange: show });
show(startingMap);
index.html
<div id="mapper" style="height: 360px"></div>
<pre id="output"></pre>
<script type="module" src="./main.js"></script>
order.map.json
{
"version": 1,
"source": {
"type": "object",
"title": "Order",
"properties": {
"Number": {
"type": "string"
},
"Status": {
"type": "string"
},
"Billing": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Shipping": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Date": {
"type": "string"
},
"Qty": {
"type": "number"
}
}
}
}
}
},
"target": {
"type": "object",
"title": "Invoice",
"properties": {
"Reference": {
"type": "string"
},
"Status": {
"type": "string"
},
"BillTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"ShipTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Units": {
"type": "number"
},
"FirstOrdered": {
"type": "string"
}
}
}
}
}
},
"loops": [
{
"source": "Lines[]",
"target": "Products[]",
"sortBy": [
{
"path": "Lines[].Date"
}
],
"groupBy": "Lines[].Sku"
}
],
"nodes": [
{
"id": "status",
"operator": "convert.lookup",
"constants": {
"1": {
"P": "Paid",
"S": "Shipped",
"C": "Cancelled"
},
"2": "Unknown"
},
"position": {
"x": 150,
"y": 50
}
},
{
"id": "units",
"operator": "aggregate.sum",
"scope": "Lines[]",
"position": {
"x": 150,
"y": 290
}
}
],
"links": [
{
"from": {
"source": "Number"
},
"to": {
"target": "Reference"
}
},
{
"from": {
"source": "Status"
},
"to": {
"node": "status",
"input": 0
}
},
{
"from": {
"node": "status"
},
"to": {
"target": "Status"
}
},
{
"from": {
"source": "Billing"
},
"to": {
"target": "BillTo"
},
"submap": "Address"
},
{
"from": {
"source": "Shipping"
},
"to": {
"target": "ShipTo"
},
"submap": "Address"
},
{
"from": {
"source": "Lines[].Sku"
},
"to": {
"target": "Products[].Sku"
}
},
{
"from": {
"source": "Lines[].Qty"
},
"to": {
"node": "units",
"input": 0
}
},
{
"from": {
"node": "units"
},
"to": {
"target": "Products[].Units"
}
},
{
"from": {
"source": "Lines[].Date"
},
"to": {
"target": "Products[].FirstOrdered"
}
}
],
"notes": [
{
"id": "note1",
"text": "One product per SKU, earliest order first. Units add up across the SKU's lines.",
"position": {
"x": 24,
"y": 22
},
"on": {
"node": "units"
}
}
],
"submaps": {
"Address": {
"source": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"target": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"nodes": [
{
"id": "postcode",
"operator": "text.upper",
"position": {
"x": 160,
"y": 90
}
}
],
"links": [
{
"from": {
"source": "Street"
},
"to": {
"target": "Line1"
}
},
{
"from": {
"source": "City"
},
"to": {
"target": "Town"
}
},
{
"from": {
"source": "Postcode"
},
"to": {
"node": "postcode",
"input": 0
}
},
{
"from": {
"node": "postcode"
},
"to": {
"target": "Postcode"
}
}
],
"notes": [
{
"id": "note1",
"text": "Both addresses on the order use this.",
"position": {
"x": 120,
"y": 190
}
}
]
}
}
}
sample-order.json
{
"Number": "SO-1042",
"Status": "S",
"Billing": {
"Street": "4 Mill Lane",
"City": "Leeds",
"Postcode": "ls1 4ab"
},
"Shipping": {
"Street": "Unit 9, Canal Wharf",
"City": "Leeds",
"Postcode": "ls11 5pq"
},
"Lines": [
{
"Sku": "MUG-01",
"Date": "2026-03-04",
"Qty": 2
},
{
"Sku": "TEA-12",
"Date": "2026-03-01",
"Qty": 1
},
{
"Sku": "MUG-01",
"Date": "2026-03-02",
"Qty": 4
}
]
}
Without a build step
Browsers can load both packages as ES modules from a CDN, such as esm.sh, with an import map:
index.html
<script type="importmap">
{
"imports": {
"@crosswalkjs/core": "https://esm.sh/@crosswalkjs/core@0.1",
"@crosswalkjs/canvas": "https://esm.sh/@crosswalkjs/canvas@0.1?external=@crosswalkjs/core"
}
}
</script>
<div id="mapper" style="height: 600px"></div>
<script type="module">
import { createMapper } from "@crosswalkjs/canvas";
const map = await fetch("./order.map.json").then((response) => response.json());
createMapper(document.getElementById("mapper"), {
document: map,
onChange: (next) => localStorage.setItem("order-map", JSON.stringify(next)),
});
</script>
?external=@crosswalkjs/core makes the canvas use the same copy of the core package as your own imports, rather than its own.
Pin a version you've tested, as above, rather than taking whatever is latest.