Cats Juice

Posted on

# Build a mesh 3D bar chart using SVG

First, you can check out the results of my application in production: Playground of SSR-Contributions-svg.

## Draw a basic cube

Think of it as a combination of multiple geometric figures placed in a Cartesian coordinate system，it may looks like(Note that in the coordinate system of svg, the positive direction of the y-axis is downwards):

Define the basic concepts of the vertices and faces as shown in the diagram below. We use vertex O as the origin of the cube.

Here, the perspectives is actually decided by the top face's shape. So we need to modify the perspectives by defining the length of the two diagonals in the horizontal and vertical directions.

Then we can get the coordinates of all the points(Suppose the coordinates of the origin O are 'ox', 'oy'):

Start coding with defining variables(write inside the `<script>` tag of a html file):

``````const sizeX = 20;
const ratio = 2;
const sizeY = sizeX / ratio;
const height = 40;
const ox = 100;
const oy = 100;
const pA = [ox - sizeX, oy + sizeY];
const pB = [ox, oy + 2 * sizeY];
const pC = [ox + sizeX, oy + sizeY];
const pD = [ox - sizeX, oy - height + sizeY];
const pE = [ox, oy - height + 2 * sizeY];
const pF = [ox + sizeX, oy - height + sizeY];
const pG = [ox, oy - height];
const pO = [ox, oy];
const face_l = [pE, pD, pA, pB];
const face_r = [pE, pF, pC, pB];
const face_t = [pE, pD, pG, pF];
``````

We will draw the geometric surface via the path tag of svg，so first define a method for generating critical paths to make it easier to write the code:

``````function d(points) {
const raw = points
.map((point) => `\${point[0]} \${point[1]}`)
.join(' ');
return "M" + raw + "z";
}
``````

Then we can get code of svg, and write it to document:

``````const svg = `<svg width="200" height="200">
<path d="\${d(face_l)}" stroke="#000" fill="transparent" />
<path d="\${d(face_r)}" stroke="#000" fill="transparent" />
<path d="\${d(face_t)}" stroke="#000" fill="transparent" />
</svg>`;
document.write(svg);
``````

## Draw a grid of cubes

Define a grid by specify rowNum and colNum:

Assume that the origin of svg lies at 0, 0 of the grid(yellow block), we have to move whole grid horizontally. And set `maxBarHeight = 100`.

The grid width should be : `sizeX * (rowNum + colNum)` , the height should be: `sizeY * (rowNum + colNum) + maxBarHeight` , the whole grid's `translateX` and `translateY` should be : `translateX = rowNum * sizeX` , `translateY = maxBarHeight`.

Define a function to draw basic cube(Just like mentioned above, but `ox`, `oy`, `height` should be passed as function's parameters):

``````function cube(ox, oy, height) {
const pO = [ox, oy];
const pA = [ox - sizeX, oy + sizeY];
const pB = [ox, oy + 2 * sizeY];
const pC = [ox + sizeX, oy + sizeY];
const pD = [ox - sizeX, oy - height + sizeY];
const pE = [ox, oy - height + 2 * sizeY];
const pF = [ox + sizeX, oy - height + sizeY];
const pG = [ox, oy - height];
const face_l = [pE, pD, pA, pB];
const face_r = [pE, pF, pC, pB];
const face_t = [pE, pD, pG, pF];

return `<g>
<path d="\${d(face_l)}" stroke="#000" fill="#555" />
<path d="\${d(face_r)}" stroke="#000" fill="#888" />
<path d="\${d(face_t)}" stroke="#000" fill="#aaa" />
</g>`;
}
``````

Define a function to coordinate `rowIndex` and `colIndex` to pixels :

``````function coordIndex(rowIndex, colIndex) {
return [
(rowIndex - colIndex) * sizeX,
(rowIndex + colIndex) * sizeY
];
}
``````

Generate random data:

``````const colNum = 6; // num of cols
const rowNum = 4; // num of rows
const cubes = [];
for (let i = 0; i < colNum; i++) {
for (let j = 0; j < rowNum; j++) {
cubes.push([
i,
j,
Math.floor(Math.random() * maxBarHeight)
]);
}
}
``````

Generate svg code:

``````// render
const svg = `<svg width="\${svgWidth}" height="\${svgHeight}">
<g transform="translate(\${translateX}, \${translateY})">
\${cubes.map((opt) => cube(
...coordIndex(...opt),
opt[2]
)).join('')}
</g>
</svg>`;
``````

## Final codes on codepen:

https://codepen.io/catsjuice/pen/MWVqNdQ