Search code examples
javascriptarrayschartshighcharts

Highcharts .update modifying original data array


I'm encountering a weird issue that's probably my doing but I've tried everything I can come across.

I have one stacked column chart with eight different data sets. I have a select field with the titles of each data set.

const charts = [
  {
    "title": "Chart One",
    "subtitle": "Subtitle One",
    "source": "<p>Source One</p>\n",
    "content": "<p>Content One</p>\n",
    "series": [
      {
        "name": "Legend One",
        "data": [0,0,0,0,0,0,0,0,0,0,null,0,0,0,0,null,0],
        "stack": 0
      },
      {
        "name": "Legend Two",
        "data": [1085,1364,2398,2362,3954,6612,6388,8841,8397,6021,null,4962,7407,2825,2143,null,4823],
        "stack": 0
      }
    ]
  },
  {
    "title": "Chart Two",
    "subtitle": "Subtitle Two",
    "source": "<p>Source Two</p>\n",
    "content": "<p>Content Two</p>\n",
    "series": [
      {
        "name": "Legend One",
        "data": [380,608,469,285,634,1496,712,3059,1821,1049,null,916,2240,612,895,null,1064],
        "stack": 0
      },
      {
        "name": "Legend Two",
        "data": [705,756,1928,2078,3320,5116,5676,5782,6576,4973,null,4046,5167,2212,1248,null,3759],
        "stack": 0
      }
    ]
  },
  {
    "title": "Chart Three",
    "subtitle": "Subtitle Three",
    "source": "<p>Source Three</p>\n",
    "content": "<p>Content Three</p>\n",
    "series": [
      {
        "name": "Legend One",
        "data": [648,932,1708,1326,2246,4646,4143,6732,6042,4222,null,3268,5723,1987,1501,null,3322],
        "stack": 0
      },
      {
        "name": "Legend Two",
        "data": [438,432,690,1037,1708,1966,2244,2110,2355,1799,null,1694,1685,838,642,null,1501],
        "stack": 0
      }
    ]
  },
  {
    "title": "Chart Four",
    "subtitle": "Subtitle Four",
    "source": "<p>Source Four</p>\n",
    "content": "<p>Content Four</p>\n",
    "series": [
      {
        "name": "Legend One",
        "data": [380,608,469,285,634,1496,712,3118,2498,3567,null,1411,2687,698,1156,null,1479 ],
        "stack": 0
      },
      {
        "name": "Legend Two",
        "data": [705,756,1928,2078,3320,5116,5676,5724,5899,2455,null,3551,4720,2126,987,null,3344],
        "stack": 0
      }
    ]
  },
  {
    "title": "Chart Five",
    "subtitle": "Subtitle Five",
    "source": "<p>Source Five</p>\n",
    "content": "<p>Content Five</p>\n",
    "series": [
      {
        "name": "Legend One",
        "data": [561,852,1362,2012,3404,5643,6195,8153,8158,5905,null,4502,6243,2646,1750,null,4305],
        "stack": 0
      },
      {
        "name": "Legend Two",
        "data": [524,512,1036,350,550,969,193,689,240,117,null,460,1165,178,393,null,518],
        "stack": 0
      }
    ]
  },
  {
    "title": "Chart Six",
    "subtitle": "Subtitle Six",
    "source": "<p>Source Six</p>\n",
    "content": "<p>Content Six</p>\n",
    "series": [
      {
        "name": "Legend One",
        "data": [120,209,397,655,1115,2584,2944,3322,3075,2266,null,1751,2606,1032,740,null,1704],
        "stack": 0
      },
      {
        "name": "Legend Two",
        "data": [965,1155,2000,1708,2840,4027,3443,5519,5323,3755,null,3211,4802,1793,1403,null,3118],
        "stack": 0
      }
    ]
  },
  {
    "title": "Chart Seven",
    "subtitle": "Subtitle Seven",
    "source": "<p>Source Seven</p>\n",
    "content": "<p>Content Seven</p>\n",
    "series": [
      {
        "name": "Legend One",
        "data": [294,494,391,800,1765,4227,5474,7212,7705,5836,null,3755,4640,2070,1463,null,3490],
        "stack": 0
      },
      {
        "name": "Legend Two",
        "data": [791,870,2007,1563,2189,2385,914,1630,692,186,null,1207,2768,755,680,null,1333],
        "stack": 0
      }
    ]
  },
  {
    "title": "Chart Eight",
    "subtitle": "Subtitle Eight",
    "source": "<p>Source Eight</p>\n",
    "content": "<p>Content Eight</p>\n",
    "series": [
      {
        "name": "Legend One",
        "data": [486,741,1160,1825,3105,5205,6012,7553,7723,5780,null,4259,5754,2424,1596,null,4035],
        "stack": 0
      },
      {
        "name": "Legend Two",
        "data": [599,623,1237,537,849,1407,376,1288,674,242,null,703,1654,401,547,null,787],
        "stack": 0
      }
    ]
  }
]

