Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to configure visibility status of a dataset, to retain visibility state of datasets hidden by clicking Legend label #8451

Closed
kashifshamaz21 opened this issue Feb 18, 2021 · 16 comments · Fixed by #8478

Comments

@kashifshamaz21
Copy link
Contributor

Feature Proposal

I'm using Chart.js (v3.x.x) with React, using chartjs-react wrapper.

I would like to know if there is a way to configure a dataset to be hidden. The use case is described below, wherein, once a dataset is hidden by clicking on its legend label, the visibility of this dataset can't be retained when new datasets are added to the graph.

Feature Use Case

When a dataset in a line chart gets hidden by clicking on its label in the legend, and then if a new dataset is added, the previously hidden dataset gets enabled/shown again as the component re-renders and I could not find a way to set the "visibility / hidden" status of a dataset in the docs.

Possible Implementation

Using the generateLabels and onClick callbacks on Legend plugin, one can keep track of which fields are in hidden state.
By exposing a hidden property in the dataset's config, consumers can configure which datasets they would like to be in hidden state.

@LeeLenaleee
Copy link
Collaborator

LeeLenaleee commented Feb 18, 2021

Seems to be a problem in the react wrapper since as seen in this example when you hide a dataset and add a new one it stays hidden

https://www.chartjs.org/samples/master/charts/line/basic.html

Also if you want to implement this yourself you can do it by checking if a dataset is hidden by first getting the right legend item and checking its state (if its hidden or not): chart.legend.legendItems[datasetIndex].hidden.

After you know its hidden or shown you can call the hide/show method for a dataset with the following line: chart.hide(datasetIndex), chart.show(datasetIndex)

EDIT: The show and hide are V3 notation, for V2 solution see #8451 (comment)

@k1r0s
Copy link

k1r0s commented Feb 18, 2021

Hello, just googling around and I came here looking for a way to programatically hide some information within the chart after has loaded.

this instruction works chart.legend.legendItems[datasetIndex].hidden

but either of this two do not: chart.hide(datasetIndex), chart.show(datasetIndex)

there are no hide or show functions defined on the prototype. (I assume 'chart' happens to be the instance returned from new Chart( ... ))

Thanks in advance

@LeeLenaleee
Copy link
Collaborator

@k1r0s guess you are using v2 of the lib, in that case you will have to do chart.getDatasetMeta(datasetIndex).hidden = true/false and then chart.update()

@kashifshamaz21
Copy link
Contributor Author

@LeeLenaleee I am not sure if this would be an issue in the React wrapper (@xr0master : any thoughts here?).

I am currently implementing this logic by doing the bookkeeping in following manner:

