Search code examples
javascriptkonvajskonva

Adding zoom to a packed circle visualisation using Konva (Scale and reposition from center)


I'm created a packed circle visualisation using d3 and drawing it using Konva. If you click on a circle within the packed circle visualisation, the viewport should zoom to that circle.

Each circle has an event handler attached, which gets called on a click event. There I am calculating the value by which to scale the whole visualisation by, and the values by which to reposition the visualisation by.

I can't quite seem to get the repositioning values correct. The circle on which the click event occurs should be centered in the viewport after scaling it.

function zoom(circle) {

  
  // By what value do we neeed to scale the group?
  let scaleBy = root.data.konva.radius() / circle.radius()
 
  group.scale({x: scaleBy, y: scaleBy})
  
  // By what values do we neeed to reposition the group?
  let newX = (circle.x() - root.data.konva.x() ) * scaleBy
  let newY = (circle.y() - root.data.konva.y()) * scaleBy
    
  group.position({x: newX, y: newY})
}

enter image description here

const data = { children: [{ children: [{ children: [] },{ children: [] }] },{ children: [{ children: [] },{ children: [{ children: [] },{ children: [] }] }] }] }

const width = 600
const height = 400

let pack = d3.pack().size([width, height])

let root = d3.hierarchy(data)
  .sum(d => {
    if(d.children && d.children.length > 0) {
      return d.children.length  
    }
    return 1
  })

pack(root)

// ---

const  stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height
 })
 
const layer = new Konva.Layer()
const group = new Konva.Group()
   
layer.add(group)
stage.add(layer)

// ---

root.descendants().forEach( (node,i) => { 
    
  const circle = new Konva.Circle({
    x: node.x,
    y: node.y,
    radius: node.r,
    fill: 'grey',
    opacity: (0.1 * node.depth) + 0.1
  })
  
  node.data.konva = circle
  circle.data = { d3: node }
  
  group.add(circle)
  
  circle.on('click', () => zoom(circle))
})

// ---


function zoom(circle) {

  
  // By what value do we neeed to scale the group?
  let scaleBy = root.data.konva.radius() / circle.radius()
  
  console.log(`Scaling by: ${scaleBy}`)

  group.scale({x: scaleBy, y: scaleBy})
  
  // By what values do we neeed to reposition the group?
  let newX = (circle.x() - root.data.konva.x() ) * scaleBy
  let newY = (circle.y() - root.data.konva.y()) * scaleBy
  
  console.log(`Repositioning by: x:${newX}, y:${newY}`)
  
  group.position({x: newX, y: newY})
}
.konvajs-content {
  background: rgba(124, 7, 12, 0.1);
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/konva.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<div id="container"></div>


Solution

  • The task is one of moving the stage position (x, y) so that the center of the target circle is in the middle of the viewport - meaning the HTML canvas element visible bounds.

    PosX = (viewPort.width / 2) - (circle.pos.x * scale) PosY = (viewPort.height/ 2) - (circle.pos.y * scale)

    And using your code that means:

     let newX = (width / 2) - (circle.x() * scaleBy)  
     let newY = (height / 2) -  (circle.y() * scaleBy) 
    

    See working snippet below, only those lines changed.

    const data = { children: [{ children: [{ children: [] },{ children: [] }] },{ children: [{ children: [] },{ children: [{ children: [] },{ children: [] }] }] }] }
    
    const width = 600
    const height = 400
    
    let pack = d3.pack().size([width, height])
    
    let root = d3.hierarchy(data)
      .sum(d => {
        if(d.children && d.children.length > 0) {
          return d.children.length  
        }
        return 1
      })
    
    pack(root)
    
    // ---
    
    const  stage = new Konva.Stage({
      container: 'container',
      width: width,
      height: height
     })
     
    const layer = new Konva.Layer()
    const group = new Konva.Group()
       
    layer.add(group)
    stage.add(layer)
    
    // ---
    
    root.descendants().forEach( (node,i) => { 
        
      const circle = new Konva.Circle({
        x: node.x,
        y: node.y,
        radius: node.r,
        fill: 'grey',
        opacity: (0.1 * node.depth) + 0.1
      })
      
      node.data.konva = circle
      circle.data = { d3: node }
      
      group.add(circle)
      
      circle.on('click', () => zoom(circle))
    })
    
    // ---
    
    
    function zoom(circle) {
    
      
      // By what value do we neeed to scale the group?
      let scaleBy = root.data.konva.radius() / circle.radius()
      
      
      console.log(`Scaling by: ${scaleBy}`)
    
      group.scale({x: scaleBy, y: scaleBy})
      
      // By what values do we neeed to reposition the group?
     let newX = (width / 2) - (circle.x() * scaleBy)  
     let newY = (height / 2) -  (circle.y() * scaleBy)
      
      console.log(`Repositioning by: x:${newX}, y:${newY}`)
      
      group.position({x: newX, y: newY})
    }
    .konvajs-content {
      background: rgba(124, 7, 12, 0.1);
    }
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/konva.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    
    <div id="container"></div>