TargetProcess/tauCharts

View on GitHub
src/utils/d3-decorators.js

Summary

Maintainability
D
2 days
Test Coverage
import * as utils from './utils';
import * as utilsDom from './utils-dom';
import * as utilsDraw from './utils-draw';
import * as d3Selection from 'd3-selection';
import * as d3Transition from 'd3-transition';
const d3 = {
    ...d3Selection,
    ...d3Transition,
};
import * as axis from '../elements/coords.cartesian.axis';
import interpolatePathPoints from './path/interpolators/path-points';
import {getLineInterpolator, getInterpolatorSplineType} from './path/interpolators/interpolators-registry';

var d3getComputedTextLength = () => utils.memoize(
    (d3Text) => d3Text.node().getComputedTextLength(),
    (d3Text) => d3Text.node().textContent.length);

var cutText = (textString, getScaleStepSize, getComputedTextLength) => {

    getComputedTextLength = getComputedTextLength || d3getComputedTextLength();

    textString.each(function () {

        var tickNode = d3.select(this.parentNode);
        var tickData = tickNode.data()[0];
        var stepSize = getScaleStepSize(tickData);

        var textD3 = d3.select(this);
        var tokens = textD3.text().split(/\s+/);

        var stop = false;
        var parts = tokens.reduce((memo, t, i) => {

            if (stop) {
                return memo;
            }

            var text = (i > 0) ? [memo, t].join(' ') : t;
            var len = getComputedTextLength(textD3.text(text));
            if (len < stepSize) {
                memo = text;
            } else {
                var available = Math.floor(stepSize / len * text.length);
                memo = text.substr(0, available - 4) + '...';
                stop = true;
            }

            return memo;

        }, '');

        textD3.text(parts);
    });
};

var wrapText = (textNode, getScaleStepSize, linesLimit, tickLabelFontHeight, isY, getComputedTextLength) => {

    getComputedTextLength = getComputedTextLength || d3getComputedTextLength();

    var addLine = (targetD3, text, lineHeight, x, y, dy, lineNumber) => {
        var dyNew = (lineNumber * lineHeight) + dy;
        return targetD3
            .append('tspan')
            .attr('x', x)
            .attr('y', y)
            .attr('dy', dyNew + 'em')
            .text(text);
    };

    textNode.each(function () {

        var tickNode = d3.select(this.parentNode);
        var tickData = tickNode.data()[0];
        var stepSize = getScaleStepSize(tickData);

        var textD3 = d3.select(this),
            tokens = textD3.text().split(/\s+/),
            lineHeight = 1.1, // ems
            x = textD3.attr('x'),
            y = textD3.attr('y'),
            dy = parseFloat(textD3.attr('dy'));

        textD3.text(null);
        var tempSpan = addLine(textD3, null, lineHeight, x, y, dy, 0);

        var stopReduce = false;
        var tokensCount = (tokens.length - 1);
        var lines = tokens
            .reduce((memo, next, i) => {

                if (stopReduce) {
                    return memo;
                }

                var isLimit = (memo.length === linesLimit) || (i === tokensCount);
                var last = memo[memo.length - 1];
                var text = (last !== '') ? (last + ' ' + next) : next;
                var tLen = getComputedTextLength(tempSpan.text(text));
                var over = tLen > stepSize;

                if (over && isLimit) {
                    var available = Math.floor(stepSize / tLen * text.length);
                    memo[memo.length - 1] = text.substr(0, available - 4) + '...';
                    stopReduce = true;
                }

                if (over && !isLimit) {
                    memo.push(next);
                }

                if (!over) {
                    memo[memo.length - 1] = text;
                }

                return memo;

            }, [''])
            .filter((l) => l.length > 0);

        y = isY ? (-1 * (lines.length - 1) * Math.floor(tickLabelFontHeight * 0.5)) : y;
        lines.forEach((text, i) => addLine(textD3, text, lineHeight, x, y, dy, i));

        tempSpan.remove();
    });
};

