3

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

Ross Berenson
  • 317
  • 1
  • 4
  • 11
  • Because you have set `series:charts[0].series` when you initialize Highchart, when you update Highchart it is setting `charts[0]` to the new series. If you instead use a separate array for display, Highchart will update that instead and leave `charts` untouched. so `series: active[0].series` and make an array `const active =[...first object from charts array to start]` – pilchard Dec 23 '20 at 18:52
  • @pilchard - Thank you for this. I think I understand. I updated https://codepen.io/rossberenson/pen/mdrWgPe. I added const active, duplicating the charts array. Then when highcharts is initialized, it's looking at active[0].series. All good there, but when I go to update via selection. The charts array is being modified again. I assume it's because I'm not setting the series to be something else. I tried to then do a similar idea for the update.```const selectedChartData = [...charts[selectedChart].series];```but still have the same issue. Hmm – Ross Berenson Dec 23 '20 at 19:15
  • I see that you spread the charts array into the new active array. This was a good thought, but unfortunately cloning the array this way doesn't clone the nested objects, so when you switch the `series` it still updates the nested array in the original `charts` array. Instead, simply paste the entire first object in the `charts` array into the active array and then it works. (You could do this functionally, but you'll need to clone every level of each object nested inside `charts`). – pilchard Dec 23 '20 at 21:57
  • If you're curious, see: [What is the most efficient way to deep clone an object in JavaScript?](https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript/5344074#5344074) if you'r curious. – pilchard Dec 23 '20 at 21:57
  • @pilchard - Thanks for all of this. It's vert insightful, and I learned some new things. – Ross Berenson Dec 24 '20 at 02:18

1 Answers1

4

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>
pilchard
  • 12,414
  • 5
  • 11
  • 23
  • 2
    This helps a great deal. Thanks for all your help. I'm surprised that Highcharts works this way, and this is a work around that works. I know in the Highcharts docs it states that data might be mutated. I just didn't totally understand why it was. Thanks for clearing that up and providing an example of a solution. – Ross Berenson Dec 24 '20 at 02:24
  • 1
    I was surprised as well. I've made a small edit – `active` doesn't need to be an array, you can just assign the chart object to it directly. – pilchard Dec 24 '20 at 09:49