<commit_before>import * as React from "react"
import { select } from "d3-selection"
import {
    first,
    last,
    findClosestYear,
    getRelativeMouse,
    isMobile,
    debounce
} from "charts/utils/Util"
import { Bounds } from "charts/utils/Bounds"
import { Analytics } from "site/client/Analytics"
import {
    observable,
    computed,
    autorun,
    action,
    runInAction,
    IReactionDisposer
} from "mobx"
import { observer } from "mobx-react"
import {
    ChartViewContext,
    ChartViewContextType
} from "charts/core/ChartViewContext"
import { faPlay } from "@fortawesome/free-solid-svg-icons/faPlay"
import { faPause } from "@fortawesome/free-solid-svg-icons/faPause"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"

import {
    isUnbounded,
    TimeBoundValue,
    TimeBound,
    Time,
    isUnboundedLeft,
    isUnboundedRight,
    getBoundFromTimeRange
} from "charts/utils/TimeBounds"
import Tippy from "@tippyjs/react"
import classNames from "classnames"

const DEFAULT_MIN_YEAR = 1900
const DEFAULT_MAX_YEAR = 2000

const HANDLE_TOOLTIP_FADE_TIME_MS = 2000

interface TimelineProps {
    years: number[]
    startYear: TimeBound
    endYear: TimeBound
    onTargetChange: ({
        targetStartYear,
        targetEndYear
    }: {
        targetStartYear: TimeBound
        targetEndYear: TimeBound
    }) => void
    onStartDrag?: () => void
    onStopDrag?: () => void
    singleYearMode?: boolean
    singleYearPlay?: boolean
    disablePlay?: boolean
}

@observer
export class Timeline extends React.Component<TimelineProps> {
    base: React.RefObject<HTMLDivElement> = React.createRef()

    static contextType = ChartViewContext
    context!: ChartViewContextType

    disposers!: IReactionDisposer[]

    @observable dragTarget?: "start" | "end" | "both"

    @computed get isDragging(): boolean {
        return !!this.dragTarget
    }

    // Used for storing the state of the timeline.
    // They are not suitable for direct access because start can be greater than end.
    // Use `startYear` and `endYear` if the correct order of bounds is needed.
    @observable startYearRaw: TimeBound = TimeBoundValue.unboundedLeft
    @observable endYearRaw: TimeBound = TimeBoundValue.unboundedRight

    @computed get startYear(): TimeBound {
        return Math.min(this.startYearRaw, this.endYearRaw)
    }

    @computed get endYear(): TimeBound {
        return Math.max(this.startYearRaw, this.endYearRaw)
    }

    constructor(props: TimelineProps) {
        super(props)

        if (this.props.years.length === 0) {
            console.warn("invoking HTMLTimeline with empty years array")
        }
    }

    @computed get isPlaying() {
        return this.context.chart.isPlaying
    }

    componentDidUpdate(prevProps: TimelineProps) {
        const { isPlaying, isDragging } = this
        const { startYear, endYear } = this.props
        if (
            !isPlaying &&
            !isDragging &&
            (prevProps.startYear != startYear || prevProps.endYear != endYear)
        ) {
            runInAction(() => {
                this.startYearRaw = this.props.startYear
                this.endYearRaw = this.props.endYear
            })
        }
    }

    @computed get years(): Time[] {
        return this.props.years
    }

    @computed get minYear(): Time {
        return first(this.props.years) ?? DEFAULT_MIN_YEAR
    }

    @computed get maxYear(): Time {
        return last(this.props.years) ?? DEFAULT_MAX_YEAR
    }

    @computed get timeDomain(): [Time, Time] {
        return [this.minYear, this.maxYear]
    }

    getClampedYear(inputYear: Time): Time {
        const { minYear, maxYear } = this
        return Math.min(maxYear, Math.max(minYear, inputYear))
    }

    getYearUI(bound: TimeBound): Time {
        if (isUnboundedLeft(bound)) return this.minYear
        if (isUnboundedRight(bound)) return this.maxYear
        return bound
    }

    getClosest(bound: TimeBound, defaultValue: TimeBound): TimeBound {
        if (isUnbounded(bound)) return bound
        return findClosestYear(this.years, bound) ?? defaultValue
    }

    @computed get startYearUI(): Time {
        return this.getYearUI(this.startYear)
    }

    @computed get startYearClosest(): TimeBound {
        return this.getClosest(this.startYear, TimeBoundValue.unboundedLeft)
    }

    @computed get endYearUI(): Time {
        return this.getYearUI(this.endYear)
    }

    @computed get endYearClosest(): TimeBound {
        return this.getClosest(this.endYear, TimeBoundValue.unboundedRight)
    }

    animRequest?: number

