SpreadJS custom shapes are a useful feature that can be implemented in many different types of ways. You can draw shapes to your specifications, and add interactions like highlights, information callouts, or take some database action. In this tutorial, we'll look at how to use custom shapes to create an interactive damage diagram for a car insurance claims application.

JavaScript car insurance app with clickable sections

With an app like this, users can go to their insurance company's website and fill out a claim by clicking the areas of their cars that have been damaged. This sample includes only the clickable interaction, but you could add a "submit" button to save info to the database, a "clear" button to start over again, or add a login page.

Download the sample (zip)

Step 1: Set up the custom shapes SpreadJS project

We can start by referencing all the required SpreadJS files for this project and initialize some of the variables we'll be using:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Car Insurance Claim</title>

    <link href="./css/gc.spread.sheets.excel2013white.12.0.0.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript" src="./scripts/gc.spread.sheets.all.12.0.0.js"></script>
    <script type="text/javascript" src="./scripts/interop/gc.spread.excelio.12.0.0.js"></script>
    <script type="text/javascript" src="./scripts/plugins/gc.spread.sheets.shapes.12.0.0.js"></script>

    <script type="text/javascript">
        GC.Spread.Sheets.LicenseKey = "YOUR KEY HERE";
    </script>

    <script>
        window.onload = function () {

            var spread = new GC.Spread.Sheets.Workbook(document.getElementById("spreadSheet"), { sheetCount: 1 });
            var activeSheet = spread.getActiveSheet();
        }
    </script>
</head>
<body>
    <div id="spreadSheet" style="width: 825px; height: 800px; border: 1px solid gray"></div>
</body>
</html>

Step 2: Add a png of the diagram to the app

Now we'll add the diagram. Basically, we'll be creating shapes on top of this diagram, so your design team can create the png for you.

We'll use the getCellRect function to figure out where we can place the diagram:

function addCarDiagram(sheet) {
    sheet.setColumnWidth(0, 200);
    var startCellRect = sheet.getCellRect(0, 0),
        endCellRect = sheet.getCellRect(22, 10),
        spreadElement = document.getElementById("spreadSheet");
    var offset = spreadElement.getBoundingClientRect();
    {
        top: offset.top + document.body.scrollTop;
        left: offset.left + document.body.scrollLeft;
    }

    var x = offset.left - sheet.getColumnWidth(0, GC.Spread.Sheets.SheetArea.rowHeader),
        y = offset.top - sheet.getRowHeight(0, GC.Spread.Sheets.SheetArea.colHeader);
    sheet.pictures.add("CarDiagram", "./Car Diagram.png", x, y, endCellRect.x - startCellRect.x, endCellRect.y - startCellRect.y);
    sheet.addSpan(1, 3, 1, 2);
    sheet.getCell(1, 3).text("Passenger Side");
    sheet.getCell(1, 3).font("12pt Arial");
    sheet.addSpan(20, 3, 1, 2);
    sheet.getCell(20, 3).text("Driver's Side");
    sheet.getCell(20, 3).font("12pt Arial");
}

Add the diagram to the app

Step 3: Add shapes on top of the diagram

For this project, we'll have a separate sheet that defines the location and size of all the shapes in the diagram. This can be done easily, by creating an area, and then setting that array on the sheet:

function initDamageAreaShapes(spread) {
    var sheet = spread.getSheet(0);
    spread.addSheet(1, new GC.Spread.Sheets.Worksheet("Damage_Areas"));

    var startColor = "lightgreen";
    var damageAreaSheet = spread.getSheet(1);
    var damageAreas = ["Area", "Left", "Top", "Width", "Height", "Stroke Color", "Line Width"],
        front = ["front", 11, 136, 57, 167, "blue", 3],
        hood = ["hood", 123, 149, 142, 140, "blue", 3],
        frontWindshield = ["frontWindshield", 257, 149, 70, 140, "blue", 3],
        roof = ["roof", 325, 165, 158, 110, "blue", 3],
        rearTop = ["rearTop", 481, 149, 158, 143, "blue", 3],
        rear = ["rear", 661, 134, 63, 171, "blue", 3],
        leftFront = ["leftFront", 110, 310, 157, 85, "blue", 3],
        leftFrontDoor = ["leftFrontDoor", 257, 283, 140, 112, "blue", 3],
        leftBackDoor = ["leftBackDoor", 392, 283, 121, 111, "blue", 3],
        leftBack = ["leftBack", 465, 284, 168, 110, "blue", 3],
        rightFront = ["rightFront", 111, 44, 156, 76, "blue", 3],
        rightFrontDoor = ["rightFrontDoor", 258, 44, 139, 113, "blue", 3],
        rightBackDoor = ["rightBackDoor", 391, 44, 123, 115, "blue", 3],
        rightBack = ["rightBack", 465, 44, 168, 110, "blue", 3];

    damageAreaSheet.setArray(0, 0, [
        damageAreas,
        front,
        hood,
        frontWindshield,
        roof,
        rearTop,
        rear,
        leftFront,
        leftFrontDoor,
        leftBackDoor,
        leftBack,
        rightFront,
        rightFrontDoor,
        rightBackDoor,
        rightBack
    ]);
}

Once that's defined, we can go ahead and create models for each shape. Since the location and size are defined in the sheet, we can reference those cells. We can also define the shape using an SVG path, which is relative to the top left point of the shape:

var frontModel = {
    left: "=Damage_Areas!B2",
    top: "=Damage_Areas!C2",
    width: "=Damage_Areas!D2",
    height: "=Damage_Areas!E2",
    options: {
        fill: {
            type: 1,
            color: startColor,
            transparency: "0.5"
        }
    },
    path: [
        [
            ["M", 1, 8],
            ["L", 17, 4],
            ["L", 19, 0],
            ["L", 31, 0],
            ["L", 33, 4],
            ["L", 51, 4],
            ["L", 56, 10],
            ["L", 57, 49],
            ["L", 52, 50],
            ["L", 51, 116],
            ["L", 57, 118],
            ["L", 57, 158],
            ["L", 51, 163],
            ["L", 34, 162],
            ["L", 31, 166],
            ["L", 19, 167],
            ["L", 18, 163],
            ["L", 1, 158],
            ["Z"]
        ]
    ]
};

Then we can add that shape using the model we just defined:

sheet.shapes.add('front', frontModel);

Step 4: Add interactions to the custom JavaScript shapes

Now that the shapes have been added, we can write some code for changing the color of the shape when the user selects it:

(function shapeClicked() {
    var host = spread.getHost();
    host.addEventListener("click", function (e) {
        var offset = getOffset(host), left = offset.left, top = offset.top;
        var x = e.pageX - left, y = e.pageY - top;
        var hitTest = activeSheet.hitTest(x, y);
        if (hitTest.shapeHitInfo) {
            var shape = hitTest.shapeHitInfo.shape;
            var shapeStyle = shape.style();
            shapeStyle.fill.color = (shapeStyle.fill.color === "rgb(255,0,0)" ? "rgb(144,238,144)" : "rgb(255,0,0)");
            shape.style(shapeStyle);
        }
    });
})();

This uses a custom function called getOffset, which is defined as such:

function getOffset(elem) {
    var docElem, win, box = { top: 0, left: 0 }, doc = elem && elem.ownerDocument;
    if (!doc) {
        return;
    }
    docElem = doc.documentElement;
    if (typeof elem.getBoundingClientRect !== void 0) {
        box = elem.getBoundingClientRect();
    }
    return {
        top: box.top + window.pageYOffset - docElem.clientTop,
        left: box.left + window.pageXOffset - docElem.clientLeft
    };
}

Add interactions to the JavaScript shapes

Step 5: Add button shapes to the app

We can add some options so the user can select what type of accident happened. We'll create button shapes and add them to a shape group:

function initAccidentType(sheet) {
    var rowHeaderWidth = sheet.getColumnWidth(0, GC.Spread.Sheets.SheetArea.rowHeader),
        colHeaderHeight = sheet.getRowHeight(0, GC.Spread.Sheets.SheetArea.colHeader),
        shapeType = GC.Spread.Sheets.Shapes.AutoShapeType.roundedRectangle,
        hAlignment = GC.Spread.Sheets.HorizontalAlign.center,
        vAlignment = GC.Spread.Sheets.VerticalAlign.center,
        accidentTypeShapeInfo = [[23, 1, "bumperDamage", "Bumper Damage"], [25, 1, "roofDamage", "Roof Damage"], [23, 5, "overheated", "Overheated"], [25, 5, "other", "Other"]],
        cellRect,
        margin = 5;

    sheet.addSpan(23, 0, 4, 1);
    var cell = sheet.getCell(23, 0);
    cell.text("Accident Type");
    cell.hAlign(hAlignment);
    cell.vAlign(vAlignment);
    cell.font("20pt Arial");

    var accidentTypeButtonsGroup = new GC.Spread.Sheets.Shapes.GroupShape(sheet, "accidentTypeButtons");

    for (var s = 0; s < accidentTypeShapeInfo.length; s++) {
        var tempShapeInfo = accidentTypeShapeInfo[s];
        sheet.addSpan(tempShapeInfo[0], tempShapeInfo[1], 2, 4);
        cellRect = sheet.getCellRect(tempShapeInfo[0], tempShapeInfo[1]);
        var tempShape = sheet.shapes.add(tempShapeInfo[2], shapeType, cellRect.x - rowHeaderWidth + margin, cellRect.y - colHeaderHeight + margin, cellRect.width - (2 * margin), cellRect.height - (2 * margin));
        tempShape.text(tempShapeInfo[3]);
        var tempShapeStyle = tempShape.style();
        tempShapeStyle.line.color = "darkgreen";
        tempShapeStyle.fill.color = "lightgreen";
        tempShapeStyle.fill.transparency = 0.5;
        tempShapeStyle.textFrame.hAlign = hAlignment;
        tempShapeStyle.textFrame.vAlign = vAlignment;
        tempShapeStyle.textEffect.color = 'black';
        tempShape.style(tempShapeStyle);
        accidentTypeButtonsGroup.add(tempShape);
    }
    return accidentTypeButtonsGroup;
}

We can also add buttons to select the severity of the accident. The code for this is mostly the same:

function initSeverity(sheet) {
    var rowHeaderWidth = sheet.getColumnWidth(0, GC.Spread.Sheets.SheetArea.rowHeader),
        colHeaderHeight = sheet.getRowHeight(0, GC.Spread.Sheets.SheetArea.colHeader),
        shapeType = GC.Spread.Sheets.Shapes.AutoShapeType.roundedRectangle,
        hAlignment = GC.Spread.Sheets.HorizontalAlign.center,
        vAlignment = GC.Spread.Sheets.VerticalAlign.center,
        severityShapeInfo = [[28, "highSeverity", "High"], [30, "mediumSeverity", "Medium"], [32, "lowSeverity", "Low"]],
        cellRect,
        margin = 5;

    sheet.addSpan(28, 0, 6, 1);
    var cell = sheet.getCell(28, 0);
    cell.text("Severity");
    cell.hAlign(hAlignment);
    cell.vAlign(vAlignment);
    cell.font("20pt Arial");

    var severityButtonsGroup = new GC.Spread.Sheets.Shapes.GroupShape(sheet, "severityButtons");

    for (var s = 0; s < severityShapeInfo.length; s++) {
        var tempShapeInfo = severityShapeInfo[s];
        sheet.addSpan(tempShapeInfo[0], 1, 2, 2);
        cellRect = sheet.getCellRect(tempShapeInfo[0], 1);
        var tempShape = sheet.shapes.add(tempShapeInfo[1], shapeType, cellRect.x - rowHeaderWidth + margin, cellRect.y - colHeaderHeight + margin, cellRect.width - (2 * margin), cellRect.height - (2 * margin));
        tempShape.text(tempShapeInfo[2]);
        var tempShapeStyle = tempShape.style();
        tempShapeStyle.line.color = "darkgreen";
        tempShapeStyle.fill.color = "lightgreen";
        tempShapeStyle.fill.transparency = 0.5;
        tempShapeStyle.textFrame.hAlign = hAlignment;
        tempShapeStyle.textFrame.vAlign = vAlignment;
        tempShapeStyle.textEffect.color = 'black';
        tempShape.style(tempShapeStyle);
        severityButtonsGroup.add(tempShape);
    }
    return severityButtonsGroup;
}

Add buttons to the JavaScript shapes

We can add some more functionality to the shapes so that the accident type is selected based on which part of the car is selected. To do this, we can add to the shapeClicked function we defined earlier:

if (hitTest.shapeHitInfo) {
    var shape = hitTest.shapeHitInfo.shape;
    var shapeStyle = shape.style();
    shapeStyle.fill.color = (shapeStyle.fill.color === "rgb(255,0,0)" ? "rgb(144,238,144)" : "rgb(255,0,0)");
    shape.style(shapeStyle);
    //Roof Damage
    if (shape.name() == "roof") {
        var button = accidentTypeButtonsGroup.find("roofDamage");
        var buttonStyle = button.style();
        buttonStyle.fill.color = (buttonStyle.fill.color === "rgb(255,0,0)" ? "rgb(144,238,144)" : "rgb(255,0,0)");
        button.style(buttonStyle);
    }//Bumper Damage
    else if (shape.name() == "front" || shape.name() == "rear") {
        var front = activeSheet.shapes.get("front"),
            rear = activeSheet.shapes.get("rear");
        var button = accidentTypeButtonsGroup.find("bumperDamage");
        if (front.style().fill.color === "rgb(255,0,0)" || rear.style().fill.color === "rgb(255,0,0)") {
            buttonStyle = button.style();
            buttonStyle.fill.color = "rgb(255,0,0)";
            button.style(buttonStyle);
        } else if (front.style().fill.color === "rgb(144,238,144)" && rear.style().fill.color === "rgb(144,238,144)") {
            buttonStyle = button.style();
            buttonStyle.fill.color = "rgb(144,238,144)";
            button.style(buttonStyle);
        }
    }//Severity
    else if (shape.name() === "highSeverity" || shape.name() === "mediumSeverity" || shape.name() === "lowSeverity") {
        var buttonArray = severityButtonsGroup.all();
        for (var s = 0; s < buttonArray.length; s++) {
            if (buttonArray[s].name() !== shape.name()) {
                var buttonStyle = buttonArray[s].style();
                buttonStyle.fill.color = "rgb(144,238,144)"
                buttonArray[s].style(buttonStyle);
            }
        }
    }
}

Add shape behavior dependent on another shape

Step 6: Add form field shapes

In order to fill out the claim, users need a place to type in their information, so we can create an info section specifically for that:

function initInfoArea(sheet) {
    var border = new GC.Spread.Sheets.LineBorder("black", GC.Spread.Sheets.LineStyle.thin);
    sheet.addSpan(28, 4, 1, 3);
    sheet.getCell(28, 4).text("Driver Name:");
    sheet.addSpan(28, 7, 1, 3);
    sheet.getRange(28, 7, 1, 3).borderBottom(border);
    sheet.addSpan(29, 4, 1, 3);
    sheet.getCell(29, 4).text("Vehicle Make/Model/Year:");
    sheet.addSpan(29, 7, 1, 3);
    sheet.getRange(29, 7, 1, 3).borderBottom(border);
    sheet.addSpan(30, 4, 1, 6);
    sheet.getCell(30, 4).text("Details:");
    sheet.addSpan(31, 4, 3, 6);
    sheet.getRange(31, 4, 3, 6).setBorder(border, { all: true });
}

Add form field shapes

Step 7: Remove spreadsheet features

Finally, we can remove some of the spreadsheet-like features that the user won't need, such as gridlines and headers:

var workbookShapes = activeSheet.shapes.all();
for (var s = 0; s < workbookShapes.length; s++) {
    workbookShapes[s].allowMove(false);
    workbookShapes[s].allowResize(false);
}

activeSheet.setRowCount(35);
activeSheet.setColumnCount(10);
activeSheet.name("Car Insurance Claim");

activeSheet.options.gridline = { showVerticalGridline: false, showHorizontalGridline: false };
activeSheet.options.colHeaderVisible = false;
activeSheet.options.rowHeaderVisible = false;
spread.options.allowUserDragDrop = false;
spread.options.tabStripVisible = false;

Working custom shapes car insurance app

That's all that's required to make a simple application with custom shapes. Of course, this is only one example; the possibilities of custom shapes in SpreadJS are endless!

Try the car insurance custom shapes demo

Download the sample (zip)

Try SpreadJS JavaScript Shapes

Download SpreadJS 12

Download Now!