Bending is electronics geek for modifying something (usually something mundane) and bending it to fit a new purpose. A very basic example is something like busting open one of those big red Staples Easy buttons and replacing the standard “That was easy” voice with one that yells “Squirrel!” In terms of Highcharts Gantt, bending means using the library’s core chart-making functionality to make something other than a Gantt chart…like a slide-block puzzle.
The rules are simple. Vertical blocks move vertically. Horizontal blocks move horizontally. You win when you free the red block.
Steps for creating a Gantt slide-block puzzle
1. Create an X-Range series using Highcharts Gantt
Figure out your block layout (I copied a layout from a slide-block puzzle app,) and lay it out as a simple Gantt chart.
I assigned different colors for the vertical and horizontal blocks. The red block is the one that needs freeing. I set up the axes as linear (instead of the default treegrid.) Here’s what my initial Gantt chart looks like.

Check out the snippet of my data below. The groupId assigned to each block (“hBlock1”, “vBlock1”) ensures that the grouped vertical blocks slide together. For example, the vertical vBlocks 1a/1b/1c have a groupId of “vBlock1” so that the blocks will drag together regardless of which block the player interacts with.
data: [
{
name:'hBlock1a',
groupId:'hBlock1',
color:'#A43677',
x: 0,
x2: 3,
y: 0,
}, {
name: 'vBlock1a',
groupId:'vBlock1',
color:'#f15c80',
x: 3,
x2: 4,
y: 0,
},{
name: 'vBlock1b',
groupId:'vBlock1',
color:'#f15c80',
x: 3,
x2: 4,
y: 1,
}, {
name: 'vBlock1c',
groupId:'vBlock1',
color:'#f15c80',
x: 3,
x2: 4,
y: 2,
}]
}]
2. Make the blocks draggable
In the plotOptions for the xRange series, add the dragDrop configuration:
dragDrop: {
draggableX: true,
draggableY: true,
dragMinY: 0,
dragMaxY: 6,
dragMinX: 0,
dragMaxX: 6,
liveRedraw: false,
groupBy: 'groupId'
}
At the series level, set draggableX and draggableY to true. We want the blocks to slide anywhere on the game board, so set the drag minimums/maximums for the x and y to 0 and 6 respectively. Finally, set the groupBy property to “groupId.”
3. Write the drag code
To make the blocks slide, we’ll use the dragStart, drag and drop functions. Under the dragDrop options, add point and set up these events:
point: {
events: {
dragStart: function (e) {},
drag: function (e) {},
drop: function (e) {}
}
}
Before coding the function, let’s set up some global variables that will store the positions and movements of the blocks.
My approach was to create several arrays to track the state of the game board and the location of each block.
The array pointMatrix track which “cells” on the game board are occupied (1) and which are free (0).
let pointMatrix = [
[1,1,1,1,0,0],
[1,1,1,1,1,1],
[1,1,1,1,0,1],
[1,0,1,1,1,1],
[0,0,1,0,1,1],
[1,1,1,0,1,0]];
The array blockProps tracks the individual position of each block, as well as the block’s groupId, starting row, starting column and width or height.
var blockProps = [
['hBlock1',0,0,3],
['hBlock2',1,4,2],
['hBlock3',3,3,2],
['hBlock4',1,1,2],
['hBlock5',2,1,2],
['hBlock6',5,0,3],
['vBlock2',1,0,3],
['vBlock1',0,3,3],
['vBlock4',2,5,3],
['vBlock5',4,4,2],
['vBlock3',3,2,2]];
The zones array holds the horizontal zones of the chart on the xAxis.
let zones = [
[0,1],
[1,2],
[2,3],
[3,4],
[4,5],
[5,6]
];
And these following global variables track block position, drag direction and some other things that will come into play a little later.
var yStart =0;
var xStart =0;
var seriesIndex=0;
var seriesGroup='';
var orientation ='';
var startRow=0;
var startCol=0;
var size=0;
var seriesData=[];
var blocksToMove = [];
The first point-event function to set up is the easiest:
drop: function () {
return false;
}
We disable the drop event because it interferes with block placement. When the user slides a block, we want it to snap to the next free cell. With drop enabled, if the player releases the block too soon, the block will stop in its tracks, creating a sloppy grid.
Next, we’ll set up the dragStart. In this function, we collect information about the drag target (i.e. the block) and the direction the user intends to drag (up/down/left/right.)
dragStart: function (e) {
yStart=e.chartY;
xStart = e.chartX;
seriesGroup = this.groupId;
seriesName = this.name;
seriesIndex = this.index;
seriesData=this.series.chart.series[0].data;
Set yStart and xStart to the x and y location of the event. We will use this x/y information to determine the direction of the drag.
The variables seriesGroup, seriesName, seriesIndex and seriesData store exactly what they’re named and make it easier to work with the chart object.
The local variable blockGroup holds the blockMatrix subarray that corresponds to the block the player intends to drag.
The blockGroup array contains the location of the block on the game board and its size. I also find blockGroup’s index and store it in blocksIndex. We’ll use this information to determine if the block(s) can be moved.
blockGroup = blockMatrix.find(function(element){
if(element[0]==seriesGroup){
return element;
}});
blocksIndex = blockMatrix.findIndex(function(element){
if(element[0]==seriesGroup){
return element
}});
Determine the block’s orientation by searching its name for “h.” Knowing the orientation will enable us to restrict the blocks’ movements. (Vertical blocks only move vertically; horizontal blocks only move horizontally.)
if(seriesName.indexOf('h')!= -1){
orientation='horizontal';
}else{
orientation='vertical';
}
The last thing is to set the global variables startRow, startCol and size so we can look up the block’s location on the game board and check if the space to the right/left/above/below is blocking the block’s path.
startRow = blockGroup[1]; //row
startCol = blockGroup[2]; //column
size = blockGroup[3]; //length or height
seriesData = this.series.chart.series[0].data;
}
Next is the drag function. Since we stored the x/y positions of the dragStart event, we can compare them to the x/y positions of the drag event. For example, if the x value is greater, the direction of the drag is right.
But before we move the block, we have to find out if it may move. Here’s how I did it.
Look up the block’s location in the pointMatrix. For example, if the player intends to drag a horizontal block to the right, check the cell to the right of the dragged block. If that cell is set to 0, the block may move one space right.
if(pointMatrix[startRow][startCol+size] == 0){
seriesData[seriesIndex].update({x:zones[startCol+1][0],x2:zones[startCol+size][1],y:newerY});
pointMatrix[startRow][startCol+size]=1;
pointMatrix[startRow][startCol]=0;
blockProps[blocksIndex][2]=startCol + 1;
}
Finally, record the block’s new position in the pointMatrix array, and update the block’s properties (row, column) in the blockProps array.
pointMatrix[startRow][startCol+size]=1;
pointMatrix[startRow][startCol]=0;
blockMatrix[blocksIndex][2]=startCol + 1;
The vertical blocks follow the same idea, except we update the y value
4. Make the escape
We need an exit for the red block. I accomplished this with plotBands on the x and y axes.
On the xAxis, set up plot bands for each “zone.” Make the last zone’s plotBand white.
On the yAxis, set up two plot bands to mask the white plot band on the x axis.
Then, in the section of the code that moves the block to the right, add the following code.
if(seriesGroup == 'hBlock5' &&seriesData[seriesIndex].x2 > 5){
seriesData[seriesIndex].update({x:6,x2:7,y:newerY});
}
When the red block reaches a certain x position on the game board, it flies free, like a bird.
5. Clean it up
Let’s get rid of some of the chart stuff.
Instead of returning the point name in the data labels, let’s add arrows to help the player understand the restrictions on the blocks’ movement.
In the dataLabel formatter function, retrieve the width of each block and make the container for the arrows slightly smaller than the block so the arrows don’t bleed over the edge.
formatter:function(){
pointWidth = Math.round(this.point.shapeArgs.width);
return '<div style="color:#F0B6D9;display:grid;grid-template-columns:1fr 1fr 1fr;width:' + (pointWidth-15) + 'px;padding:0px 0px"><div><i style="font-size:14px;" class="fas fa-arrow-left"></i></div><div style="text-align:center"><span style="font-size:14px;font-weight:700;"></span></div><div style="text-align:right"><i style="font-size:14px;" class="fas fa-arrow-right"></i></div></div>'
}
Oops. The vertical blocks have left/right arrows, not up/down arrows. To fix this, we set data labels at the point level for the vertical blocks. The “a” blocks get the up arrow, and the “c” blocks get the down arrow.
dataLabels: {
x:-5,
formatter:function(){
return '<i style="color:#F0B6D9;font-size:14px;" class="fas fa-arrow-down"></i>'
}
Give the red block a unique label. I labeled mine “free me” and left off the arrows to make it even more distinct.
Add an instructive title and change the axes’ gridLine and tickColor to ‘transparent.’
The puzzle layout looks cramped on the left and right, and the exit isn’t visible. To fix this, set the xAxis min to -0.2 and the max to 6.2. Then, change the “from” value on your first xAxis plotBand to the new min, and the “to” value on your last plotBand to 6.2.
Now, try and free the block!
Comments
There are no comments on this post
Want to leave a comment?
Comments are moderated. They will publish only if they add to the discussion in a constructive way. Please be polite.