    @action.bound onStartPlaying() {
        Analytics.logChartTimelinePlay(this.context.chart.props.slug)

        let lastTime: number | undefined
        const ticksPerSec = 5

        const playFrame = action((time: number) => {
            const { isPlaying, endYearUI, years, minYear, maxYear } = this
            if (!isPlaying) return

            if (lastTime === undefined) {
                // If we start playing from the end, loop around to beginning
                if (endYearUI >= maxYear) {
                    this.startYearRaw = minYear
                    this.endYearRaw = minYear
                }
            } else {<commit_msg>Play animation has duration Either 45 seconds or 2 years per second, whichever is faster<commit_after>import * as React from "react"
import { select } from "d3-selection"
import {
    first,
    last,
    findClosestYear,
    getRelativeMouse,
    isMobile,
    debounce
} from "charts/utils/Util"
import { Bounds } from "charts/utils/Bounds"
import { Analytics } from "site/client/Analytics"
import {
    observable,
    computed,
    autorun,
    action,
    runInAction,
    IReactionDisposer
} from "mobx"
import { observer } from "mobx-react"
import {
    ChartViewContext,
    ChartViewContextType
} from "charts/core/ChartViewContext"
import { faPlay } from "@fortawesome/free-solid-svg-icons/faPlay"
import { faPause } from "@fortawesome/free-solid-svg-icons/faPause"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"

import {
    isUnbounded,
    TimeBoundValue,
    TimeBound,
    Time,
    isUnboundedLeft,
    isUnboundedRight,
    getBoundFromTimeRange
} from "charts/utils/TimeBounds"
import Tippy from "@tippyjs/react"
import classNames from "classnames"

const DEFAULT_MIN_YEAR = 1900
const DEFAULT_MAX_YEAR = 2000

const HANDLE_TOOLTIP_FADE_TIME_MS = 2000

interface TimelineProps {
    years: number[]
    startYear: TimeBound
    endYear: TimeBound
    onTargetChange: ({
        targetStartYear,
        targetEndYear
    }: {
        targetStartYear: TimeBound
        targetEndYear: TimeBound
    }) => void
    onStartDrag?: () => void
    onStopDrag?: () => void
    singleYearMode?: boolean
    singleYearPlay?: boolean
    disablePlay?: boolean
}

@observer
export class Timeline extends React.Component<TimelineProps> {
    base: React.RefObject<HTMLDivElement> = React.createRef()

    static contextType = ChartViewContext
    context!: ChartViewContextType

    disposers!: IReactionDisposer[]

    @observable dragTarget?: "start" | "end" | "both"

    @computed get isDragging(): boolean {
        return !!this.dragTarget
    }

    // Used for storing the state of the timeline.
    // They are not suitable for direct access because start can be greater than end.
    // Use `startYear` and `endYear` if the correct order of bounds is needed.
    @observable startYearRaw: TimeBound = TimeBoundValue.unboundedLeft
    @observable endYearRaw: TimeBound = TimeBoundValue.unboundedRight

    @computed get startYear(): TimeBound {
        return Math.min(this.startYearRaw, this.endYearRaw)
    }

    @computed get endYear(): TimeBound {
        return Math.max(this.startYearRaw, this.endYearRaw)
    }

    constructor(props: TimelineProps) {
        super(props)

        if (this.props.years.length === 0) {
            console.warn("invoking HTMLTimeline with empty years array")
        }
    }

    @computed get isPlaying() {
        return this.context.chart.isPlaying
    }

    componentDidUpdate(prevProps: TimelineProps) {
        const { isPlaying, isDragging } = this
        const { startYear, endYear } = this.props
        if (
            !isPlaying &&
            !isDragging &&
            (prevProps.startYear != startYear || prevProps.endYear != endYear)
        ) {
            runInAction(() => {
                this.startYearRaw = this.props.startYear
                this.endYearRaw = this.props.endYear
            })
        }
    }

    @computed get years(): Time[] {
        return this.props.years
    }

    @computed get minYear(): Time {
        return first(this.props.years) ?? DEFAULT_MIN_YEAR
    }

    @computed get maxYear(): Time {
        return last(this.props.years) ?? DEFAULT_MAX_YEAR
    }

    @computed get timeDomain(): [Time, Time] {
        return [this.minYear, this.maxYear]
    }

    getClampedYear(inputYear: Time): Time {
        const { minYear, maxYear } = this
        return Math.min(maxYear, Math.max(minYear, inputYear))
    }

    getYearUI(bound: TimeBound): Time {
        if (isUnboundedLeft(bound)) return this.minYear
        if (isUnboundedRight(bound)) return this.maxYear
        return bound
    }

    getClosest(bound: TimeBound, defaultValue: TimeBound): TimeBound {
        if (isUnbounded(bound)) return bound
        return findClosestYear(this.years, bound) ?? defaultValue
    }

    @computed get startYearUI(): Time {
        return this.getYearUI(this.startYear)
    }

    @computed get startYearClosest(): TimeBound {
        return this.getClosest(this.startYear, TimeBoundValue.unboundedLeft)
    }

    @computed get endYearUI(): Time {
        return this.getYearUI(this.endYear)
    }

    @computed get endYearClosest(): TimeBound {
        return this.getClosest(this.endYear, TimeBoundValue.unboundedRight)
    }

    animRequest?: number

    private readonly PLAY_ANIMATION_SECONDS = 45
    @action.bound onStartPlaying() {
        Analytics.logChartTimelinePlay(this.context.chart.props.slug)

        let lastTime: number | undefined
        const ticksPerSec = Math.max(
            this.years.length / this.PLAY_ANIMATION_SECONDS,
            2
        )

        const playFrame = action((time: number) => {
            const { isPlaying, endYearUI, years, minYear, maxYear } = this
            if (!isPlaying) return

            if (lastTime === undefined) {
                // If we start playing from the end, loop around to beginning
                if (endYearUI >= maxYear) {
                    this.startYearRaw = minYear
                    this.endYearRaw = minYear
                }
            } else {
{'fraction_non_alphanumeric': 0.09530914855917187, 'fraction_numerical': 0.0032640119369579407, 'mean_word_length': 2.5986577181208053, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 19, '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': '4484166', 'n_tokens_mistral': 3284, 'n_tokens_neox': 3117, 'n_words': 761}