Implementation of Winged Edge in Lua - data-structures

I am working on a game with an irregular grid and I was recommended to use the Winged Edge data structure. Reading what it's used for seems straight forward enough but I simply cannot figure out how to implement it.
What my ultimate goal is to be able to detect which faces I am clicking on, and which faces are adjacent to the clicked face. Could someone possible walk me in the right direction?

After some further reading and discussion, I managed to write a pretty snazzy Winged Edge library. It is licensed under the MIT license so if anyone wants to use it, you're more than welcome!
https://github.com/karai17/Lua-Winged-Edge
The way this works is as follows:
The key feature of Winged Edge is how every set of data references every other set of data. Each vertex lists all of the edges that are connected to it, each edge lists connected vertices and faces, and each face lists connected vertices and edges. The usefulness of this will be come more apparent below.
First I parsed a Wavefront Object file to get a list of vertices and faces. Each vertex has XYZ coordinates, and each face has a list of vertices it is connected to:
obj.vertices = {
{ x=0, y=0, z=0 },
{ x=1, y=0, z=0 },
{ x=1, y=1, z=0 },
...
}
obj.faces = {
{ 1, 2, 3 },
{ 1, 2, 4 },
{ 3, 4, 5, 6 },
...
}
I then created three tables for my winged edge (WE) object: vertices, edges, and faces. I parsed the vertices and created WE vertices:
WE.vertices = {
{ edges={}, position={ 0, 0, 0 } },
{ edges={}, position={ 1, 0, 0 } },
...
}
I then parsed the faces to create WE edges and WE face, filling in all the references. The edges have a special case, not only do they reference the faces they are connected to, but they also reference the previous and next edges for each face. This is extremely important as it allows you to traverse the edges of a face to find the faces adjacent to the current:
WE.vertices = {
{ edges={ 1, 2, 3 }, position={ 0, 0, 0 } },
{ edges={ 2, 3, 4 }, position={ 1, 0, 0 } },
...
}
WE.edges = {
{ vertices={ 1, 2 }, faces={ face=1, prev_edge=3, next_edge=2 } },
{ vertices={ 2, 3 }, faces={ face=1, prev_edge=1, next_edge=3 } },
{ vertices={ 3, 1 }, faces={ face=1, prev_edge=2, next_edge=1 } },
...
}
WE.faces = {
{ edges={ 1, 2, 3 }, vertices={ 1, 2, 3 } },
{ edges={ 2, 3, 4 }, vertices={ 2, 3, 4 } },
...
}
With the data structure set up like this, you can create a function that checks which faces an edge belongs to, and traversing through those edges can allow you to gather up all of the faces adjacent to any particular face, and where they are in space.
What I used this for was to check which faces surrounded a face that I click on to determine if I am allowed to activate that face based on some game logic, like if any of the surrounded faces were already activated. Another use of this is for flood filling faces.
For any further details such as exact execution, you are welcome to snoop around the code linked above. :)

Related

Comparing 2D Paths for Similarity

