Geogebra Substitutes: Konva and JsxGraph

I am having problems with geogebra, and thus looked for substitutes.
I found two js libraries KONVA: https://cdn.jsdelivr.net/npm/konva@8/konva.min.js
and JSXGraph https://cdn.jsdelivr.net/npm/jsxgraph/distrib/jsxgraphcore.mjs

of the two, Konva was somewhat easier to handle (although getting the scripts to load proved a pain!)
I am appending the same simplistic vector decomposition example for both for comparison.

  • KONVA
class VectorDecomposition(JSComponent):
    # Parameters
    s1       = param.List(default=[1, 0], doc="First basis vector (dx, dy).")
    s2       = param.List(default=[0, 1], doc="Second basis vector (dx, dy).")
    b        = param.List(default=[3, 5], doc="Vector to decompose (dx, dy).")
    fixed    = param.Boolean(default=False, doc="Disallow moving endpoints of basis vectors.")
    vec_name = param.String(default="s", doc="Base name for the basis vectors (e.g., 's' or 'e').")
    offset   = param.List(default=[100, 100], doc="Offset from the bottom-left corner for the origin (dx, dy).")

    _importmap = {
        "imports": {  # does not seem to work: load in html cell instead!
            "konva": "https://cdn.jsdelivr.net/npm/konva@8/konva.min.js",
        }
    }

    _esm = r"""
export function render({ model, el }) {
  if (typeof window.Konva === 'undefined') {
    throw new Error('Konva is not loaded correctly!');
  }
  const Konva = window.Konva;

  // Create container div
  const container = document.createElement('div');
  container.style.width = '100%';
  container.style.height = '100%';
  el.appendChild(container);

  // Initialize Konva stage
  const stage = new Konva.Stage({
    container: container,
    width: 400,
    height: 400,
  });

  const offsetX = model.offset[0];
  const offsetY = model.offset[1];
  const margin = 50; // Margin for padding around the plot

  const originX = offsetX + margin;
  const originY = stage.height() - offsetY - margin;

  // Extract parameters
  const s1 = model.s1;
  const s2 = model.s2;
  const b = model.b;
  const fixed = model.fixed;

  // Check for vec_name existence
  const vec_name = model.vec_name || "s"; // Fallback to "s" if undefined

  // Calculate dynamic scaling factor
  const maxMagnitude = Math.max(
    Math.sqrt(b[0] ** 2 + b[1] ** 2),
    Math.sqrt(s1[0] ** 2 + s1[1] ** 2),
    Math.sqrt(s2[0] ** 2 + s2[1] ** 2)
  );
  const availableSpace = Math.min(stage.width() - 2 * margin, stage.height() - 2 * margin);
  const scale = availableSpace / (maxMagnitude * 2); // Scale down to fit comfortably

  // Layers
  const gridLayer = new Konva.Layer();
  const vectorLayer = new Konva.Layer();
  const controlLayer = new Konva.Layer();
  stage.add(gridLayer, vectorLayer, controlLayer);

  // ============================================== Helper Functions
  function drawGrid() {
    gridLayer.find('Line').forEach(line => line.remove()); // Clear grid

    const gridSize = 10; // Number of steps in the grid in each direction

    for (let i = -gridSize; i <= gridSize; i++) {
      for (let j = -gridSize; j <= gridSize; j++) {
        const x = originX + i * s1[0] * scale + j * s2[0] * scale;
        const y = originY - (i * s1[1] * scale + j * s2[1] * scale);

        const parallelToS1 = new Konva.Line({
          points: [x, y, x + s1[0] * scale, y - s1[1] * scale],
          stroke: '#ddd',
          strokeWidth: 1,
        });

        const parallelToS2 = new Konva.Line({
          points: [x, y, x + s2[0] * scale, y - s2[1] * scale],
          stroke: '#ddd',
          strokeWidth: 1,
        });

        gridLayer.add(parallelToS1);
        gridLayer.add(parallelToS2);
      }
    }

    gridLayer.draw();
  }
  // ========================================================================
  function addLabel(layer, text, x, y, color = 'black') {
    const label = new Konva.Text({
      text: text,
      x: x + 10, // Offset slightly to avoid overlap
      y: y - 10,
      fontSize: 16,
      fontFamily: 'Arial',
      fill: color,
    });
    layer.add(label);
  }
  // ========================================================================
  function drawVectors() {
    vectorLayer.find('Arrow, Line, Text').forEach(el => el.remove()); // Clear previous vectors and labels

    const s1EndX = originX + s1[0] * scale;
    const s1EndY = originY - s1[1] * scale;
    const s2EndX = originX + s2[0] * scale;
    const s2EndY = originY - s2[1] * scale;

    const c = "#3B3651";

    const basisVector1 = new Konva.Arrow({
      points: [originX, originY, s1EndX, s1EndY],
      pointerLength: 10,
      pointerWidth: 10,
      fill: c,
      stroke: c,
      strokeWidth: 3,
    });

    const basisVector2 = new Konva.Arrow({
      points: [originX, originY, s2EndX, s2EndY],
      pointerLength: 10,
      pointerWidth: 10,
      fill: c,
      stroke: c,
      strokeWidth: 3,
    });

    addLabel(vectorLayer, `${vec_name}_1`, s1EndX, s1EndY, 'black');
    addLabel(vectorLayer, `${vec_name}_2`, s2EndX, s2EndY, 'black');

  // Target vector (b)
  const bEndX = originX + b[0] * scale;
  const bEndY = originY - b[1] * scale;

  const targetVector = new Konva.Arrow({
    points: [originX, originY, bEndX, bEndY],
    pointerLength: 10,
    pointerWidth: 10,
    fill: 'blue',
    stroke: 'blue',
    strokeWidth: 3,
  });


  // Add label for b
  addLabel(vectorLayer, 'b', bEndX, bEndY, 'blue');


    const det = s1[0] * s2[1] - s1[1] * s2[0];
    const alpha = (b[0] * s2[1] - b[1] * s2[0]) / det;
    const beta = (b[1] * s1[0] - b[0] * s1[1]) / det;

    const alphaX = originX + alpha * s1[0] * scale;
    const alphaY = originY - alpha * s1[1] * scale;
    const betaX = originX + beta * s2[0] * scale;
    const betaY = originY - beta * s2[1] * scale;

    const alphaVector = new Konva.Arrow({
      points: [originX, originY, alphaX, alphaY],
      pointerLength: 10,
      pointerWidth: 10,
      fill: 'orange',
      stroke: 'orange',
      strokeWidth: 2,
    });

    const betaVector = new Konva.Arrow({
      points: [originX, originY, betaX, betaY],
      pointerLength: 10,
      pointerWidth: 10,
      fill: 'orange',
      stroke: 'orange',
      strokeWidth: 2,
    });

    const alphaToB = new Konva.Line({
      points: [alphaX, alphaY, bEndX, bEndY],
      stroke: 'orange',
      strokeWidth: 2,
      dash: [10, 5],
    });

    const betaToB = new Konva.Line({
      points: [betaX, betaY, bEndX, bEndY],
      stroke: 'orange',
      strokeWidth: 2,
      dash: [10, 5],
    });

    vectorLayer.add(alphaToB, betaToB, alphaVector, betaVector, basisVector1, basisVector2, targetVector);
    vectorLayer.add(basisVector1, basisVector2);
    vectorLayer.add(targetVector);

    addLabel(vectorLayer, `${vec_name}_1`, s1EndX, s1EndY, 'black');
    addLabel(vectorLayer, `${vec_name}_2`, s2EndX, s2EndY, 'black');
    addLabel(vectorLayer, 'b', bEndX, bEndY, 'blue');
    //addLabel(vectorLayer, `α=${alpha.toFixed(2)}`, alphaX, alphaY, 'orange');
    //addLabel(vectorLayer, `β=${beta.toFixed(2)}`, betaX, betaY, 'orange');

    vectorLayer.draw();
  }
  // ========================================================================

  function drawControlPoints() {
    controlLayer.find('Circle').forEach(el => el.remove());
    if (fixed) return;
    const s1Control = new Konva.Circle({
      x: originX + s1[0] * scale,
      y: originY - s1[1] * scale,
      radius: 6,
      fill: null,
      stroke: 'lightgray',
      draggable: !fixed,
    });

    const s2Control = new Konva.Circle({
      x: originX + s2[0] * scale,
      y: originY - s2[1] * scale,
      radius: 6,
      fill: null,
      stroke: 'lightgray',
      draggable: !fixed,
    });

    controlLayer.add(s1Control, s2Control);

    if (!fixed) {
      s1Control.on('dragmove', () => {
        s1[0] = (s1Control.x() - originX) / scale;
        s1[1] = (originY - s1Control.y()) / scale;
        gridLayer.destroyChildren();
        drawGrid();
        drawVectors();
      });

      s2Control.on('dragmove', () => {
        s2[0] = (s2Control.x() - originX) / scale;
        s2[1] = (originY - s2Control.y()) / scale;
        gridLayer.destroyChildren();
        drawGrid();
        drawVectors();
      });
    }

    controlLayer.draw();
  }
  // ========================================================================
  // Initial draw
  drawGrid();
  drawVectors();
  drawControlPoints();
}
"""
  • JSXGraph
