<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
import ReactTooltip from 'react-tooltip';
import 'gemini-scrollbar/gemini-scrollbar.css';

import { formatDuration } from 'common/format';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';

import Events from './Events';

import './SpellTimeline.css';

class SpellTimeline extends React.PureComponent {
  static propTypes = {
    historyBySpellId: PropTypes.object.isRequired,
    abilities: PropTypes.object.isRequired,
    spellId: PropTypes.number,
    start: PropTypes.number.isRequired,
    end: PropTypes.number.isRequired,
  };

  constructor() {
    super();
    this.handleMouseWheel = this.handleMouseWheel.bind(this);
    this.state = {
      zoom: 1,
    };
  }

  handleMouseWheel(e) {
    // This translate vertical scrolling into horizontal scrolling
    if (!this.gemini || !this.gemini.scrollbar) {
      return;
    }

    e.preventDefault();
    e.stopPropagation();
    if (e.detail > 0) {
      // noinspection JSSuspiciousNameCombination
      this.gemini.scrollbar._viewElement.scrollLeft -= e.deltaY;
    } else {
      // noinspection JSSuspiciousNameCombination
      this.gemini.scrollbar._viewElement.scrollLeft += e.deltaY;
    }
  }

  componentDidMount() {
    this.componentDidUpdate();
  }
  componentDidUpdate() {
    ReactTooltip.rebuild();
  }

  get spells() {
    const { spellId, historyBySpellId, abilities } = this.props;
    const spellIds = spellId ? [spellId] : Object.keys(historyBySpellId).map(Number);

    return spellIds.sort((a, b) => {
      const aCooldown = abilities.getExpectedCooldownDuration(Number(a));
      const bCooldown = abilities.getExpectedCooldownDuration(Number(b));
      return aCooldown - bCooldown;
    });
  }