I want to compare two 2D paths (arrays of points) for similarity, and, if possible, get a percentage of how similar the paths are (100% is identical, 0% completely different)
A path in this case is:
an ordered array, of zero or greater length, of coordinates (x, y)
continuous
basically a polyline
Paths may overlap and contain a different number of points.
Position and scale doesn't matter, i.e. two otherwise identical paths can be far apart, and one can be smaller than the other, and they will be found to be the same.
The only criteria for similarity is the shape the paths make, and the order of the coordinates i.e. a vertical line drawn top to bottom is different to a vertical line drawn bottom to top (although the order is the least important, solutions are welcome without meeting that criteria).
So in the image below, the green path would be found to be identical to the black one, the blue one would be maybe ~80% similar, and the red path would be maybe <1% similar.
I've searched the web and can't find anything that quite fits my criteria and situation - ideally it's not going to be complex like neural networks.
I don't know but i have an idea that maybe could work.
Hows about scaling the 2 that going to be compared to the same ratio, overlay them each other and compare the points the path along to the end.
Then you could give Points maybe For the difference of the Points?
Like exact is 1 Point, diff < idk 10? is 0.5 points, and so on.
At the end you then could get the percentage from points reached
I hope i could give an Idea or smth
I suggest this function to solve the distance (the opposite of similarity that you search for ) between two paths from which you can infer the similarity from (1- distance), but before that you have to find a way to normalize the distance to be in the range [0,1], the logic behind this solution is as follows:
Starting from one of the two sets of points, we have to find the nearest point in the other set for each point in the set we started from, with this we will get the sum of the distances between convergent points. We do the same from the other set of points.
https://ibb.co/K7SZZ3P
function distancepathpath(path1, path2) {
let sum = 0;
for (const i in path1) {
let minDist = Number.MAX_SAFE_INTEGER;
for (const j in path2) {
let dist = Math.sqrt(Math.pow(path1[i].x - path2[j].x, 2) + Math.pow(path1[i].y - path2[j].y, 2))
if (dist < minDist) minDist = dist;
}
sum += minDist
}
for (const i in path2) {
let minDist = Number.MAX_SAFE_INTEGER;
for (const j in path1) {
let dist = Math.sqrt(Math.pow(path1[j].x - path2[i].x, 2) + Math.pow(path1[j].y - path2[i].y, 2))
if (dist < minDist) minDist = dist;
}
sum += minDist
}
return sum / (path1.length * path2.length * 2);
}
console.log(distancepathpath(
[
{ x: 10, y: 9 },
{ x: 2, y: 1 },
{ x: 4, y: 4 }
], [
{ x: 3, y: 7 },
{ x: 1, y: 1 },
{ x: 7, y: 4 },
{ x: 10, y: 9 },
{ x: 9, y: 1 },
{ x: 4, y: 6 }
]))

Porting d3-voronoi to d3-delunay

What I want to do is replicate a data-structure produced by processing the output from d3-voronoi, but by using d3-delaunay instead.
The data-structure in question is the one produced by this makeMesh function:
function makeMesh(pts, extent) {
extent = extent || defaultExtent;
var vor = voronoi(pts, extent);
var vxs = [];
var vxids = {};
var adj = [];
var edges = [];
var tris = [];
for (var i = 0; i < vor.edges.length; i++) {
var e = vor.edges[i];
if (e == undefined) continue;
var e0 = vxids[e[0]];
var e1 = vxids[e[1]];
if (e0 == undefined) {
e0 = vxs.length;
vxids[e[0]] = e0;
vxs.push(e[0]);
}
if (e1 == undefined) {
e1 = vxs.length;
vxids[e[1]] = e1;
vxs.push(e[1]);
}
adj[e0] = adj[e0] || [];
adj[e0].push(e1);
adj[e1] = adj[e1] || [];
adj[e1].push(e0);
edges.push([e0, e1, e.left, e.right]);
tris[e0] = tris[e0] || [];
if (!tris[e0].includes(e.left)) tris[e0].push(e.left);
if (e.right && !tris[e0].includes(e.right)) tris[e0].push(e.right);
tris[e1] = tris[e1] || [];
if (!tris[e1].includes(e.left)) tris[e1].push(e.left);
if (e.right && !tris[e1].includes(e.right)) tris[e1].push(e.right);
}
var mesh = {
pts: pts,
vxs: vxs,
adj: adj,
tris: tris,
edges: edges,
extent: extent
}
mesh.map = function (f) {
var mapped = vxs.map(f);
mapped.mesh = mesh;
return mapped;
}
return mesh;
}
I've been trying to solve this for a while now and have finally made some progress here on observablehq:
https://observablehq.com/#folcon/original-code-by-martin-oleary-mewo2
I'm assessing how well it works by comparing the rendered images:
I want to produce these smooth colour transitions, which requires a correct mapping between vertices, heightmap and triangles.
Attempt 1:
Well I got something rendered, if my approach below doesn't work, it might be instructive to come back to this.
Attempt 2:
This one I feel is a lot better, I don't appear to be getting extra triangles (no black ones visible), but the issue appears to be I'm rendering in the wrong order. I was attempting to go from the leftmost point and then walk around the cell edges, this seems like the right idea, but the ordering is wrong...
I've dug into the delunator's guide to datastructures and at this point I feel like I'm pretty close, but missing something obvious.
Useful Notes / Assumptions:
The mesh uses the edges of the cells, not the triangles for "edges", if you look at the adj (adjacencies) it's never higher than 3, which is in keeping with using the cells as each vertex of the cell has no more than 3 neighbours.
Given that an edge is an edge of a cell, the left and the right of the edge should therefore be the two cells that edge sits on.
Hopefully that's clear.
To answer #thclark:
The Output Data Structure:
{
pts: pts,
vxs: vxs,
adj: adj,
tris: tris,
edges: edges,
extent: extent
}
pts is the original points array, which is a list of pairs of [x, y] coordinates.
vxs is the vertices of the cell as pairs of [x, y] coordinates. The points here are unique, and their index in the array is the authoritative id for that vertex.
I will use one of redblobgame's images to clarify:
The red dots in this image are the original points, the blue one's are the vertices of the cell.
edges is are comprised of 4 values, the first two are the indexes of the vertices that make up the edges of a cell, which in the image above are in white. The second two are the circumcenter's corresponding left and right cells which I've illustrated in an image above, but hopefully this will be clearer:
adj is a mapping from a vertex index to the other vertex index's which are connected to it, because we're using the cells, no entry in adj should have more than 3 indexes. As you can see in the image below, each vertex (red), can only have 3 neighbours (green).
tris is an array of triangles, the original data-structure does not always have complete triangles, but they indexed by the vertex index, to the circumcenter's corresponding left and right cells.
You can see in the above image, that by combining the left and right circumcenter's of three edges, it describes a triangle.
The Input Data Structure:
Delaunator's data-structures guide has a lot more detail, but a quick overview is this:
Delaunator takes an array of [x, y] coordinates of length N and makes a points array of length 2N where each coordinate in the original [x, y] array now sits at it's original index * 2 if it was the x coord, and * 2 + 1 if it was the y.
For example, the coords [[1, 2], [3, 4], [5, 6]] would become: [1, 2, 3, 4, 5, 6].
It then builds a delaunay triangulation, where each triangle edge is comprised of two halfedge's.
A halfedge takes a bi-directional edge and splits it into two directional edges.
So a triangle made up of 3 edges, now has 6 halfedges like so:
It also constructs two arrays:
delaunay.triangles which takes a halfedge index and returns the point id (an index into the points array described previously) where the halfedge begins.
delaunay.halfedges which takes a halfedge index and returns the opposite halfedge in the adjacent triangle:
Hopefully that's sufficient detail?
I've tried to make the setup runnable, so if someone wants to poke around with it to test out a quick hypothesis, they can do so easily, just edit the notebook or fork it.
I've also added at the bottom a more complete example that's purely focused on the "physical" things the map derives from the mesh+heightmap, which is basically the coastline and rivers.

