UNPKG

@antv/s2

Version:

effective spreadsheet render core lib

352 lines 15.9 kB
"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.filterValidChartData = 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 g_utils_1 = require("../utils/g-utils"); /** * 坐标转换 */ 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; let intervalX; if (type === constant_1.MiniChartType.Bar) { intervalX = (xEnd - xStart - (measures.length - 1) * intervalPadding) / measures.length + intervalPadding; } else { intervalX = measures.length > 1 ? (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 折线相关 ============================== /** * 过滤掉 NaN 值,返回有效的图表数据 */ const filterValidChartData = (chartData) => { const { data, encode } = chartData; const validData = data.filter((item) => { const yValue = item === null || item === void 0 ? void 0 : item[encode.y]; return typeof yValue === 'number' && !Number.isNaN(yValue); }); return Object.assign(Object.assign({}, chartData), { data: validData }); }; exports.filterValidChartData = filterValidChartData; /** * 绘制单元格内的 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; } // 过滤掉 NaN 值,只保留有效数据点 const validChartData = (0, exports.filterValidChartData)(chartData); // 如果过滤后没有有效数据,不绘制任何内容 if ((0, lodash_1.isEmpty)(validChartData.data)) { 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)(validChartData, cell); // 当只有一个有效数据点时,只绘制一个点,而不是线 // 这是 BI / 数据可视化领域的通用共识 if (points.length > 1) { (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; const style = { x: x + width * zeroScale, y: y + height / 2 - barChartHeight / 2, width: width * intervalScale, height: barChartHeight, fill, }; const conditionIntervalShape = cell.getConditionIntervalShape(); if (conditionIntervalShape) { (0, g_utils_1.batchSetStyle)(conditionIntervalShape, style); return conditionIntervalShape; } return (0, g_renders_1.renderRect)(cell, style); } }; 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