  gemini = null;
  render() {
    const { start, end, historyBySpellId } = this.props;
    const duration = end - start;
    const seconds = Math.ceil(duration / 1000);

    const secondWidth = 40 / this.state.zoom;
    const skipInterval = Math.ceil(40 / secondWidth);

    // 9 for the scrollbar height
    // 4 for margin
    // 36 for the ruler
    // 28 for each spell
    const totalHeight = 9 + 4 + 36 + 28 * this.spells.length;

    const totalWidth = seconds * secondWidth;

    return (
      <div className="spell-timeline flex">
        <div className="flex-sub legend">
          <div className="lane ruler-lane">
            <div className="btn-group">
              {[1, 2, 3, 5].map(zoom => (
                <button key={zoom} className={`btn btn-default btn-xs ${zoom === this.state.zoom ? 'active' : ''}`} onClick={() => this.setState({ zoom })}>{zoom}x</button>
              ))}
            </div>
          </div>
          {this.spells.map(spellId => (
            <div className="lane" key={spellId}>
              <SpellIcon id={spellId} noLink /> <SpellLink id={spellId} />
            </div>
          ))}
        </div>
        <GeminiScrollbar
          className="timeline flex-main"
          style={{ height: totalHeight }}
          onWheel={this.handleMouseWheel}
          ref={comp => (this.gemini = comp)}
        >
          <div className={`ruler interval-${skipInterval}`} style={{ width: totalWidth }}>
            {[...Array(seconds)].map((_, second) => {
              if (second % skipInterval !== 0) {
                // Skip every second second when the text width becomes larger than the container
                return null;
              }
              return (
                <div key={second} className="lane" style={{ width: secondWidth * skipInterval }}>
                  {formatDuration(second)}
                </div>
              );
            })}
          </div>
          {this.spells.map(spellId => (
            <Events key={spellId} className="lane" events={historyBySpellId[spellId] || []} start={start} totalWidth={totalWidth} secondWidth={secondWidth} />
          ))}
        </GeminiScrollbar><commit_msg>Fix crash at start of the fight with spell timeline open<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar';
import ReactTooltip from 'react-tooltip';
import 'gemini-scrollbar/gemini-scrollbar.css';

import { formatDuration } from 'common/format';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';

import Events from './Events';

import './SpellTimeline.css';

class SpellTimeline extends React.PureComponent {
  static propTypes = {
    historyBySpellId: PropTypes.object.isRequired,
    abilities: PropTypes.object.isRequired,
    spellId: PropTypes.number,
    start: PropTypes.number.isRequired,
    end: PropTypes.number.isRequired,
  };

  constructor() {
    super();
    this.handleMouseWheel = this.handleMouseWheel.bind(this);
    this.state = {
      zoom: 1,
    };
  }

  handleMouseWheel(e) {
    // This translate vertical scrolling into horizontal scrolling
    if (!this.gemini || !this.gemini.scrollbar) {
      return;
    }

    e.preventDefault();
    e.stopPropagation();
    if (e.detail > 0) {
      // noinspection JSSuspiciousNameCombination
      this.gemini.scrollbar._viewElement.scrollLeft -= e.deltaY;
    } else {
      // noinspection JSSuspiciousNameCombination
      this.gemini.scrollbar._viewElement.scrollLeft += e.deltaY;
    }
  }

  componentDidMount() {
    this.componentDidUpdate();
  }
  componentDidUpdate() {
    ReactTooltip.rebuild();
  }

  get spells() {
    const { spellId, historyBySpellId, abilities } = this.props;
    const spellIds = spellId ? [spellId] : Object.keys(historyBySpellId).map(Number);

    return spellIds.sort((a, b) => {
      const aCooldown = abilities.getExpectedCooldownDuration(Number(a));
      const bCooldown = abilities.getExpectedCooldownDuration(Number(b));
      return aCooldown - bCooldown;
    });
  }

  gemini = null;
  render() {
    const { start, end, historyBySpellId } = this.props;
    const duration = end - start;
    const seconds = Math.ceil(duration / 1000);

    const secondWidth = 40 / this.state.zoom;
    const skipInterval = Math.ceil(40 / secondWidth);

    // 9 for the scrollbar height
    // 4 for margin
    // 36 for the ruler
    // 28 for each spell
    const totalHeight = 9 + 4 + 36 + 28 * this.spells.length;

    const totalWidth = seconds * secondWidth;

    return (
      <div className="spell-timeline flex">
        <div className="flex-sub legend">
          <div className="lane ruler-lane">
            <div className="btn-group">
              {[1, 2, 3, 5].map(zoom => (
                <button key={zoom} className={`btn btn-default btn-xs ${zoom === this.state.zoom ? 'active' : ''}`} onClick={() => this.setState({ zoom })}>{zoom}x</button>
              ))}
            </div>
          </div>
          {this.spells.map(spellId => (
            <div className="lane" key={spellId}>
              <SpellIcon id={spellId} noLink /> <SpellLink id={spellId} />
            </div>
          ))}
        </div>
        <GeminiScrollbar
          className="timeline flex-main"
          style={{ height: totalHeight }}
          onWheel={this.handleMouseWheel}
          ref={comp => (this.gemini = comp)}
        >
          <div className={`ruler interval-${skipInterval}`} style={{ width: totalWidth }}>
            {seconds && [...Array(seconds)].map((_, second) => {
              if (second % skipInterval !== 0) {
                // Skip every second second when the text width becomes larger than the container
                return null;
              }
              return (
                <div key={second} className="lane" style={{ width: secondWidth * skipInterval }}>
                  {formatDuration(second)}
                </div>
              );
            })}
          </div>
          {this.spells.map(spellId => (
            <Events key={spellId} className="lane" events={historyBySpellId[spellId] || []} start={start} totalWidth={totalWidth} secondWidth={secondWidth} />
          ))}
        </GeminiScrollbar>
{'fraction_non_alphanumeric': 0.13144266337854502, 'fraction_numerical': 0.006658446362515413, 'mean_word_length': 2.4588486140724948, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 59, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1061437', 'n_tokens_mistral': 2416, 'n_tokens_neox': 2386, 'n_words': 534}