UNPKG

@antv/s2

Version:

effective spreadsheet render core lib

455 lines (454 loc) 21.6 kB
/** * 获取tooltip中需要显示的数据项 */ import { assign, compact, concat, every, filter, flatMap, forEach, get, groupBy, isEmpty, isEqual, isFunction, isNumber, isObject, isString, last, map, mapKeys, noop, pick, sumBy, } from 'lodash'; import { CellType, DARK_THEME_CLS, EXTRA_FIELD, PRECISION, VALUE_FIELD, } from '../common/constant'; import { TOOLTIP_CONTAINER_HIDE_CLS, TOOLTIP_CONTAINER_SHOW_CLS, TOOLTIP_POSITION_OFFSET, } from '../common/constant/tooltip'; import { i18n } from '../common/i18n'; import { CellData } from '../data-set/cell-data'; import { getLeafColumnsWithKey } from '../facet/utils'; import { getDataSumByField, isNotNumber } from '../utils/number-calculate'; import { customMerge } from './merge'; import { getEmptyPlaceholder } from './text'; /** * calculate tooltip show position */ export const getAutoAdjustPosition = ({ spreadsheet, position, tooltipContainer, autoAdjustBoundary, }) => { const canvas = spreadsheet.getCanvasElement(); let x = position.x + TOOLTIP_POSITION_OFFSET.x; let y = position.y + TOOLTIP_POSITION_OFFSET.y; if (!autoAdjustBoundary || !canvas) { return { x, y, }; } const isAdjustBodyBoundary = autoAdjustBoundary === 'body'; const { maxX, maxY } = spreadsheet.facet.panelBBox; const { width, height } = spreadsheet.options; const { top: canvasOffsetTop, left: canvasOffsetLeft } = canvas.getBoundingClientRect(); const { width: tooltipWidth, height: tooltipHeight } = tooltipContainer.getBoundingClientRect(); const { width: viewportWidth, height: viewportHeight } = document.body.getBoundingClientRect(); const maxWidth = isAdjustBodyBoundary ? viewportWidth : Math.min(width, maxX) + canvasOffsetLeft; const maxHeight = isAdjustBodyBoundary ? viewportHeight : Math.min(height, maxY) + canvasOffsetTop; if (x + tooltipWidth >= maxWidth) { x = maxWidth - tooltipWidth; } if (y + tooltipHeight >= maxHeight) { y = maxHeight - tooltipHeight; } return { x, y, }; }; export const getTooltipDefaultOptions = (options) => { return Object.assign({ operator: { menu: { onClick: noop, items: [], selectedKeys: [], }, }, enableFormat: true }, options); }; export const getMergedQuery = (meta) => { return Object.assign(Object.assign({}, meta === null || meta === void 0 ? void 0 : meta.colQuery), meta === null || meta === void 0 ? void 0 : meta.rowQuery); }; export const setTooltipContainerStyle = (container, options) => { if (!container) { return; } const { style, className = [], visible, dark = false } = options; if (style) { Object.assign(container.style, style); } if (className.length) { const classList = className.filter(Boolean); container.classList.add(...classList); } container.classList.toggle(TOOLTIP_CONTAINER_SHOW_CLS, visible); container.classList.toggle(TOOLTIP_CONTAINER_HIDE_CLS, !visible); container.classList.toggle(DARK_THEME_CLS, dark); }; export const getListItem = (spreadsheet, { data, field, valueField, useCompleteDataForFormatter = true, targetCell, }) => { var _a, _b; const defaultFieldName = (_a = spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet) === null || _a === void 0 ? void 0 : _a.getFieldName(field); const name = spreadsheet.isCustomRowFields() ? (spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet.getCustomRowFieldName(targetCell)) || defaultFieldName : defaultFieldName; const formatter = (_b = spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet) === null || _b === void 0 ? void 0 : _b.getFieldFormatter(field); // 非数值类型的 data 不展示 (趋势分析表/迷你图/G2 图表),上层通过自定义 tooltip 的方式去自行定制 const dataValue = CellData.getFieldValue(data, field); const displayDataValue = isObject(dataValue) ? null : dataValue; const value = formatter(valueField || displayDataValue, useCompleteDataForFormatter ? data : undefined); return { name, value, }; }; export const getFieldList = (spreadsheet, fields, activeData) => { const currentFields = filter(concat([], fields), (field) => field !== EXTRA_FIELD && CellData.getFieldValue(activeData, field)); return map(currentFields, (field) => getListItem(spreadsheet, { data: activeData, field, useCompleteDataForFormatter: false, })); }; /** * 获取选中格行/列头信息 * @param spreadsheet * @param activeData */ export const getHeadInfo = (spreadsheet, activeData, options) => { var _a, _b, _c, _d; const { isTotals } = options || {}; let colList = []; let rowList = []; if (activeData) { const colFields = (_b = (_a = spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet) === null || _a === void 0 ? void 0 : _a.fields) === null || _b === void 0 ? void 0 : _b.columns; const rowFields = (_d = (_c = spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet) === null || _c === void 0 ? void 0 : _c.fields) === null || _d === void 0 ? void 0 : _d.rows; colList = getFieldList(spreadsheet, getLeafColumnsWithKey(colFields || []), activeData); rowList = getFieldList(spreadsheet, rowFields, activeData); } // 此时是总计-总计 if (isEmpty(colList) && isEmpty(rowList) && isTotals) { colList = [{ value: i18n('总计') }]; } return { cols: colList, rows: rowList }; }; /** * 获取数据明细 * @param spreadsheet * @param activeData * @param options */ export const getTooltipDetailList = (spreadsheet, activeData, options, targetCell) => { if (!activeData) { return []; } const { isTotals } = options; const field = activeData[EXTRA_FIELD]; const detailList = []; if (isTotals) { // total/subtotal detailList.push(getListItem(spreadsheet, { data: activeData, field, targetCell, valueField: activeData[VALUE_FIELD], })); } else { detailList.push(getListItem(spreadsheet, { data: activeData, field, targetCell })); } return detailList; }; export const getSummaryName = (spreadsheet, currentField, isTotals) => { var _a; if (isTotals) { return i18n('总计'); } const name = (_a = spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet) === null || _a === void 0 ? void 0 : _a.getFieldName(currentField); return name && name !== 'undefined' ? name : ''; }; const getRowOrColSelectedIndexes = (nodes, leafNodes, isRow = true) => { const selectedIndexes = []; forEach(leafNodes, (_, index) => { forEach(nodes, (item) => { if (!isRow && item.colIndex !== -1) { selectedIndexes.push([index, item.colIndex]); } else if (isRow && item.rowIndex !== -1) { selectedIndexes.push([item.rowIndex, index]); } }); }); return selectedIndexes; }; export const getSelectedCellIndexes = (spreadsheet) => { var _a; const rowLeafNodes = spreadsheet.facet.getRowLeafNodes(); const colLeafNodes = spreadsheet.facet.getColLeafNodes(); const { nodes = [], cells = [] } = spreadsheet.interaction.getState(); const cellType = (_a = cells === null || cells === void 0 ? void 0 : cells[0]) === null || _a === void 0 ? void 0 : _a.type; // 高亮所有的子节点, 但是只有叶子节点需要参与数据计算 https://github.com/antvis/S2/pull/1443 const needCalcNodes = spreadsheet.isHierarchyTreeType() ? nodes : nodes.filter((node) => node === null || node === void 0 ? void 0 : node.isLeaf); if (cellType === CellType.COL_CELL) { return getRowOrColSelectedIndexes(needCalcNodes, rowLeafNodes, false); } if (cellType === CellType.ROW_CELL) { return getRowOrColSelectedIndexes(needCalcNodes, colLeafNodes); } return []; }; export const getSelectedCellsData = (spreadsheet, targetCell, onlyShowCellText) => { const { facet } = spreadsheet; /** * 当开启小计/总计后 * 1. [点击列头单元格时], 选中列所对应的数值单元格的数据如果是小计/总计, 则不应该参与计算: * - 1.1 [小计/总计 位于行头]: 点击的都是 (普通列头), 需要去除 (数值单元格) 对应 (行头为小计) 的单元格的数据 * - 1.2 [小计/总计 位于列头]: 点击的是 (普通列头/小计/总计列头), 由于行头没有, 所有数值单元格参与计算即可 * - 1.3 [小计/总计 同时位于行头/列头]: 和 1.1 处理一致 * 2. [点击行头单元格时]: * - 2.1 如果本身就是小计/总计单元格, 且列头无小计/总计, 则当前行所有 (数值单元格) 参与计算 * - 2.2 如果本身不是小计/总计单元格, 去除当前行/列 (含子节点) 所对应小计/总计数据 * * 3. [刷选/多选], 暂不考虑这种场景 * - 3.1 如果全部是小计或全部是总计单元格, 则正常计算 * - 3.2 如果部分是, 如何处理? 小计/总计不应该被选中, 还是数据不参与计算? * - 3.3 如果选中的含有小计, 并且有总计, 数据参与计算也没有意义, 如何处理? */ const isBelongTotalCell = (cellMeta) => { if (!cellMeta) { return false; } const targetCellMeta = targetCell === null || targetCell === void 0 ? void 0 : targetCell.getMeta(); // target: 当前点击的单元格类型 const isTargetTotalCell = targetCellMeta === null || targetCellMeta === void 0 ? void 0 : targetCellMeta.isTotals; const isTargetColCell = (targetCell === null || targetCell === void 0 ? void 0 : targetCell.cellType) === CellType.COL_CELL; const isTargetRowCell = (targetCell === null || targetCell === void 0 ? void 0 : targetCell.cellType) === CellType.ROW_CELL; if (!isTargetColCell && !isTargetRowCell) { return false; } const currentColCellNode = facet.getColNodeByIndex(cellMeta.colIndex); const currentRowCellNode = facet.getRowNodeByIndex(cellMeta.rowIndex); // 行头点击, 去除列头对应的小计/总计, 列头相反 const isTotalCell = isTargetColCell ? currentRowCellNode === null || currentRowCellNode === void 0 ? void 0 : currentRowCellNode.isTotals : currentColCellNode === null || currentColCellNode === void 0 ? void 0 : currentColCellNode.isTotals; return ((!isTargetTotalCell && (cellMeta === null || cellMeta === void 0 ? void 0 : cellMeta.isTotals)) || (isTargetTotalCell && isTotalCell)); }; // 列头选择和行头选择没有存所有selected的cell,因此要遍历index对比,而selected则不需要 if (onlyShowCellText) { // 行头列头单选多选 const selectedCellIndexes = getSelectedCellIndexes(spreadsheet); return compact(map(selectedCellIndexes, ([rowIndex, colIndex]) => { const currentCellMeta = facet.getCellMeta(rowIndex, colIndex); if (isBelongTotalCell(currentCellMeta)) { return; } return (currentCellMeta === null || currentCellMeta === void 0 ? void 0 : currentCellMeta.data) || getMergedQuery(currentCellMeta); })); } // 其他(刷选,data cell多选) const cells = spreadsheet.interaction.getCells(); return cells .filter((cellMeta) => { const meta = facet.getCellMeta(cellMeta.rowIndex, cellMeta.colIndex); return !isBelongTotalCell(meta); }) .map((cellMeta) => { const meta = facet.getCellMeta(cellMeta.rowIndex, cellMeta.colIndex); return (meta === null || meta === void 0 ? void 0 : meta.data) || getMergedQuery(meta); }); }; export const getCustomFieldsSummaries = (summaries) => { const customFieldGroup = groupBy(summaries, 'name'); return Object.keys(customFieldGroup || {}).map((name) => { const cellsData = customFieldGroup[name]; const selectedData = flatMap(cellsData, (cellData) => cellData.selectedData); const validCellsData = cellsData.filter((item) => isNumber(item.value)); const value = isEmpty(validCellsData) ? null : sumBy(validCellsData, 'value'); return { name, selectedData, value, }; }); }; export const getSummaries = (params) => { const { spreadsheet, targetCell, options = {} } = params; const summaries = []; const summary = {}; const isTableMode = spreadsheet.isTableMode(); if (isTableMode && (options === null || options === void 0 ? void 0 : options.onlyShowCellText)) { const meta = targetCell === null || targetCell === void 0 ? void 0 : targetCell.getMeta(); // 如果是列头, 获取当前列所有数据, 其他则获取当前整行 (1条数据) const selectedCellsData = ((meta === null || meta === void 0 ? void 0 : meta.field) ? spreadsheet.dataSet.getCellMultiData({ query: { field: meta.field } }) : [spreadsheet.dataSet.getRowData(meta)]); return [{ selectedData: selectedCellsData, name: '', value: '' }]; } // 拿到选择的所有 dataCell的数据 const selectedCellsData = getSelectedCellsData(spreadsheet, targetCell, options.onlyShowCellText); forEach(selectedCellsData, (item) => { var _a; const key = item === null || item === void 0 ? void 0 : item[EXTRA_FIELD]; if (summary[key]) { (_a = summary[key]) === null || _a === void 0 ? void 0 : _a.push(item); } else { summary[key] = [item]; } }); mapKeys(summary, (selected, field) => { var _a, _b; const name = spreadsheet.isCustomHeaderFields() ? spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet.getCustomRowFieldName(targetCell) : getSummaryName(spreadsheet, field, options === null || options === void 0 ? void 0 : options.isTotals); let value = ''; let originVal = ''; if (every(selected, (item) => isNotNumber(get(item, VALUE_FIELD)))) { const { placeholder } = spreadsheet.options; const emptyPlaceholder = getEmptyPlaceholder(summary, placeholder); // 如果选中的单元格都无数据,则显示"-" 或 options 里配置的占位符 value = emptyPlaceholder; originVal = emptyPlaceholder; } else { const formatter = (_a = spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet) === null || _a === void 0 ? void 0 : _a.getFieldFormatter(field); const dataSum = getDataSumByField(selected, VALUE_FIELD); originVal = dataSum; value = (_b = formatter === null || formatter === void 0 ? void 0 : formatter(dataSum, selected)) !== null && _b !== void 0 ? _b : parseFloat(dataSum.toPrecision(PRECISION)); } summaries.push({ selectedData: selected, name: name || '', value, originValue: originVal, }); }); if (spreadsheet.isCustomHeaderFields()) { return getCustomFieldsSummaries(summaries); } return summaries; }; export const getTooltipData = (params) => { var _a; const { spreadsheet, cellInfos = [], options = {}, targetCell } = params; let name = null; let summaries = []; let headInfo = null; let details = null; const description = spreadsheet.dataSet.getCustomFieldDescription(targetCell); const firstCellInfo = (cellInfos[0] || {}); if (!(options === null || options === void 0 ? void 0 : options.hideSummary)) { // 计算多项的 sum(默认为 sum,可自定义) summaries = getSummaries({ spreadsheet, options, targetCell, }); } else if (options.onlyShowCellText) { // 行列头 hover & 明细表所有 hover const value = CellData.getFieldValue(firstCellInfo, 'value'); const valueField = CellData.getFieldValue(firstCellInfo, 'valueField'); const formatter = (_a = spreadsheet === null || spreadsheet === void 0 ? void 0 : spreadsheet.dataSet) === null || _a === void 0 ? void 0 : _a.getFieldFormatter(valueField); const formattedValue = formatter === null || formatter === void 0 ? void 0 : formatter(value); const cellText = options.enableFormat ? spreadsheet.dataSet.getFieldName(value) || formattedValue : spreadsheet.dataSet.getFieldName(valueField); // https://github.com/antvis/S2/issues/2843 name = isString(cellText) ? cellText : ''; } else { headInfo = getHeadInfo(spreadsheet, firstCellInfo, options); details = getTooltipDetailList(spreadsheet, firstCellInfo, options, targetCell); } const { interpretation, infos, tips } = (firstCellInfo || {}); return { name, summaries, interpretation, infos, tips, headInfo, details, description, }; }; export const mergeCellInfo = (cells) => map(cells, (stateCell) => { const stateCellMeta = stateCell.getMeta(); return assign({}, stateCellMeta.query || {}, pick(stateCellMeta, ['colIndex', 'rowIndex'])); }); export const getCellsTooltipData = (spreadsheet) => { if (!spreadsheet.interaction.isSelectedState()) { return []; } const { valueInCols } = spreadsheet.dataCfg.fields; // 包括不在可视区域内的格子, 如: 滚动刷选 return spreadsheet.interaction .getCells() .reduce((tooltipData, cellMeta) => { const meta = spreadsheet.facet.getCellMeta(cellMeta === null || cellMeta === void 0 ? void 0 : cellMeta.rowIndex, cellMeta === null || cellMeta === void 0 ? void 0 : cellMeta.colIndex); const query = getMergedQuery(meta); if (isEmpty(meta) || isEmpty(query)) { return []; } const currentCellInfo = Object.assign(Object.assign({}, query), { colIndex: valueInCols ? meta === null || meta === void 0 ? void 0 : meta.colIndex : null, rowIndex: !valueInCols ? meta === null || meta === void 0 ? void 0 : meta.rowIndex : null }); const isEqualCellInfo = tooltipData.find((cellInfo) => isEqual(currentCellInfo, cellInfo)); if (!isEqualCellInfo) { tooltipData.push(currentCellInfo); } return tooltipData; }, []); }; export const getTooltipOptionsByCellType = (cellTooltipConfig = {}, cellType) => { const getOptionsByCell = (cellConfig) => customMerge(cellTooltipConfig, cellConfig); const { colCell, rowCell, dataCell, cornerCell } = cellTooltipConfig; if (cellType === CellType.COL_CELL) { return getOptionsByCell(colCell); } if (cellType === CellType.ROW_CELL) { return getOptionsByCell(rowCell); } if (cellType === CellType.DATA_CELL) { return getOptionsByCell(dataCell); } if (cellType === CellType.CORNER_CELL) { return getOptionsByCell(cornerCell); } return Object.assign({}, cellTooltipConfig); }; export const getTooltipOptions = (spreadsheet, event) => { var _a; if (!event || !spreadsheet) { return null; } const { options, interaction } = spreadsheet; const cellType = (_a = spreadsheet.getCellType) === null || _a === void 0 ? void 0 : _a.call(spreadsheet, event === null || event === void 0 ? void 0 : event.target); // 如果没有 cellType, 说明是刷选丢失 event target 的场景, 此时从产生过交互状态的单元格里取, 避免刷选读取不到争取 tooltip 配置的问题 const sampleCell = last(interaction.getInteractedCells()); return getTooltipOptionsByCellType(options.tooltip, cellType || (sampleCell === null || sampleCell === void 0 ? void 0 : sampleCell.cellType)); }; export const getTooltipVisibleOperator = (operation, options) => { var _a; const { defaultMenus = [], cell } = options; const getDisplayMenus = (menus = []) => menus .filter((menu) => { var _a; return isFunction(menu.visible) ? menu.visible(cell) : (_a = menu.visible) !== null && _a !== void 0 ? _a : true; }) .map((menu) => { if (menu.children) { menu.children = getDisplayMenus(menu.children); } return menu; }); const displayMenus = getDisplayMenus((_a = operation === null || operation === void 0 ? void 0 : operation.menu) === null || _a === void 0 ? void 0 : _a.items); return { menu: Object.assign(Object.assign({}, operation === null || operation === void 0 ? void 0 : operation.menu), { items: compact([...defaultMenus, ...displayMenus]) }), }; }; export const verifyTheElementInTooltip = (parent, child) => { let result = false; let currentNode = child; while (currentNode && currentNode !== document.body) { if (parent === currentNode) { result = true; break; } currentNode = currentNode.parentElement; } return result; }; //# sourceMappingURL=tooltip.js.map