feat: 优化泡泡玛特的图表
This commit is contained in:
@@ -1,12 +1,49 @@
|
||||
import { col } from '../csv.js';
|
||||
import {
|
||||
PALETTE, FONT, axisCommon, standalonePeriodAxis, titleStyle, legendStyle,
|
||||
financialViewDisclosure, financialViewLabel,
|
||||
financialViewDisclosure, financialViewLabel, niceAxisMax, stackedSharePoint,
|
||||
lineSeries, footnote, watermarks, LAYOUT,
|
||||
} from '../theme.js';
|
||||
|
||||
/**
|
||||
* 混合正负堆叠的矮段标签外置偏移。
|
||||
* ECharts 对负值段自 0 向下堆叠,theme 的 fanTinyLabelOffsets 按全正累计
|
||||
* 算不出正确中心,这里按符号分两摞累计(屏幕坐标 y 向下为正),
|
||||
* 矮段(塞不进段内文字的)标签扇形排开在柱体右侧。
|
||||
*/
|
||||
function expenseFanOffsets(rows, keys, pxPerUnit, { minInsidePx = 15, gap = 13 } = {}) {
|
||||
return rows.map((row) => {
|
||||
let posCum = 0;
|
||||
let negCum = 0;
|
||||
const centers = {};
|
||||
keys.forEach((k) => {
|
||||
const v = Number.isFinite(row[k]) ? row[k] : 0;
|
||||
const mid = v >= 0 ? posCum + v / 2 : negCum + v / 2;
|
||||
centers[k] = -mid * pxPerUnit;
|
||||
if (v >= 0) posCum += v; else negCum += v;
|
||||
});
|
||||
const tiny = keys.filter((k) => Number.isFinite(row[k]) && row[k] !== 0
|
||||
&& Math.abs(row[k]) * pxPerUnit < minInsidePx);
|
||||
if (!tiny.length) return {};
|
||||
const ys = tiny.map((k) => centers[k]);
|
||||
for (let i = 1; i < ys.length; i += 1) ys[i] = Math.max(ys[i], ys[i - 1] + gap);
|
||||
const offsets = {};
|
||||
tiny.forEach((k, i) => { offsets[k] = centers[k] - ys[i]; });
|
||||
return offsets;
|
||||
});
|
||||
}
|
||||
|
||||
/** 泡泡玛特期间费用结构:观察销售投入、管理成本与费用率的规模效应。 */
|
||||
export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||
const keys = ['sales_expense', 'admin_expense', 'financial_expense'];
|
||||
const posStackMax = Math.max(...rows.map((r) => keys.reduce((s, k) => s + Math.max(r[k] ?? 0, 0), 0)));
|
||||
const negMin = Math.min(...rows.map((r) => keys.reduce((s, k) => s + Math.min(r[k] ?? 0, 0), 0)));
|
||||
// 轴范围随数据走并取整刻度,保证矮段像素判定(pxPerUnit)有确定基准
|
||||
const axisMax = niceAxisMax(posStackMax);
|
||||
const axisMin = Math.min(0, Math.floor(negMin / 10) * 10);
|
||||
const pxPerUnit = (height - LAYOUT.gridTop - 90) / (axisMax - axisMin);
|
||||
const rowOffsets = expenseFanOffsets(rows, keys, pxPerUnit);
|
||||
|
||||
return {
|
||||
backgroundColor: '#fff',
|
||||
title: {
|
||||
@@ -35,6 +72,9 @@ export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||
nameLocation: 'middle',
|
||||
nameGap: 52,
|
||||
nameTextStyle: { color: PALETTE.grey, fontSize: 13, fontFamily: FONT },
|
||||
min: axisMin,
|
||||
max: axisMax,
|
||||
interval: 10,
|
||||
},
|
||||
{
|
||||
...axisCommon,
|
||||
@@ -52,9 +92,34 @@ export function expenseStructureChart(rows, { width, height, view = 'all' }) {
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{ name: '销售费用', type: 'bar', stack: 'expenses', data: col(rows, 'sales_expense'), itemStyle: { color: PALETTE.orange } },
|
||||
{ name: '管理费用', type: 'bar', stack: 'expenses', data: col(rows, 'admin_expense'), itemStyle: { color: PALETTE.purple } },
|
||||
{ name: '财务费用(净额)', type: 'bar', stack: 'expenses', data: col(rows, 'financial_expense'), itemStyle: { color: PALETTE.blue } },
|
||||
...keys.map((key, ci) => ({
|
||||
name: { sales_expense: '销售费用', admin_expense: '管理费用', financial_expense: '财务费用(净额)' }[key],
|
||||
type: 'bar',
|
||||
stack: 'expenses',
|
||||
// 每段都标数值:矮段(含负值段)外置到柱体右侧,见 expenseFanOffsets
|
||||
data: rows.map((row, ri) => stackedSharePoint(row[key], {
|
||||
pxPerUnit,
|
||||
labelColor: '#fff',
|
||||
dy: rowOffsets[ri][key] ?? 0,
|
||||
})),
|
||||
itemStyle: { color: [PALETTE.orange, PALETTE.purple, PALETTE.blue][ci] },
|
||||
emphasis: { focus: 'series' },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
fontFamily: FONT,
|
||||
fontWeight: 500,
|
||||
overflow: 'none',
|
||||
// 财务费用净额多在 ±0.1 亿内,一位小数会显示成 ±0.0,此时退到两位小数
|
||||
formatter: (p) => {
|
||||
const t = p.value.toFixed(1);
|
||||
if (t === '0.0' || t === '-0.0') return Math.abs(p.value) < 0.005 ? '0.0' : p.value.toFixed(2);
|
||||
return t;
|
||||
},
|
||||
},
|
||||
})),
|
||||
{
|
||||
...lineSeries({
|
||||
name: '四项费用率',
|
||||
|
||||
Reference in New Issue
Block a user