Skip to main content

Interactive Scatter Plot with 20,000 Nodes

The purpose of this lab is to demonstrate the sheer number of nodes that Konva can handle by rendering 20,000 circles. Each circle is sensitive to mouseover events, and can be drag and dropped. This lab is also a great demonstration of event delegation, in which a single event handler attached to the stage handles the circle events.

Instructions: Mouse over the nodes to see more information, and then drag and drop them around the stage.

import Konva from 'konva';

// create stage
const width = window.innerWidth;
const height = window.innerHeight;

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

// function to add a node to layer
function addNode(obj, layer) {
  const node = new Konva.Circle({
    x: obj.x,
    y: obj.y,
    radius: 4,
    fill: obj.color,
    id: obj.id,
  });

  layer.add(node);
}

// Create a single layer for all circles
const circlesLayer = new Konva.Layer();
const tooltipLayer = new Konva.Layer();
const dragLayer = new Konva.Layer();

// create tooltip
const tooltip = new Konva.Label({
  opacity: 0.75,
  visible: false,
  listening: false,
});

tooltip.add(
  new Konva.Tag({
    fill: 'black',
    pointerDirection: 'down',
    pointerWidth: 10,
    pointerHeight: 10,
    lineJoin: 'round',
    shadowColor: 'black',
    shadowBlur: 10,
    shadowOffsetX: 10,
    shadowOffsetY: 10,
    shadowOpacity: 0.2,
  })
);

tooltip.add(
  new Konva.Text({
    text: '',
    fontFamily: 'Calibri',
    fontSize: 18,
    padding: 5,
    fill: 'white',
  })
);

tooltipLayer.add(tooltip);

// build data
const data = [];
const colors = ['red', 'orange', 'cyan', 'green', 'blue', 'purple'];

for (let n = 0; n < 20000; n++) {
  const x = Math.random() * width;
  const y = height + Math.random() * 200 - 100 + (height / width) * -1 * x;
  data.push({
    x: x,
    y: y,
    id: n.toString(),
    color: colors[Math.round(Math.random() * 5)],
  });
}

// Add all nodes to a single layer
for (let n = 0; n < data.length; n++) {
  addNode(data[n], circlesLayer);
}

// Add all layers to stage
stage.add(circlesLayer);
stage.add(dragLayer);
stage.add(tooltipLayer);

// handle events
let originalLayer;

stage.on('mouseover mousemove dragmove', function (evt) {
  const node = evt.target;
  if (node === stage) {
    return;
  }
  if (node) {
    // update tooltip
    const mousePos = node.getStage().getPointerPosition();
    tooltip.position({
      x: mousePos.x,
      y: mousePos.y - 5,
    });
    tooltip
      .getText()
      .text('node: ' + node.id() + ', color: ' + node.fill());
    tooltip.show();
  }
});

stage.on('mouseout', function (evt) {
  tooltip.hide();
});

stage.on('mousedown', function (evt) {
  const shape = evt.target;
  if (shape) {
    originalLayer = shape.getLayer();
    shape.moveTo(dragLayer);
    // manually trigger drag and drop
    shape.startDrag();
  }
});

stage.on('mouseup', function (evt) {
  const shape = evt.target;
  if (shape) {
    shape.moveTo(originalLayer);
  }
});