@antv/s2
Version:
effective spreadsheet render core lib
455 lines (454 loc) • 21.6 kB
JavaScript
/**
* 获取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