chartist icon indicating copy to clipboard operation
chartist copied to clipboard

.detach() not removing the window resize listener.

Open Kelnor277 opened this issue 8 years ago • 4 comments

I have this react component

class ChartCell extends React.Component{
    constructor(props, context) {
        super(props, context);
        this.chartCreated = this.chartCreated.bind(this);
    }
    componentDidMount(){
        let mergedOptions = this.props.chartOptions;
        console.log('mounted');
        mergedOptions = _.assign(mergedOptions, defaultOptions);
        this.chartObj = new Chartist.Line(this.refs.chart, this.props.chartData, mergedOptions);
        this.chartObj.on('created', this.chartCreated); // Tried to call detach on "created" doesn't work
        this.chartObj.on('initial', this.chartCreated); // Neither does calling on "initial"
    }
    chartCreated(){
        console.log('detached');
        this.chartObj.detach(); //doesn't remove the event listener
        window.removeEventListener('resize', this.chartObj.resizeListener); //successfully removes the listener
    }
    render(){
        return (<td><div ref="chart" className="chart-div"/></td>)
    }
}

It does not clear the resize event listener. Which causes my page to lock up while redrawing every chart on a resize. I'm using a static sized div to contain the charts so I don't need to resize the charts on window redraw.

Investigation: So base.detach() will only not remove the listener if timeout is not set:

function detach() {
    // Only detach if initialization already occurred on this chart. If this chart still hasn't initialized (therefore
    // the initializationTimeoutId is still a valid timeout reference, we will clear the timeout
    if(!this.initializeTimeoutId) {
      window.removeEventListener('resize', this.resizeListener);
      this.optionsProvider.removeMediaQueryListeners();
    } else {
      window.clearTimeout(this.initializeTimeoutId);
    }

    return this;
  }

My guess here is base.initialize is the problem:

  function initialize() {
    // Add window resize listener that re-creates the chart
    window.addEventListener('resize', this.resizeListener);

    // Obtain current options based on matching media queries (if responsive options are given)
    // This will also register a listener that is re-creating the chart based on media changes
    this.optionsProvider = Chartist.optionsProvider(this.options, this.responsiveOptions, this.eventEmitter);
    // Register options change listener that will trigger a chart update
    this.eventEmitter.addEventHandler('optionsChanged', function() {
      this.update();
    }.bind(this));

    // Before the first chart creation we need to register us with all plugins that are configured
    // Initialize all relevant plugins with our chart object and the plugin options specified in the config
    if(this.options.plugins) {
      this.options.plugins.forEach(function(plugin) {
        if(plugin instanceof Array) {
          plugin[0](this, plugin[1]);
        } else {
          plugin(this);
        }
      }.bind(this));
    }

    // Event for data transformation that allows to manipulate the data before it gets rendered in the charts
    this.eventEmitter.emit('data', {
      type: 'initial',
      data: this.data
    });

    // Create the first chart
    this.createChart(this.optionsProvider.getCurrentOptions());

    // As chart is initialized from the event loop now we can reset our timeout reference
    // This is important if the chart gets initialized on the same element twice
    this.initializeTimeoutId = undefined;
  }

this.initializeTimeoutId = undefined; is cleared AFTER the 'initial' event is emitted. That clearly shows why trying to detach() after 'initial' doesn't work. As for it not working on 'created" I don't know the lifecycle but maybe there's a race condition where initial fires, which causes created to fire later but before the initialize() function finishes and clears the timeout? So my question is A: should initializeTimeoutId be cleared before emitting the 'initial' event. OR B: which event should I wait for before detaching?

Kelnor277 avatar Apr 06 '17 17:04 Kelnor277

Running into the exact same issue here. Did you ever find a workaround?

phegman avatar Sep 25 '18 16:09 phegman

I think I did, but I don’t remember and the code is with my previous employer sorry :(

Kelnor277 avatar Sep 26 '18 16:09 Kelnor277

No problem, I found a solution. It's not pretty, but for anybody else that runs into this issue this may help:

const chart = new Chartist.Bar(chartEl, options)

// Remove chartist resize event
setTimeout(() => {
  window.removeEventListener('resize', chart.resizeListener)
}, 100)

// Add our own resize event with iOS fix thanks to https://stackoverflow.com/questions/8898412/iphone-ipad-triggering-unexpected-resize-events
let windowWidth = window.innerWidth
window.addEventListener('resize', () => {
  // Check window width has actually changed and it's not just iOS triggering a resize event on scroll
  if (window.innerWidth != windowWidth) {
    // Update the window width for next time
    windowWidth = window.innerWidth
    chart.update()
  }
})

I am not sure why but I had to set a very short timeout to remove the chartist resize event. Makes me sad how hacky it feels but what are you gonna do 😢

phegman avatar Sep 26 '18 16:09 phegman

Is this still an issue years later?

Do we need to manually manage window resize, in relation to iOS (for instance), with Chartist in 2024?

kalnode avatar Mar 20 '24 14:03 kalnode