Search code examples
javascripthtmljquerycsshighcharts

highcharts tree graph panning and zoom in and out


I'm using a Highcharts tree graph chart. I managed to enable the scrollbar with some help. Another thing I'm trying to do but keep failing is to be able to move on the graph like on Google maps and zoom in and zoom out with scroll wheel.

I might have wrote the correct JS but then didn't have right CSS or had a CSS that made the code no work.

Highcharts.chart('container', {
  chart: {
    spacingBottom: 150,
    marginRight: 120,
    // margin: [50, 50, 50, 50], // Top, right, bottom, left
    height: 3500,
    width: 2500
  },
  title: {
    text: 'Phylogenetic language tree'
  },
  series: [{
    type: 'treegraph',
    keys: ['parent', 'id', 'level'],
    clip: false,
    data: [
      [undefined, 'Proto Indo-European'],
      ['Proto Indo-European', 'Balto-Slavic'],
      ['Proto Indo-European', 'Germanic'],
      ['Proto Indo-European', 'Celtic'],
      ['Proto Indo-European', 'Italic'],
      ['Proto Indo-European', 'Hellenic'],
      ['Proto Indo-European', 'Anatolian'],
      ['Proto Indo-European', 'Indo-Iranian'],
      ['Proto Indo-European', 'Tocharian'],
      ['Indo-Iranian', 'Dardic'],
      ['Indo-Iranian', 'Indic'],
      ['Indo-Iranian', 'Iranian'],
      ['Iranian', 'Old Persian'],
      ['Old Persian', 'Middle Persian'],
      ['Indic', 'Sanskrit'],
      ['Italic', 'Osco-Umbrian'],
      ['Italic', 'Latino-Faliscan'],
      ['Latino-Faliscan', 'Latin'],
      ['Celtic', 'Brythonic'],
      ['Celtic', 'Goidelic'],
      ['Germanic', 'North Germanic'],
      ['Germanic', 'West Germanic'],
      ['Germanic', 'East Germanic'],
      ['North Germanic', 'Old Norse'],
      ['North Germanic', 'Old Swedish'],
      ['North Germanic', 'Old Danish'],
      ['West Germanic', 'Old English'],
      ['West Germanic', 'Old Frisian'],
      ['West Germanic', 'Old Dutch'],
      ['West Germanic', 'Old Low German'],
      ['West Germanic', 'Old High German'],
      ['Old Norse', 'Old Icelandic'],
      ['Old Norse', 'Old Norwegian'],
      ['Old Swedish', 'Middle Swedish'],
      ['Old Danish', 'Middle Danish'],
      ['Old English', 'Middle English'],
      ['Old Dutch', 'Middle Dutch'],
      ['Old Low German', 'Middle Low German'],
      ['Old High German', 'Middle High German'],
      ['Balto-Slavic', 'Baltic'],
      ['Balto-Slavic', 'Slavic'],
      ['Slavic', 'East Slavic'],
      ['Slavic', 'West Slavic'],
      ['Slavic', 'South Slavic'],
      ['Polish', 'POLISH2'],
      ['PPPPPPPPP1', 'QQQQQQQQQ'],
      // Leaves:
      ['Proto Indo-European', 'Phrygian', 6],
      ['Proto Indo-European', 'Armenian', 6],
      ['Proto Indo-European', 'Albanian', 6],
      ['Proto Indo-European', 'Thracian', 6],
      ['Tocharian', 'Tocharian A', 6],
      ['Tocharian', 'Tocharian B', 6],
      ['Anatolian', 'Hittite', 6],
      ['Anatolian', 'Palaic', 6],
      ['Anatolian', 'Luwic', 6],
      ['Anatolian', 'Lydian', 6],
      ['Iranian', 'Balochi', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Kurdish', 6],
      ['Iranian', 'Pashto', 6],
      ['Iranian', 'Sogdian', 6],
      ['Old Persian', 'Pahlavi', 6],
      ['Middle Persian', 'Persian', 6],
      ['Hellenic', 'Greek', 6],
      ['Dardic', 'Dard', 6],
      ['Sanskrit', 'Sindhi', 6],
      ['Sanskrit', 'Romani', 6],
      ['Sanskrit', 'Urdu', 6],
      ['Sanskrit', 'Hindi', 6],
      ['Sanskrit', 'Bihari', 6],
      ['Sanskrit', 'Assamese', 6],
      ['Sanskrit', 'Bengali', 6],
      ['Sanskrit', 'Marathi', 6],
      ['Sanskrit', 'Gujarati', 6],
      ['Sanskrit', 'Punjabi', 6],
      ['Sanskrit', 'Sinhalese', 6],
      ['Osco-Umbrian', 'Umbrian', 6],
      ['Osco-Umbrian', 'Oscan', 6],
      ['Latino-Faliscan', 'Faliscan', 6],
      ['Latin', 'Portugese', 6],
      ['Latin', 'Spanish', 6],
      ['Latin', 'French', 6],
      ['Latin', 'Romanian', 6],
      ['Latin', 'Italian', 6],
      ['Latin', 'Catalan', 6],
      ['Latin', 'Franco-Provençal', 6],
      ['Latin', 'Rhaeto-Romance', 6],
      ['Brythonic', 'Welsh', 6],
      ['Brythonic', 'Breton', 6],
      ['Brythonic', 'Cornish', 6],
      ['Brythonic', 'Cuymbric', 6],
      ['Goidelic', 'Modern Irish', 6],
      ['Goidelic', 'Scottish Gaelic', 6],
      ['Goidelic', 'Manx', 6],
      ['East Germanic', 'Gothic', 6],
      ['Middle Low German', 'Low German', 6],
      ['Middle High German', '(High) German', 6],
      ['Middle High German', 'Yiddish', 6],
      ['Middle English', 'English', 6],
      ['Middle Dutch', 'Hollandic', 6],
      ['Middle Dutch', 'Flemish', 6],
      ['Middle Dutch', 'Dutch', 6],
      ['Middle Dutch', 'Limburgish', 6],
      ['Middle Dutch', 'Brabantian', 6],
      ['Middle Dutch', 'Rhinelandic', 6],
      ['Old Frisian', 'Frisian', 6],
      ['Middle Danish', 'Danish', 6],
      ['Middle Swedish', 'Swedish', 6],
      ['Old Norwegian', 'Norwegian', 6],
      ['Old Norse', 'Faroese', 6],
      ['Old Icelandic', 'Icelandic', 6],
      ['Baltic', 'Old Prussian', 6],
      ['Baltic', 'Lithuanian', 6],
      ['Baltic', 'Latvian', 6],
      ['West Slavic', 'Polish', 6],
      ['POLISH2', 'PPPPPPPPP', 7],
      ['POLISH2', 'PPPPPPPPP', 8],
      ['POLISH2', 'PPPPPPPPP', 8],
      ['POLISH2', 'PPPPPPPPP1', 8],
      ['POLISH2', 'PPPPPPPPP', 8],
      ['POLISH2', 'PPPPPPPPP', 8],
      ['POLISH2', 'PPPPPPPPP', 8],
      ['POLISH2', 'PPPPPPPPP', 8],
      ['QQQQQQQQQ', '_=========_', 10],
      ['West Slavic', 'Slovak', 6],
      ['West Slavic', 'Czech', 6],
      ['West Slavic', 'Wendish', 6],
      ['East Slavic', 'Bulgarian', 6],
      ['East Slavic', 'Old Church Slavonic', 6],
      ['East Slavic', 'Macedonian', 6],
      ['East Slavic', 'Serbo-Croatian', 6],
      ['East Slavic', 'Slovene', 6],
      ['South Slavic', 'Russian', 6],
      ['South Slavic', 'Ukrainian', 6],
      ['South Slavic', 'Belarusian', 6],
      ['South Slavic', 'Rusyn', 6]

    ],
    marker: {
      symbol: 'circle',
      radius: 6,
      fillColor: '#ffffff',
      lineWidth: 3
    },
    dataLabels: {
      align: 'left',
      pointFormat: '{point.id}',
      style: {
        color: '#000000',
        textOutline: '3px #ffffff',
        whiteSpace: 'nowrap'
      },
      x: 24,
      crop: false,
      overflow: 'none'
    },
    levels: [{
        level: 1,
        levelIsConstant: false
      },
      {
        level: 2,
        colorByPoint: true
      },
      {
        level: 3,
        colorVariation: {
          key: 'brightness',
          to: -0.5
        }
      },
      {
        level: 4,
        colorVariation: {
          key: 'brightness',
          to: 0.5
        }
      },
      {
        level: 6,
        dataLabels: {
          x: 10
        },
        marker: {
          radius: 4
        }
      }
    ]
  }]
});
html,
body {
  height: 100%;
  margin: 0;
}

.navbar {
  font-family: 'Consolas', monospace;
  font-size: 14px;
}

.sidebar {
  font-family: 'Consolas', monospace;
  font-size: 12px;
  position: fixed;
  top: 0;
  right: 0;
  width: 350px;
  height: 100%;
  padding-top: 56px;
  display: flex;
  flex-direction: column;
}

.chart-container {
  margin-right: 350px;
  height: calc(100vh - 56px);
  padding-top: 56px;
}

table {
  width: 100%;
  height: 100%;
  border-collapse: collapse;
  border: none;
  position: relative;
}

td {
  border: none;
  padding: 10px;
  position: relative;
}

.label {
  position: absolute;
  top: 10px;
  left: 10px;
  background-color: white;
  padding: 0 5px;
  font-weight: bold;
  z-index: 1;
}

tr:first-child td {
  height: 20%;
}

tr:last-child td {
  height: 80%;
}

.label {
  position: absolute;
  top: 10px;
  left: 10px;
  background-color: white;
  padding: 0 5px;
  font-weight: bold;
  z-index: 1;
}

textarea {
  position: absolute;
  font-family: 'Consolas', monospace;
  font-size: 11px;
  top: 40px;
  left: 10px;
  right: 10px;
  bottom: 80px;
  padding: 10px;
  box-sizing: border-box;
  width: calc(100% - 20px);
  height: calc(100% - 80px);
  white-space: nowrap;
  overflow: auto;
  resize: none;
  border: 1px solid white;
}

.highcharts-figure {
  /* top: 80px;
            max-width: 1200px;
            min-width: 360px; */
  max-height: 1200px;
  height: 100%;
  width: 100%;
}

.btn {
  font-family: 'Consolas', monospace;
  font-size: 14px;
}

#container {
  width: 3000px;
  height: 3400px;
}

