<div id="paper-container"></div>
<label>
  <span>Isometric Transformation:</span>
  <input type="checkbox" id="isometric-switch" checked />
</label>
<a target="_blank" href="https://www.jointjs.com">
  <img id="logo" src="https://assets.codepen.io/7589991/jj-logo-red.svg" width="200" height="50"></img>
</a>
#paper-container {
  position: absolute;
  right: 0;
  top: 0;
  left: 0;
  bottom: 0;
}

#logo {
  position: absolute;
  bottom: 20px;
  right: 0;
}

label {
  position: absolute;
  top: 30px;
  right: 30px;
  font-family: sans-serif;
}

label input {
 vertical-align: text-top;
}

const { dia, shapes, util } = joint;

const GRID_SIZE = 20;
const GRID_COUNT = 12;
// The value which ensures visibility of elements in the Z direction
const PAPER_Z_OFFSET = GRID_SIZE * 4;

// Matrix of the isometric transformation and its parameters

const SCALE = 1;
const ISOMETRIC_SCALE = 0.8602;
const ROTATION_DEGREES = 30;

const transformationMatrix = () => {
    return V.createSVGMatrix()
        .translate(GRID_COUNT * GRID_SIZE * SCALE * ISOMETRIC_SCALE + GRID_SIZE, PAPER_Z_OFFSET + GRID_SIZE)
        .rotate(ROTATION_DEGREES)
        .skewX(-ROTATION_DEGREES)
        .scaleNonUniform(SCALE, SCALE * ISOMETRIC_SCALE);
};

// Here we are specifying element markup and 
// constant properties of markup parts
const IsometricElement = dia.Element.define(
    'IsometricElement',
    {
        attrs: {
            top1: {
                strokeWidth: 2,
                stroke: '#333333',
                fill: '#ff0000',
                fillOpacity: 0.7,
            },
            top2: {
                strokeWidth: 2,
                stroke: '#333333',
                fill: '#ff0000',
                fillOpacity: 0.7,
            },
            side1: {
                strokeWidth: 2,
                stroke: '#333333',
                fill: '#ffff00',
                fillOpacity: 0.7,
            },
            side2: {
                strokeWidth: 2,
                stroke: '#333333',
                fill: '#ffff00',
                fillOpacity: 0.7
            },
            front1: {
                strokeWidth: 2,
                stroke: '#333333',
                fill: '#0000ff',
                fillOpacity: 0.7,
            },
            front2: {
                strokeWidth: 2,
                stroke: '#333333',
                fill: '#0000ff',
                fillOpacity: 0.7,
            }
        }
    },
    {
        markup: util.svg/* xml */ `
      <polygon @selector="top1"></polygon>
      <polygon @selector="top2"></polygon>
      <polygon @selector="side1"></polygon>
      <polygon @selector="side2"></polygon>
      <polygon @selector="front1"></polygon>
      <polygon @selector="front2"></polygon>
    `
    }
);

// converting dimension parameters into path properties of markup parts
// so the element can be created in a more flexible way

