DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Build a mesh 3D bar chart using SVG
Cats Juice
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

Let's start with one of the most basic cube.

Image description

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):

Basic cube

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.

Basic concepts

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.

Control perspectives

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

Points coordinates

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];
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Draw a grid ofΒ cubes

Define a grid by specify rowNum and colNum:

Mesh

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>`;
}
Enter fullscreen mode Exit fullscreen mode

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

function coordIndex(rowIndex, colIndex) {
    return [
        (rowIndex - colIndex) * sizeX, 
        (rowIndex + colIndex) * sizeY
    ];
}
Enter fullscreen mode Exit fullscreen mode

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)
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

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>`;
Enter fullscreen mode Exit fullscreen mode

Final codes on codepen:

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

Top comments (0)

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.