UNPKG

@antv/s2

Version:

effective spreadsheet render core lib

268 lines 12.2 kB
import { clone, isArray, isEmpty, isFunction, isNil, isNumber, map, size, trim, } from 'lodash'; import { CellType, EMPTY_FIELD_VALUE, EMPTY_PLACEHOLDER, } from '../common/constant'; import { CellClipBox, } from '../common/interface'; import { renderIcon } from '../utils/g-renders'; import { getHorizontalTextIconPosition, getVerticalIconPosition, getVerticalTextPosition, } from './cell/cell'; import { getIconPosition } from './condition/condition'; import { renderMiniChart } from './g-mini-charts'; import { resolveNillString } from './layout'; export const getDisplayText = (text, placeholder) => { const displayText = resolveNillString(text); const emptyPlaceholder = placeholder !== null && placeholder !== void 0 ? placeholder : EMPTY_PLACEHOLDER; const isInvalidNumber = isNumber(displayText) && Number.isNaN(displayText); // 对应维度缺少维度数据时, 会使用 EMPTY_FIELD_VALUE 填充, 实际渲染时统一转成 "-" const isEmptyString = displayText === '' || displayText === EMPTY_FIELD_VALUE; const isEmptyText = isNil(displayText) || isInvalidNumber || isEmptyString; return isEmptyText ? emptyPlaceholder : resolveNillString(displayText); }; export const replaceEmptyFieldValue = (value) => value === EMPTY_FIELD_VALUE ? EMPTY_PLACEHOLDER : value; /** * To decide whether the data is positive or negative. * Two cases needed to be considered since the derived value could be number or string. * @param value */ export const isUpDataValue = (value) => { if (isNumber(value)) { return value >= 0; } return !!value && !trim(value).startsWith('-'); }; /** * Determines whether the data is actually equal to 0 or empty or nil * example: "0.00%" => true * @param value */ export const isZeroOrEmptyValue = (value) => { return (isNil(value) || value === '' || Number(String(value).replace(/[^0-9.]+/g, '')) === 0); }; /** * Determines whether the data is actually equal to 0 or empty or nil or equals to compareValue * example: "0.00%" => true * @param value * @param compareValue */ export const isUnchangedValue = (value, compareValue) => { return isZeroOrEmptyValue(value) || value === compareValue; }; /** * 根据单元格对齐方式计算文本的 x 坐标 * @param x 单元格的 x 坐标 * @param padding @Padding * @param extraWidth 额外的宽度 * @param textAlign 文本对齐方式 */ const calX = (x, padding, extraWidth, textAlign = 'left') => { const { right = 0, left = 0 } = padding; const extra = extraWidth || 0; if (textAlign === 'left') { return x + right / 2 + extra; } if (textAlign === 'right') { return x - right / 2 - extra; } return x + left / 2 + extra; }; /** * 返回需要绘制的 cell 主题 * @param cell 目标 cell * @returns cell 主题和具体 text 主题 */ const getDrawStyle = (cell) => { var _a, _b; const { isTotals } = cell.getMeta(); const isMeasureField = (_b = (_a = cell).isMeasureField) === null || _b === void 0 ? void 0 : _b.call(_a); const cellStyle = cell.getStyle(cell.cellType || CellType.DATA_CELL); let textStyle; if (isMeasureField) { textStyle = cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.measureText; } else if (isTotals) { textStyle = cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.bolderText; } else { textStyle = cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.text; } return { cellStyle, textStyle, }; }; /** * 获取当前文字的绘制样式 */ const getCurrentTextStyle = ({ rowIndex, colIndex, meta, data, textStyle, textCondition, cell, }) => { var _a; const style = (_a = textCondition === null || textCondition === void 0 ? void 0 : textCondition.mapping) === null || _a === void 0 ? void 0 : _a.call(textCondition, data, { rowIndex, colIndex, meta, }, cell); return Object.assign(Object.assign({}, textStyle), style); }; /** * 获取自定义空值占位符 */ export const getEmptyPlaceholder = (meta, placeHolder) => isFunction(placeHolder === null || placeHolder === void 0 ? void 0 : placeHolder.cell) ? placeHolder === null || placeHolder === void 0 ? void 0 : placeHolder.cell(meta) : placeHolder === null || placeHolder === void 0 ? void 0 : placeHolder.cell; /** * @desc 获取多指标情况下每一个指标的内容包围盒 * -------------------------------------------- * | text icon | text icon | text icon | * |-------------|-------------|-------------| * | text icon | text icon | text icon | * -------------------------------------------- * @param box SimpleBBox 整体绘制内容包围盒 * @param textValues SimpleDataItem[][] 指标集合 * @param widthPercent number[] 每行指标的宽度百分比 */ export const getContentAreaForMultiData = (box, textValues, widthPercent) => { const { x, y, width, height } = box; const avgHeight = height / size(textValues); const boxes = []; let curX; let curY; let avgWidth; let totalWidth = 0; const percents = map(widthPercent, (item) => (item > 1 ? item / 100 : item)); for (let i = 0; i < size(textValues); i++) { curY = y + avgHeight * i; const rows = []; curX = x; totalWidth = 0; for (let j = 0; j < size(textValues[i]); j++) { // 指标个数相同,任取其一即可 avgWidth = !isEmpty(percents) ? width * percents[j] : width / size(textValues[0]); curX = calX(x, { left: 0, right: 0 }, totalWidth, 'left'); totalWidth += avgWidth; rows.push({ x: curX, y: curY, width: avgWidth, height: avgHeight, }); } boxes.push(rows); } return boxes; }; /** * @desc draw text shape of object * @param cell * @multiData 自定义文本内容 * @useCondition 是否使用条件格式 */ // eslint-disable-next-line max-lines-per-function export const drawCustomContent = (cell, multiData, useCondition = true) => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const { x, y, height: totalTextHeight, width: totalTextWidth, } = cell.getBBoxByType(CellClipBox.CONTENT_BOX); const meta = cell.getMeta(); const text = multiData || meta.fieldValue; const { values: textValues } = text; const { options } = meta.spreadsheet; // 趋势分析表默认只作用一个条件(因为指标挂行头,每列都不一样,直接在回调里判断是否需要染色即可) const textCondition = (_b = (_a = options === null || options === void 0 ? void 0 : options.conditions) === null || _a === void 0 ? void 0 : _a.text) === null || _b === void 0 ? void 0 : _b[0]; const iconCondition = (_d = (_c = options === null || options === void 0 ? void 0 : options.conditions) === null || _c === void 0 ? void 0 : _c.icon) === null || _d === void 0 ? void 0 : _d[0]; if (!isArray(textValues)) { renderMiniChart(cell, textValues); return; } const widthPercent = (_g = (_f = (_e = options.style) === null || _e === void 0 ? void 0 : _e.dataCell) === null || _f === void 0 ? void 0 : _f.valuesCfg) === null || _g === void 0 ? void 0 : _g.widthPercent; let labelHeight = 0; // 绘制单元格主标题 if (text === null || text === void 0 ? void 0 : text.label) { const dataCellStyle = cell.getStyle(CellType.DATA_CELL); const labelStyle = dataCellStyle.bolderText; labelHeight = totalTextHeight / (textValues.length + 1); cell.renderTextShape(Object.assign(Object.assign({}, labelStyle), { x, y: y + labelHeight / 2, text: text.label, maxLines: 1, wordWrapWidth: totalTextWidth, wordWrap: true, textOverflow: 'ellipsis' }), { shallowRender: true }); } // 绘制指标 const { cellStyle, textStyle } = getDrawStyle(cell); const iconStyle = cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.icon; const iconCfg = iconCondition && iconCondition.mapping && { name: '', size: iconStyle === null || iconStyle === void 0 ? void 0 : iconStyle.size, margin: iconStyle === null || iconStyle === void 0 ? void 0 : iconStyle.margin, position: getIconPosition(iconCondition), }; let curText; const contentBoxes = getContentAreaForMultiData({ x, y: y + labelHeight, height: totalTextHeight - labelHeight, width: totalTextWidth, }, textValues, widthPercent); for (let i = 0; i < textValues.length; i++) { const measures = clone(textValues[i]); for (let j = 0; j < measures.length; j++) { curText = measures[j]; const curStyle = useCondition ? getCurrentTextStyle({ rowIndex: i, colIndex: j, meta, data: curText, textStyle, textCondition, cell, }) : textStyle; const maxTextWidth = contentBoxes[i][j].width - (iconStyle === null || iconStyle === void 0 ? void 0 : iconStyle.size) - ((iconCfg === null || iconCfg === void 0 ? void 0 : iconCfg.position) === 'left' ? (_h = iconStyle === null || iconStyle === void 0 ? void 0 : iconStyle.margin) === null || _h === void 0 ? void 0 : _h.right : (_j = iconStyle === null || iconStyle === void 0 ? void 0 : iconStyle.margin) === null || _j === void 0 ? void 0 : _j.left); const groupedIcons = { left: [], right: [], }; if (iconCfg) { groupedIcons[iconCfg.position].push(iconCfg); } cell.renderTextShape(Object.assign(Object.assign({}, curStyle), { x: 0, y: 0, // 多列文本不换行 maxLines: 1, text: curText, wordWrapWidth: maxTextWidth }), { shallowRender: true, }); const actualTextWidth = cell.getActualTextWidth(); const { textX, leftIconX, rightIconX } = getHorizontalTextIconPosition({ bbox: contentBoxes[i][j], textAlign: curStyle.textAlign, textWidth: actualTextWidth, iconStyle, groupedIcons, }); const textY = getVerticalTextPosition(contentBoxes[i][j], curStyle.textBaseline); cell.updateTextPosition({ x: textX, y: textY }); // 绘制条件格式的 icon if (iconCondition && useCondition) { const attrs = (_k = iconCondition === null || iconCondition === void 0 ? void 0 : iconCondition.mapping) === null || _k === void 0 ? void 0 : _k.call(iconCondition, curText, { rowIndex: i, colIndex: j, meta: cell === null || cell === void 0 ? void 0 : cell.getMeta(), }, cell); const iconX = (iconCfg === null || iconCfg === void 0 ? void 0 : iconCfg.position) === 'left' ? leftIconX : rightIconX; const iconY = getVerticalIconPosition(iconStyle.size, textY, curStyle.fontSize, curStyle.textBaseline); if (attrs) { const iconShape = renderIcon(cell, { x: iconX, y: iconY, name: attrs.icon, width: iconStyle === null || iconStyle === void 0 ? void 0 : iconStyle.size, height: iconStyle === null || iconStyle === void 0 ? void 0 : iconStyle.size, fill: attrs.fill, }); cell.addConditionIconShape(iconShape); } } } } }; /** * 根据 dataCell 配置获取当前单元格宽度 */ export const getCellWidth = (dataCell, labelSize = 1) => (dataCell === null || dataCell === void 0 ? void 0 : dataCell.width) * labelSize; //# sourceMappingURL=text.js.map