Chartwerk Bar Pod
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

183 lines
5.8 KiB

import { DataRow, DataItem } from './data_processor';
import { BarConfig } from './bar_options';
import { PodState } from '@chartwerk/core';
import { BarOptions, BarPodType, BarSerie, SizeType } from '../types';
import * as d3 from 'd3';
import * as _ from 'lodash';
export class BarGroup {
constructor(
protected overlay: d3.Selection<SVGRectElement, unknown, null, undefined>, // overlay from core. It should be global
protected container: d3.Selection<SVGGElement, unknown, null, undefined>,
protected dataRow: DataRow,
protected options: BarConfig,
protected state: PodState<BarSerie, BarOptions>,
protected boxParams: { width: number, height: number, zeroHorizon: number },
protected allDataLength: number,
) {
this.setGroupWidth();
this.setBarWidth();
this.renderBarGroup();
}
protected _groupContainer: d3.Selection<SVGGElement, unknown, null, undefined>;
protected _groupWidth: number;
protected _barWidth: number;
public getGroupContainer(): d3.Selection<SVGGElement, unknown, null, undefined> {
return this._groupContainer;
}
public getGroupWidth(): number {
return this._groupWidth;
}
public getBarWidth(): number {
return this._barWidth;
}
protected renderBarGroup(): void {
this._groupContainer = this.container.append('g').attr('class', `bar-group group-${this.dataRow.key}`);
_.forEach(this.dataRow.items, (data: DataItem, serieIdx: number) => {
this.renderStackedBars(data, serieIdx);
});
}
protected renderStackedBars(data: DataItem, serieIdx: number): void {
const barsHeightPositiveList = [];
const barsHeightNegativeList = [];
data.values.forEach((value: number, valueIdx: number) => {
const barHeight = this.getBarHeight(value);
const config = {
position: {
x: this.getBarPositionX(serieIdx),
y: this.getBarPositionY(value, barsHeightPositiveList, barsHeightNegativeList, barHeight),
},
width: this._barWidth,
height: barHeight,
color: data.colors[valueIdx],
opacity: data.opacity[valueIdx],
value,
};
if(value >= 0) {
barsHeightPositiveList.push(barHeight);
} else {
barsHeightNegativeList.push(barHeight);
}
this.renderBar(config);
});
}
protected getBarPositionX(idx: number): number {
return this.state.xScale(this.dataRow.key) + this._barWidth * idx;
}
protected getBarPositionY(
value: number, barsHeightPositiveList: number[], barsHeightNegativeList: number[], barHeight: number
): number {
if(value >= 0) {
const previousBarsHeight = _.sum(barsHeightPositiveList);
return this.boxParams.zeroHorizon - previousBarsHeight - barHeight;
}
const previousBarsHeight = _.sum(barsHeightNegativeList);
return this.boxParams.zeroHorizon + previousBarsHeight;
}
protected getBarHeight(value): number {
return Math.abs(this.boxParams.zeroHorizon - this.state.yScale(value));
}
protected setGroupWidth(): void {
switch(this.options.barType) {
case BarPodType.DISCRETE:
this.setDiscreteGroupWidth();
return;
case BarPodType.NON_DISCRETE:
this.setNonDiscreteGroupWidth();
return;
}
}
protected setDiscreteGroupWidth(): void {
const groupSizeConfig = this.options.dicreteConfig.groupSize.width;
let width;
if(groupSizeConfig.type === SizeType.PX) {
width = groupSizeConfig.value;
}
if(groupSizeConfig.type === SizeType.PERCENT) {
const factor = groupSizeConfig.value / 100;
width = (this.boxParams.width / this.allDataLength) * factor;
}
this._groupWidth = _.clamp(width, this.options.dicreteConfig.groupSize.min || width, this.options.dicreteConfig.groupSize.max || width);
}
protected setNonDiscreteGroupWidth(): void {
const widthConfig = this.options.nonDicreteConfig.barWidth;
if(widthConfig.estimated.value === undefined) {
const groupWidth = this.boxParams.width / (this.allDataLength * this.dataRow.items.length);
this._groupWidth = groupWidth * 0.5;
return;
}
let width;
if(widthConfig.estimated.type === SizeType.PX) {
width = widthConfig.estimated.value;
}
if(widthConfig.estimated.type === SizeType.UNIT) {
width = this.state.absXScale(widthConfig.estimated.value);
}
this._groupWidth = _.clamp(width, widthConfig.min || width, widthConfig.max || width);
}
protected setBarWidth(): void {
switch(this.options.barType) {
case BarPodType.DISCRETE:
this._barWidth = this._groupWidth / this.dataRow.items.length;
return;
case BarPodType.NON_DISCRETE:
this._barWidth = this._groupWidth;
return;
}
}
protected renderBar(config: any): void {
this._groupContainer.append('rect')
.attr('x', config.position.x)
.attr('y', config.position.y)
.attr('width', config.width)
.attr('height', config.height)
.style('fill', config.color)
.attr('opacity', config.opacity)
.on('contextmenu', this.contextMenu.bind(this))
.on('mouseover', this.redirectEventToOverlay.bind(this))
.on('mousemove', this.redirectEventToOverlay.bind(this))
.on('mouseout', this.redirectEventToOverlay.bind(this))
.on('mousedown', () => { d3.event.stopPropagation(); });
}
contextMenu(): void {
d3.event.preventDefault(); // do not open browser's context menu.
const event = d3.mouse(this.container.node());
this.options.callbackContextMenu({
position: {
eventX: event[0],
eventY: event[1],
pageX: d3.event.pageX,
pageY: d3.event.pageY,
valueX: this.state.xScale.invert(event[0]),
valueY: this.state.yScale.invert(event[1]),
},
data: this.dataRow,
});
}
redirectEventToOverlay(): void {
this.overlay?.node().dispatchEvent(new MouseEvent(d3.event.type, d3.event));
}
}