@antv/s2
Version:
effective spreadsheet render core lib
317 lines • 14.6 kB
JavaScript
"use strict";
/**
* 基于 g 绘制简单 mini 图工具库
* https://github.com/antvis/g
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderMiniChart = exports.drawBullet = exports.drawInterval = exports.transformRatioToPercent = exports.getBulletRangeColor = exports.drawBar = exports.drawLine = exports.scale = void 0;
const lodash_1 = require("lodash");
const constant_1 = require("../common/constant");
const interface_1 = require("../common/interface");
const condition_1 = require("../utils/condition/condition");
const formatter_1 = require("../utils/formatter");
const g_renders_1 = require("../utils/g-renders");
/**
* 坐标转换
*/
const scale = (chartData, cell) => {
var _a;
const { data, encode, type } = chartData;
const { x, y, height, width } = cell.getMeta();
const dataCellStyle = cell.getStyle(constant_1.CellType.DATA_CELL);
const { cell: cellStyle, miniChart } = dataCellStyle;
const measures = [];
const encodedData = (0, lodash_1.map)(data, (item) => {
measures.push(item === null || item === void 0 ? void 0 : item[encode.y]);
return {
x: item[encode.x],
y: item[encode.y],
};
});
const maxMeasure = (0, lodash_1.max)(measures) || 0;
const minMeasure = (0, lodash_1.min)(measures) || 0;
let measureRange = maxMeasure - minMeasure;
const { left = 0, right = 0, top = 0, bottom = 0 } = cellStyle.padding;
const xStart = x + left;
const xEnd = x + width - right;
const yStart = y + top;
const yEnd = y + height - bottom;
const heightRange = yEnd - yStart;
const intervalPadding = (_a = miniChart === null || miniChart === void 0 ? void 0 : miniChart.bar) === null || _a === void 0 ? void 0 : _a.intervalPadding;
const intervalX = type === constant_1.MiniChartType.Bar
? (xEnd - xStart - (measures.length - 1) * intervalPadding) /
measures.length +
intervalPadding
: (xEnd - xStart) / (measures.length - 1) || 0;
const box = [];
const points = (0, lodash_1.map)(encodedData, (item, key) => {
const positionX = xStart + key * intervalX;
let positionY;
if (measureRange !== 0) {
positionY =
yEnd - (((item === null || item === void 0 ? void 0 : item.y) - minMeasure) / measureRange) * heightRange;
}
else {
positionY = minMeasure > 0 ? yStart : yEnd;
}
if (type === constant_1.MiniChartType.Bar) {
let baseLinePositionY;
let barHeight;
if (minMeasure < 0 && maxMeasure > 0 && measureRange !== 0) {
// 基准线(0 坐标)在中间
baseLinePositionY =
yEnd - ((0 - minMeasure) / measureRange) * heightRange;
barHeight = Math.abs(positionY - baseLinePositionY);
if ((item === null || item === void 0 ? void 0 : item.y) < 0) {
// 如果值小于 0 需要从基准线为起始 y 坐标开始绘制
positionY = baseLinePositionY;
}
}
else {
// 有三种情况:全为正数 / 全为负数 / 全为零值
// baseLinePositionY = minMeasure < 0 ? yStart : yEnd;
measureRange = (0, lodash_1.max)([Math.abs(maxMeasure), Math.abs(minMeasure)]);
if (measureRange === 0 && minMeasure === 0 && maxMeasure === 0) {
// 全为零值: 没有bar
barHeight = 0;
}
else {
// 全为非零值: 有bar 高度相同
barHeight =
measureRange === 0
? heightRange
: (Math.abs((item === null || item === void 0 ? void 0 : item.y) - 0) / measureRange) * heightRange;
}
if (minMeasure < 0) {
positionY = yStart;
}
else {
positionY = yEnd - barHeight;
}
}
const barWidth = intervalX - intervalPadding;
box.push([barWidth, barHeight]);
}
return [positionX, positionY];
});
return {
points,
box,
};
};
exports.scale = scale;
// ========================= mini 折线相关 ==============================
/**
* 绘制单元格内的 mini 折线图
*/
const drawLine = (chartData, cell) => {
if ((0, lodash_1.isEmpty)(chartData === null || chartData === void 0 ? void 0 : chartData.data) || (0, lodash_1.isEmpty)(cell)) {
return;
}
const dataCellStyle = cell.getStyle(constant_1.CellType.DATA_CELL);
const { miniChart } = dataCellStyle;
const { point, linkLine } = miniChart === null || miniChart === void 0 ? void 0 : miniChart.line;
const { points } = (0, exports.scale)(chartData, cell);
(0, g_renders_1.renderPolyline)(cell, {
points,
stroke: linkLine === null || linkLine === void 0 ? void 0 : linkLine.fill,
lineWidth: linkLine === null || linkLine === void 0 ? void 0 : linkLine.size,
opacity: linkLine === null || linkLine === void 0 ? void 0 : linkLine.opacity,
});
for (let i = 0; i < points.length; i++) {
(0, g_renders_1.renderCircle)(cell, {
cx: points[i][0],
cy: points[i][1],
r: point.size,
fill: point.fill,
fillOpacity: point === null || point === void 0 ? void 0 : point.opacity,
});
}
};
exports.drawLine = drawLine;
// ========================= mini 柱状图相关 ==============================
/**
* 绘制单元格内的 mini 柱状图
*/
const drawBar = (chartData, cell) => {
if ((0, lodash_1.isEmpty)(chartData === null || chartData === void 0 ? void 0 : chartData.data) || (0, lodash_1.isEmpty)(cell)) {
return;
}
const dataCellStyle = cell.getStyle(constant_1.CellType.DATA_CELL);
const { miniChart } = dataCellStyle;
const { bar } = miniChart;
const { points, box } = (0, exports.scale)(chartData, cell);
for (let i = 0; i < points.length; i++) {
(0, g_renders_1.renderRect)(cell, {
x: points[i][0],
y: points[i][1],
width: box[i][0],
height: box[i][1],
fill: bar.fill,
fillOpacity: bar.opacity,
});
}
};
exports.drawBar = drawBar;
// ======================== mini 子弹图相关 ==============================
/**
* 根据当前值和目标值获取子弹图填充色
*/
const getBulletRangeColor = (measure, target, rangeColors) => {
const delta = Number(target) - Number(measure);
if (Number.isNaN(delta) || Number(measure) < 0) {
return rangeColors.bad;
}
if (delta <= 0.1) {
return rangeColors.good;
}
if (delta > 0.1 && delta <= 0.2) {
return rangeColors.satisfactory;
}
return rangeColors.bad;
};
exports.getBulletRangeColor = getBulletRangeColor;
// 比率转百分比, 简单解决计算精度问题
const transformRatioToPercent = (ratio, fractionDigits = { min: 0, max: 0 }) => {
var _a, _b;
const value = Number(ratio);
if (Number.isNaN(value)) {
return ratio;
}
const minimumFractionDigits = (_a = fractionDigits === null || fractionDigits === void 0 ? void 0 : fractionDigits.min) !== null && _a !== void 0 ? _a : fractionDigits;
const maximumFractionDigits = (_b = fractionDigits === null || fractionDigits === void 0 ? void 0 : fractionDigits.max) !== null && _b !== void 0 ? _b : fractionDigits;
const formatter = new Intl.NumberFormat('en-US', {
minimumFractionDigits,
maximumFractionDigits,
// 禁用自动分组: "12220%" => "12,220%"
useGrouping: false,
style: 'percent',
});
return formatter.format(value);
};
exports.transformRatioToPercent = transformRatioToPercent;
// ========================= 条件格式柱图相关 ==============================
/**
* 绘制单元格内的 条件格式 柱图
*/
const drawInterval = (cell) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
if ((0, lodash_1.isEmpty)(cell)) {
return;
}
const { x, y, height, width } = cell.getBBoxByType(interface_1.CellClipBox.PADDING_BOX);
const intervalCondition = cell.findFieldCondition((_a = cell.cellConditions) === null || _a === void 0 ? void 0 : _a.interval);
if (intervalCondition === null || intervalCondition === void 0 ? void 0 : intervalCondition.mapping) {
const attrs = cell.mappingValue(intervalCondition);
if (!attrs) {
return;
}
const defaultValueRange = cell.getValueRange();
const valueRange = attrs.isCompare ? attrs : defaultValueRange;
const minValue = (0, formatter_1.parseNumberWithPrecision)((_b = valueRange.minValue) !== null && _b !== void 0 ? _b : defaultValueRange.minValue);
const maxValue = (0, formatter_1.parseNumberWithPrecision)((_c = valueRange.maxValue) !== null && _c !== void 0 ? _c : defaultValueRange.maxValue);
const fieldValue = (0, lodash_1.isNil)(attrs === null || attrs === void 0 ? void 0 : attrs.fieldValue)
? (0, formatter_1.parseNumberWithPrecision)(cell.getMeta().fieldValue)
: (0, formatter_1.parseNumberWithPrecision)(attrs === null || attrs === void 0 ? void 0 : attrs.fieldValue);
// 对于超出设定范围的值不予显示
if (fieldValue < minValue || fieldValue > maxValue) {
return;
}
const cellStyle = cell.getStyle();
const barChartHeight = (_e = (_d = cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.miniChart) === null || _d === void 0 ? void 0 : _d.interval) === null || _e === void 0 ? void 0 : _e.height;
const barChartFillColor = (_g = (_f = cellStyle === null || cellStyle === void 0 ? void 0 : cellStyle.miniChart) === null || _f === void 0 ? void 0 : _f.interval) === null || _g === void 0 ? void 0 : _g.fill;
const getScale = (0, condition_1.getIntervalScale)(minValue, maxValue);
const { zeroScale, scale: intervalScale } = getScale(fieldValue);
const fill = (_h = attrs.fill) !== null && _h !== void 0 ? _h : barChartFillColor;
return (0, g_renders_1.renderRect)(cell, {
x: x + width * zeroScale,
y: y + height / 2 - barChartHeight / 2,
width: width * intervalScale,
height: barChartHeight,
fill,
});
}
};
exports.drawInterval = drawInterval;
/**
* 绘制单元格内的 mini 子弹图
*/
const drawBullet = (value, cell) => {
const dataCellStyle = cell.getStyle(constant_1.CellType.DATA_CELL);
const { x, y, height, width } = cell.getMeta();
if ((0, lodash_1.isEmpty)(value)) {
cell.renderTextShape(Object.assign(Object.assign({}, dataCellStyle.text), { x: x + width - dataCellStyle.cell.padding.right, y: y + height / 2, text: '' }));
return;
}
const bulletStyle = dataCellStyle.miniChart.bullet;
const { progressBar, comparativeMeasure, rangeColors, backgroundColor } = bulletStyle;
const { measure, target } = value;
const displayMeasure = Math.max(Number(measure), 0);
const displayTarget = Math.max(Number(target), 0);
/*
* 原本是 "0%", 需要精确到浮点数后两位, 保证数值很小时能正常显示, 显示的百分比格式为 "0.22%"
* 所以子弹图需要为数值预留宽度
* 对于负数, 进度条计算按照 0 处理, 但是展示还是要显示原来的百分比
*/
const measurePercent = (0, exports.transformRatioToPercent)(measure, 2);
const widthPercent = (progressBar === null || progressBar === void 0 ? void 0 : progressBar.widthPercent) > 1
? (progressBar === null || progressBar === void 0 ? void 0 : progressBar.widthPercent) / 100
: progressBar === null || progressBar === void 0 ? void 0 : progressBar.widthPercent;
const padding = dataCellStyle.cell.padding;
const contentWidth = width - padding.left - padding.right;
// 子弹图先占位 (bulletWidth),剩下空间给文字 (measureWidth)
const bulletWidth = widthPercent * contentWidth;
const measureWidth = contentWidth - bulletWidth;
/**
* 绘制子弹图 (右对齐)
* 1. 背景
*/
const positionX = x + width - padding.right - bulletWidth;
const positionY = y + height / 2 - progressBar.height / 2;
(0, g_renders_1.renderRect)(cell, {
x: positionX,
y: positionY,
width: bulletWidth,
height: progressBar.height,
fill: backgroundColor,
});
// 2. 进度条
const displayBulletWidth = Math.max(Math.min(bulletWidth * displayMeasure, bulletWidth), 0);
(0, g_renders_1.renderRect)(cell, {
x: positionX,
y: positionY + (progressBar.height - progressBar.innerHeight) / 2,
width: displayBulletWidth,
height: progressBar.innerHeight,
fill: (0, exports.getBulletRangeColor)(displayMeasure, displayTarget, rangeColors),
});
// 3.测量标记线
const lineX = positionX + bulletWidth * displayTarget;
(0, g_renders_1.renderLine)(cell, {
x1: lineX,
y1: y + (height - comparativeMeasure.height) / 2,
x2: lineX,
y2: y + (height - comparativeMeasure.height) / 2 + comparativeMeasure.height,
stroke: comparativeMeasure === null || comparativeMeasure === void 0 ? void 0 : comparativeMeasure.fill,
lineWidth: comparativeMeasure.width,
opacity: comparativeMeasure === null || comparativeMeasure === void 0 ? void 0 : comparativeMeasure.opacity,
});
// 4.绘制指标
const maxTextWidth = measureWidth - padding.right;
cell.renderTextShape(Object.assign(Object.assign({}, dataCellStyle.text), { x: positionX - padding.right, y: y + height / 2, text: measurePercent, wordWrapWidth: maxTextWidth }));
};
exports.drawBullet = drawBullet;
const renderMiniChart = (cell, data) => {
switch (data === null || data === void 0 ? void 0 : data.type) {
case constant_1.MiniChartType.Line:
(0, exports.drawLine)(data, cell);
break;
case constant_1.MiniChartType.Bar:
(0, exports.drawBar)(data, cell);
break;
default:
(0, exports.drawBullet)(data, cell);
break;
}
};
exports.renderMiniChart = renderMiniChart;
//# sourceMappingURL=g-mini-charts.js.map