const createIsometricElement = (properties) => {
    const d = {
        x: properties.size.width,
        y: properties.size.height,
        z: properties.isometricHeight
    };
    properties.attrs = properties.attrs || {};
    properties.attrs.top1 = {
        points: `0,0 ${d.x},0 ${d.x},${GRID_SIZE} ${d.x - GRID_SIZE},${GRID_SIZE} ${d.x - GRID_SIZE},${d.y} 0,${d.y}`,
        transform: `translate(${-d.z}, ${-d.z})`
    };
    properties.attrs.top2 = {
        points: `${d.x - GRID_SIZE},${GRID_SIZE} ${d.x},${GRID_SIZE} ${d.x},${d.y} ${d.x - GRID_SIZE},${d.y}`,
        transform: `translate(${-d.z + GRID_SIZE * 2}, ${-d.z + GRID_SIZE * 2})`
    };
    properties.attrs.side1 = {
        points: `0,0 ${d.x - GRID_SIZE},0 ${d.x - GRID_SIZE},${GRID_SIZE * 2} ${d.x},${GRID_SIZE * 2} ${d.x},${d.z} 0,${d.z}`,
        transform: V.matrixToTransformString(
            new DOMMatrixReadOnly()
                .translate(-d.z, -d.z + d.y)
                .skewX(45)
        )
    };
    properties.attrs.side2 = {
        points: `0,0 ${GRID_SIZE},0 ${GRID_SIZE},${GRID_SIZE * 2} 0,${GRID_SIZE * 2}`,
        transform: V.matrixToTransformString(
            new DOMMatrixReadOnly()
                .translate(-d.z + d.x - GRID_SIZE, -d.z + GRID_SIZE)
                .skewX(45)
        )
    };
    properties.attrs.front1 = {
        points: `0,0 ${d.z},0 ${d.z},${d.y} ${GRID_SIZE * 2},${d.y}, ${GRID_SIZE * 2},${GRID_SIZE} 0,${GRID_SIZE}`,
        transform: V.matrixToTransformString(
            new DOMMatrixReadOnly()
                .translate(-d.z + d.x, -d.z)
                .skewY(45)
        )
    },
    properties.attrs.front2 = {
        points: `0,0 ${GRID_SIZE * 2},0 ${GRID_SIZE * 2},${d.y - GRID_SIZE} 0,${d.y - GRID_SIZE}`,
        transform: V.matrixToTransformString(
            new DOMMatrixReadOnly()
                .translate(-d.z + d.x - GRID_SIZE, -d.z + GRID_SIZE)
                .skewY(45)
        )
    };

    return new IsometricElement(properties);
};

// Paper

const cellNamespace = { ...shapes, IsometricElement };

const graph = new dia.Graph({}, { cellNamespace });

const paper = new dia.Paper({
    el: document.getElementById('paper-container'),
    model: graph,
    restrictTranslate: {
        x: 0,
        y: 0,
        width: GRID_SIZE * GRID_COUNT,
        height: GRID_SIZE * GRID_COUNT
    },
    width: '100%',
    height: '100%',
    gridSize: GRID_SIZE,
    async: true,
    autoFreeze: true,
    sorting: dia.Paper.sorting.APPROX,
    cellViewNamespace: cellNamespace
});

// Make the paper isometric by applying the isometric matrix to all
// SVG content it contains.
paper.matrix(transformationMatrix());

// Add isometric element to the graph.
// You can specify dimensions of the element using element's size and additional z parameter

const element = createIsometricElement({
    isometricHeight: GRID_SIZE * 4,
    size: { width: GRID_SIZE * 3, height: GRID_SIZE * 6 },
    position: { x: GRID_SIZE * 6, y: GRID_SIZE * 6 }
});

element.addTo(graph);

// A function to draw the grid.

drawGrid(paper);

function drawGrid(paper) {
    const gridData = [];
    const j = GRID_COUNT;
    for (let i = 0; i <= j; i++) {
        gridData.push(`M 0,${i * GRID_SIZE} ${j * GRID_SIZE},${i * GRID_SIZE}`);
        gridData.push(`M ${i * GRID_SIZE},0 ${i * GRID_SIZE},${j * GRID_SIZE}`);
    }

    const gridEl = V('path').attr({
        d: gridData.join(' '),
        fill: 'none',
        stroke: 'lightgray'
    }).node;

    // When the grid is appended to one of the paper's layer, it gets automatically transformed
    // by the isometric matrix
    paper.getLayerNode(dia.Paper.Layers.BACK).append(gridEl);
}

// Add switch to toggle the isometric view with 2d for demonstration purposes

document
    .getElementById('isometric-switch')
    .addEventListener('change', (evt) => {
        if (evt.target.checked) {
            paper.matrix(transformationMatrix());
        } else {
            paper.matrix(
                V.createSVGMatrix().translate(
                    GRID_SIZE * GRID_COUNT,
                    PAPER_Z_OFFSET + GRID_SIZE
                )
            );
        }
    });

External CSS

  1. https://cdn.jsdelivr.net/npm/jointjs@3.7.2/dist/joint.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js
  4. https://cdn.jsdelivr.net/npm/jointjs@3.7.2/dist/joint.min.js