<commit_before>/* HeightedLegend.tsx
 * ================
 *
 */

import * as React from 'react'
import { some, noop, includes, cloneDeep, max, sortBy } from './Util'
import { defaultTo } from './Util'
import { computed } from 'mobx'
import { observer } from 'mobx-react'
import TextWrap from './TextWrap'
import AxisScale from './AxisScale'
import Bounds from './Bounds'

export interface HeightedLegendProps {
    items: HeightedLegendItem[],
    maxWidth?: number,
    fontSize: number
}

export interface HeightedLegendItem {
    key: string,
    label: string,
    color: string,
    yValue: number
}

interface HeightedLegendMark {
    item: HeightedLegendItem,
    textWrap: TextWrap,
    width: number,
    height: number
}

export default class HeightedLegend {
    props: HeightedLegendProps

    @computed get fontSize(): number { return 0.8*this.props.fontSize }
    @computed get rectSize(): number { return 10 }
    @computed get rectPadding(): number { return 5 }
    @computed get maxWidth() { return defaultTo(this.props.maxWidth, Infinity) }

    @computed.struct get marks(): HeightedLegendMark[] {
        const { fontSize, rectSize, rectPadding, maxWidth } = this
        const maxTextWidth = maxWidth - rectSize - rectPadding

        return this.props.items.map(item => {
            const textWrap = new TextWrap({ text: item.label, maxWidth: maxTextWidth, fontSize: fontSize })
            return {
                item: item,
                textWrap: textWrap,
                width: rectSize + rectPadding + textWrap.width,
                height: Math.max(textWrap.height, rectSize / 4)
            }
        })
    }

    @computed get width(): number {
        if (this.marks.length === 0)
            return 0
        else
            return defaultTo(max(this.marks.map(d => d.width)), 0)
    }

    constructor(props: HeightedLegendProps) {
        this.props = props
    }
}

export interface HeightedLegendViewProps {
    x: number,
    legend: HeightedLegend,
    yScale: AxisScale,
    focusKeys: string[],
    onMouseOver?: (key: string) => void,
    onClick?: (key: string) => void,
    onMouseLeave?: () => void
}

@observer
export class HeightedLegendView extends React.Component<HeightedLegendViewProps> {
    @computed get onMouseOver(): any { return defaultTo(this.props.onMouseOver, noop) }
    @computed get onMouseLeave(): any { return defaultTo(this.props.onMouseLeave, noop) }
    @computed get onClick(): any { return defaultTo(this.props.onClick, noop) }

    @computed get isFocusMode() {
        return this.props.focusKeys.length !== this.props.legend.marks.length && some(this.props.legend.marks, m => includes(this.props.focusKeys, m.item.key))
    }

    // Naive initial placement of each mark at the target height, before collision detection
    @computed get initialMarks() {
        const { legend, x, yScale } = this.props

        return sortBy(legend.marks.map(m => {
            const y = yScale.place(m.item.yValue)

            // Don't let them go off the edge
            /*if (y+m.height > yScale.rangeMax) {
                y = yScale.rangeMax-m.height
            } else if (y < yScale.rangeMin) {
                y = yScale.rangeMin
            }*/

            const bounds = new Bounds(x, y - m.height / 2, m.width, m.height)

            return {
                mark: m,
                origBounds: bounds,
                bounds: bounds,
                isOverlap: false
            }
        }), m => m.bounds.y)
    }

    // Each mark starts at target height. When a conflict is detected, the lower label is pushed down a bit.
    @computed get topDownPlacement() {
        const { initialMarks } = this
        const { yScale } = this.props

        const marks = cloneDeep(initialMarks)
        for (let i = 0; i < marks.length; i++) {
            for (let j = i + 1; j < marks.length; j++) {
                const m1 = marks[i]
                const m2 = marks[j]
                const isOverlap = m1.bounds.intersects(m2.bounds)
                if (isOverlap) {
                    const overlapHeight = (m1.bounds.y + m1.bounds.height) - m2.bounds.y
                    const newBounds = m2.bounds.extend({ y: m2.bounds.y + overlapHeight })

                    // Don't push off the edge of the chart
                    if (newBounds.bottom > yScale.rangeMax) {
                        m2.isOverlap = true
                    } else {
                        m2.bounds = newBounds
                    }
                }
            }
        }

        return marks
    }<commit_msg>Fix rare layout bug in stacked area charts<commit_after>/* HeightedLegend.tsx
 * ================
 *
 */

import * as React from 'react'
import { some, noop, includes, cloneDeep, max, sortBy } from './Util'
import { defaultTo } from './Util'
import { computed } from 'mobx'
import { observer } from 'mobx-react'
import TextWrap from './TextWrap'
import AxisScale from './AxisScale'
import Bounds from './Bounds'

export interface HeightedLegendProps {
    items: HeightedLegendItem[],
    maxWidth?: number,
    fontSize: number
}

export interface HeightedLegendItem {
    key: string,
    label: string,
    color: string,
    yValue: number
}

interface HeightedLegendMark {
    item: HeightedLegendItem,
    textWrap: TextWrap,
    width: number,
    height: number
}

export default class HeightedLegend {
    props: HeightedLegendProps

    @computed get fontSize(): number { return 0.8*this.props.fontSize }
    @computed get rectSize(): number { return 10 }
    @computed get rectPadding(): number { return 5 }
    @computed get maxWidth() { return defaultTo(this.props.maxWidth, Infinity) }

    @computed.struct get marks(): HeightedLegendMark[] {
        const { fontSize, rectSize, rectPadding, maxWidth } = this
        const maxTextWidth = maxWidth - rectSize - rectPadding

        return this.props.items.map(item => {
            const textWrap = new TextWrap({ text: item.label, maxWidth: maxTextWidth, fontSize: fontSize })
            return {
                item: item,
                textWrap: textWrap,
                width: rectSize + rectPadding + textWrap.width,
                height: Math.max(textWrap.height, rectSize / 4)
            }
        })
    }

    @computed get width(): number {
        if (this.marks.length === 0)
            return 0
        else
            return defaultTo(max(this.marks.map(d => d.width)), 0)
    }

    constructor(props: HeightedLegendProps) {
        this.props = props
    }
}

export interface HeightedLegendViewProps {
    x: number,
    legend: HeightedLegend,
    yScale: AxisScale,
    focusKeys: string[],
    onMouseOver?: (key: string) => void,
    onClick?: (key: string) => void,
    onMouseLeave?: () => void
}

@observer
export class HeightedLegendView extends React.Component<HeightedLegendViewProps> {
    @computed get onMouseOver(): any { return defaultTo(this.props.onMouseOver, noop) }
    @computed get onMouseLeave(): any { return defaultTo(this.props.onMouseLeave, noop) }
    @computed get onClick(): any { return defaultTo(this.props.onClick, noop) }

    @computed get isFocusMode() {
        return this.props.focusKeys.length !== this.props.legend.marks.length && some(this.props.legend.marks, m => includes(this.props.focusKeys, m.item.key))
    }

    // Naive initial placement of each mark at the target height, before collision detection
    @computed get initialMarks() {
        const { legend, x, yScale } = this.props

        return sortBy(legend.marks.map(m => {
            const y = yScale.place(m.item.yValue)

            // Don't let them go off the edge
            /*if (y+m.height > yScale.rangeMax) {
                y = yScale.rangeMax-m.height
            } else if (y < yScale.rangeMin) {
                y = yScale.rangeMin
            }*/

            const bounds = new Bounds(x, y - m.height / 2, m.width, m.height)

            return {
                mark: m,
                y: y,
                origBounds: bounds,
                bounds: bounds,
                isOverlap: false
            }
        }), m => m.y)
    }

    // Each mark starts at target height. When a conflict is detected, the lower label is pushed down a bit.
    @computed get topDownPlacement() {
        const { initialMarks } = this
        const { yScale } = this.props

        const marks = cloneDeep(initialMarks)
        for (let i = 0; i < marks.length; i++) {
            for (let j = i + 1; j < marks.length; j++) {
                const m1 = marks[i]
                const m2 = marks[j]
                const isOverlap = m1.bounds.intersects(m2.bounds)
                if (isOverlap) {
                    const overlapHeight = (m1.bounds.y + m1.bounds.height) - m2.bounds.y
                    const newBounds = m2.bounds.extend({ y: m2.bounds.y + overlapHeight })

                    // Don't push off the edge of the chart
                    if (newBounds.bottom > yScale.rangeMax) {
                        m2.isOverlap = true
                    } else {
                        m2.bounds = newBounds
                    }
                }
            }
        }

        return marks
    }
{'fraction_non_alphanumeric': 0.10916412712233348, 'fraction_numerical': 0.00500653025685677, 'mean_word_length': 2.0, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 25, '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': '1526499', 'n_tokens_mistral': 2758, 'n_tokens_neox': 2620, 'n_words': 741}