.footer {
  font-family: 'Consolas', monospace;
  font-size: 12px;
  background-color: #212529;
  color: white;
  text-align: center;
  padding: 5px;
  text-shadow: 1px 1px 3px #44996e;
  user-select: none;
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  z-index: 1000;
}

.footer p {
  margin: 0;
}

.btn {
  border: 1px solid white;
  color: white;
  background-color: transparent;
  transition: border-color 0.3s ease, color 0.3s ease;
}
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/treemap.js"></script>
<script src="https://code.highcharts.com/modules/treegraph.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>

<div id="navbar-placeholder"></div>

<figure class="highcharts-figure">
  <div id="container"></div>
  <p class="highcharts-description">
  </p>
</figure>



<!-- Chart Container -->
<div class="chart-container">
  <div id="highchart-container" style="width: 100%; height: 100%;"></div>
</div>

<div class="sidebar bg-dark text-white">
  <table>
    <tr>
      <td>
        <span class="label text-white bg-dark">toolbox</span>
        <div class="button-container d-flex flex-wrap justify-content-center">
          <button class="btn btn-secondary m-2">Button 1</button>
          <button class="btn btn-secondary m-2">Button 2</button>
          <button class="btn btn-secondary m-2">Button 3</button>
          <button class="btn btn-secondary m-2">Button 4</button>
          <button class="btn btn-secondary m-2">Button 5</button>
          <button class="btn btn-secondary m-2">Button 6</button>
          <button class="btn btn-secondary m-2">Button 7</button>
          <button class="btn btn-secondary m-2">Button 8</button>
          <button class="btn btn-secondary m-2">Button 9</button>
        </div>
      </td>
    </tr>
    <tr>
      <td>
        <span class="label text-white bg-dark">details</span>
        <!-- Label for the second row -->
        <div class="textarea-wrapper">
          <textarea class="text-white bg-dark" placeholder="..."></textarea>
        </div>
      </td>
    </tr>
  </table>