generateLabels: () => {
     return selectedKeys.map((statKey, index) => {
            const isHidden = hiddenStatKeysMap.has(statKey);
            return {
                text: ..., 
                hidden: isHidden, 
                datasetIndex: index
            }
    }
}
onClick: (event: ChartEvent, legendItem:LegendItem, legend: any) => {
        const index = legendItem.datasetIndex;
        const ci = legend.chart;
        const statKey = this.getStatKeyAtIndex(index);
        const isStatHidden = hiddenStatKeysMap.has(statKey);
        if (isStatHidden) {
            ci.show(index);
            legendItem.hidden = false;
            hiddenStatKeysMap.remove(statKey);
        } else {
            ci.hide(index);
            legendItem.hidden = true;
            hiddenStatKeysMap.set(statKey, true);
        }
}

While creating datasets config:

this.callStatsToFetch.forEach(statKey => {
      const statConfig = CallStatsFieldsConfig.get(statKey);
      let datasetConfig: LineChartDataset;

      if(statConfig) {
          const statLabel = statConfig.header;
          let yAxisKey = `result.${statKey}`;

          datasetConfig = {
              data: this.callStatsData,
              label: statLabel,
              parsing: { 
                  yAxisKey: yAxisKey, 
                  xAxisKey: 'timestamp'
              },
              yAxisID: useMultipleYAxes ? `yAxis${statKey}` : 'default'
          };
          
          // Remove config options for hidden stats so that their Y-Axis is removed 
          if(hiddenStatKeys.has(statKey)) {
              datasetConfig.parsing = {};
              delete datasetConfig.yAxisID; 
          } 
          datasets.push(datasetConfig);
      }
 });
const statsChartData: LineChartData = {
    labels: [],
    datasets: datasets
};
return statsChartData;

I think a config option in dataset, to configure whether that dataset is hidden or not, would be of great value! The current logic we have to add for achieving this seems quite complicated.

@kurkle
Copy link
Member

kurkle commented Feb 19, 2021

As you can see from the sample linked, the state is maintained internally. Is the wrapper destroying the chart on every update?

@kashifshamaz21
Copy link
Contributor Author

@kurkle From what I observed during addition/removal of datasets, it did seem like the Chart was getting destroyed and re-created, because all the previously hidden lines are being re-displayed and there previous visibility state isn't being respected.
@xr0master From https://github.com/xr0master/chartjs-react/blob/master/src/ReactChart.tsx, I thought the following useEffect should ideally have not destroyed and created the graph afresh when new datasets are added. Am i missing something?

useEffect(() => {
    chartInstance.current.options = options;
    chartInstance.current.data = data;

    chartInstance.current.update(updateMode);
  }, [data, options]);

@xr0master
Copy link
Contributor

xr0master commented Feb 19, 2021

@kashifshamaz21 This is out of this project, but the chart will be re-created only if the canvas element has been unmounted/re-mounted.

I think the Chart.js behavior is correct, since the datasets come again, the chart cannot know which of them was hidden before... @kurkle maybe it's possible to achieve by dataset id?

Seems to be a problem in the react wrapper since as seen in this example when you hide a dataset and add a new one it stays hidden
https://www.chartjs.org/samples/master/charts/line/basic.html

@kashifshamaz21 In the example, the reference to the dataset is saved and only pushed a new dataset inside, but not all data. It is not an easy task to keep the reference, especially for wrappers. It is necessary to get chartInstance and pull out a dataset from it.

@kashifshamaz21
Copy link
Contributor Author

@xr0master Yeah I get your point. Hence my feature request to see if its possible for Chart.js to support a visible: boolean on the dataset. Then the client code can pass the visibility status per dataset, and retain the hidden status of fields which were clicked using legend labels. Currently I'm able to accomplish my requirement, but the above code which I have come up with for doing this, seemed a bit involved.
Not sure if other users who integrate Chart.js-v3 in React would run into this issue in future, to know if its worth adding the support for this.

@etimberg
Copy link
Member

One option here is to wrap the datasets in useMemo before passing them to the chart component. That way they will only change when there are actual changes and not when a re-render occurs

@kurkle
Copy link
Member

kurkle commented Feb 19, 2021

You can also add hidden: true in the dataset: https://codepen.io/pen?editors=1010

@LeeLenaleee
Copy link
Collaborator

Doesn't look like its documentated in the dataset properties for the charts

https://www.chartjs.org/docs/master/charts/line#dataset-properties

@kashifshamaz21
Copy link
Contributor Author

kashifshamaz21 commented Feb 20, 2021

@kurkle Thanks for letting me know about this hidden property :D

I tried using the hidden: true property on a dataset to achieve it and while the dataset's line is getting hidden with that property, one issue I observed is that the y-axis of this hidden dataset still lingers on the graph.

Here's how I modified my code to try setting hidden: true instead of my previous workaround:

While creating datasets config:

this.callStatsToFetch.forEach(statKey => {
      const statConfig = CallStatsFieldsConfig.get(statKey);
      let datasetConfig: LineChartDataset;

      if(statConfig) {
          const statLabel = statConfig.header;
          let yAxisKey = `result.${statKey}`;

          datasetConfig = {
              data: this.callStatsData,
              label: statLabel,
              parsing: { 
                  yAxisKey: yAxisKey, 
                  xAxisKey: 'timestamp'
              },
              yAxisID: useMultipleYAxes ? `yAxis${statKey}` : 'default'
          };
          
          /* 
                  Using "hidden: true" property to hide a previously hidden dataset. 
                  Doing this way, the Y-axis of a hidden dataset still shows up on the graph.
          */
          if(hiddenStatKeys.has(statKey)) {
                datasetConfig.hidden = true;
         }
          
          /* commenting out below code block. Using this way achieves hiding of dataset as well as its y-axis.
          // Remove config options for hidden stats so that their Y-Axis is removed 
          if(hiddenStatKeys.has(statKey)) {
              datasetConfig.parsing = {};
              delete datasetConfig.yAxisID; 
          } */

          datasets.push(datasetConfig);
      }
 });
const statsChartData: LineChartData = {
    labels: [],
    datasets: datasets
};
return statsChartData;

And as pointed by @LeeLenaleee this property doesn't seem to be present in the Types: LineChartDataset type doesn't have this field mentioned.

Any thoughts if the above y-axis showing up on the graph for hidden datasets is expected ?

@kurkle
Copy link
Member

kurkle commented Feb 20, 2021

display: 'auto' on scales should hide the ones not having any datasets.

@kashifshamaz21
Copy link
Contributor Author

kashifshamaz21 commented Feb 20, 2021

@kurkle Awesome, your tips have solved this issue for me 👍

For anyone else who stumbles upon this issue, below is what is needed, to hide a dataset and its y-axis (if you are using separate y-axis per dataset)

  1. Set hidden: true property in the config of the dataset which you want hidden.
  2. Set display: false in the config of y-axes scales of the dataset which are hidden.

Below are my final working code snippets:

Method generating datasets (check "hidden" field being set on dataset):

get parsedStatsData() {
      const datasets: LineChartDataset[] = [];
      const useMultipleYAxes = this.useMultipleYAxes;
      const hiddenStatKeysMap = this.hiddenStatKeysMap;

      this.callStatsToFetch.forEach(statKey => {
              const statConfig = CallStatsFieldsConfig.get(statKey);
              let datasetConfig: any;
              if(statConfig) {
                  const fieldLineColor = statConfig.fieldLineColor;
                  const statLabel = statConfig.header;
                  let yAxisKey = `result.${statKey}`;

                  datasetConfig = {
                      data: this.callStatsData,
                      borderColor: fieldLineColor,
                      backgroundColor: fieldLineColor,
                      borderWidth: 1,
                      label: statLabel,
                      parsing: { 
                          yAxisKey: yAxisKey, 
                          xAxisKey: 'timestamp'
                      },
                      hidden: hiddenStatKeysMap.has(statKey),
                      yAxisID: useMultipleYAxes ? `yAxis${statKey}` : 'default'
                  };

                  datasets.push(datasetConfig);
              }
      });
      const statsChartData: LineChartData = {
          labels: [],
          datasets: datasets
      };
      return statsChartData;
  }

Generating Chart options config:

get chartOptionsConfig() {
      const selectedTimezone = this.globalDataStore.selectedTimezone;
      const statKeys = this.callStatsToFetch;
      const hiddenStatKeysMap = this.hiddenStatKeysMap;
      const useMultipleYAxes = this.useMultipleYAxes;

      const yAxes:any = {};
      const defaultYAxisConfig = {
          axis: 'y'
      };
      if(useMultipleYAxes) {
          let axesIndex = 0;
          statKeys.forEach((statKey) => {
              const statConfig = CallStatsFieldsConfig.get(statKey);
              if(statConfig) {
                  const yAxisKey = `yAxis${statKey}`;
                  yAxes[yAxisKey] = {
                      position: (axesIndex % 2 === 0) ? 'left' : 'right',
                      ticks: { color: statConfig.fieldLineColor },
                      display: hiddenStatKeysMap.has(statKey) ? false : true,
                      ...defaultYAxisConfig
                  }
              }
              if(!hiddenStatKeysMap.has(statKey)) axesIndex++;
          });
      } else {
          yAxes['default'] = {
              position: 'left',
              ...defaultYAxisConfig
          }
      }
      
      const options: LineChartOptions =  {
          plugins: {
              tooltip: {
                  mode: 'index',
                  intersect: false,
              },
              legend: {
                  display: true,
                  position: "bottom",
                  align: "left",
                  labels: {
                      boxWidth: 15,
                      generateLabels: () => {
                          return statKeys.map((statKey, index) => {
                              const statConfig = CallStatsFieldsConfig.get(statKey);
                              const isHidden = hiddenStatKeysMap.has(statKey);
                              
                              return {
                                  text: statConfig?.header ?? '',
                                  fillStyle: statConfig?.fieldLineColor ?? '',
                                  hidden: isHidden,
                                  datasetIndex: index
                              }
                          })
                      }
                  },
                  onClick: (event, legendItem, legend: any) => {
                      const index = legendItem.datasetIndex;
                      const ci = legend.chart;
                      const statKey = this.getStatKeyAtIndex(index);

                      if(statKey) {
                          const isStatHidden = hiddenStatKeysMap.has(statKey);
                          if (isStatHidden) {
                              ci.show(index);
                              legendItem.hidden = false;
                              this.statAtIndexHidden(index, false);
                          } else {
                              ci.hide(index);
                              legendItem.hidden = true;
                              this.statAtIndexHidden(index, true);
                          }
                      }
                      
                  }
              }
          },
          maintainAspectRatio: false,
          hover: {
              mode: 'index',
              intersect: false
          },
          elements: {
              point: { radius: 2 }
          },
          scales: {
              x: {
                  type: 'time',
                  display: true,
                  adapters: {
                      date: {
                        zone: selectedTimezone
                      }
                  },
                  time: {
                      tooltipFormat: 'LLL dd hh:mm:ss a ZZZZ',
                      displayFormats: {
                          second: 'h:mm:ss',
                          minute: 'h:mm'
                      },
                      minUnit: 'second'
                  },
                  scaleLabel: {
                      display: true,
                      labelString: 'Time'
                  },
                  ticks: {
                      major: {
					enabled: true
				},
                      source: 'data',
                      autoSkip: true,
                      font: { size: 11 }
                  }
              },
              ...yAxes
          },
      }
      return options;
  }

@kurkle Thanks a lot for your help on this issue. One last thing, for fixing the type LineChartDataset, to add hidden: boolean to it, should I file a ticket separately on this repo?

@kurkle
Copy link
Member

kurkle commented Feb 20, 2021

Yeah, either that or create a PR for it :)

@kashifshamaz21
Copy link
Contributor Author

@kurkle Raised #8478 for the types update and added documentation for the "hidden" field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants