TargetProcess/tauCharts

View on GitHub
src/elements/decorators/layer-labels-rules.ts

Summary

Maintainability
F
1 wk
Test Coverage
import {LayerLabelsModel, LayerLabelsModelObj} from './layer-labels-model';
import {ScaleFunction} from '../../definitions';
import {hasXOverflow, hasYOverflow} from '../../utils/utils';
var rules: {[alias: string]: LabelRule} = {};

type LabelRule = (prev?: LayerLabelsModelObj, args?) => LayerLabelsModelObj;

export class LayerLabelsRules {

    static regRule(alias: string, func: LabelRule) {
        rules[alias] = func;
        return this;
    }

    static getRule(alias: string) {
        return rules[alias];
    }
}

const findCutIndex = (text: string, labelWidth: number, availableSpace: number) => {
    return ((availableSpace < labelWidth) ?
        (Math.max(1, Math.floor(availableSpace * text.length / labelWidth)) - 1) :
        (text.length));
};

const cutString = (str: string, index: number) => ((index === 0) ?
    '' :
    str.slice(0, index).replace(/\.+$/g, '') + '\u2026');

const cutLines = ({lines, linesWidths, separator}, availableSpace: number) => {
    return lines
        .map(function (line, lineIndex) {
            const index = findCutIndex(line, linesWidths[lineIndex], availableSpace);
            return ((index < line.length) ? cutString(line, index) : line);
        })
        .join(separator);
};

var getPadDivider = (prev, row): number => {
    var labelInfo = prev.labelLinesAndSeparator(row);
    var pad = 5 - (labelInfo.lines.length - 1);
    return pad < 1 ? 1 : pad;
};
var isPositive = (scale, row) => scale.discrete || (!scale.discrete && row[scale.dim] >= 0);
var isNegative = (scale, row) => !scale.discrete && row[scale.dim] < 0;
var getXPad = (prev, row) => ((prev.w(row) / 2) + Math.floor(prev.model.size(row) / getPadDivider(prev, row)));
var getYPad = (prev, row) => ((prev.h(row) / 2) + Math.floor(prev.model.size(row) / getPadDivider(prev, row)));
var alignByX = (exp) => {
    return (prev) => {
        return {
            dx: (row) => {

                var ordinateScale = prev.model.scaleY;

                if ((exp[2] === '+') && !isPositive(ordinateScale, row)) {
                    return prev.dx(row);
                }

                if ((exp[2] === '-') && !isNegative(ordinateScale, row)) {
                    return prev.dx(row);
                }

                var k = (exp[1]);
                var u = (exp[0] === exp[0].toUpperCase()) ? 1 : 0;

                return prev.dx(row) + (k * u * prev.model.size(row) / 2) + (k * getXPad(prev, row));
            }
        };
    };
};

var alignByY = (exp) => {
    return (prev) => {
        return {
            dy: (row) => {

                var ordinateScale = prev.model.scaleY;

                if ((exp[2] === '+') && !isPositive(ordinateScale, row)) {
                    return prev.dy(row);
                }

                if ((exp[2] === '-') && !isNegative(ordinateScale, row)) {
                    return prev.dy(row);
                }

                var k = (exp[1]);
                var u = (exp[0] === exp[0].toUpperCase()) ? 1 : 0;

                return prev.dy(row) + (k * u * prev.model.size(row) / 2) + (k * getYPad(prev, row));
            }
        };
    };
};