export function avoidTickTextCollision(ticks, isHorizontal) {

    const textOffsetStep = 11;
    const refOffsetStart = isHorizontal ? -10 : 20;
    const translateParam = isHorizontal ? 0 : 1;
    const directionKoeff = isHorizontal ? 1 : -1;
    var layoutModel = [];
    ticks
        .each(function () {
            var tick = d3.select(this);

            var translateXStr = tick
                .attr('transform')
                .replace('translate(', '')
                .replace(' ', ',') // IE specific
                .split(',')[translateParam];

            var translateX = directionKoeff * parseFloat(translateXStr);
            var tNode = tick.select('text');

            var textWidth = tNode.node().getBBox().width;

            var halfText = (textWidth / 2);
            var s = translateX - halfText;
            var e = translateX + halfText;
            layoutModel.push({c: translateX, s: s, e: e, l: 0, textRef: tNode, tickRef: tick});
        });

    var iterateByTriples = (coll, iterator) => {
        return coll.map((curr, i, list) => {
            return iterator(
                list[i - 1] || {e: -Infinity, s: -Infinity, l: 0},
                curr,
                list[i + 1] || {e: Infinity, s: Infinity, l: 0}
            );
        });
    };

    var resolveCollide = (prevLevel, prevCollide) => {

        var rules = {
            '[T][1]': -1,
            '[T][-1]': 0,
            '[T][0]': 1,
            '[F][0]': -1
        };

        var k = `[${prevCollide.toString().toUpperCase().charAt(0)}][${prevLevel}]`;

        return (rules.hasOwnProperty(k)) ? rules[k] : 0;
    };

    var axisLayoutModel = layoutModel.sort((a, b) => (a.c - b.c));

    iterateByTriples(axisLayoutModel, (prev, curr, next) => {

        var collideL = (prev.e > curr.s);
        var collideR = (next.s < curr.e);

        if (collideL || collideR) {

            curr.l = resolveCollide(prev.l, collideL);

            var size = curr.textRef.size();
            var text = curr.textRef.text();

            if (size > 1) {
                text = text.replace(/([.]*$)/gi, '') + '...';
            }

            var dy = (curr.l * textOffsetStep); // -1 | 0 | +1
            var newY = isHorizontal ? (parseFloat(curr.textRef.attr('y')) + dy) : 0;
            let tx = isHorizontal ? 0 : dy;
            let ty = isHorizontal ? dy : 0;
            var tr = (function (transform) {
                var rotate = 0;
                if (!transform) {
                    return rotate;
                }
                var rs = transform.indexOf('rotate(');
                if (rs >= 0) {
                    var re = transform.indexOf(')', rs + 7);
                    var rotateStr = transform.substring(rs + 7, re);
                    rotate = parseFloat(rotateStr.trim());
                }
                return rotate;
            })(curr.textRef.attr('transform'));

            curr.textRef
                .text((d, i) => i === 0 ? text : '')
                .attr('transform', 'translate(' + tx + ',' + ty + ') rotate(' + tr + ')');

            var attrs = {
                x1: 0,
                x2: 0,
                y1: newY + (isHorizontal ? -1 : 5),
                y2: refOffsetStart
            };

            if (!isHorizontal) {
                attrs.transform = 'rotate(-90)';
            }

            utilsDom.selectOrAppend(curr.tickRef, 'line.label-ref')
            // eslint-disable-next-line @typescript-eslint/no-use-before-define
                .call(d3_setAttrs(attrs));
        } else {
            curr.tickRef.selectAll('line.label-ref').remove();
        }

        return curr;
    });
}

var d3_transition = (selection, animationSpeed, nameSpace) => {
    if (animationSpeed > 0 && !document.hidden) {
        selection = selection.transition(nameSpace).duration(animationSpeed);
        // eslint-disable-next-line @typescript-eslint/no-use-before-define
        selection.attr = d3_transition_attr;
    }
    selection.onTransitionEnd = function (callback) {
        // eslint-disable-next-line @typescript-eslint/no-use-before-define
        d3_add_transition_end_listener(this, callback);
        return this;
    };
    return selection;
};

var d3_transition_attr = function (keyOrMap, value) {
    var d3AttrResult = d3.transition.prototype.attr.apply(this, arguments);

    if (arguments.length === 0) {
        throw new Error('Unexpected `transition().attr()` arguments.');
    }
    var attrs;
    if (arguments.length === 1) {
        attrs = keyOrMap;
    } else if (arguments.length > 1) {
        attrs = {[keyOrMap]: value};
    }

    // Store transitioned attributes values
    // until transition ends.
    var store = '__transitionAttrs__';
    var idStore = '__lastTransitions__';
    // eslint-disable-next-line @typescript-eslint/no-use-before-define
    var id = getTransitionAttrId();
    this.each(function () {
        var newAttrs = {};
        for (var key in attrs) {
            if (typeof attrs[key] === 'function') {
                newAttrs[key] = attrs[key].apply(this, arguments);
            } else {
                newAttrs[key] = attrs[key];
            }
        }
        this[store] = Object.assign(
            this[store] || {},
            newAttrs
        );

        // NOTE: As far as d3 `interrupt` event is called asynchronously,
        // we have to store ID to prevent removing attribute value from store,
        // when new transition is applied for the same attribute.
        if (!this[store][idStore]) {
            Object.defineProperty(this[store], idStore, {value: {}});
        }
        Object.keys(newAttrs).forEach((key) => this[store][idStore][key] = id);
    });
    var onTransitionEnd = function () {
        if (this[store]) {
            Object.keys(attrs)
                .filter((k) => this[store][idStore][k] === id)
                .forEach((k) => delete this[store][k]);
            if (Object.keys(this[store]).length === 0) {
                delete this[store];
            }
        }
    };
    this.on(`interrupt.${id}`, () => this.each(onTransitionEnd));
    this.on(`end.${id}`, () => this.each(onTransitionEnd));

    return d3AttrResult;
};
var transitionsCounter = 0;
var getTransitionAttrId = function () {
    return ++transitionsCounter;
};

