UNPKG

zz-chart

Version:

Alauda Chart components by Alauda Frontend Team

240 lines 9.53 kB
import { select } from 'd3'; import * as d3 from 'd3'; import { ChartEvent } from '../../types/index.js'; import { createSvg, generateName, PolarShapeType, } from '../../utils/index.js'; import { PolarShape } from './index.js'; import { get, isNumber } from 'lodash-es'; import { measureText } from '../../strategy/utils.js'; import { UPLOT_DEFAULT_OPTIONS } from '../../strategy/config.js'; /** * 堆叠 柱状图 */ export default class BarStacked extends PolarShape { constructor() { super(...arguments); this.type = PolarShapeType.BarStacked; this.data = this.getData(); this.size = { width: 0, height: 0, }; this.margin = { top: 0, right: 0, bottom: 0, left: 0, }; } get nullData() { return this.data.every(d => !isNumber(d.value)); } get totalValue() { return this.data.reduce((prev, item) => prev + item.value, 0); } get colorVar() { return this.ctrl.getTheme().colorVar; } init() { // do nothing. } getTextsWidth(texts) { const widths = texts.map(text => measureText(text, 14).width); return Math.max(...widths); } render() { this.ctrl.container.style.display = 'flex'; this.ctrl.container.style.flexDirection = 'column'; this.option = get(this.ctrl.getOption(), this.type, {}); this.svgEl = this.svgEl || createSvg(select(this.ctrl.container)); this.container = this.container || this.svgEl.append('g'); this.categories = get(this.ctrl.getOption(), 'axis.x.categories', []) || []; this.margin = { top: 16, right: 0, bottom: 0, left: this.getTextsWidth(this.categories), }; const legendRef = this.ctrl.components.get('legend'); this.ctrl.emit(ChartEvent.U_PLOT_READY); setTimeout(() => { const { clientWidth, clientHeight } = this.ctrl.container; const legendEl = this.ctrl.chartContainer.querySelector(`.${generateName('legend')}`); const position = get(this.ctrl.getOption().legend, 'position', ''); const chartWidth = clientWidth - this.margin.left - this.margin.right; const chartHeight = clientHeight - this.margin.bottom - this.margin.top; this.container.attr('transform', `translate(${this.margin.left},${this.margin.top})`); const legendH = position.includes('bottom') ? legendEl?.clientHeight : 0; const headerH = this.ctrl.chartContainer.querySelector(`.${generateName('header')}`) ?.clientHeight || 0; const height = chartHeight - legendH - headerH; this.size = { width: chartWidth, height, }; this.renderBar(chartWidth, height); this.addListener(); // this.renderLabel(); this.ctrl.on(ChartEvent.LEGEND_ITEM_CLICK, () => { this.data = this.getData().filter(d => !legendRef.inactivatedSet.has(d.name)); this.renderBar(chartWidth, height); this.addListener(); }); }); } renderBar(clientWidth, clientHeight) { this.legendRef = this.ctrl.components.get('legend'); this.yScale = d3 .scaleBand() .domain(this.categories) .range([0, clientHeight]) .padding(0.25); this.updateBar(clientWidth, clientHeight); } updateBar(width, height) { this.container.selectAll('*').remove(); const data = this.getData(); // 过滤掉隐藏的系列 const visibleSeries = data.filter(s => !this.legendRef.inactivatedSet.has(s.name)); const stackGroups = d3.group(visibleSeries, d => d.stack || d.name); const groupCount = stackGroups.size; const band = this.yScale.bandwidth(); let barH = this.option.barWidth ? this.option.barWidth : band; barH = Math.max(1, barH); const groupHeight = (barH / groupCount) * 0.8; // 给堆叠组之间留空隙 const groupPadding = (barH / groupCount) * 0.5; const maxValue = d3.max(this.categories, (_cat, i) => { let maxForCat = 0; for (let [_stackName, items] of stackGroups) { const sum = items.reduce((s, item) => s + item.values[i].y, 0); maxForCat = Math.max(maxForCat, sum); } return maxForCat; }) || 0; const x = d3.scaleLinear().domain([0, maxValue]).range([0, width]).nice(); // X 轴 this.container .append('g') .attr('class', 'x-grid') .attr('transform', `translate(0,${height})`) .call(d3.axisBottom(x).tickSize(-height)) .call(g => g .selectAll('line') .attr('stroke', this.ctrl.getTheme().yAxis.gridStroke) .attr('stroke-dasharray', '4,4')) .selectAll('text') .style('font', UPLOT_DEFAULT_OPTIONS.axes[0].font) .each(function (_d, i, nodes) { if (i === nodes.length - 1) { const text = d3.select(this); const self = this; const bb = self.getBBox(); const tickG = d3.select(self.parentNode); const transform = tickG.attr('transform'); const tx = +transform.match(/translate\(([^,]+)/)[1]; const tickRight = tx + bb.width / 2; const chartRight = width; if (tickRight > chartRight) { const shift = chartRight - tickRight - 2; text.attr('dx', shift); } } }); let groupIndex = 0; for (let [stackName, items] of stackGroups) { const keys = items.map(s => s.name); const data = this.categories.map((cat, i) => { const obj = { category: cat }; items.forEach(s => { obj[s.name] = s.values[i].y; obj.color = s.color; }); return obj; }); const stack = d3.stack().keys(keys); const stackedSeries = stack(data); this.container .selectAll(`g.stack-${stackName}`) .data(stackedSeries) .join('g') .attr('fill', d => { const value = items.find(item => item.name === d.key); return value?.color || this.ctrl.color.getChartColor(value.name || d.key); }) .selectAll('rect') .data(d => d) .join('rect') .attr('y', d => { return (this.yScale(d.data.category) + band / 2 - groupHeight - groupPadding / 2 + groupIndex * (groupHeight + groupPadding)); }) .attr('x', d => x(d[0])) .attr('width', d => x(d[1]) - x(d[0])) .attr('height', groupHeight); groupIndex++; } // Y 轴 this.container .append('g') .call(d3.axisLeft(this.yScale)) .selectAll('text') .style('font', UPLOT_DEFAULT_OPTIONS.axes[1].font); this.container .selectAll('text') .style('fill', this.ctrl.getTheme().yAxis.stroke); this.container .selectAll('line') .style('stroke', this.ctrl.getTheme().yAxis.tickStroke); this.container .selectAll('.domain') .style('stroke', this.ctrl.getTheme().yAxis.gridStroke); } addListener() { const ctrl = this.ctrl; this.cursor = document.createElement('div'); this.cursor.className = 'cursor'; this.cursor.style.position = 'absolute'; this.cursor.style.top = '0'; this.cursor.style.left = '0'; this.ctrl.container.appendChild(this.cursor); this.container .selectAll('rect.overlay') .data(this.categories) .join('rect') .attr('class', 'overlay') .attr('x', 0) .attr('y', d => this.yScale(d)) .attr('width', this.size.width) .attr('height', this.yScale.bandwidth()) .attr('fill', 'transparent') .on('mousemove', (event, name) => { this.cursor.style.transform = `translate(${event.offsetX}px,${event.offsetY}px)`; const i = this.categories.indexOf(name); const dd = this.data.map(item => { return { ...item, value: item.values[i].y }; }); ctrl.emit(ChartEvent.ELEMENT_MOUSEMOVE, { self: this, event, data: dd, }); if (!ctrl.hideTooltip) { ctrl.emit(ChartEvent.U_PLOT_SET_CURSOR, { anchor: this.cursor || event.target, title: name, values: dd, }); ctrl.components.get('tooltip').showTooltip(); } }) .on('mouseout', () => { if (!ctrl.hideTooltip) { ctrl.components.get('tooltip').hideTooltip(); } }); } redraw() { } } //# sourceMappingURL=bar-stacked.js.map