Is there an algorithm to separate polygons that share an edge?

I have a list of vertices that define a polygon, a list of perimeter edges that connect those vertices and define the outline of the polygon, and a list of inner edges connecting vertices, effectively splitting the polygon. None of the edges intersect each other (they only meet at the start and end points).
I want to partition the bigger polygon into its smaller components by splitting it at the inner edges. Basically I need to know which sets of vertices are part of a polygon that has no intersecting edges.
Basically, this is the information I have:
The vertices [0, 1, 3, 5, 7, 9, 11, 13, 15] define the outside perimeter of my polygon and the blue edge (5, 13) is an inner edge splitting that perimeter into two polygons. (Disregard the horizontal purple lines, they are the result of the trapezoidalization of the polygon to find the edge (5, 13). They have no further meaning)
This is what I want to get to:
One polygon is defined by the vertices [0, 1, 3, 5, 13, 15] and the other is defined by [5, 7, 9, 11, 13].
I need a solution that works for any number of splitting inner edges.
In the end, I would like to be able to partition a polygon like the following:
The sub-polygons are not necessarily convex. That might be of importance for any algorithm depending on it. The red sub-polygon has a concave vertex (13) for example.
My idea initially was to traverse the inside of each sub-polygon in a clockwise or counter-clockwise direction, keeping track of the vertices I encounter and their order. However I am having trouble finding a starting edge/vertex and guaranteeing that the next cw or ccw point is in fact on the inside of that sub-polygon I want to extract.
I tried to google for solutions but this is a field of mathematics I am too unfamiliar with to know what to search for. An idea/algorithm of how to tackle this problem would be much appreciated!
(I don't need any actual code, just an explanation of how to do this or pseudocode would totally suffice)
Now, unfortunately I don't have some code to show as I need a concept to try out first. I don't know how to tackle this problem and thus can't code anything that could accomplish what I need it to do.
EDIT:
This is just one step in what I am trying to do ultimately, which is polygon triangulation. I have read numerous solutions to that problem and wanted to implement it via trapezoidalization to get monotone polygons and ultimately triangulate those. This is basically the last step of (or I guess the next step after) the trapezoidalization, which is never explained in any resources on the topic that i could find.
The end result of the trapezoidalization are the inner edges which define the split into (in my case vertically monotone) polygons. I just need to split the polygons along those edges "datastructurally" so I can work on the subpolygons individually. I hope that helps to clarify things.
The key of the algorithm that you need, is to know how edges can be ordered:
Finding out which is the next edge in (counter)clockwise order
You can calculate the absolute angle, of an edge from node i to node j, with the following formula:
atan2(jy-iy, jx-ix)
See atan2
The relative angle between an edge (i, j) and (j, k) is then given by:
atan2(ky-jy, kx-jx) - atan2(jy-iy, jx-ix)
This expression may yield angles outside of the [-𝜋, 𝜋] range, so you should map the result back into that range by adding or subtracting 2𝜋.
So when you have traversed an edge (i, j) and need to choose the next edge (j, k), you can then pick the edge that has the smallest relative angle.
The Algorithm
The partitioning algorithm does not really need to know upfront which edges are internal edges, so I'll assume you just have an edge list. The algorithm could look like this:
Create an adjacency list, so you have a list of neighboring vertices for every vertex.
Add every edge to this adjacency list in both directions, so actually adding two directed edges for each original edge
Pick a directed edge (i, j) from the adjacency list, and remove it from there.
Define a new polygon and add vertex i as its first vertex.
Repeat until you get back to the first vertex in the polygon that's being constructed:
add vertex j to the polygon
find vertex k among the neighbors of vertex j that is not vertex i, and which minimizes the relative angle with the above given formula.
add this angle to a sum
Delete this directed edge from the neighbors of vertex j, so it will never be visited again
let i = j, j = k
If the sum of angles is positive (it will be 2𝜋) then add the polygon to the list of "positive" polygons, otherwise (it will be -2𝜋) add it to an alternative list.
Keep repeating from step 2 until there are no more directed edges in the adjacency list.
Finally you'll have two lists of polygons. One list will only have one polygon. This will be the original, outer polygon, and can be ignored. The other list will have the partitioning.
As a demo, here is some code in a runnable JavaScript snippet. It uses one of the examples you pictured in your question (but will consecutive vertex numbering), finds the partitioning according to this algorithm, and shows the result by coloring the polygons that were identified:
function partition(nodes, edges) {
// Create an adjacency list
let adj = [];
for (let i = 0; i < nodes.length; i++) {
adj[i] = []; // initialise the list for each node as an empty one
}
for (let i = 0; i < edges.length; i++) {
let a = edges[i][0]; // Get the two nodes (a, b) that this edge connects
let b = edges[i][1];
adj[a].push(b); // Add as directed edge in both directions
adj[b].push(a);
}
// Traverse the graph to identify polygons, until none are to be found
let polygons = [[], []]; // two lists of polygons, one per "winding" (clockwise or ccw)
let more = true;
while (more) {
more = false;
for (let i = 0; i < nodes.length; i++) {
if (adj[i].length) { // we have unvisited directed edge(s) here
let start = i;
let polygon = [i]; // collect the vertices on a new polygon
let sumAngle = 0;
// Take one neighbor out of this node's neighbor list and follow a path
for (let j = adj[i].pop(), next; j !== start; i = j, j = next) {
polygon.push(j);
// Get coordinates of the current edge's end-points
let ix = nodes[i][0];
let iy = nodes[i][1];
let jx = nodes[j][0];
let jy = nodes[j][1];
let startAngle = Math.atan2(jy-iy, jx-ix);
// In the adjacency list of node j, find the next neighboring vertex in counterclockwise order
// relative to node i where we came from.
let minAngle = 10; // Larger than any normalised angle
for (let neighborIndex = 0; neighborIndex < adj[j].length; neighborIndex++) {
let k = adj[j][neighborIndex];
if (k === i) continue; // ignore the reverse of the edge we came from
let kx = nodes[k][0];
let ky = nodes[k][1];
let relAngle = Math.atan2(ky-jy, kx-jx) - startAngle; // The "magic"
// Normalise the relative angle to the range [-PI, +PI)
if (relAngle < -Math.PI) relAngle += 2*Math.PI;
if (relAngle >= Math.PI) relAngle -= 2*Math.PI;
if (relAngle < minAngle) { // this one comes earlier in counterclockwise order
minAngle = relAngle;
nextNeighborIndex = neighborIndex;
}
}
sumAngle += minAngle; // track the sum of all the angles in the polygon
next = adj[j][nextNeighborIndex];
// delete the chosen directed edge (so it cannot be visited again)
adj[j].splice(nextNeighborIndex, 1);
}
let winding = sumAngle > 0 ? 1 : 0; // sumAngle will be 2*PI or -2*PI. Clockwise or ccw.
polygons[winding].push(polygon);
more = true;
}
}
}
// return the largest list of polygons, so to exclude the whole polygon,
// which will be the only one with a winding that's different from all the others.
return polygons[0].length > polygons[1].length ? polygons[0] : polygons[1];
}
// Sample input:
let nodes = [[59,25],[26,27],[9,59],[3,99],[30,114],[77,116],[89,102],[102,136],[105,154],[146,157],[181,151],[201,125],[194,83],[155,72],[174,47],[182,24],[153,6],[117,2],[89,9],[97,45]];
let internalEdges = [[6, 13], [13, 19], [19, 6]];
// Join outer edges with inner edges to an overall list of edges:
let edges = nodes.map((a, i) => [i, (i+1)%nodes.length]).concat(internalEdges);
// Apply algorithm
let polygons = partition(nodes, edges);
// Report on results
document.querySelector("div").innerHTML =
"input polygon has these points, numbered 0..n<br>" +
JSON.stringify(nodes) + "<br>" +
"resulting polygons, by vertex numbers<br>" +
JSON.stringify(polygons)
// Graphics handling
let io = {
ctx: document.querySelector("canvas").getContext("2d"),
drawEdges(edges) {
for (let [a, b] of edges) {
this.ctx.moveTo(...a);
this.ctx.lineTo(...b);
this.ctx.stroke();
}
},
colorPolygon(polygon, color) {
this.ctx.beginPath();
this.ctx.moveTo(...polygon[0]);
for (let p of polygon.slice(1)) {
this.ctx.lineTo(...p);
}
this.ctx.closePath();
this.ctx.fillStyle = color;
this.ctx.fill();
}
};
// Display original graph
io.drawEdges(edges.map(([a,b]) => [nodes[a], nodes[b]]));
// Color the polygons that the algorithm identified
let colors = ["red", "blue", "silver", "purple", "green", "brown", "orange", "cyan"];
for (let polygon of polygons) {
io.colorPolygon(polygon.map(i => nodes[i]), colors.pop());
}
<canvas width="400" height="180"></canvas>
<div></div>

Terrain Quadtree LOD can't detect out where T-junctions appears that generates cracks

I implemented a Quadtree that stores a single mesh of data with different LOD levels, where each four children have their own vertices and their corresponding indexes to the four corners(x0, y0, x1, y1 -> from top to bottom) that makes up their corresponding LOD level.
QuadTree.prototype.fillTree = function(currentNode, depth, currentBox, currentIndex) {
if(depth === 0 || !currentBox.checkPartition())
return;
let node = {
"vertices" : [],
"lines" : [],
"children" : [],
"box" : currentBox
};
currentNode.push(node);
currentBox.getVerticesCoord(this.cols, currentIndex).forEach((coord) => {
this.getPlaneVertices(node.vertices, coord);
});
currentBox.getLinesCoord(this.cols, currentIndex).forEach((coord) => {
this.getPlaneVertices(node.lines, coord);
});
currentBox.getPartitions().forEach((box, index) => {
this.fillTree(node["children"], depth-1, box, index);
});
};
I have also a checkFrustumBoundaries method where I calculate the minimum distance between a LOD level and the camera location [0, 0, 0] and also checks if it's visible by being within [-1, 1] range for all the coordinates by being multiplied by the projection matrix and divided by w.
Finally I have the method that selects needed LOD levels for the current state by finding the minimum distance between camera origins and their corresponding depth and all 4 children being checked if they are within the visible zone and store them into an array that will be rendered.
Note: That I want children to inherit the depth of their sibling with lowest Complexity level if he is ready to be rendered, thus I will always have a 4 four square with same LOD level.
QuadTree.prototype.readComplexity = function(projection, viewCamera, currentNode, currentIndex, currentDepth = 1) {
let childrenToBeRendered = [];
let minDepth = this.depth;
currentNode.children.forEach((child, index) => {
let frustumBoundaries = this.checkFrustumBoundaries(child.vertices, projection, viewCamera);
if(frustumBoundaries.withinFrustum) {
//Section is the 1.0/this.depth
let depth = this.depth-Math.ceil(frustumBoundaries.minDistance/this.section);
minDepth = Math.min(minDepth, depth);
childrenToBeRendered.push({
"child" : child,
"index" : index
});
}
});
childrenToBeRendered.forEach((child => {
if(minDepth <= currentDepth) {
//Even complexity, or the others quarters inherits on of their siblings with lowest complexity level
this.fetchLines(projection, viewCamera, child.child, currentDepth, child.index);
} else if(minDepth > currentDepth) {
//Needs more complexity because it's too near to camera origins
this.readComplexity(projection, viewCamera, child.child, child.index, currentDepth+1);
}
}));
};
But here I stumbled on the biggest problem, it appears T-junctions are causing cracks between different LOD levels:
I figured out that I could remove the cracks by disabling the vertices that make up the T-junction by using a stack and append to it the 2 vertices that makes a half diamond and use the following child where I use his vertex whose index is different from the previous two used. By cycling, starting from the top-left to top-right, bottom-right, bottom-left with a flag in case there is a LOD difference between the top-right and the left neighbor of him.
But before doing that, I need to know if the child's neighbors have less complexity or equal, thus if the child let's say is at top-left, I need to know if there is a LOD level at left and top that takes four times more space and by logic has less complexity.
How can I manage to do it, how can I reach for neighbors if they can be located at different quad-tree levels, if I try to use the node's box to generate the two neighbors' boxes and calculate their depth, I can't compare them with the node because during the selection process, there is the possibility that the neighbor inherited his siblings depth, thus the comparison will be wrong. But, if I chose to not use the rule of four, consequently I can't use the tactics of diamond selection I mentioned above.

Kendo UI Zoom Lock Y Axis Not Working

I am trying to lock the Y axis when zooming in Kendo UI. According to the docs, I should be able to do it like this:
zoomable: { mousewheel: { lock: "y" } }
But somehow it is not working, even the demo in Kendo has the same problem, see
http://demos.telerik.com/kendo-ui/bar-charts/pan-and-zoom
When I zoom in, you can see y axis max change from 12 to 10 and lower.
Am I understanding the docs correctly or there is a bug with kendo?
Thank you!
You can see that if you zoom on a bar with a height of 10, the y axis max remains at 12.
The behavior you're seeing is not zooming but changing the axis max to better suit the heights of the bars you see in the view. You can see the difference in the fact that the minimum value of the axis remains 0 but when you enable zooming in the y axis, the minimum value also changes.
You can avoid this behavior by setting a permanent axis max value:
valueAxis: {
max: number
}
The reason for this behavior is that if you add/remove/change your values, you'll want the grid to accommodate the new bars and not have them exceed the top of the chart. Setting the max y axis value is a good idea only if your data isn't going to change after the chart is shown for the first time.
The y-axis is not zooming, rather the max value is being auto-calculated depending on which bars are currently visible. You can avoid this be setting the valueAxis.max value. In the example, add
valueAxis:{
max: 12
},
The answer of setting the valueAxis.max is nearly there. But the issue I have with this is when you reach maximum zoom decimals appear because the y-axis changes based on which bars are currently visible. If I do a valueAxis.labels.format: '{0:0}', yes the decimals are gone but on maximum zoom it would appear that the bars are not leveled correctly to the y-axis. In fact the bar height/level is correct, the y-axis label is wrong, it's missing a decimal. The key is setting valueAxis.majorUnit. To demostrate, here is my options for a Kendo Chart for jQuery. Comment/uncomment the valueAxis.labels and valueAxis.majorUnit to see the difference.
{
series: [{
name: 'Grand Total',
data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
type: 'column',
}],
zoomable: {
mousewheel: {
lock: 'y',
},
selection: {
lock: 'y',
},
},
pannable: {
lock: 'y',
},
valueAxis: {
max: 12,
// labels: {
// format: '{0:0}',
// },
majorUnit: 1,
},
categoryAxis: {
categories: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
},
}
Hope this helps.

Resources