class JXG_VectorDecomposition(JSComponent):
    """
    A JSXGraph-based component to visualize vectors s1 and s2.
    """

    # Parameters
    origin   = param.List(default=[0, 0], doc="Origin of the plot (x, y).")
    s1       = param.List(default=[1, 0], doc="First basis vector (dx, dy).")
    s2       = param.List(default=[0, 1], doc="Second basis vector (dx, dy).")
    b        = param.List(default=[4, 7], doc="Vector to decompose (dx, dy).")
    fixed    = param.Boolean(default=True, doc="Disallow moving endpoints of basis vectors.")
    vec_name = param.String(default="s", doc="Base name for the basis vectors (e.g., 's' or 'e').")

    _esm = r"""
    import JXG from 'https://cdn.jsdelivr.net/npm/jsxgraph/distrib/jsxgraphcore.mjs';
    import MJAX from 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js';

    export function render({ model, el }) {

      // Create a div element to hold the JSXGraph board
      let boardDiv          = document.createElement("div");
      boardDiv.style.width  = "300px";
      boardDiv.style.height = "300px";
      el.appendChild(boardDiv);

      // Global JSXGraph options for rendering
      JXG.Options.label.autoPosition = true;
      JXG.Options.text.useMathJax    = true;
      JXG.Options.text.fontSize      = 20;

      // Extract parameters
      const origin   = model.origin;
      const s1       = [...model.s1];
      const s2       = [...model.s2];
      const b        = [...model.b];
      const fixed    = model.fixed;
      const baseName = model.vec_name;

      // =========== Initialize the JSXGraph board ======================================
      let board = JXG.JSXGraph.initBoard(boardDiv, {
        boundingbox: [-5, 5, 5, -5],
        showCopyright:  false, showNavigation: false, axis: false });

      // Create a fixed invisible point at the origin
      const originPoint = board.create('point', origin, {
        name: '',
        visible: false,
        fixed: true,
      });

      // Store gridlines and decomposition arrows for clearing before redraw
      let gridlines = [];
      let decompositionArrows = [];

      // =========== Function to Draw Gridlines =========================================
      function draw_gridlines(s1, s2, range = 12, color = '#ccc') {
        // Clear existing gridlines
        gridlines.forEach((line) => board.removeObject(line));
        gridlines = [];

        // Generate gridlines parallel to s1
        for (let i = -range; i <= range; i++) {
          const offsetX1 = i * s2[0];
          const offsetY1 = i * s2[1];
          const line = board.create('line', [
            [origin[0] + offsetX1, origin[1] + offsetY1],
            [origin[0] + offsetX1 + s1[0], origin[1] + offsetY1 + s1[1]]
          ], { strokeColor: color, dash: 2, fixed: true });
          gridlines.push(line);
        }

        // Generate gridlines parallel to s2
        for (let i = -range; i <= range; i++) {
          const offsetX2 = i * s1[0];
          const offsetY2 = i * s1[1];
          const line = board.create('line', [
            [origin[0] + offsetX2, origin[1] + offsetY2],
            [origin[0] + offsetX2 + s2[0], origin[1] + offsetY2 + s2[1]]
          ], { strokeColor: color, dash: 2, fixed: true });
          gridlines.push(line);
        }
      }

      // =========== Function to Draw Arrows =========================================
        function draw_arrow(endpoint, color, fixed, onUpdate = null, baseName=null) {
          const endPointCoords = [origin[0] + endpoint[0], origin[1] + endpoint[1]];
          let endPoint = null;
          if (!fixed) {  // Create a visible and draggable endpoint if the vector is not fixed
            endPoint = board.create('point', endPointCoords, {
                      name: '', visible: true, fixed: false, size: 3, fillColor: color, strokeColor: color,  });

            endPoint.on('drag', function () { // Add interactivity if the endpoint is movable

              const newEndpoint = [endPoint.X() - origin[0], endPoint.Y() - origin[1]];
              onUpdate && onUpdate(newEndpoint); // Call onUpdate if provided
            });
          } else {                            // Use a non-interactive virtual point for fixed vectors
            endPoint = board.create('point', endPointCoords, {
                      name: '',  visible: false, fixed:   true,  });
          }
          // Draw the arrow from the origin point to the endpoint and return it
          const arrow = board.create('arrow', [originPoint, endPoint], {
                    strokeWidth: 2, strokeColor: color,  });
          if (baseName) {           // Add a label to the arrow if baseName is provided
            board.create('text', [  // Create a dynamic label whose position updates with the arrow
                      function () {
                        return (originPoint.X() + endPoint.X()) / 2 - 0.5;
                      },
                      function () {
                        return (originPoint.Y() + endPoint.Y()) / 2 - 0.5; // Midpoint Y, slightly offset above the arrow
                      },
                      `\\(\\vec{${baseName}}\\)`
                    ], {
                      anchorX: 'middle',
                      anchorY: 'middle',
                      useMathJax: true,
            });
          }
          return arrow;
        }

      // =========== Function to Decompose and Draw Arrows ==============================
        function draw_decomposition(s1, s2, b) {
          // Clear existing decomposition arrows
          decompositionArrows.forEach((arrow) => board.removeObject(arrow));
          decompositionArrows = [];

          const denominator = s1[0] * s2[1] - s1[1] * s2[0];
          if (denominator === 0) {
            console.error("Vectors s1 and s2 are linearly dependent. Decomposition not possible.");
            return;
          }

          const alpha = (b[0] * s2[1] - b[1] * s2[0]) / denominator;
          const beta  = (b[1] * s1[0] - b[0] * s1[1]) / denominator;

          const alphaS1 = [alpha * s1[0], alpha * s1[1]];
          const betaS2  = [beta * s2[0], beta * s2[1]];
          const bCoords = [b[0], b[1]];

          const color="green";
          // Draw arrows for alpha s1 and beta s2 and add them to decompositionArrows
          decompositionArrows.push(draw_arrow(alphaS1, color, true));
          decompositionArrows.push(draw_arrow(betaS2,  color, true));

          // Draw arrows from alpha s1 to b and beta s2 to b and add them to decompositionArrows
          const arrow1 = board.create('arrow', [
            [origin[0] + alphaS1[0], origin[1] + alphaS1[1]],
            [origin[0] + bCoords[0], origin[1] + bCoords[1]],
          ], { strokeColor: color, strokeWidth: 2, fixed: true });

          const arrow2 = board.create('arrow', [
            [origin[0] + betaS2[0], origin[1] + betaS2[1]],
            [origin[0] + bCoords[0], origin[1] + bCoords[1]],
          ], { strokeColor: color, strokeWidth: 2, fixed: true });

          decompositionArrows.push(arrow1);
          decompositionArrows.push(arrow2);
        }

      // =========== Draw Arrows and Attach Interactivity ================================
      let currentS1 = [...s1];
      let currentS2 = [...s2];

      draw_arrow(b, "blue", true, null, 'b');

      // Draw initial gridlines and decomposition
      draw_gridlines(s1, s2);
      draw_decomposition(s1, s2, b);

      draw_arrow(s1, "#60708B", fixed, (newEndpoint) => {
        currentS1 = newEndpoint;
        draw_gridlines(currentS1, currentS2);        // Update gridlines when s1 changes
        draw_decomposition(currentS1, currentS2, b); // Update decomposition when s1 changes
      }, baseName+'_1' );

      draw_arrow(s2, "#60708B", fixed, (newEndpoint) => {
        currentS2 = newEndpoint;
        draw_gridlines(currentS1, currentS2);        // Update gridlines when s2 changes
        draw_decomposition(currentS1, currentS2, b); // Update decomposition when s2 changes
      }, baseName+'_2' );

      // =========== Cleanup when the component is removed ==============================
      model.on('remove', () => {
        //console.log("JSXGraphComponent removed.");
        JXG.JSXGraph.freeBoard(board);
      });
   }
"""
1 Like