When a user selects the chart they want to view, I then grab the data needed charts[selectedIndex].series and pass that to:

chartObj.update({
    series: charts[selectedIndex].series
}, false );
chartObj.redraw();

This overall works and the chart is updated correctly, however, chart[0].series gets updated with the last selected chart data. The chart array is getting modified for some odd reason. It only modifies the first chart in the array. For the life of me, I can't figure out why this is occurring. Any insight would be greatly appreciated.

I've also tried a for loop going through the chartObj series and matching it to the desired charts array item, and using .setData with the 'data' but that had it's own issues. This did not modify the original array, however the chart would no longer update. I tried the same loop with .update as well.

Codepen with the error: https://codepen.io/rossberenson/pen/mdrWgPe


Solution

  • I dug a little deeper and found that the chart.update() call was switching out the nested series array but wasn't updating the entire chart object, more than that even cloning the series array using map(o => ({...o})) doesn't clone the nested data arrays so they are still prone to mutation. (you could use map(o => ({...o, data: [...o.data]})) but it's fragile if your series/data structure changes).

    So... here is a quick snippet that stores the datasets in their own array and sets the Highchart.series option to a separate active object.

    The active object is initially assigned a cloned version of the first chart object using JSON for deep cloning – see this question for deeper discussion of its shortcomings and better options What is the most efficient way to deep clone an object in JavaScript?

    When the select change event fires, the chart object at the newly selected index is cloned using the same method and assigned to the active object.

    Finally, chart.update() is called pointing to the same active.series as when the highchart class was instantiated.

    const cloneSample = (sample) => {
      // Crude JSON deep clone – see referenced question for discussion
      return JSON.parse(JSON.stringify(sample));
    }
    
    let active = cloneSample(samples[0]);
    
    const highchart = Highcharts.chart('container', {
      chart: { type: 'bar' }, title: { text: active.title },
      xAxis: { categories: ['Apples', 'Oranges', 'Pears', 'Grapes', 'Bananas'] },
      yAxis: { min: 0, title: { text: 'Total fruit consumption' } }, legend: { reversed: false },
      plotOptions: { series: { stacking: 'normal' } },
      series: active.series
    });
    
    const select = document.querySelector('.select');
    select.addEventListener('change', () => {
      let
        selected = select.value,
        newSample = cloneSample(samples[selected]);
      
      active = newSample;
    
      highchart.update({
        title: { text: active.title },
        series: active.series,
      });
    
    });
    <script src="https://code.highcharts.com/highcharts.js"></script>
    <select class="select">
        <option value="0">Sample One</option>
        <option value="1">Sample Two</option>
        <option value="2">Sample Three</option>
    </select>
    
    <div id="container"></div>
    
    <script>
    const samples = [
      {
        "title": "Sample One",
        "series": [
          {
            name: 'John',
            data: [5, 3, 4, 7, 2]
          }, {
            name: 'Jane',
            data: [2, 2, 3, 2, 1]
          }, {
            name: 'Tara',
            data: [3, 4, 4, 2, 5]
          }
        ]
      },
      {
        "title": "Sample Two",
        "series": [
          {
            name: 'Donna',
            data: [2, 1, 4, 6, 2]
          }, {
            name: 'Mark',
            data: [1, 4, 3, 4, 1]
          }, {
            name: 'Tim',
            data: [6, 4, 9, 3, 5]
          }
        ]
      },
      {
        "title": "Sample Three",
        "series": [
          {
            name: 'Mara',
            data: [3, 3, 1, 7, 2]
          }, {
            name: 'Tom',
            data: [0, 1, 2, 4, 1]
          }, {
            name: 'Lara',
            data: [3, 8, 5, 2, 5]
          }
        ]
      }
    ]
    </script>