LayerLabelsRules
    .regRule('l', alignByX(['l', -1, null]))
    .regRule('L', alignByX(['L', -1, null]))
    .regRule('l+', alignByX(['l', -1, '+']))
    .regRule('l-', alignByX(['l', -1, '-']))
    .regRule('L+', alignByX(['L', -1, '+']))
    .regRule('L-', alignByX(['L', -1, '-']))

    .regRule('r', alignByX(['r', 1, null]))
    .regRule('R', alignByX(['R', 1, null]))
    .regRule('r+', alignByX(['r', 1, '+']))
    .regRule('r-', alignByX(['r', 1, '-']))
    .regRule('R+', alignByX(['R', 1, '+']))
    .regRule('R-', alignByX(['R', 1, '-']))

    .regRule('t', alignByY(['t', -1, null]))
    .regRule('T', alignByY(['T', -1, null]))
    .regRule('t+', alignByY(['t', -1, '+']))
    .regRule('t-', alignByY(['t', -1, '-']))
    .regRule('T+', alignByY(['T', -1, '+']))
    .regRule('T-', alignByY(['T', -1, '-']))

    .regRule('b', alignByY(['b', 1, null]))
    .regRule('B', alignByY(['B', 1, null]))
    .regRule('b+', alignByY(['b', 1, '+']))
    .regRule('b-', alignByY(['b', 1, '-']))
    .regRule('B+', alignByY(['B', 1, '+']))
    .regRule('B-', alignByY(['B', 1, '-']))

    .regRule('rotate-on-size-overflow', (prev, {data, lineBreakAvailable}) => {

        var out = ((row) => prev.model.size(row) < prev.w(row));
        var overflowCount = data.reduce((memo, row) => (memo + (out(row) ? 1 : 0)), 0);

        var isRot = ((overflowCount / data.length) > 0.5);

        var changes = {};
        if (isRot) {
            var padKoeff = lineBreakAvailable ? -0.5 : 0.5;
            changes = {
                angle: () => -90,
                w: (row) => prev.h(row),
                h: (row) => prev.w(row),
                dx: (row) => (prev.h(row) * padKoeff - 2),
                dy: () => 0
            };
        }

        return changes;
    })

    .regRule('hide-by-label-height-vertical', (prev) => {

        return {

            hide: (row) => {

                let availableSpace;
                let requiredSpace;
                if (prev.angle(row) === 0) {
                    requiredSpace = prev.h(row);
                    availableSpace = Math.abs(prev.model.y0(row) - prev.model.yi(row));
                } else {
                    requiredSpace = prev.w(row);
                    availableSpace = prev.model.size(row);
                }

                if (requiredSpace > availableSpace) {
                    return true;
                }

                return prev.hide(row);
            }
        };
    })

    .regRule('cut-label-vertical', (prev) => {

        return {

            h: (row) => {
                const reserved = prev.h(row);
                if (Math.abs(prev.angle(row)) > 0) {
                    const available = Math.abs(prev.model.y0(row) - prev.model.yi(row));
                    return ((available < reserved) ? available : reserved);
                }

                return reserved;
            },

            w: (row) => {
                const reserved = prev.w(row);
                if (prev.angle(row) === 0) {
                    const available = prev.model.size(row);
                    return ((available < reserved) ? available : reserved);
                }

                return reserved;
            },

            label: (row) => {
                let available;

                if (prev.angle(row) === 0) {
                    available = prev.model.size(row);
                } else {
                    available = Math.abs(prev.model.y0(row) - prev.model.yi(row));
                }

                return cutLines(prev.labelLinesAndSeparator(row), available);
            },

            dy: (row) => {
                const prevDy = prev.dy(row);

                if (prev.angle(row) !== 0) {
                    const reserved = prev.h(row);
                    const available = Math.abs(prev.model.y0(row) - prev.model.yi(row));

                    return ((available < reserved) ?
                            (available * prevDy / reserved) :
                            (prevDy)
                    );
                }

                return prevDy;
            }
        };
    })

    .regRule('cut-outer-label-vertical', (prev) => {

        return {

            h: (row, args) => {
                const reserved = prev.h(row);
                if (Math.abs(prev.angle(row)) > 0) {
                    const available = (prev.model.y0(row) < prev.model.yi(row) ?
                        (args.maxHeight - prev.model.yi(row)) :
                        (prev.model.yi(row)));
                    return ((available < reserved) ? available : reserved);
                }

                return reserved;
            },

            w: (row) => {
                const reserved = prev.w(row);
                if (prev.angle(row) === 0) {
                    const available = prev.model.size(row);
                    return ((available < reserved) ? available : reserved);
                }

                return reserved;
            },

            label: (row, args) => {
                let available;

                if (prev.angle(row) === 0) {
                    available = prev.model.size(row);
                } else {
                    available = (prev.model.y0(row) < prev.model.yi(row) ?
                        (args.maxHeight - prev.model.yi(row)) :
                        (prev.model.yi(row)));
                }

                return cutLines(prev.labelLinesAndSeparator(row), available);
            },

            dy: (row, args) => {
                const prevDy = prev.dy(row);

                if (prev.angle(row) !== 0) {
                    const reserved = prev.h(row);
                    const available = (prev.model.y0(row) < prev.model.yi(row) ?
                        (args.maxHeight - prev.model.yi(row)) :
                        (prev.model.yi(row)));

                    return ((available < reserved) ?
                            (available * prevDy / reserved) :
                            (prevDy)
                    );
                }

                return prevDy;
            }
        };
    })

    .regRule('from-beginning', (prev) => {
        var y0 = (row) => prev.model.y0(row);
        return (prev.model.flip ? {x: y0} : {y: y0});
    })

    .regRule('to-end', (prev) => {
        var yi = (row) => prev.model.yi(row);
        return (prev.model.flip ? {x: yi} : {y: yi});
    })

    .regRule('towards', (prev) => {
        var getSign = (prev, row) => (prev.model.yi(row) - prev.model.y0(row) >= 0 ? 1 : -1);
        var getPad = (prev.model.flip ? getXPad : getYPad);
        var dy = (row) => (getSign(prev, row) * getPad(prev, row));
        return (prev.model.flip ? {dx: dy} : {dy: dy});
    })

    .regRule('inside-start-then-outside-end-horizontal', (prev, args) => {

        var innerStart = [
            LayerLabelsRules.getRule('from-beginning'),
            LayerLabelsRules.getRule('towards'),
            LayerLabelsRules.getRule('cut-label-horizontal')
        ].reduce((p, r) => LayerLabelsModel.compose(p, r(p, args)), prev);

        var outerEnd = [
            LayerLabelsRules.getRule('to-end'),
            LayerLabelsRules.getRule('towards'),
            LayerLabelsRules.getRule('cut-outer-label-horizontal')
        ].reduce((p, r) => LayerLabelsModel.compose(p, r(p, args)), prev);

        var betterInside = (row) => (innerStart.label(row).length >= outerEnd.label(row).length);

        return Object.assign(
            {},
            innerStart,
            ['x', 'dx', 'hide', 'label'].reduce((obj, prop) => {
                obj[prop] = (row) => ((betterInside(row) ? innerStart : outerEnd)[prop](row));
                return obj;
            }, {})
        );

    })

    .regRule('inside-start-then-outside-end-vertical', (prev, args) => {

        var innerStart = [
            LayerLabelsRules.getRule('from-beginning'),
            LayerLabelsRules.getRule('towards'),
            LayerLabelsRules.getRule('cut-label-vertical')
        ].reduce((p, r) => LayerLabelsModel.compose(p, r(p, args)), prev);

        var outerEnd = [
            LayerLabelsRules.getRule('to-end'),
            LayerLabelsRules.getRule('towards'),
            LayerLabelsRules.getRule('cut-outer-label-vertical')
        ].reduce((p, r) => LayerLabelsModel.compose(p, r(p, args)), prev);

        var betterInside = (row) => (innerStart.label(row).length >= outerEnd.label(row).length);

        return Object.assign(
            {},
            innerStart,
            ['y', 'dy', 'hide', 'label'].reduce((obj, prop) => {
                obj[prop] = (row) => ((betterInside(row) ? innerStart : outerEnd)[prop](row));
                return obj;
            }, {})
        );

    })

    .regRule('outside-then-inside-horizontal', (prev, args) => {

        var outer = ['r+', 'l-', 'cut-outer-label-horizontal']
            .map(LayerLabelsRules.getRule)
            .reduce((p, r) => LayerLabelsModel.compose(p, r(p, args)), prev);

        var inner = ['r-', 'l+', 'hide-by-label-height-horizontal', 'cut-label-horizontal']
            .map(LayerLabelsRules.getRule)
            .reduce((p, r) => LayerLabelsModel.compose(p, r(p, args)), prev);

        var betterInside = (row) => (inner.label(row).length > outer.label(row).length);

        return Object.assign(
            {},
            outer,
            ['x', 'dx', 'hide', 'label'].reduce((obj, prop) => {
                obj[prop] = (row) => ((betterInside(row) ? inner : outer)[prop](row));
                return obj;
            }, {})
        );
    })

    .regRule('outside-then-inside-vertical', (prev, args) => {

        var outer = ['t+', 'b-', 'cut-outer-label-vertical']
            .map(LayerLabelsRules.getRule)
            .reduce((p, r) => LayerLabelsModel.compose(p, r(p, args)), prev);

        var inner = ['t-', 'b+', 'hide-by-label-height-vertical', 'cut-label-vertical']
            .map(LayerLabelsRules.getRule)
            .reduce((p, r) => LayerLabelsModel.compose(p, r(p, args)), prev);

        var betterInside = (row) => {
            var yPosition = outer.y(row, args) + outer.dy(row, args);
            return yPosition <= 0 || yPosition >= args.maxHeight;
        };

        return Object.assign(
            {},
            outer,
            ['y', 'dy', 'hide', 'label'].reduce((obj, prop) => {
                obj[prop] = (row) => ((betterInside(row) ? inner : outer)[prop](row, args));
                return obj;
            }, {})
        );
    })

    .regRule('hide-by-label-height-horizontal', (prev) => {

        return {

            hide: (row) => {

                if (prev.model.size(row) < prev.h(row)) {
                    return true;
                }

                return prev.hide(row);
            }
        };
    })

    .regRule('cut-label-horizontal', (prev) => {

        return {

            dx: (row) => {
                const required = prev.w(row);
                const available = Math.abs(prev.model.y0(row) - prev.model.yi(row));
                const prevDx = prev.dx(row);
                return ((available < required) ? (available * prevDx / required) : (prevDx));
            },

            w: (row) => {
                const required = prev.w(row);
                const available = Math.abs(prev.model.y0(row) - prev.model.yi(row));
                return ((available < required) ? available : required);
            },

            label: (row) => {
                const available = Math.abs(prev.model.y0(row) - prev.model.yi(row));

                return cutLines(prev.labelLinesAndSeparator(row), available);
            }
        };
    })

    .regRule('cut-outer-label-horizontal', (prev, args) => {

        return {

            dx: (row) => {
                const required = prev.w(row);
                const available = (prev.model.y0(row) < prev.model.yi(row) ?
                    (args.maxWidth - prev.model.yi(row)) :
                    (prev.model.yi(row)));
                const prevDx = prev.dx(row);
                return ((available < required) ? (available * prevDx / required) : (prevDx));
            },

            w: (row) => {
                const required = prev.w(row);
                const available = (prev.model.y0(row) < prev.model.yi(row) ?
                    (args.maxWidth - prev.model.yi(row)) :
                    (prev.model.yi(row)));
                return ((available < required) ? available : required);
            },

            label: (row) => {
                const available = (prev.model.y0(row) < prev.model.yi(row) ?
                    (args.maxWidth - prev.model.yi(row)) :
                    (prev.model.yi(row)));

                return cutLines(prev.labelLinesAndSeparator(row), available);
            }
        };
    })

    .regRule('keep-within-diameter-or-top', (prev) => {
        return {
            dy: (row) => {

                if ((prev.model.size(row) / prev.w(row)) < 1) {
                    return (prev.dy(row) - (prev.h(row) / 2) - (prev.model.size(row) / 2));
                }

                return prev.dy(row);
            }
        };
    })

    .regRule('keep-in-box', (prev, {maxWidth, maxHeight}) => {
        return {
            dx: (row) => {
                var dx = prev.dx(row);
                var x = prev.x(row) + dx;
                var w = prev.w(row);
                var l = x - w / 2;
                var r = x + w / 2;

                var dl = 0 - l;
                if (dl > 0) {
                    return dx + dl;
                }

                var dr = r - maxWidth;
                if (dr > 0) {
                    return dx - dr;
                }

                return dx;
            },
            dy: (row) => {
                var dy = prev.dy(row);
                var y = prev.y(row) + dy;
                var h = prev.h(row);
                var t = y - h / 2;
                var b = y + h / 2;

                var dt = 0 - t;
                if (dt > 0) {
                    return 0;
                }

                var db = b - maxHeight;
                if (db > 0) {
                    return dy - db;
                }

                return dy;
            }
        };
    })

    .regRule('multiline-label-left-align', (prev) => {
        return {
            dy: (row) => {
                const prevDy = prev.dy(row);

                if (prev.angle(row) === -90) {
                    return prevDy + (prev.h(row) / 2);
                }

                return prevDy;
            }
        };
    })

    .regRule('multiline-label-vertical-center-align', (prev) => {
        return {
            dy: (row) => {
                return prev.dy(row) - (prev.h(row) / 2);
            }
        };
    })

    .regRule('multiline-hide-on-container-overflow', (prev, {maxWidth, maxHeight}) => {
        return {
            hide: (row) => {
                var angle = prev.angle(row);
                var x = prev.x(row) + prev.dx(row);
                var y = prev.y(row) + prev.dy(row);

                if (hasXOverflow(x, prev.w(row), angle, maxWidth) || hasYOverflow(y, prev.h(row), angle, maxHeight)) {
                    return true;
                }

                return prev.hide(row);
            }
        };
    });