devextreme
Version: 
HTML5 JavaScript Component Suite for Responsive Web Development
1,071 lines (1,047 loc) • 40.9 kB
JavaScript
/**
 * DevExtreme (cjs/viz/components/legend.js)
 * Version: 24.2.6
 * Build date: Mon Mar 17 2025
 *
 * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
 * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
 */
"use strict";
exports.plugin = exports.Legend = void 0;
var _utils = require("../core/utils");
var _extend2 = require("../../core/utils/extend");
var _layout_element = require("../core/layout_element");
var _type = require("../../core/utils/type");
var _title = require("../core/title");
var _object = require("../../core/utils/object");
var _common = require("../../core/utils/common");
var _renderer = require("../core/renderers/renderer");
var _deferred = require("../../core/utils/deferred");
const _Number = Number;
const _math = Math;
const _round = _math.round;
const _max = _math.max;
const _min = _math.min;
const _ceil = _math.ceil;
const _isDefined = _type.isDefined;
const _isFunction = _type.isFunction;
const _enumParser = _utils.enumParser;
const _normalizeEnum = _utils.normalizeEnum;
const _extend = _extend2.extend;
const DEFAULT_MARGIN = 10;
const DEFAULT_MARKER_HATCHING_WIDTH = 2;
const DEFAULT_MARKER_HATCHING_STEP = 5;
const CENTER = "center";
const RIGHT = "right";
const LEFT = "left";
const TOP = "top";
const BOTTOM = "bottom";
const HORIZONTAL = "horizontal";
const VERTICAL = "vertical";
const INSIDE = "inside";
const OUTSIDE = "outside";
const NONE = "none";
const HEIGHT = "height";
const WIDTH = "width";
const parseHorizontalAlignment = _enumParser([LEFT, CENTER, RIGHT]);
const parseVerticalAlignment = _enumParser([TOP, BOTTOM]);
const parseOrientation = _enumParser([VERTICAL, HORIZONTAL]);
const parseItemTextPosition = _enumParser([LEFT, RIGHT, TOP, BOTTOM]);
const parsePosition = _enumParser([OUTSIDE, INSIDE]);
const parseItemsAlignment = _enumParser([LEFT, CENTER, RIGHT]);
function getState(state, color, stateName) {
    if (!state) {
        return
    }
    const colorFromAction = state.fill;
    return (0, _extend2.extend)({}, {
        state: stateName,
        fill: colorFromAction === NONE ? color : colorFromAction,
        opacity: state.opacity,
        filter: state.filter,
        hatching: _extend({}, state.hatching, {
            step: 5,
            width: 2
        })
    })
}
function getAttributes(item, state, size) {
    const attrs = (0, _renderer.processHatchingAttrs)(item, state);
    if (attrs.fill && 0 === attrs.fill.indexOf("DevExpress")) {
        attrs.fill = (0, _renderer.getFuncIri)(attrs.fill)
    }
    attrs.opacity = attrs.opacity >= 0 ? attrs.opacity : 1;
    return (0, _extend2.extend)({}, attrs, {
        size: size
    })
}
function parseMargins(options) {
    let margin = options.margin;
    if (margin >= 0) {
        margin = _Number(options.margin);
        margin = {
            top: margin,
            bottom: margin,
            left: margin,
            right: margin
        }
    } else {
        margin = {
            top: margin.top >= 0 ? _Number(margin.top) : 10,
            bottom: margin.bottom >= 0 ? _Number(margin.bottom) : 10,
            left: margin.left >= 0 ? _Number(margin.left) : 10,
            right: margin.right >= 0 ? _Number(margin.right) : 10
        }
    }
    options.margin = margin
}
function getSizeItem(options, markerBBox, labelBBox) {
    let width;
    let height;
    switch (options.itemTextPosition) {
        case LEFT:
        case RIGHT:
            width = markerBBox.width + 7 + labelBBox.width;
            height = _max(markerBBox.height, labelBBox.height);
            break;
        case TOP:
        case BOTTOM:
            width = _max(markerBBox.width, labelBBox.width);
            height = markerBBox.height + 4 + labelBBox.height
    }
    return {
        width: width,
        height: height
    }
}
function calculateBBoxLabelAndMarker(markerBBox, labelBBox) {
    const bBox = {};
    bBox.left = _min(markerBBox.x, labelBBox.x);
    bBox.top = _min(markerBBox.y, labelBBox.y);
    bBox.right = _max(markerBBox.x + markerBBox.width, labelBBox.x + labelBBox.width);
    bBox.bottom = _max(markerBBox.y + markerBBox.height, labelBBox.y + labelBBox.height);
    return bBox
}
function applyMarkerState(id, idToIndexMap, items, stateName) {
    const item = idToIndexMap && items[idToIndexMap[id]];
    if (item) {
        item.renderMarker(item.states[stateName])
    }
}
function parseOptions(options, textField, allowInsidePosition) {
    if (!options) {
        return null
    }
    parseMargins(options);
    options.horizontalAlignment = parseHorizontalAlignment(options.horizontalAlignment, RIGHT);
    options.verticalAlignment = parseVerticalAlignment(options.verticalAlignment, options.horizontalAlignment === CENTER ? BOTTOM : TOP);
    options.orientation = parseOrientation(options.orientation, options.horizontalAlignment === CENTER ? HORIZONTAL : VERTICAL);
    options.itemTextPosition = parseItemTextPosition(options.itemTextPosition, options.orientation === HORIZONTAL ? BOTTOM : RIGHT);
    options.position = allowInsidePosition ? parsePosition(options.position, OUTSIDE) : OUTSIDE;
    options.itemsAlignment = parseItemsAlignment(options.itemsAlignment, null);
    options.hoverMode = _normalizeEnum(options.hoverMode);
    options.customizeText = _isFunction(options.customizeText) ? options.customizeText : function() {
        return this[textField]
    };
    options.customizeHint = _isFunction(options.customizeHint) ? options.customizeHint : _common.noop;
    options._incidentOccurred = options._incidentOccurred || _common.noop;
    return options
}
function createSquareMarker(renderer, size) {
    return renderer.rect(0, 0, size, size)
}
function createCircleMarker(renderer, size) {
    return renderer.circle(size / 2, size / 2, size / 2)
}
function isCircle(type) {
    return "circle" === _normalizeEnum(type)
}
function inRect(rect, x, y) {
    return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom
}
function checkLinesSize(lines, layoutOptions, countItems, margins) {
    const position = {
        x: 0,
        y: 0
    };
    let maxMeasureLength = 0;
    let maxAltMeasureLength = 0;
    let margin = 0;
    if ("y" === layoutOptions.direction) {
        margin = margins.top + margins.bottom
    } else {
        margin = margins.left + margins.right
    }
    lines.forEach((function(line, i) {
        const firstItem = line[0];
        const lineLength = line.length;
        line.forEach((function(item, index) {
            const offset = item.offset || layoutOptions.spacing;
            position[layoutOptions.direction] += item[layoutOptions.measure] + (index !== lineLength - 1 ? offset : 0);
            maxMeasureLength = _max(maxMeasureLength, position[layoutOptions.direction])
        }));
        position[layoutOptions.direction] = 0;
        position[layoutOptions.altDirection] += firstItem[layoutOptions.altMeasure] + firstItem.altOffset || layoutOptions.altSpacing;
        maxAltMeasureLength = _max(maxAltMeasureLength, position[layoutOptions.altDirection])
    }));
    if (maxMeasureLength + margin > layoutOptions.length) {
        layoutOptions.countItem = decreaseItemCount(layoutOptions, countItems);
        return true
    }
}
function decreaseItemCount(layoutOptions, countItems) {
    layoutOptions.altCountItem++;
    return _ceil(countItems / layoutOptions.altCountItem)
}
function getLineLength(line, layoutOptions) {
    return line.reduce(((lineLength, item) => {
        const offset = item.offset || layoutOptions.spacing;
        return lineLength + item[layoutOptions.measure] + offset
    }), 0)
}
function getMaxLineLength(lines, layoutOptions) {
    return lines.reduce(((maxLineLength, line) => _max(maxLineLength, getLineLength(line, layoutOptions))), 0)
}
function getInitPositionForDirection(line, layoutOptions, maxLineLength) {
    const lineLength = getLineLength(line, layoutOptions);
    let initPosition;
    switch (layoutOptions.itemsAlignment) {
        case RIGHT:
            initPosition = maxLineLength - lineLength;
            break;
        case CENTER:
            initPosition = (maxLineLength - lineLength) / 2;
            break;
        default:
            initPosition = 0
    }
    return initPosition
}
function getPos(layoutOptions) {
    switch (layoutOptions.itemTextPosition) {
        case BOTTOM:
            return {
                horizontal: CENTER, vertical: TOP
            };
        case TOP:
            return {
                horizontal: CENTER, vertical: BOTTOM
            };
        case LEFT:
            return {
                horizontal: RIGHT, vertical: CENTER
            };
        case RIGHT:
            return {
                horizontal: LEFT, vertical: CENTER
            }
    }
}
function getLines(lines, layoutOptions, itemIndex) {
    const tableLine = {};
    if (itemIndex % layoutOptions.countItem === 0) {
        if (layoutOptions.markerOffset) {
            lines.push([], [])
        } else {
            lines.push([])
        }
    }
    if (layoutOptions.markerOffset) {
        tableLine.firstLine = lines[lines.length - 1];
        tableLine.secondLine = lines[lines.length - 2]
    } else {
        tableLine.firstLine = tableLine.secondLine = lines[lines.length - 1]
    }
    return tableLine
}
function setMaxInLine(line, measure) {
    const maxLineSize = line.reduce(((maxLineSize, item) => {
        const itemMeasure = item ? item[measure] : maxLineSize;
        return _max(maxLineSize, itemMeasure)
    }), 0);
    line.forEach((item => {
        if (item) {
            item[measure] = maxLineSize
        }
    }))
}
function transpose(array) {
    const width = array.length;
    const height = array[0].length;
    let i;
    let j;
    const transposeArray = [];
    for (i = 0; i < height; i++) {
        transposeArray[i] = [];
        for (j = 0; j < width; j++) {
            transposeArray[i][j] = array[j][i]
        }
    }
    return transposeArray
}
function getAlign(position) {
    switch (position) {
        case TOP:
        case BOTTOM:
            return CENTER;
        case LEFT:
            return RIGHT;
        case RIGHT:
            return LEFT
    }
}
let getMarkerCreator = function(type) {
    return isCircle(type) ? createCircleMarker : createSquareMarker
};
function getTitleHorizontalAlignment(options) {
    if (options.horizontalAlignment === CENTER) {
        return CENTER
    } else if (options.itemTextPosition === RIGHT) {
        return LEFT
    } else if (options.itemTextPosition === LEFT) {
        return RIGHT
    } else {
        return CENTER
    }
}
let Legend = function(settings) {
    this._renderer = settings.renderer;
    this._legendGroup = settings.group;
    this._backgroundClass = settings.backgroundClass;
    this._itemGroupClass = settings.itemGroupClass;
    this._textField = settings.textField;
    this._getCustomizeObject = settings.getFormatObject;
    this._titleGroupClass = settings.titleGroupClass;
    this._allowInsidePosition = settings.allowInsidePosition;
    this._widget = settings.widget;
    this._updated = false
};
exports.Legend = Legend;
const _Legend = Legend;
const legendPrototype = _Legend.prototype = (0, _object.clone)(_layout_element.LayoutElement.prototype);
(0, _extend2.extend)(legendPrototype, {
    constructor: _Legend,
    getOptions: function() {
        return this._options
    },
    update: function() {
        let data = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : [];
        let options = arguments.length > 1 ? arguments[1] : void 0;
        let themeManagerTitleOptions = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {};
        const that = this;
        options = that._options = parseOptions(options, that._textField, that._allowInsidePosition) || {};
        const initMarkerSize = options.markerSize;
        this._updated = true;
        this._data = data.map((dataItem => {
            dataItem.size = _Number(dataItem.size > 0 ? dataItem.size : initMarkerSize);
            dataItem.marker = getAttributes(dataItem, dataItem.states.normal);
            Object.defineProperty(dataItem.marker, "size", {
                get: () => dataItem.size,
                set(value) {
                    dataItem.size = value
                }
            });
            Object.defineProperty(dataItem.marker, "opacity", {
                get: () => dataItem.states.normal.opacity,
                set(value) {
                    dataItem.states.normal.opacity = dataItem.states.hover.opacity = dataItem.states.selection.opacity = value
                }
            });
            return dataItem
        }));
        if (options.customizeItems) {
            that._data = options.customizeItems(data.slice()) || data
        }
        that._boundingRect = {
            width: 0,
            height: 0,
            x: 0,
            y: 0
        };
        if (that.isVisible()) {
            var _that$_title;
            null === (_that$_title = that._title) || void 0 === _that$_title || _that$_title.dispose();
            that._title = new _title.Title({
                renderer: that._renderer,
                cssClass: that._titleGroupClass,
                root: that._legendGroup
            })
        }
        if (that._title) {
            const titleOptions = options.title;
            themeManagerTitleOptions.horizontalAlignment = getTitleHorizontalAlignment(options);
            that._title.update(themeManagerTitleOptions, titleOptions)
        }
        this.erase();
        return that
    },
    isVisible: function() {
        return this._options && this._options.visible
    },
    draw: function(width, height) {
        const that = this;
        const items = that._getItemData();
        that.erase();
        if (!(that.isVisible() && items && items.length)) {
            return that
        }
        that._insideLegendGroup = that._renderer.g().enableLinks().append(that._legendGroup);
        that._title.changeLink(that._insideLegendGroup);
        that._createBackground();
        if (that._title.hasText()) {
            const horizontalPadding = that._background ? 2 * that._options.paddingLeftRight : 0;
            that._title.draw(width - horizontalPadding, height)
        }
        that._markersGroup = that._renderer.g().attr({
            class: that._itemGroupClass
        }).append(that._insideLegendGroup);
        that._createItems(items);
        that._updateElementsPosition(width, height);
        return that
    },
    _measureElements: function() {
        const options = this._options;
        let maxBBoxHeight = 0;
        this._items.forEach((item => {
            const labelBBox = item.label.getBBox();
            const markerBBox = item.marker.getBBox();
            item.markerBBox = markerBBox;
            item.markerSize = Math.max(markerBBox.width, markerBBox.height);
            const bBox = getSizeItem(options, markerBBox, labelBBox);
            item.labelBBox = labelBBox;
            item.bBox = bBox;
            maxBBoxHeight = _max(maxBBoxHeight, bBox.height)
        }));
        if (options.equalRowHeight) {
            this._items.forEach((item => item.bBox.height = maxBBoxHeight))
        }
    },
    _updateElementsPosition: function(width, height) {
        const that = this;
        const options = that._options;
        this._size = {
            width: width,
            height: height
        };
        that._measureElements();
        that._locateElements(options);
        that._finalUpdate(options);
        const size = that.getLayoutOptions();
        if (size.width > width || size.height > height) {
            that.freeSpace()
        }
    },
    _createItems: function(items) {
        const that = this;
        const options = that._options;
        const renderer = that._renderer;
        const createMarker = getMarkerCreator(options.markerShape);
        that._markersId = {};
        const templateFunction = !options.markerTemplate ? (dataItem, group) => {
            const attrs = dataItem.marker;
            createMarker(renderer, attrs.size).attr({
                fill: attrs.fill,
                opacity: attrs.opacity,
                filter: attrs.filter
            }).append({
                element: group
            })
        } : options.markerTemplate;
        const template = that._widget._getTemplate(templateFunction);
        const markersGroup = that._markersGroup;
        markersGroup.css((0, _utils.patchFontOptions)(options.font));
        that._deferredItems = [];
        that._templatesGroups = [];
        that._items = (items || []).map(((dataItem, i) => {
            const stateOfDataItem = dataItem.states;
            const normalState = stateOfDataItem.normal;
            const normalStateFill = normalState.fill;
            dataItem.size = dataItem.marker.size;
            const states = {
                normal: (0, _extend2.extend)(normalState, {
                    fill: normalStateFill || options.markerColor || options.defaultColor,
                    state: "normal"
                }),
                hover: getState(stateOfDataItem.hover, normalStateFill, "hovered"),
                selection: getState(stateOfDataItem.selection, normalStateFill, "selected")
            };
            dataItem.states = states;
            const itemGroup = renderer.g().append(markersGroup);
            const markerGroup = renderer.g().attr({
                class: "dxl-marker"
            }).append(itemGroup);
            that._deferredItems[i] = new _deferred.Deferred;
            that._templatesGroups.push(markerGroup);
            const item = {
                label: that._createLabel(dataItem, itemGroup),
                marker: markerGroup,
                renderer: renderer,
                group: itemGroup,
                tracker: {
                    id: dataItem.id,
                    argument: dataItem.argument,
                    argumentIndex: dataItem.argumentIndex
                },
                states: states,
                itemTextPosition: options.itemTextPosition,
                markerOffset: 0,
                bBoxes: [],
                renderMarker(state) {
                    dataItem.marker = getAttributes(item, state, dataItem.size);
                    markerGroup.clear();
                    template.render({
                        model: dataItem,
                        container: markerGroup.element,
                        onRendered: that._deferredItems[i].resolve
                    })
                }
            };
            item.renderMarker(states.normal);
            that._createHint(dataItem, itemGroup);
            if (void 0 !== dataItem.id) {
                that._markersId[dataItem.id] = i
            }
            return item
        }))
    },
    getTemplatesGroups: function() {
        return this._templatesGroups || []
    },
    getTemplatesDef: function() {
        return this._deferredItems || []
    },
    _getItemData: function() {
        let items = this._data || [];
        const options = this._options || {};
        if (options.inverted) {
            items = items.slice().reverse()
        }
        return items.filter((i => i.visible))
    },
    _finalUpdate: function(options) {
        this._adjustBackgroundSettings(options);
        this._setBoundingRect(options.margin)
    },
    erase: function() {
        const insideLegendGroup = this._insideLegendGroup;
        insideLegendGroup && insideLegendGroup.dispose();
        this._insideLegendGroup = this._markersGroup = this._x1 = this._x2 = this._y2 = this._y2 = null;
        return this
    },
    _locateElements: function(locationOptions) {
        this._moveInInitialValues();
        this._locateRowsColumns(locationOptions)
    },
    _moveInInitialValues: function() {
        this._title.hasText() && this._title.move([0, 0]);
        this._legendGroup && this._legendGroup.move(0, 0);
        this._background && this._background.attr({
            x: 0,
            y: 0,
            width: 0,
            height: 0
        })
    },
    applySelected: function(id) {
        applyMarkerState(id, this._markersId, this._items, "selection");
        return this
    },
    applyHover: function(id) {
        applyMarkerState(id, this._markersId, this._items, "hover");
        return this
    },
    resetItem: function(id) {
        applyMarkerState(id, this._markersId, this._items, "normal");
        return this
    },
    _createLabel: function(data, group) {
        const labelFormatObject = this._getCustomizeObject(data);
        const options = this._options;
        const align = getAlign(options.itemTextPosition);
        const text = options.customizeText.call(labelFormatObject, labelFormatObject);
        const fontStyle = _isDefined(data.textOpacity) ? {
            color: options.font.color,
            opacity: data.textOpacity
        } : {};
        return this._renderer.text(text, 0, 0).css((0, _utils.patchFontOptions)(fontStyle)).attr({
            align: align,
            class: options.cssClass
        }).append(group)
    },
    _createHint: function(data, group) {
        const labelFormatObject = this._getCustomizeObject(data);
        const text = this._options.customizeHint.call(labelFormatObject, labelFormatObject);
        if (_isDefined(text) && "" !== text) {
            group.setTitle(text)
        }
    },
    _createBackground: function() {
        const that = this;
        const isInside = that._options.position === INSIDE;
        const color = that._options.backgroundColor;
        const fill = color || (isInside ? that._options.containerBackgroundColor : NONE);
        if (that._options.border.visible || (isInside || color) && color !== NONE) {
            that._background = that._renderer.rect(0, 0, 0, 0).attr({
                fill: fill,
                class: that._backgroundClass
            }).append(that._insideLegendGroup)
        }
    },
    _locateRowsColumns: function(options) {
        const that = this;
        let iteration = 0;
        const layoutOptions = that._getItemsLayoutOptions();
        const countItems = that._items.length;
        let lines;
        do {
            lines = [];
            that._createLines(lines, layoutOptions);
            that._alignLines(lines, layoutOptions);
            iteration++
        } while (checkLinesSize(lines, layoutOptions, countItems, options.margin) && iteration < countItems);
        that._applyItemPosition(lines, layoutOptions)
    },
    _createLines: function(lines, layoutOptions) {
        this._items.forEach(((item, i) => {
            const tableLine = getLines(lines, layoutOptions, i);
            const labelBox = {
                width: item.labelBBox.width,
                height: item.labelBBox.height,
                element: item.label,
                bBox: item.labelBBox,
                pos: getPos(layoutOptions),
                itemIndex: i
            };
            const markerBox = {
                width: item.markerBBox.width,
                height: item.markerBBox.height,
                element: item.marker,
                pos: {
                    horizontal: CENTER,
                    vertical: CENTER
                },
                bBox: {
                    width: item.markerBBox.width,
                    height: item.markerBBox.height,
                    x: item.markerBBox.x,
                    y: item.markerBBox.y
                },
                itemIndex: i
            };
            let firstItem;
            let secondItem;
            const offsetDirection = layoutOptions.markerOffset ? "altOffset" : "offset";
            if (layoutOptions.inverseLabelPosition) {
                firstItem = labelBox;
                secondItem = markerBox
            } else {
                firstItem = markerBox;
                secondItem = labelBox
            }
            firstItem[offsetDirection] = layoutOptions.labelOffset;
            tableLine.secondLine.push(firstItem);
            tableLine.firstLine.push(secondItem)
        }))
    },
    _alignLines: function(lines, layoutOptions) {
        let i;
        let measure = layoutOptions.altMeasure;
        lines.forEach((line => setMaxInLine(line, measure)));
        measure = layoutOptions.measure;
        if (layoutOptions.itemsAlignment) {
            if (layoutOptions.markerOffset) {
                for (i = 0; i < lines.length;) {
                    transpose([lines[i++], lines[i++]]).forEach(processLine)
                }
            }
        } else {
            transpose(lines).forEach(processLine)
        }
        function processLine(line) {
            setMaxInLine(line, measure)
        }
    },
    _applyItemPosition: function(lines, layoutOptions) {
        const that = this;
        const position = {
            x: 0,
            y: 0
        };
        const maxLineLength = getMaxLineLength(lines, layoutOptions);
        lines.forEach((line => {
            const firstItem = line[0];
            const altOffset = firstItem.altOffset || layoutOptions.altSpacing;
            position[layoutOptions.direction] = getInitPositionForDirection(line, layoutOptions, maxLineLength);
            line.forEach((item => {
                const offset = item.offset || layoutOptions.spacing;
                const wrap = new _layout_element.WrapperLayoutElement(item.element, item.bBox);
                const itemBBoxOptions = {
                    x: position.x,
                    y: position.y,
                    width: item.width,
                    height: item.height
                };
                const itemBBox = new _layout_element.WrapperLayoutElement(null, itemBBoxOptions);
                const itemLegend = that._items[item.itemIndex];
                wrap.position({
                    of: itemBBox,
                    my: item.pos,
                    at: item.pos
                });
                itemLegend.bBoxes.push(itemBBox);
                position[layoutOptions.direction] += item[layoutOptions.measure] + offset
            }));
            position[layoutOptions.altDirection] += firstItem[layoutOptions.altMeasure] + altOffset
        }));
        this._items.forEach((item => {
            const itemBBox = calculateBBoxLabelAndMarker(item.bBoxes[0].getLayoutOptions(), item.bBoxes[1].getLayoutOptions());
            const horizontal = that._options.columnItemSpacing / 2;
            const vertical = that._options.rowItemSpacing / 2;
            item.tracker.left = itemBBox.left - horizontal;
            item.tracker.right = itemBBox.right + horizontal;
            item.tracker.top = itemBBox.top - vertical;
            item.tracker.bottom = itemBBox.bottom + vertical
        }))
    },
    _getItemsLayoutOptions: function() {
        const that = this;
        const options = that._options;
        const orientation = options.orientation;
        const layoutOptions = {
            itemsAlignment: options.itemsAlignment,
            orientation: options.orientation
        };
        const width = that._size.width - (that._background ? 2 * options.paddingLeftRight : 0);
        const height = that._size.height - (that._background ? 2 * options.paddingTopBottom : 0);
        if (orientation === HORIZONTAL) {
            layoutOptions.length = width;
            layoutOptions.spacing = options.columnItemSpacing;
            layoutOptions.direction = "x";
            layoutOptions.measure = WIDTH;
            layoutOptions.altMeasure = HEIGHT;
            layoutOptions.altDirection = "y";
            layoutOptions.altSpacing = options.rowItemSpacing;
            layoutOptions.countItem = options.columnCount;
            layoutOptions.altCountItem = options.rowCount;
            layoutOptions.marginTextLabel = 4;
            layoutOptions.labelOffset = 7;
            if (options.itemTextPosition === BOTTOM || options.itemTextPosition === TOP) {
                layoutOptions.labelOffset = 4;
                layoutOptions.markerOffset = true
            }
        } else {
            layoutOptions.length = height;
            layoutOptions.spacing = options.rowItemSpacing;
            layoutOptions.direction = "y";
            layoutOptions.measure = HEIGHT;
            layoutOptions.altMeasure = WIDTH;
            layoutOptions.altDirection = "x";
            layoutOptions.altSpacing = options.columnItemSpacing;
            layoutOptions.countItem = options.rowCount;
            layoutOptions.altCountItem = options.columnCount;
            layoutOptions.marginTextLabel = 7;
            layoutOptions.labelOffset = 4;
            if (options.itemTextPosition === RIGHT || options.itemTextPosition === LEFT) {
                layoutOptions.labelOffset = 7;
                layoutOptions.markerOffset = true
            }
        }
        if (!layoutOptions.countItem) {
            if (layoutOptions.altCountItem) {
                layoutOptions.countItem = _ceil(that._items.length / layoutOptions.altCountItem)
            } else {
                layoutOptions.countItem = that._items.length
            }
        }
        if (options.itemTextPosition === TOP || options.itemTextPosition === LEFT) {
            layoutOptions.inverseLabelPosition = true
        }
        layoutOptions.itemTextPosition = options.itemTextPosition;
        layoutOptions.altCountItem = layoutOptions.altCountItem || _ceil(that._items.length / layoutOptions.countItem);
        return layoutOptions
    },
    _adjustBackgroundSettings: function(locationOptions) {
        if (!this._background) {
            return
        }
        const border = locationOptions.border;
        const legendBox = this._calculateTotalBox();
        const backgroundSettings = {
            x: _round(legendBox.x - locationOptions.paddingLeftRight),
            y: _round(legendBox.y - locationOptions.paddingTopBottom),
            width: _round(legendBox.width) + 2 * locationOptions.paddingLeftRight,
            height: _round(legendBox.height),
            opacity: locationOptions.backgroundOpacity
        };
        if (border.visible && border.width && border.color && border.color !== NONE) {
            backgroundSettings["stroke-width"] = border.width;
            backgroundSettings.stroke = border.color;
            backgroundSettings["stroke-opacity"] = border.opacity;
            backgroundSettings.dashStyle = border.dashStyle;
            backgroundSettings.rx = border.cornerRadius || 0;
            backgroundSettings.ry = border.cornerRadius || 0
        }
        this._background.attr(backgroundSettings)
    },
    _setBoundingRect: function(margin) {
        if (!this._insideLegendGroup) {
            return
        }
        const box = this._calculateTotalBox();
        box.height += margin.top + margin.bottom;
        box.widthWithoutMargins = box.width;
        box.width += margin.left + margin.right;
        box.x -= margin.left;
        box.y -= margin.top;
        this._boundingRect = box
    },
    _calculateTotalBox: function() {
        const markerBox = this._markersGroup.getBBox();
        const titleBox = this._title.getCorrectedLayoutOptions();
        const box = this._insideLegendGroup.getBBox();
        const verticalPadding = this._background ? 2 * this._options.paddingTopBottom : 0;
        box.height = markerBox.height + titleBox.height + verticalPadding;
        titleBox.width > box.width && (box.width = titleBox.width);
        return box
    },
    getActionCallback: function(point) {
        const that = this;
        if (that._options.visible) {
            return function(act) {
                that[act](point.index)
            }
        } else {
            return _common.noop
        }
    },
    getLayoutOptions: function() {
        const options = this._options;
        const boundingRect = this._insideLegendGroup ? this._boundingRect : {
            width: 0,
            height: 0,
            x: 0,
            y: 0
        };
        if (options) {
            boundingRect.verticalAlignment = options.verticalAlignment;
            boundingRect.horizontalAlignment = options.horizontalAlignment;
            if (options.orientation === HORIZONTAL) {
                boundingRect.cutLayoutSide = options.verticalAlignment;
                boundingRect.cutSide = "vertical"
            } else if (options.horizontalAlignment === CENTER) {
                boundingRect.cutLayoutSide = options.verticalAlignment;
                boundingRect.cutSide = "vertical"
            } else {
                boundingRect.cutLayoutSide = options.horizontalAlignment;
                boundingRect.cutSide = "horizontal"
            }
            boundingRect.position = {
                horizontal: options.horizontalAlignment,
                vertical: options.verticalAlignment
            };
            return boundingRect
        }
        return null
    },
    shift: function(x, y) {
        const that = this;
        let box = {};
        if (that._insideLegendGroup) {
            that._insideLegendGroup.attr({
                translateX: x - that._boundingRect.x,
                translateY: y - that._boundingRect.y
            })
        }
        that._title && that._shiftTitle(that._boundingRect.widthWithoutMargins);
        that._markersGroup && that._shiftMarkers();
        if (that._insideLegendGroup) {
            box = that._legendGroup.getBBox()
        }
        that._x1 = box.x;
        that._y1 = box.y;
        that._x2 = box.x + box.width;
        that._y2 = box.y + box.height;
        return that
    },
    _shiftTitle: function(boxWidth) {
        const that = this;
        const title = that._title;
        const titleBox = title.getCorrectedLayoutOptions();
        if (!titleBox || !title.hasText()) {
            return
        }
        const width = boxWidth - (that._background ? 2 * that._options.paddingLeftRight : 0);
        const titleOptions = title.getOptions();
        let titleY = titleBox.y + titleOptions.margin.top;
        let titleX = 0;
        if (titleOptions.verticalAlignment === BOTTOM && that._markersGroup) {
            titleY += that._markersGroup.getBBox().height
        }
        if (titleOptions.horizontalAlignment === RIGHT) {
            titleX = width - titleBox.width
        } else if (titleOptions.horizontalAlignment === CENTER) {
            titleX = (width - titleBox.width) / 2
        }
        title.shift(titleX, titleY)
    },
    _shiftMarkers: function() {
        const titleBox = this._title.getLayoutOptions();
        const markerBox = this._markersGroup.getBBox();
        const titleOptions = this._title.getOptions() || {};
        let center = 0;
        let y = 0;
        if (titleBox.width > markerBox.width && this._options.horizontalAlignment === CENTER) {
            center = titleBox.width / 2 - markerBox.width / 2
        }
        if (titleOptions.verticalAlignment === TOP) {
            y = titleBox.height
        }
        if (0 !== center || 0 !== y) {
            this._markersGroup.attr({
                translateX: center,
                translateY: y
            });
            this._items.forEach((item => {
                item.tracker.left += center;
                item.tracker.right += center;
                item.tracker.top += y;
                item.tracker.bottom += y
            }))
        }
    },
    getPosition: function() {
        return this._options.position
    },
    coordsIn: function(x, y) {
        return x >= this._x1 && x <= this._x2 && y >= this._y1 && y <= this._y2
    },
    getItemByCoord: function(x, y) {
        const items = this._items;
        const legendGroup = this._insideLegendGroup;
        x -= legendGroup.attr("translateX");
        y -= legendGroup.attr("translateY");
        for (let i = 0; i < items.length; i++) {
            if (inRect(items[i].tracker, x, y)) {
                return items[i].tracker
            }
        }
        return null
    },
    dispose: function() {
        this._title && this._title.dispose();
        this._legendGroup = this._insideLegendGroup = this._title = this._renderer = this._options = this._data = this._items = null;
        return this
    },
    layoutOptions: function() {
        if (!this.isVisible()) {
            return null
        }
        const pos = this.getLayoutOptions();
        return {
            horizontalAlignment: this._options.horizontalAlignment,
            verticalAlignment: this._options.verticalAlignment,
            side: pos.cutSide,
            priority: 1,
            position: this.getPosition()
        }
    },
    measure: function(size) {
        if (this._updated || !this._insideLegendGroup) {
            this.draw(size[0], size[1]);
            this._updated = false
        } else {
            this._items.forEach((item => {
                item.bBoxes = []
            }));
            this._updateElementsPosition(size[0], size[1])
        }
        const rect = this.getLayoutOptions();
        return [rect.width, rect.height]
    },
    move: function(rect) {
        this.shift(rect[0], rect[1])
    },
    freeSpace: function() {
        this._options._incidentOccurred("W2104");
        this.erase()
    }
});
const plugin = exports.plugin = {
    name: "legend",
    init: function() {
        const group = this._renderer.g().attr({
            class: this._rootClassPrefix + "-legend"
        }).enableLinks().append(this._renderer.root);
        this._legend = new Legend({
            renderer: this._renderer,
            group: group,
            widget: this,
            itemGroupClass: this._rootClassPrefix + "-item",
            titleGroupClass: this._rootClassPrefix + "-title",
            textField: "text",
            getFormatObject: function(data) {
                return {
                    item: data.item,
                    text: data.text
                }
            }
        });
        this._layout.add(this._legend)
    },
    extenders: {
        _applyTilesAppearance: function() {
            const that = this;
            this._items.forEach((function(item) {
                that._applyLegendItemStyle(item.id, item.getState())
            }))
        },
        _buildNodes: function() {
            this._createLegendItems()
        }
    },
    members: {
        _applyLegendItemStyle: function(id, state) {
            const legend = this._legend;
            switch (state) {
                case "hover":
                    legend.applyHover(id);
                    break;
                case "selection":
                    legend.applySelected(id);
                    break;
                default:
                    legend.resetItem(id)
            }
        },
        _createLegendItems: function() {
            if (this._legend.update(this._getLegendData(), this._getOption("legend"), this._themeManager.theme("legend").title)) {
                this._requestChange(["LAYOUT"])
            }
        }
    },
    dispose: function() {
        this._legend.dispose()
    },
    customize: function(constructor) {
        constructor.prototype._proxyData.push((function(x, y) {
            if (this._legend.coordsIn(x, y)) {
                const item = this._legend.getItemByCoord(x, y);
                if (item) {
                    return {
                        id: item.id,
                        type: "legend"
                    }
                }
            }
        }));
        constructor.addChange({
            code: "LEGEND",
            handler: function() {
                this._createLegendItems()
            },
            isThemeDependent: true,
            option: "legend",
            isOptionChange: true
        })
    }
};