@antv/s2
Version:
effective spreadsheet render core lib
268 lines • 12.2 kB
JavaScript
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