<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow
import * as React from 'react';
import { timeCode } from '../../../utils/time-code';
import classNames from 'classnames';
import Tooltip from '../Tooltip';

import type { CssPixels, DevicePixels } from '../../../types/units';

type Props<HoveredItem> = {
  containerWidth: CssPixels,
  containerHeight: CssPixels,
  className: string,
  onMouseDown?: (HoveredItem | null) => void,
  onDoubleClickItem: (HoveredItem | null) => void,
  getHoveredItemInfo: HoveredItem => React.Node,
  drawCanvas: (CanvasRenderingContext2D, HoveredItem | null) => void,
  isDragging: boolean,
  hitTest: (x: CssPixels, y: CssPixels) => HoveredItem | null,
};

type State<HoveredItem> = {
  hoveredItem: HoveredItem | null,
  mouseX: CssPixels,
  mouseY: CssPixels,
};

require('./Canvas.css');

// This isn't a PureComponent on purpose: we always want to update if the parent updates
// But we still conditionally update the canvas itself, see componentDidUpdate.
export default class ChartCanvas<HoveredItem> extends React.Component<
  Props<HoveredItem>,
  State<HoveredItem>
> {
  _devicePixelRatio: number;
  _ctx: CanvasRenderingContext2D;
  _canvas: HTMLCanvasElement | null;

  constructor(props: Props<HoveredItem>) {
    super(props);
    this._devicePixelRatio = 1;
    this.state = {
      hoveredItem: null,
      mouseX: 0,
      mouseY: 0,
    };

    (this: any)._setCanvasRef = this._setCanvasRef.bind(this);
    (this: any)._onMouseDown = this._onMouseDown.bind(this);
    (this: any)._onMouseMove = this._onMouseMove.bind(this);
    (this: any)._onMouseOut = this._onMouseOut.bind(this);
    (this: any)._onDoubleClick = this._onDoubleClick.bind(this);
    (this: any)._getHoveredItemInfo = this._getHoveredItemInfo.bind(this);
  }

  _scheduleDraw() {
    const { className, drawCanvas } = this.props;
    window.requestAnimationFrame(() => {
      if (this._canvas) {
        timeCode(`${className} render`, () => {
          this._prepCanvas();
          drawCanvas(this._ctx, this.state.hoveredItem);
        });
      }
    });
  }

  _prepCanvas() {
    const canvas = this._canvas;
    const { containerWidth, containerHeight } = this.props;
    const { devicePixelRatio } = window;
    const pixelWidth: DevicePixels = containerWidth * devicePixelRatio;
    const pixelHeight: DevicePixels = containerHeight * devicePixelRatio;
    if (!canvas) {
      return;
    }
    // Satisfy the null check for Flow.
    const ctx = this._ctx || canvas.getContext('2d', { alpha: false });
    if (!this._ctx) {
      this._ctx = ctx;
    }
    if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
      canvas.width = pixelWidth;
      canvas.height = pixelHeight;
      canvas.style.width = containerWidth + 'px';
      canvas.style.height = containerHeight + 'px';
      ctx.scale(this._devicePixelRatio, this._devicePixelRatio);
    }
    if (this._devicePixelRatio !== devicePixelRatio) {
      // Make sure and multiply by the inverse of the previous ratio, as the scaling
      // operates off of the previous set scale.
      const scale = 1 / this._devicePixelRatio * devicePixelRatio;
      ctx.scale(scale, scale);
      this._devicePixelRatio = devicePixelRatio;
    }
  }

  _onMouseDown() {
    if (this.props.onMouseDown !== undefined) {
      this.props.onMouseDown(this.state.hoveredItem);
    }
  }

  _hoveredItemFromMouseEvent(event: SyntheticMouseEvent<>): HoveredItem | null {
    if (!this._canvas) {
      throw new Error('Canvas ref not set');
    }

    const rect = this._canvas.getBoundingClientRect();
    const x: CssPixels = event.pageX - rect.left;
    const y: CssPixels = event.pageY - rect.top;

    return this.props.hitTest(x, y);
  }

  _onMouseMove(event: SyntheticMouseEvent<>) {
    if (!this._canvas) {
      return;
    }

    const maybeHoveredItem = this._hoveredItemFromMouseEvent(event);

    if (maybeHoveredItem !== null) {
      this.setState({
        hoveredItem: maybeHoveredItem,
        mouseX: event.pageX,
        mouseY: event.pageY,
      });
    } else if (this.state.hoveredItem !== null) {
      this.setState({
        hoveredItem: null,
      });
    }
  }

  _onMouseOut() {
    if (this.state.hoveredItem !== null) {
      this.setState({ hoveredItem: null });
    }
  }

  _onDoubleClick(event: SyntheticMouseEvent<>) {
    if (!this._canvas) {
      return;
    }

    // The mouseDownListener in the Viewport triggers state updates
    // which in turn causes componentWillReceiveProps in this
    // component to clear the hovered item.  Thus, at the time when
    // this event handler is called, `this.state.hoveredItem` is null.
    // Obtain the hovered item from the mouse event instead.
    const maybeHoveredItem = this._hoveredItemFromMouseEvent(event);
    this.props.onDoubleClickItem(maybeHoveredItem);
  }

  _getHoveredItemInfo(): React.Node {
    const { hoveredItem } = this.state;
    if (hoveredItem === null) {
      return null;
    }
    return this.props.getHoveredItemInfo(hoveredItem);
  }

  _setCanvasRef(canvas: HTMLCanvasElement | null) {
    this._canvas = canvas;
  }

  componentWillReceiveProps() {
    // It is possible that the data backing the chart has been
    // changed, for instance after symbolication. Clear hoveredItem so
    // that it doesn't point to possibly invalid data.
    if (this.state.hoveredItem !== null) {
      this.setState({ hoveredItem: null });<commit_msg>Only get hovered item from mouse event if it's not in state (#927)<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// @flow
import * as React from 'react';
import { timeCode } from '../../../utils/time-code';
import classNames from 'classnames';
import Tooltip from '../Tooltip';

import type { CssPixels, DevicePixels } from '../../../types/units';

type Props<HoveredItem> = {
  containerWidth: CssPixels,
  containerHeight: CssPixels,
  className: string,
  onMouseDown?: (HoveredItem | null) => void,
  onDoubleClickItem: (HoveredItem | null) => void,
  getHoveredItemInfo: HoveredItem => React.Node,
  drawCanvas: (CanvasRenderingContext2D, HoveredItem | null) => void,
  isDragging: boolean,
  hitTest: (x: CssPixels, y: CssPixels) => HoveredItem | null,
};

type State<HoveredItem> = {
  hoveredItem: HoveredItem | null,
  mouseX: CssPixels,
  mouseY: CssPixels,
};

require('./Canvas.css');

// This isn't a PureComponent on purpose: we always want to update if the parent updates
// But we still conditionally update the canvas itself, see componentDidUpdate.
export default class ChartCanvas<HoveredItem> extends React.Component<
  Props<HoveredItem>,
  State<HoveredItem>
> {
  _devicePixelRatio: number;
  _ctx: CanvasRenderingContext2D;
  _canvas: HTMLCanvasElement | null;

  constructor(props: Props<HoveredItem>) {
    super(props);
    this._devicePixelRatio = 1;
    this.state = {
      hoveredItem: null,
      mouseX: 0,
      mouseY: 0,
    };

    (this: any)._setCanvasRef = this._setCanvasRef.bind(this);
    (this: any)._onMouseDown = this._onMouseDown.bind(this);
    (this: any)._onMouseMove = this._onMouseMove.bind(this);
    (this: any)._onMouseOut = this._onMouseOut.bind(this);
    (this: any)._onDoubleClick = this._onDoubleClick.bind(this);
    (this: any)._getHoveredItemInfo = this._getHoveredItemInfo.bind(this);
  }

  _scheduleDraw() {
    const { className, drawCanvas } = this.props;
    window.requestAnimationFrame(() => {
      if (this._canvas) {
        timeCode(`${className} render`, () => {
          this._prepCanvas();
          drawCanvas(this._ctx, this.state.hoveredItem);
        });
      }
    });
  }

  _prepCanvas() {
    const canvas = this._canvas;
    const { containerWidth, containerHeight } = this.props;
    const { devicePixelRatio } = window;
    const pixelWidth: DevicePixels = containerWidth * devicePixelRatio;
    const pixelHeight: DevicePixels = containerHeight * devicePixelRatio;
    if (!canvas) {
      return;
    }
    // Satisfy the null check for Flow.
    const ctx = this._ctx || canvas.getContext('2d', { alpha: false });
    if (!this._ctx) {
      this._ctx = ctx;
    }
    if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
      canvas.width = pixelWidth;
      canvas.height = pixelHeight;
      canvas.style.width = containerWidth + 'px';
      canvas.style.height = containerHeight + 'px';
      ctx.scale(this._devicePixelRatio, this._devicePixelRatio);
    }
    if (this._devicePixelRatio !== devicePixelRatio) {
      // Make sure and multiply by the inverse of the previous ratio, as the scaling
      // operates off of the previous set scale.
      const scale = 1 / this._devicePixelRatio * devicePixelRatio;
      ctx.scale(scale, scale);
      this._devicePixelRatio = devicePixelRatio;
    }
  }

  _onMouseDown() {
    if (this.props.onMouseDown !== undefined) {
      this.props.onMouseDown(this.state.hoveredItem);
    }
  }

  _hoveredItemFromMouseEvent(event: SyntheticMouseEvent<>): HoveredItem | null {
    if (!this._canvas) {
      throw new Error('Canvas ref not set');
    }

    const rect = this._canvas.getBoundingClientRect();
    const x: CssPixels = event.pageX - rect.left;
    const y: CssPixels = event.pageY - rect.top;

    return this.props.hitTest(x, y);
  }

  _onMouseMove(event: SyntheticMouseEvent<>) {
    if (!this._canvas) {
      return;
    }

    const maybeHoveredItem = this._hoveredItemFromMouseEvent(event);

    if (maybeHoveredItem !== null) {
      this.setState({
        hoveredItem: maybeHoveredItem,
        mouseX: event.pageX,
        mouseY: event.pageY,
      });
    } else if (this.state.hoveredItem !== null) {
      this.setState({
        hoveredItem: null,
      });
    }
  }

  _onMouseOut() {
    if (this.state.hoveredItem !== null) {
      this.setState({ hoveredItem: null });
    }
  }

  _onDoubleClick(event: SyntheticMouseEvent<>) {
    if (!this._canvas) {
      return;
    }

    // The mouseDownListener in the Viewport triggers state updates
    // which in turn causes componentWillReceiveProps in this
    // component to clear the hovered item.  Thus, at the time when
    // this event handler is called, `this.state.hoveredItem` is null.
    // Obtain the hovered item from the mouse event instead.
    const maybeHoveredItem =
      this.state.hoveredItem === null
        ? this._hoveredItemFromMouseEvent(event)
        : this.state.hoveredItem;
    this.props.onDoubleClickItem(maybeHoveredItem);
  }

  _getHoveredItemInfo(): React.Node {
    const { hoveredItem } = this.state;
    if (hoveredItem === null) {
      return null;
    }
    return this.props.getHoveredItemInfo(hoveredItem);
  }

  _setCanvasRef(canvas: HTMLCanvasElement | null) {
    this._canvas = canvas;
  }

  componentWillReceiveProps() {
    // It is possible that the data backing the chart has been
    // changed, for instance after symbolication. Clear hoveredItem so
    // that it doesn't point to possibly invalid data.
    if (this.state.hoveredItem !== null) {
      this.setState({ hoveredItem: null });
{'fraction_non_alphanumeric': 0.11966637401229148, 'fraction_numerical': 0.0021949078138718174, 'mean_word_length': 3.1226927252985885, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 37, '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': '7304596', 'n_tokens_mistral': 3594, 'n_tokens_neox': 3489, 'n_words': 901}