UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

532 lines (516 loc) • 19.8 kB
/** * DevExtreme (viz/tree_map/tree_map.base.js) * Version: 18.1.3 * Build date: Tue May 15 2018 * * Copyright (c) 2012 - 2018 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ "use strict"; var common = require("./common"), Node = require("./node"), _getTilingAlgorithm = require("./tiling").getAlgorithm, _getColorizer = require("./colorizing").getColorizer, _patchFontOptions = require("../core/utils").patchFontOptions, _buildRectAppearance = common.buildRectAppearance, _buildTextAppearance = common.buildTextAppearance, _noop = require("../../core/utils/common").noop, _max = Math.max, directions = { lefttoprightbottom: [1, 1], leftbottomrighttop: [1, -1], righttopleftbottom: [-1, 1], rightbottomlefttop: [-1, -1] }; require("./tiling.squarified"); require("./tiling").setDefaultAlgorithm("squarified"); require("./colorizing.discrete"); require("./colorizing").setDefaultColorizer("discrete"); function pickPositiveInteger(val) { return val > 0 ? Math.round(val) : 0 } var dxTreeMap = require("../core/base_widget").inherit({ _handlers: { beginBuildNodes: _noop, buildNode: _noop, endBuildNodes: _noop, setTrackerData: _noop, calculateState: function(options) { return _buildRectAppearance(options) } }, _rootClass: "dxtm-tree-map", _rootClassPrefix: "dxtm", _getDefaultSize: function() { return { width: 400, height: 400 } }, _createThemeManager: function() { return new ThemeManager }, _init: function() { var that = this; that._rectOffsets = {}; that._handlers = Object.create(that._handlers); that._context = { suspend: function() { if (!that._applyingChanges) { that._suspendChanges() } }, resume: function() { if (!that._applyingChanges) { that._resumeChanges() } }, change: function(codes) { that._change(codes) }, settings: [{}, {}], calculateState: that._handlers.calculateState, calculateLabelState: _buildTextAppearance }; that._root = that._topNode = { nodes: [] }; that.callBase.apply(that, arguments) }, _initialChanges: ["DATA_SOURCE"], _initCore: function() { var that = this, renderer = that._renderer; that._createProxyType(); that._tilesGroup = renderer.g().linkOn(renderer.root, "tiles").linkAppend(); that._labelsGroup = renderer.g().linkOn(renderer.root, "labels").linkAppend() }, _createProxyType: _noop, _disposeCore: function() { var that = this; that._filter && that._filter.dispose(); that._labelsGroup.linkOff(); that._tilesGroup.linkOff() }, _applySize: function(rect) { this._tilingRect = rect.slice(); this._change(["TILING"]) }, _optionChangesMap: { dataSource: "DATA_SOURCE", valueField: "NODES_CREATE", childrenField: "NODES_CREATE", colorField: "TILES", colorizer: "TILES", labelField: "LABELS", tile: "TILE_SETTINGS", group: "GROUP_SETTINGS", maxDepth: "MAX_DEPTH", layoutAlgorithm: "TILING", layoutDirection: "TILING", resolveLabelOverflow: "LABEL_OVERFLOW" }, _themeDependentChanges: ["TILE_SETTINGS", "GROUP_SETTINGS", "MAX_DEPTH"], _changeDataSource: function() { var that = this; that._isDataExpected = that._isSyncData = true; that._updateDataSource(); that._isSyncData = false; if (that._isDataExpected) { that._suspendChanges() } }, _dataSourceChangedHandler: function() { var that = this; if (that._isDataExpected) { that._isDataExpected = false; that._change(["NODES_CREATE"]); if (!that._isSyncData) { that._resumeChanges() } } else { that._requestChange(["NODES_CREATE"]) } }, _optionChangesOrder: ["DATA_SOURCE", "TILE_SETTINGS", "GROUP_SETTINGS", "MAX_DEPTH", "LABEL_OVERFLOW"], _change_DATA_SOURCE: function() { this._changeDataSource() }, _change_TILE_SETTINGS: function() { this._changeTileSettings() }, _change_GROUP_SETTINGS: function() { this._changeGroupSettings() }, _change_LABEL_OVERFLOW: function() { this._changeTileSettings(); this._changeGroupSettings() }, _change_MAX_DEPTH: function() { this._changeMaxDepth() }, _customChangesOrder: ["NODES_CREATE", "NODES_RESET", "TILES", "LABELS", "TILING", "LABELS_LAYOUT"], _change_NODES_CREATE: function() { this._buildNodes() }, _change_NODES_RESET: function() { this._resetNodes() }, _change_TILES: function() { this._applyTilesAppearance() }, _change_LABELS: function() { this._applyLabelsAppearance() }, _change_TILING: function() { this._performTiling() }, _change_LABELS_LAYOUT: function() { this._performLabelsLayout() }, _applyChanges: function() { var that = this; that.callBase.apply(that, arguments); if (!that._isDataExpected) { that._drawn() } that._context.forceReset = false }, _buildNodes: function() { var processedData, that = this, root = that._root = that._topNode = new Node; root._id = 0; root.parent = {}; root.data = {}; root.level = root.index = -1; root.ctx = that._context; root.label = null; that._nodes = [root]; that._handlers.beginBuildNodes(); processedData = that._processDataSourceItems(that._dataSourceItems() || []); traverseDataItems(root, processedData.items, 0, { itemsField: !processedData.isPlain && that._getOption("childrenField", true) || "items", valueField: that._getOption("valueField", true) || "value", buildNode: that._handlers.buildNode, ctx: that._context, nodes: that._nodes }); that._onNodesCreated(); that._handlers.endBuildNodes(); that._change(["NODES_RESET"]) }, _onNodesCreated: _noop, _processDataSourceItems: function(items) { return { items: items, isPlain: false } }, _changeTileSettings: function() { var that = this, options = that._getOption("tile"), offsets = that._rectOffsets, borderWidth = pickPositiveInteger(options.border.width), edgeOffset = borderWidth / 2, innerOffset = 1 & borderWidth ? .5 : 0, labelOptions = options.label, settings = that._context.settings[0]; that._change(["TILES", "LABELS"]); settings.state = that._handlers.calculateState(options); that._filter = that._filter || that._renderer.shadowFilter("-50%", "-50%", "200%", "200%"); that._filter.attr(labelOptions.shadow); that._calculateLabelSettings(settings, labelOptions, that._filter.id); if (offsets.tileEdge !== edgeOffset || offsets.tileInner !== innerOffset) { offsets.tileEdge = edgeOffset; offsets.tileInner = innerOffset; that._change(["TILING"]) } }, _changeGroupSettings: function() { var that = this, options = that._getOption("group"), labelOptions = options.label, offsets = that._rectOffsets, borderWidth = pickPositiveInteger(options.border.width), edgeOffset = borderWidth / 2, innerOffset = 1 & borderWidth ? .5 : 0, headerHeight = 0, groupPadding = pickPositiveInteger(options.padding), settings = that._context.settings[1]; that._change(["TILES", "LABELS"]); settings.state = that._handlers.calculateState(options); that._calculateLabelSettings(settings, labelOptions); if (options.headerHeight >= 0) { headerHeight = pickPositiveInteger(options.headerHeight) } else { headerHeight = settings.labelParams.height + 2 * pickPositiveInteger(labelOptions.paddingTopBottom) } if (that._headerHeight !== headerHeight) { that._headerHeight = headerHeight; that._change(["TILING"]) } if (that._groupPadding !== groupPadding) { that._groupPadding = groupPadding; that._change(["TILING"]) } if (offsets.headerEdge !== edgeOffset || offsets.headerInner !== innerOffset) { offsets.headerEdge = edgeOffset; offsets.headerInner = innerOffset; that._change(["TILING"]) } }, _calculateLabelSettings: function(settings, options, filter) { var bBox = this._getTextBBox(options.font), paddingLeftRight = pickPositiveInteger(options.paddingLeftRight), paddingTopBottom = pickPositiveInteger(options.paddingTopBottom); settings.labelState = _buildTextAppearance(options, filter); settings.labelState.visible = !("visible" in options) || !!options.visible; settings.labelParams = { height: bBox.height, rtlEnabled: this._getOption("rtlEnabled", true), paddingTopBottom: paddingTopBottom, paddingLeftRight: paddingLeftRight, resolveLabelOverflow: this._getOption("resolveLabelOverflow", true) } }, _changeMaxDepth: function() { var maxDepth = this._getOption("maxDepth", true); maxDepth = maxDepth >= 1 ? Math.round(maxDepth) : 1 / 0; if (this._maxDepth !== maxDepth) { this._maxDepth = maxDepth; this._change(["NODES_RESET"]) } }, _resetNodes: function() { var that = this; that._tilesGroup.clear(); that._renderer.initHatching(); that._context.forceReset = true; that._context.minLevel = that._topNode.level + 1; that._context.maxLevel = that._context.minLevel + that._maxDepth - 1; that._change(["TILES", "LABELS", "TILING"]) }, _processNodes: function(context, process) { processNodes(context, this._topNode, process) }, _applyTilesAppearance: function() { var that = this, colorizer = _getColorizer(that._getOption("colorizer"), that._themeManager, that._topNode); that._processNodes({ renderer: that._renderer, group: that._tilesGroup, setTrackerData: that._handlers.setTrackerData, colorField: that._getOption("colorField", true) || "color", getColor: colorizer }, processTileAppearance) }, _applyLabelsAppearance: function() { var that = this; that._labelsGroup.clear(); that._processNodes({ renderer: that._renderer, group: that._labelsGroup, setTrackerData: that._handlers.setTrackerData, labelField: that._getOption("labelField", true) || "name" }, processLabelAppearance); that._change(["LABELS_LAYOUT"]) }, _performTiling: function() { var that = this, context = { algorithm: _getTilingAlgorithm(that._getOption("layoutAlgorithm", true)), directions: directions[String(that._getOption("layoutDirection", true)).toLowerCase()] || directions.lefttoprightbottom, headerHeight: that._headerHeight, groupPadding: that._groupPadding, rectOffsets: that._rectOffsets }; that._topNode.innerRect = that._tilingRect; calculateRects(context, that._topNode); that._processNodes(context, processTiling); that._change(["LABELS_LAYOUT"]); that._onTilingPerformed() }, _onTilingPerformed: _noop, _performLabelsLayout: function() { this._processNodes(null, processLabelsLayout) }, _getTextBBox: function(fontOptions) { var bBox, renderer = this._renderer, text = this._textForCalculations || renderer.text("0", 0, 0); this._textForCalculations = text; text.css(_patchFontOptions(fontOptions)).append(renderer.root); bBox = text.getBBox(); text.remove(); return bBox } }); function traverseDataItems(root, dataItems, level, params) { var node, i, dataItem, items, nodes = [], allNodes = params.nodes, ii = dataItems.length, totalValue = 0; for (i = 0; i < ii; ++i) { dataItem = dataItems[i]; node = new Node; node._id = allNodes.length; node.ctx = params.ctx; node.parent = root; node.level = level; node.index = nodes.length; node.data = dataItem; params.buildNode(node); allNodes.push(node); nodes.push(node); items = dataItem[params.itemsField]; if (items && items.length) { traverseDataItems(node, items, level + 1, params) } if (dataItem[params.valueField] > 0) { node.value = Number(dataItem[params.valueField]) } totalValue += node.value } root.nodes = nodes; root.value = totalValue } function processNodes(context, root, process) { var node, i, nodes = root.nodes, ii = nodes.length; for (i = 0; i < ii; ++i) { node = nodes[i]; process(context, node); if (node.isNode()) { processNodes(context, node, process) } } } function processTileAppearance(context, node) { node.color = node.data[context.colorField] || context.getColor(node) || node.parent.color; node.updateStyles(); node.tile = !node.ctx.forceReset && node.tile || createTile[Number(node.isNode())](context, node); node.applyState() } var createTile = [createLeaf, createGroup]; function createLeaf(context, node) { var tile = context.renderer.simpleRect().append(context.group); context.setTrackerData(node, tile); return tile } function createGroup(context, node) { var outer = context.renderer.simpleRect().append(context.group), inner = context.renderer.simpleRect().append(context.group); context.setTrackerData(node, inner); return { outer: outer, inner: inner } } function processLabelAppearance(context, node) { node.updateLabelStyle(); if (node.labelState.visible) { createLabel(context, node, node.labelState, node.labelParams) } } function createLabel(context, currentNode, settings, params) { var textData = currentNode.data[context.labelField]; currentNode.label = textData ? String(textData) : null; textData = currentNode.customLabel || currentNode.label; if (textData) { currentNode.text = context.renderer.text(textData).attr(settings.attr).css(settings.css).append(context.group); context.setTrackerData(currentNode, currentNode.text) } } var emptyRect = [0, 0, 0, 0]; function calculateRects(context, root) { var i, nodes = root.nodes, items = [], rects = [], sum = 0, ii = items.length = rects.length = nodes.length; for (i = 0; i < ii; ++i) { sum += nodes[i].value; items[i] = { value: nodes[i].value, i: i } } if (sum > 0) { context.algorithm({ items: items.slice(), sum: sum, rect: root.innerRect.slice(), isRotated: 1 & nodes[0].level, directions: context.directions }) } for (i = 0; i < ii; ++i) { rects[i] = items[i].rect || emptyRect } root.rects = rects } function processTiling(context, node) { var headerHeight, rect = node.parent.rects[node.index], rectOffsets = context.rectOffsets; if (node.isNode()) { setRectAttrs(node.tile.outer, buildTileRect(rect, node.parent.innerRect, rectOffsets.headerEdge, rectOffsets.headerInner)); rect = marginateRect(rect, context.groupPadding); headerHeight = Math.min(context.headerHeight, rect[3] - rect[1]); node.rect = [rect[0], rect[1], rect[2], rect[1] + headerHeight]; setRectAttrs(node.tile.inner, marginateRect(node.rect, rectOffsets.headerEdge)); rect[1] += headerHeight; node.innerRect = rect; calculateRects(context, node) } else { node.rect = rect; setRectAttrs(node.tile, buildTileRect(rect, node.parent.innerRect, rectOffsets.tileEdge, rectOffsets.tileInner)) } } function marginateRect(rect, margin) { return [rect[0] + margin, rect[1] + margin, rect[2] - margin, rect[3] - margin] } function buildTileRect(rect, outer, edgeOffset, innerOffset) { return [rect[0] + (rect[0] === outer[0] ? edgeOffset : +innerOffset), rect[1] + (rect[1] === outer[1] ? edgeOffset : +innerOffset), rect[2] - (rect[2] === outer[2] ? edgeOffset : -innerOffset), rect[3] - (rect[3] === outer[3] ? edgeOffset : -innerOffset)] } function setRectAttrs(element, rect) { element.attr({ x: rect[0], y: rect[1], width: _max(rect[2] - rect[0], 0), height: _max(rect[3] - rect[1], 0) }) } function processLabelsLayout(context, node) { if (node.text && node.labelState.visible) { layoutTextNode(node, node.labelParams) } } function layoutTextNode(node, params) { var rect = node.rect, text = node.text, bBox = text.getBBox(), paddingLeftRight = params.paddingLeftRight, paddingTopBottom = params.paddingTopBottom, effectiveWidth = rect[2] - rect[0] - paddingLeftRight, fitByHeight = params.height + paddingTopBottom <= rect[3] - rect[1], fitByWidth = bBox.width <= effectiveWidth; if ("ellipsis" === params.resolveLabelOverflow && fitByHeight) { text.applyEllipsis(effectiveWidth); if (!fitByWidth) { bBox = text.getBBox(); fitByWidth = bBox.width <= effectiveWidth } } text.attr({ visibility: fitByHeight && fitByWidth ? "visible" : "hidden" }); if (fitByHeight && fitByWidth) { text.move(params.rtlEnabled ? rect[2] - paddingLeftRight - bBox.x - bBox.width : rect[0] + paddingLeftRight - bBox.x, rect[1] + paddingTopBottom - bBox.y) } } var ThemeManager = require("../core/base_theme_manager").BaseThemeManager.inherit({ _themeSection: "treeMap", _fontFields: ["tile.label.font", "group.label.font", "loadingIndicator.font", "title.font", "title.subtitle.font", "tooltip.font", "export.font"] }); require("../../core/component_registrator")("dxTreeMap", dxTreeMap); module.exports = dxTreeMap; dxTreeMap.addPlugin(require("../core/data_source").plugin);