rozetko
10 months ago
15 changed files with 4455 additions and 2015 deletions
@ -0,0 +1,61 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
||||||
|
<meta content="utf-8" http-equiv="encoding"> |
||||||
|
|
||||||
|
<script src="../dist/index.dev.js" type="text/javascript"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="chart" style="width: 50%; height: 500px;"></div> |
||||||
|
|
||||||
|
<script type="text/javascript"> |
||||||
|
var pod = new ChartwerkBarPod( |
||||||
|
document.getElementById('chart'), |
||||||
|
[ |
||||||
|
{ target: 'test11', datapoints: getData(), matchedKey: 'm-1', color: 'red', colorFormatter: (data) => ['green', 'yellow'][data.rowIndex] }, |
||||||
|
// { target: 'test12', datapoints: [[100, 10], [200, 20], [300, 10]], matchedKey: 'm-1', color: 'green' }, |
||||||
|
// { target: 'test21', datapoints: [[130, 10], [230, 26], [330, 15]], matchedKey: 'm-2', color: 'yellow'}, |
||||||
|
// { target: 'test22', datapoints: [[130, 10], [230, 27], [330, 10]], matchedKey: 'm-2', color: 'blue' }, |
||||||
|
], |
||||||
|
{ |
||||||
|
usePanning: false, |
||||||
|
axis: { |
||||||
|
x: { format: 'custom', invert: false, valueFormatter: (value) => { return 'L' + value; } }, |
||||||
|
y: { format: 'custom', invert: false, range: [0, 30], valueFormatter: (value) => { return value + '%'; } } |
||||||
|
}, |
||||||
|
stacked: false, |
||||||
|
matching: false, |
||||||
|
maxBarWidth: 20, |
||||||
|
minBarWidth: 4, |
||||||
|
zoomEvents: { |
||||||
|
scroll: { zoom: { isActive: false }, pan: { isActive: false } }, |
||||||
|
}, |
||||||
|
annotations: [ |
||||||
|
{ key: 'm-1', color: 'red' }, |
||||||
|
{ key: 'm-2', color: 'green' } |
||||||
|
], |
||||||
|
eventsCallbacks: { |
||||||
|
zoomIn: (range) => { console.log('range', range) } |
||||||
|
}, |
||||||
|
renderLegend: false, |
||||||
|
} |
||||||
|
); |
||||||
|
console.time('render'); |
||||||
|
pod.render(); |
||||||
|
console.timeEnd('render'); |
||||||
|
|
||||||
|
function getData() { |
||||||
|
return [ |
||||||
|
[100, 15], [200, 20], [300, 10], |
||||||
|
]; |
||||||
|
} |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
|
||||||
|
<style> |
||||||
|
.overlay { |
||||||
|
fill: black; |
||||||
|
} |
||||||
|
</style> |
@ -1,55 +0,0 @@ |
|||||||
<!DOCTYPE html> |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
|
||||||
<meta content="utf-8" http-equiv="encoding"> |
|
||||||
|
|
||||||
<script src="../dist/index.dev.js" type="text/javascript"></script> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
<div id="chart" style="width: 50%; height: 500px;"></div> |
|
||||||
|
|
||||||
<script type="text/javascript"> |
|
||||||
var pod = new ChartwerkBarPod( |
|
||||||
document.getElementById('chart'), |
|
||||||
[ |
|
||||||
{ target: 'serie1', datapoints: [[100, 10, 20, 5], [180, 15, 30, 5], [300, 20, 35, 15]], color: ['red', 'pink', 'blue'], annotation: { enable: true, color: 'green'} }, |
|
||||||
{ target: 'serie2', datapoints: [[150, 10, 10], [220, 20, 15], [330, 30, 5]], color: ['green', 'yellow'], annotation: { enable: true, color: 'black' } }, |
|
||||||
], |
|
||||||
{ |
|
||||||
usePanning: false, |
|
||||||
axis: { |
|
||||||
x: { format: 'custom', invert: false, valueFormatter: (value) => { return 'L' + value; }, ticksCount: 6, range: [50, 380] }, |
|
||||||
y: { format: 'custom', invert: false, valueFormatter: (value) => { return value + '%'; }, ticksCount: 8 } |
|
||||||
}, |
|
||||||
zoomEvents: { |
|
||||||
scroll: { zoom: { isActive: false }, pan: { isActive: false } }, |
|
||||||
}, |
|
||||||
eventsCallbacks: { |
|
||||||
zoomIn: (range) => { console.log('range', range) } |
|
||||||
}, |
|
||||||
renderLegend: false, |
|
||||||
type: { |
|
||||||
['non-discrete']: { |
|
||||||
enable: true, |
|
||||||
barWidth: { |
|
||||||
estimated: { value: 35, type: 'unit' }, |
|
||||||
max: 30, |
|
||||||
min: 10, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
); |
|
||||||
console.time('render'); |
|
||||||
pod.render(); |
|
||||||
console.timeEnd('render'); |
|
||||||
</script> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
|
|
||||||
<style> |
|
||||||
.overlay { |
|
||||||
fill: none; |
|
||||||
} |
|
||||||
</style> |
|
@ -1,57 +0,0 @@ |
|||||||
<!DOCTYPE html> |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
|
||||||
<meta content="utf-8" http-equiv="encoding"> |
|
||||||
|
|
||||||
<script src="../dist/index.dev.js" type="text/javascript"></script> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
<div id="chart" style="width: 50%; height: 500px;"></div> |
|
||||||
|
|
||||||
<script type="text/javascript"> |
|
||||||
var pod = new ChartwerkBarPod( |
|
||||||
document.getElementById('chart'), |
|
||||||
[ |
|
||||||
{ target: 'serie1', datapoints: [[100, 10, 20, 5], [200, 15, 30, 5], [300, 20, 35, 15]], color: ['red', 'pink', 'blue'] }, |
|
||||||
{ target: 'serie2', datapoints: [[100, 10, 10], [200, 20, 15], [300, 30, 5]], color: ['green', 'yellow'] }, |
|
||||||
{ target: 'serie3', datapoints: [[100, -10, 5], [200, -5, -5], [300, -20, 15]], color: ['red', 'pink', 'blue'] }, |
|
||||||
{ target: 'serie4', datapoints: [[100, 10, 10], [200, 20, 15], [300, 30, 5]], color: ['green', 'yellow'] }, |
|
||||||
], |
|
||||||
{ |
|
||||||
usePanning: false, |
|
||||||
axis: { |
|
||||||
x: { format: 'custom', invert: false, valueFormatter: (value) => { return 'L' + value; }, ticksCount: 6 }, |
|
||||||
y: { format: 'custom', invert: false, valueFormatter: (value) => { return value + '%'; }, ticksCount: 8 } |
|
||||||
}, |
|
||||||
zoomEvents: { |
|
||||||
scroll: { zoom: { isActive: false }, pan: { isActive: false } }, |
|
||||||
}, |
|
||||||
eventsCallbacks: { |
|
||||||
zoomIn: (range) => { console.log('range', range) } |
|
||||||
}, |
|
||||||
renderLegend: false, |
|
||||||
// discrete type config |
|
||||||
type: { |
|
||||||
discrete: { |
|
||||||
enable: true, |
|
||||||
step: 100, |
|
||||||
groupSize: { |
|
||||||
width: { value: 60, type: 'percent', } |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
); |
|
||||||
console.time('render'); |
|
||||||
pod.render(); |
|
||||||
console.timeEnd('render'); |
|
||||||
</script> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
|
|
||||||
<style> |
|
||||||
.overlay { |
|
||||||
fill: none; |
|
||||||
} |
|
||||||
</style> |
|
@ -1,57 +0,0 @@ |
|||||||
<!DOCTYPE html> |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
|
||||||
<meta content="utf-8" http-equiv="encoding"> |
|
||||||
|
|
||||||
<script src="../dist/index.dev.js" type="text/javascript"></script> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
<div id="chart" style="width: 50%; height: 500px;"></div> |
|
||||||
|
|
||||||
<script type="text/javascript"> |
|
||||||
// use formatters for color and opacity |
|
||||||
const colorFormatter = (event) => (event.value > 50 ? 'red' : 'green'); |
|
||||||
const opacityFormatter = (event) => (event.key >= 200 ? 0.5 : 1); |
|
||||||
|
|
||||||
var pod = new ChartwerkBarPod( |
|
||||||
document.getElementById('chart'), |
|
||||||
[ |
|
||||||
{ target: 'serie1', datapoints: [[0, 50], [100, 70], [200, 40], [300, 90]], color: colorFormatter, opacity: opacityFormatter }, |
|
||||||
], |
|
||||||
{ |
|
||||||
usePanning: false, |
|
||||||
axis: { |
|
||||||
x: { format: 'custom', invert: false, valueFormatter: (value) => { return 'L' + value; }, ticksCount: 6 }, |
|
||||||
y: { format: 'custom', invert: false, valueFormatter: (value) => { return value + '%'; }, ticksCount: 8 } |
|
||||||
}, |
|
||||||
zoomEvents: { |
|
||||||
scroll: { zoom: { isActive: false }, pan: { isActive: false } }, |
|
||||||
}, |
|
||||||
eventsCallbacks: { |
|
||||||
zoomIn: (range) => { console.log('range', range) } |
|
||||||
}, |
|
||||||
renderLegend: false, |
|
||||||
type: { |
|
||||||
discrete: { |
|
||||||
enable: true, |
|
||||||
step: 100, |
|
||||||
groupSize: { |
|
||||||
width: { value: 60, type: 'percent', } |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
); |
|
||||||
console.time('render'); |
|
||||||
pod.render(); |
|
||||||
console.timeEnd('render'); |
|
||||||
</script> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
|
|
||||||
<style> |
|
||||||
.overlay { |
|
||||||
fill: none; |
|
||||||
} |
|
||||||
</style> |
|
@ -1,56 +0,0 @@ |
|||||||
<!DOCTYPE html> |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
|
||||||
<meta content="utf-8" http-equiv="encoding"> |
|
||||||
|
|
||||||
<script src="../dist/index.dev.js" type="text/javascript"></script> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
<div id="chart" style="width: 50%; height: 500px;"></div> |
|
||||||
|
|
||||||
<script type="text/javascript"> |
|
||||||
var pod = new ChartwerkBarPod( |
|
||||||
document.getElementById('chart'), |
|
||||||
[ |
|
||||||
{ target: 'serie1', datapoints: [[100, 10, 20, 5], [180, 15, 30, 5], [300, 20, 35, 15]], color: ['red', 'pink', 'blue'] }, |
|
||||||
{ target: 'serie2', datapoints: [[150, 10, 10], [220, 20, 15], [330, 30, 5]], color: ['green', 'yellow'] }, |
|
||||||
], |
|
||||||
{ |
|
||||||
usePanning: false, |
|
||||||
axis: { |
|
||||||
x: { format: 'custom', invert: false, valueFormatter: (value) => { return 'L' + value; }, ticksCount: 6, range: [50, 380] }, |
|
||||||
y: { format: 'custom', invert: false, valueFormatter: (value) => { return value + '%'; }, ticksCount: 8 } |
|
||||||
}, |
|
||||||
zoomEvents: { |
|
||||||
scroll: { zoom: { isActive: false }, pan: { isActive: false } }, |
|
||||||
}, |
|
||||||
eventsCallbacks: { |
|
||||||
zoomIn: (range) => { console.log('range', range) } |
|
||||||
}, |
|
||||||
renderLegend: false, |
|
||||||
// non_discrete type config |
|
||||||
type: { |
|
||||||
['non-discrete']: { |
|
||||||
enable: true, |
|
||||||
barWidth: { |
|
||||||
estimated: { value: 35, type: 'unit' }, |
|
||||||
max: 30, |
|
||||||
min: 10, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
); |
|
||||||
console.time('render'); |
|
||||||
pod.render(); |
|
||||||
console.timeEnd('render'); |
|
||||||
</script> |
|
||||||
</body> |
|
||||||
</html> |
|
||||||
|
|
||||||
<style> |
|
||||||
.overlay { |
|
||||||
fill: none; |
|
||||||
} |
|
||||||
</style> |
|
@ -1,69 +0,0 @@ |
|||||||
import * as d3 from 'd3'; |
|
||||||
import * as _ from 'lodash'; |
|
||||||
|
|
||||||
|
|
||||||
const DEFAULT_ANNOTATION_OFFSET_Y = 4; //offset between triangle and bar in px
|
|
||||||
const DEFAULT_ANNOTATION_COLOR = 'red'; |
|
||||||
|
|
||||||
export class BarAnnotation { |
|
||||||
position: { x: number, y: number }; |
|
||||||
|
|
||||||
constructor( |
|
||||||
protected overlay: d3.Selection<SVGRectElement, unknown, null, undefined>, // overlay from core. It should be global
|
|
||||||
protected groupContainer: d3.Selection<SVGGElement, unknown, null, undefined>, // group - bars as one item
|
|
||||||
protected annotationOptions: { size: number, max?: number, min?: number, offset?: number, color?: string }, |
|
||||||
) { |
|
||||||
this.position = this.getGroupLastRectPosition(); |
|
||||||
this.renderAnnotation(); |
|
||||||
} |
|
||||||
|
|
||||||
protected renderAnnotation(): void { |
|
||||||
this.groupContainer.append('path') |
|
||||||
.attr('d', () => this.getTrianglePath()) |
|
||||||
.attr('fill', this.annotationOptions.color || DEFAULT_ANNOTATION_COLOR) |
|
||||||
.on('mouseover', this.redirectEventToOverlay.bind(this)) |
|
||||||
.on('mousemove', this.redirectEventToOverlay.bind(this)) |
|
||||||
.on('mouseout', this.redirectEventToOverlay.bind(this)) |
|
||||||
.on('mousedown', () => { d3.event.stopPropagation(); }); |
|
||||||
} |
|
||||||
|
|
||||||
getGroupLastRectPosition(): { x: number, y: number } { |
|
||||||
const lastRect = _.last(this.groupContainer.selectAll('rect')?.nodes()); |
|
||||||
const barSelection = d3.select(lastRect); |
|
||||||
return { |
|
||||||
x: Math.ceil(_.toNumber(barSelection.attr('x'))), |
|
||||||
y: Math.ceil(_.toNumber(barSelection.attr('y'))), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
protected getTrianglePath(): string { |
|
||||||
// (x, y) - top left corner of bar
|
|
||||||
const minTriangleSize = this.annotationOptions?.min; |
|
||||||
const maxTriagleSize = this.annotationOptions?.max; |
|
||||||
const yOffset = this.annotationOptions?.offset || DEFAULT_ANNOTATION_OFFSET_Y; |
|
||||||
const centerX = this.position.x + this.annotationOptions.size / 2; |
|
||||||
const correctedLength = _.clamp(this.annotationOptions.size, minTriangleSize, maxTriagleSize); |
|
||||||
|
|
||||||
const topY = Math.max(this.position.y - correctedLength - yOffset, 4); |
|
||||||
const topLeftCorner = { |
|
||||||
x: centerX - correctedLength / 2, |
|
||||||
y: topY, |
|
||||||
}; |
|
||||||
const topRightCorner = { |
|
||||||
x: centerX + correctedLength / 2, |
|
||||||
y: topY, |
|
||||||
}; |
|
||||||
const bottomMiddleCorner = { |
|
||||||
x: centerX, |
|
||||||
y: topY + correctedLength, |
|
||||||
}; |
|
||||||
|
|
||||||
return `M ${topLeftCorner.x} ${topLeftCorner.y} |
|
||||||
L ${topRightCorner.x} ${topRightCorner.y} |
|
||||||
L ${bottomMiddleCorner.x} ${bottomMiddleCorner.y} z`;
|
|
||||||
} |
|
||||||
|
|
||||||
redirectEventToOverlay(): void { |
|
||||||
this.overlay?.node().dispatchEvent(new MouseEvent(d3.event.type, d3.event)); |
|
||||||
} |
|
||||||
} |
|
@ -1,183 +0,0 @@ |
|||||||
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)); |
|
||||||
} |
|
||||||
} |
|
@ -1,46 +0,0 @@ |
|||||||
import { PodState } from '@chartwerk/core'; |
|
||||||
import { DataRow } from './data_processor'; |
|
||||||
import { BarConfig } from './bar_options'; |
|
||||||
|
|
||||||
import { BarOptions, BarSerie, BarPodType, DiscreteConfig } from '../types'; |
|
||||||
|
|
||||||
import * as _ from 'lodash'; |
|
||||||
|
|
||||||
// It is not a real bar state. Just rewrite some core params. like xScale/yScale
|
|
||||||
// TODO: import core scale and extend it, same to bar_series and bar_options
|
|
||||||
|
|
||||||
export function setBarScaleX(state: PodState<BarSerie, BarOptions>, dataRows: DataRow[], options: BarConfig): void { |
|
||||||
switch(options.barType) { |
|
||||||
case BarPodType.DISCRETE: |
|
||||||
const config: DiscreteConfig = options.dicreteConfig; |
|
||||||
let discreteStep = 0; |
|
||||||
if(_.isNumber(config.step)) { |
|
||||||
discreteStep = config.step; |
|
||||||
} else { |
|
||||||
if(!_.isEmpty(dataRows)) { |
|
||||||
discreteStep = (_.last(dataRows).key - _.first(dataRows).key) / dataRows.length; |
|
||||||
} |
|
||||||
} |
|
||||||
state.xValueRange = [state.xValueRange[0], state.xValueRange[1] + discreteStep]; |
|
||||||
return; |
|
||||||
case BarPodType.NON_DISCRETE: |
|
||||||
if(!_.isEmpty(options.axis.x.range)) { |
|
||||||
return; |
|
||||||
} |
|
||||||
let nonDiscreteStep = 0; |
|
||||||
if(!_.isEmpty(dataRows)) { |
|
||||||
nonDiscreteStep = (_.last(dataRows).key - _.first(dataRows).key) / dataRows.length; |
|
||||||
} |
|
||||||
state.xValueRange = [state.xValueRange[0], state.xValueRange[1] + nonDiscreteStep]; |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export function setBarScaleY(state: PodState<BarSerie, BarOptions>, dataRows: DataRow[], options: BarOptions): void { |
|
||||||
if(!_.isEmpty(options.axis.y.range)) { |
|
||||||
return; |
|
||||||
} |
|
||||||
const maxValue = _.max(_.map(dataRows, (dataRow: DataRow) => dataRow.maxSumm)); |
|
||||||
const minValue = _.min(_.map(dataRows, (dataRow: DataRow) => dataRow.minSubtraction)); |
|
||||||
state.yValueRange = [minValue, maxValue]; |
|
||||||
} |
|
@ -1,143 +0,0 @@ |
|||||||
import { |
|
||||||
Datapoint, Color, ValueX, Opacity, |
|
||||||
BarPodType, BarSerie |
|
||||||
} from '../types'; |
|
||||||
|
|
||||||
import { BarConfig } from './bar_options'; |
|
||||||
|
|
||||||
import * as _ from 'lodash'; |
|
||||||
|
|
||||||
export type DataRow = { |
|
||||||
key: number; // x
|
|
||||||
items: DataItem[]; |
|
||||||
maxSumm: number; |
|
||||||
minSubtraction: number; |
|
||||||
}; |
|
||||||
export type DataItem = { |
|
||||||
target: string; // serie target
|
|
||||||
values: number[]; // y list
|
|
||||||
colors: string[]; |
|
||||||
opacity: number[]; |
|
||||||
}; |
|
||||||
const DEFAULT_BAR_COLOR = 'green'; |
|
||||||
const DEFAULT_OPACITY = 1; |
|
||||||
|
|
||||||
export class DataProcessor { |
|
||||||
_formattedDataRows: DataRow[] = []; |
|
||||||
|
|
||||||
constructor(protected visibleSeries: BarSerie[], protected options: BarConfig) { |
|
||||||
this.validateData(); |
|
||||||
switch(this.options.barType) { |
|
||||||
case BarPodType.DISCRETE: |
|
||||||
this.setDataRowsForDiscreteType(); |
|
||||||
break; |
|
||||||
case BarPodType.NON_DISCRETE: |
|
||||||
this.setDataRowsForNonDiscreteType(); |
|
||||||
break; |
|
||||||
default: |
|
||||||
throw new Error(`Bar DataProcessor: Unknown BarPodType ${this.options.barType}`); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public get dataRows(): DataRow[] { |
|
||||||
return this._formattedDataRows; |
|
||||||
} |
|
||||||
|
|
||||||
protected setDataRowsForDiscreteType(): void { |
|
||||||
const zippedData = _.zip(..._.map(this.visibleSeries, serie => serie.datapoints)); |
|
||||||
this._formattedDataRows = _.map(zippedData, (dataRow: Datapoint[], rowIdx: number) => { |
|
||||||
return { |
|
||||||
key: _.first(_.first(dataRow)), // x
|
|
||||||
items: _.map(dataRow, (datapoint: Datapoint, pointIdx: number) => { |
|
||||||
const serie = this.visibleSeries[pointIdx]; |
|
||||||
const values = _.tail(datapoint); |
|
||||||
return { |
|
||||||
target: serie.target, |
|
||||||
values, // y list
|
|
||||||
colors: this.getColorsForEachValue(values, serie.color, datapoint[0], serie.target), |
|
||||||
opacity: this.getOpacityForEachValue(values, serie.opacity, datapoint[0], serie.target), |
|
||||||
} |
|
||||||
}), |
|
||||||
maxSumm: _.max(_.map(dataRow, (datapoint: Datapoint) => this.getMaxSumm(_.tail(datapoint)))), // max y axis scale
|
|
||||||
minSubtraction: _.min(_.map(dataRow, (datapoint: Datapoint) => this.getMinSubtraction(_.tail(datapoint)))), // min y axis scale
|
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
protected setDataRowsForNonDiscreteType(): void { |
|
||||||
for(let serie of this.visibleSeries) { |
|
||||||
const rows = _.map(serie.datapoints, (datapoint: Datapoint) => { |
|
||||||
const values = _.tail(datapoint); |
|
||||||
return { |
|
||||||
key: datapoint[0], // x
|
|
||||||
items: [ |
|
||||||
{ |
|
||||||
target: serie.target, |
|
||||||
values, // y list
|
|
||||||
colors: this.getColorsForEachValue(values, serie.color, datapoint[0], serie.target), |
|
||||||
opacity: this.getOpacityForEachValue(values, serie.opacity, datapoint[0], serie.target), |
|
||||||
} |
|
||||||
], |
|
||||||
maxSumm: this.getMaxSumm(values), // max y axis scale
|
|
||||||
minSubtraction: this.getMinSubtraction(values), // min y axis scale
|
|
||||||
} |
|
||||||
}); |
|
||||||
this._formattedDataRows = _.concat(this._formattedDataRows, rows); |
|
||||||
} |
|
||||||
this._formattedDataRows = _.sortBy(this._formattedDataRows, 'key'); |
|
||||||
} |
|
||||||
|
|
||||||
getColorsForEachValue(values: number[], color: Color, key: ValueX, target: string): string[] { |
|
||||||
if(_.isString(color)) { |
|
||||||
return _.map(values, value => color); |
|
||||||
} |
|
||||||
if(_.isArray(color)) { |
|
||||||
return _.map(values, (value, i) => color[i] || DEFAULT_BAR_COLOR); |
|
||||||
} |
|
||||||
if(_.isFunction(color)) { |
|
||||||
return _.map(values, (value, i) => (color as Function)({ value, barIndex: i, key, target })); |
|
||||||
} |
|
||||||
throw new Error(`Unknown type of serie color: ${target} ${color}`); |
|
||||||
} |
|
||||||
|
|
||||||
getOpacityForEachValue(values: number[], opacity: Opacity | undefined, key: ValueX, target: string): number[] { |
|
||||||
if(opacity === undefined) { |
|
||||||
return _.map(values, value => DEFAULT_OPACITY); |
|
||||||
} |
|
||||||
if(_.isNumber(opacity)) { |
|
||||||
return _.map(values, value => opacity); |
|
||||||
} |
|
||||||
if(_.isArray(opacity)) { |
|
||||||
return _.map(values, (value, i) => !_.isNil(opacity[i]) ? opacity[i] : DEFAULT_OPACITY); |
|
||||||
} |
|
||||||
if(_.isFunction(opacity)) { |
|
||||||
return _.map(values, (value, i) => (opacity as Function)({ value, barIndex: i, key, target })); |
|
||||||
} |
|
||||||
throw new Error(`Unknown type of serie opacity: ${target} ${opacity}`); |
|
||||||
} |
|
||||||
|
|
||||||
getMaxSumm(values: number[]): number { |
|
||||||
return values.reduce((prev, curr) => curr > 0 ? prev + curr : prev, 0); |
|
||||||
} |
|
||||||
|
|
||||||
getMinSubtraction(values: number[]): number { |
|
||||||
return values.reduce((prev, curr) => curr < 0 ? prev + curr : prev, 0); |
|
||||||
} |
|
||||||
|
|
||||||
validateData(): void { |
|
||||||
if(this.options.barType === BarPodType.NON_DISCRETE) { |
|
||||||
return; |
|
||||||
} |
|
||||||
// TODO: move non-empty datapoints validation to core
|
|
||||||
if(_.isEmpty(this.visibleSeries)) { |
|
||||||
return; |
|
||||||
} |
|
||||||
const xValuesList = _.map(_.first(this.visibleSeries).datapoints, dp => dp[0]); |
|
||||||
_.forEach(this.visibleSeries, serie => { |
|
||||||
const serieXList = _.map(serie.datapoints, dp => dp[0]); |
|
||||||
if(!_.isEqual(xValuesList, serieXList)) { |
|
||||||
throw new Error(`Bar DataProcessor: All series should have equal X values lists`); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue