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.
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");
}
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
};
}
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;
}
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);
}
}
}
}
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 });
}
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;
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!