</div>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>


Solution

  • Unfortunately, Highcharts doesn't support zooming in this series type natively. Here: https://github.com/highcharts/highcharts/issues/4106 you can find a reported feature request for that.

    However, you can implement zooming and panning by yourself. For example:

    const content = document.getElementById("content")
    
    let scale = 1
    let originX = 0
    let originY = 0
    let isPanning = false
    let startX = 0
    let startY = 0
    
    content.addEventListener("wheel", (event) => {
      event.preventDefault()
    
      const { offsetX, offsetY } = event // Cursor position relative to the element
      const delta = event.deltaY > 0 ? 0.9 : 1.1 // Zoom factor
      const newScale = scale * delta // New scale
    
      // Calculating new offset considering the cursor position
      const rect = content.getBoundingClientRect()
      const cx = offsetX // Cursor position inside the element
      const cy = offsetY
    
      // Calculating offset based on the new scale
      const newOriginX = originX - cx * (newScale - scale)
      const newOriginY = originY - cy * (newScale - scale)
    
      // Updating scale and offset
      scale = newScale // Updating the scale
      originX = newOriginX // Setting the new offset
      originY = newOriginY
    
      applyTransform() // Applying the transformation
    })
    
    function applyTransform() {
      content.style.transform = `translate(${originX}px, ${originY}px) scale(${scale})`
    }
    

    Live example: https://jsfiddle.net/BlackLabel/en9bgmt8/