var d3_add_transition_end_listener = (selection, callback) => {
    if (!d3.transition.prototype.isPrototypeOf(selection) || selection.empty()) {
        // If selection is not transition or empty,
        // execute callback immediately.
        callback.call(null, selection);
        return;
    }
    var onTransitionEnd = () => callback.call(null, selection);
    selection.on('interrupt.d3_on_transition_end', onTransitionEnd);
    selection.on('end.d3_on_transition_end', onTransitionEnd);
    return selection;
};

var d3_animationInterceptor = (speed, initAttrs, doneAttrs, afterUpdate) => {

    const xAfterUpdate = afterUpdate || ((x) => x);
    const afterUpdateIterator = function () {
        xAfterUpdate(this);
    };

    return function (selection) {

        var flow = selection;

        if (initAttrs) {
            // eslint-disable-next-line @typescript-eslint/no-use-before-define
            flow = flow.call(d3_setAttrs(utils.defaults(initAttrs, doneAttrs)));
        }

        flow = d3_transition(flow, speed);

        // eslint-disable-next-line @typescript-eslint/no-use-before-define
        flow = flow.call(d3_setAttrs(doneAttrs));

        if (speed > 0) {
            flow.on('end.d3_animationInterceptor', () => flow.each(afterUpdateIterator));
        } else {
            flow.each(afterUpdateIterator);
        }

        return flow;
    };
};

var d3_selectAllImmediate = (container, selector) => {
    var node = container.node();
    return container.selectAll(selector).filter(function () {
        return this.parentNode === node;
    });
};

var d3_createPathTween = (
    attr,
    pathStringBuilder,
    pointConvertors,
    idGetter,
    interpolationType = 'linear'
) => {
    const pointStore = '__pathPoints__';

    return function (data) {
        if (!this[pointStore]) {
            this[pointStore] = pointConvertors.map(() => []);
        }

        const frames = pointConvertors.map((convertor, i) => {
            const points = utils.unique(data, idGetter).map(convertor);
            const interpolateLine = (
                getLineInterpolator(interpolationType) ||
                getLineInterpolator('linear')
            );
            const pointsTo = interpolateLine(points);
            const pointsFrom = this[pointStore][i];

            const interpolate = interpolatePathPoints(
                pointsFrom,
                pointsTo,
                getInterpolatorSplineType(interpolationType)
            );

            return {
                pointsFrom,
                pointsTo,
                interpolate
            };
        });

        return (t) => {
            if (t === 0) {
                let pointsFrom = frames.map((f) => f.pointsFrom);
                return pathStringBuilder(...pointsFrom);
            }
            if (t === 1) {
                let pointsTo = frames.map((f) => f.pointsTo);
                this[pointStore] = pointsTo;
                return pathStringBuilder(...pointsTo);
            }

            const intermediate = frames.map((f) => f.interpolate(t));

            // Save intermediate points to be able
            // to continue transition after interrupt
            this[pointStore] = intermediate;

            return pathStringBuilder(...intermediate);
        };
    };
};

var d3_setAttrs = (attrs) => {
    return (sel) => {
        Object.keys(attrs).forEach((k) => sel.attr(k, attrs[k]));
        return sel;
    };
};

var d3_setClasses = (classMap) => {
    return (sel) => {
        Object.keys(classMap).forEach((k) => sel.classed(k, classMap[k]));
        return sel;
    };
};

export {
    d3_animationInterceptor,
    d3_createPathTween,
    d3_selectAllImmediate,
    d3_setAttrs,
    d3_setClasses,
    d3_transition,
    wrapText,
    cutText
};