|
|
@ -20,12 +20,10 @@ import { |
|
|
|
ScrollPanOrientation, |
|
|
|
ScrollPanOrientation, |
|
|
|
ScrollPanDirection, |
|
|
|
ScrollPanDirection, |
|
|
|
AxisOption, |
|
|
|
AxisOption, |
|
|
|
SvgElParams, |
|
|
|
|
|
|
|
} from './types'; |
|
|
|
} from './types'; |
|
|
|
import { uid } from './utils'; |
|
|
|
import { uid } from './utils'; |
|
|
|
import { palette } from './colors'; |
|
|
|
import { palette } from './colors'; |
|
|
|
|
|
|
|
|
|
|
|
// we import only d3 types here
|
|
|
|
|
|
|
|
import * as d3 from 'd3'; |
|
|
|
import * as d3 from 'd3'; |
|
|
|
|
|
|
|
|
|
|
|
import defaultsDeep from 'lodash/defaultsDeep'; |
|
|
|
import defaultsDeep from 'lodash/defaultsDeep'; |
|
|
@ -44,7 +42,6 @@ const DEFAULT_MARGIN: Margin = { top: 30, right: 20, bottom: 20, left: 30 }; |
|
|
|
const DEFAULT_TICK_COUNT = 4; |
|
|
|
const DEFAULT_TICK_COUNT = 4; |
|
|
|
const DEFAULT_TICK_SIZE = 2; |
|
|
|
const DEFAULT_TICK_SIZE = 2; |
|
|
|
const MILISECONDS_IN_MINUTE = 60 * 1000; |
|
|
|
const MILISECONDS_IN_MINUTE = 60 * 1000; |
|
|
|
const DEFAULT_AXIS_RANGE = [0, 1]; |
|
|
|
|
|
|
|
const DEFAULT_SCROLL_PAN_STEP = 50; |
|
|
|
const DEFAULT_SCROLL_PAN_STEP = 50; |
|
|
|
const DEFAULT_OPTIONS: Options = { |
|
|
|
const DEFAULT_OPTIONS: Options = { |
|
|
|
confidence: 0, |
|
|
|
confidence: 0, |
|
|
@ -147,8 +144,6 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
protected grid: Grid; |
|
|
|
protected grid: Grid; |
|
|
|
|
|
|
|
|
|
|
|
constructor( |
|
|
|
constructor( |
|
|
|
// maybe it's not the best idea
|
|
|
|
|
|
|
|
_d3: typeof d3, |
|
|
|
|
|
|
|
protected readonly el: HTMLElement, |
|
|
|
protected readonly el: HTMLElement, |
|
|
|
_series: T[] = [], |
|
|
|
_series: T[] = [], |
|
|
|
_options: O |
|
|
|
_options: O |
|
|
@ -160,9 +155,8 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
defaultsDeep(options, DEFAULT_OPTIONS); |
|
|
|
defaultsDeep(options, DEFAULT_OPTIONS); |
|
|
|
this.options = options; |
|
|
|
this.options = options; |
|
|
|
this.series = cloneDeep(_series); |
|
|
|
this.series = cloneDeep(_series); |
|
|
|
this.d3 = _d3; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.d3Node = this.d3.select(this.el); |
|
|
|
this.d3Node = d3.select(this.el); |
|
|
|
this.addEventListeners(); |
|
|
|
this.addEventListeners(); |
|
|
|
|
|
|
|
|
|
|
|
this.createSvg(); |
|
|
|
this.createSvg(); |
|
|
@ -315,7 +309,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
.attr('id', 'x-axis-container') |
|
|
|
.attr('id', 'x-axis-container') |
|
|
|
.style('pointer-events', 'none') |
|
|
|
.style('pointer-events', 'none') |
|
|
|
.call( |
|
|
|
.call( |
|
|
|
this.d3.axisBottom(this.xScale) |
|
|
|
d3.axisBottom(this.xScale) |
|
|
|
.ticks(this.options.axis.x.ticksCount) |
|
|
|
.ticks(this.options.axis.x.ticksCount) |
|
|
|
.tickSize(DEFAULT_TICK_SIZE) |
|
|
|
.tickSize(DEFAULT_TICK_SIZE) |
|
|
|
.tickFormat(this.getAxisTicksFormatter(this.options.axis.x)) |
|
|
|
.tickFormat(this.getAxisTicksFormatter(this.options.axis.x)) |
|
|
@ -336,7 +330,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
.style('pointer-events', 'none') |
|
|
|
.style('pointer-events', 'none') |
|
|
|
// TODO: number of ticks shouldn't be hardcoded
|
|
|
|
// TODO: number of ticks shouldn't be hardcoded
|
|
|
|
.call( |
|
|
|
.call( |
|
|
|
this.d3.axisLeft(this.yScale) |
|
|
|
d3.axisLeft(this.yScale) |
|
|
|
.ticks(this.options.axis.y.ticksCount) |
|
|
|
.ticks(this.options.axis.y.ticksCount) |
|
|
|
.tickSize(DEFAULT_TICK_SIZE) |
|
|
|
.tickSize(DEFAULT_TICK_SIZE) |
|
|
|
.tickFormat(this.getAxisTicksFormatter(this.options.axis.y)) |
|
|
|
.tickFormat(this.getAxisTicksFormatter(this.options.axis.y)) |
|
|
@ -346,7 +340,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
if(ticks === undefined || ticks[index] === undefined) { |
|
|
|
if(ticks === undefined || ticks[index] === undefined) { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
this.d3.select(ticks[index]).attr('color', color); |
|
|
|
d3.select(ticks[index]).attr('color', color); |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -362,7 +356,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
.style('pointer-events', 'none') |
|
|
|
.style('pointer-events', 'none') |
|
|
|
// TODO: number of ticks shouldn't be hardcoded
|
|
|
|
// TODO: number of ticks shouldn't be hardcoded
|
|
|
|
.call( |
|
|
|
.call( |
|
|
|
this.d3.axisRight(this.y1Scale) |
|
|
|
d3.axisRight(this.y1Scale) |
|
|
|
.ticks(DEFAULT_TICK_COUNT) |
|
|
|
.ticks(DEFAULT_TICK_COUNT) |
|
|
|
.tickSize(DEFAULT_TICK_SIZE) |
|
|
|
.tickSize(DEFAULT_TICK_SIZE) |
|
|
|
.tickFormat(this.getAxisTicksFormatter(this.options.axis.y1)) |
|
|
|
.tickFormat(this.getAxisTicksFormatter(this.options.axis.y1)) |
|
|
@ -432,17 +426,17 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
} |
|
|
|
} |
|
|
|
switch(this.options.zoomEvents.mouse.zoom.orientation) { |
|
|
|
switch(this.options.zoomEvents.mouse.zoom.orientation) { |
|
|
|
case BrushOrientation.VERTICAL: |
|
|
|
case BrushOrientation.VERTICAL: |
|
|
|
this.brush = this.d3.brushY(); |
|
|
|
this.brush = d3.brushY(); |
|
|
|
break; |
|
|
|
break; |
|
|
|
case BrushOrientation.HORIZONTAL: |
|
|
|
case BrushOrientation.HORIZONTAL: |
|
|
|
this.brush = this.d3.brushX(); |
|
|
|
this.brush = d3.brushX(); |
|
|
|
break; |
|
|
|
break; |
|
|
|
case BrushOrientation.SQUARE: |
|
|
|
case BrushOrientation.SQUARE: |
|
|
|
case BrushOrientation.RECTANGLE: |
|
|
|
case BrushOrientation.RECTANGLE: |
|
|
|
this.brush = this.d3.brush(); |
|
|
|
this.brush = d3.brush(); |
|
|
|
break; |
|
|
|
break; |
|
|
|
default: |
|
|
|
default: |
|
|
|
this.brush = this.d3.brushX(); |
|
|
|
this.brush = d3.brushX(); |
|
|
|
} |
|
|
|
} |
|
|
|
const keyEvent = this.options.zoomEvents.mouse.zoom.keyEvent; |
|
|
|
const keyEvent = this.options.zoomEvents.mouse.zoom.keyEvent; |
|
|
|
this.brush.extent([ |
|
|
|
this.brush.extent([ |
|
|
@ -463,9 +457,9 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
// TODO: refactor
|
|
|
|
// TODO: refactor
|
|
|
|
switch(key) { |
|
|
|
switch(key) { |
|
|
|
case KeyEvent.MAIN: |
|
|
|
case KeyEvent.MAIN: |
|
|
|
return () => !this.d3.event.shiftKey; |
|
|
|
return () => !d3.event.shiftKey; |
|
|
|
case KeyEvent.SHIFT: |
|
|
|
case KeyEvent.SHIFT: |
|
|
|
return () => this.d3.event.shiftKey; |
|
|
|
return () => d3.event.shiftKey; |
|
|
|
default: |
|
|
|
default: |
|
|
|
throw new Error(`Unknown type of KeyEvent: ${key}`); |
|
|
|
throw new Error(`Unknown type of KeyEvent: ${key}`); |
|
|
|
} |
|
|
|
} |
|
|
@ -506,7 +500,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
if(this.options.axis.y1.isActive === true) { |
|
|
|
if(this.options.axis.y1.isActive === true) { |
|
|
|
this.initScaleY1 = this.y1Scale.copy(); |
|
|
|
this.initScaleY1 = this.y1Scale.copy(); |
|
|
|
} |
|
|
|
} |
|
|
|
this.pan = this.d3.zoom() |
|
|
|
this.pan = d3.zoom() |
|
|
|
.on('zoom', this.onPanning.bind(this)) |
|
|
|
.on('zoom', this.onPanning.bind(this)) |
|
|
|
.on('end', this.onPanningEnd.bind(this)); |
|
|
|
.on('end', this.onPanningEnd.bind(this)); |
|
|
|
|
|
|
|
|
|
|
@ -626,7 +620,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
protected onPanning(): void { |
|
|
|
protected onPanning(): void { |
|
|
|
const event = this.d3.event; |
|
|
|
const event = d3.event; |
|
|
|
if(event.sourceEvent === null || event.sourceEvent === undefined) { |
|
|
|
if(event.sourceEvent === null || event.sourceEvent === undefined) { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
@ -708,19 +702,19 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
// TODO: refactor, divide between classes
|
|
|
|
// TODO: refactor, divide between classes
|
|
|
|
rescaleAxisX(transformX: number): void { |
|
|
|
rescaleAxisX(transformX: number): void { |
|
|
|
this.state.transform = { x: transformX }; |
|
|
|
this.state.transform = { x: transformX }; |
|
|
|
const rescaleX = this.d3.event.transform.rescaleX(this.initScaleX); |
|
|
|
const rescaleX = d3.event.transform.rescaleX(this.initScaleX); |
|
|
|
this.xAxisElement.call(this.d3.axisBottom(this.xScale).scale(rescaleX)); |
|
|
|
this.xAxisElement.call(d3.axisBottom(this.xScale).scale(rescaleX)); |
|
|
|
this.state.xValueRange = [rescaleX.invert(0), rescaleX.invert(this.width)]; |
|
|
|
this.state.xValueRange = [rescaleX.invert(0), rescaleX.invert(this.width)]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
rescaleAxisY(transformY: number): void { |
|
|
|
rescaleAxisY(transformY: number): void { |
|
|
|
this.state.transform = { y: transformY }; |
|
|
|
this.state.transform = { y: transformY }; |
|
|
|
const rescaleY = this.d3.event.transform.rescaleY(this.initScaleY); |
|
|
|
const rescaleY = d3.event.transform.rescaleY(this.initScaleY); |
|
|
|
this.yAxisElement.call(this.d3.axisLeft(this.yScale).scale(rescaleY)); |
|
|
|
this.yAxisElement.call(d3.axisLeft(this.yScale).scale(rescaleY)); |
|
|
|
this.state.yValueRange = [rescaleY.invert(0), rescaleY.invert(this.height)]; |
|
|
|
this.state.yValueRange = [rescaleY.invert(0), rescaleY.invert(this.height)]; |
|
|
|
if(this.y1AxisElement) { |
|
|
|
if(this.y1AxisElement) { |
|
|
|
const rescaleY1 = this.d3.event.transform.rescaleY(this.initScaleY1); |
|
|
|
const rescaleY1 = d3.event.transform.rescaleY(this.initScaleY1); |
|
|
|
this.y1AxisElement.call(this.d3.axisLeft(this.y1Scale).scale(rescaleY1)); |
|
|
|
this.y1AxisElement.call(d3.axisLeft(this.y1Scale).scale(rescaleY1)); |
|
|
|
this.state.y1ValueRange = [rescaleY1.invert(0), rescaleY1.invert(this.height)]; |
|
|
|
this.state.y1ValueRange = [rescaleY1.invert(0), rescaleY1.invert(this.height)]; |
|
|
|
// TODO: y1 axis jumps on panning
|
|
|
|
// TODO: y1 axis jumps on panning
|
|
|
|
this.y1AxisElement.selectAll('line').attr('x2', 2); |
|
|
|
this.y1AxisElement.selectAll('line').attr('x2', 2); |
|
|
@ -778,7 +772,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
protected onBrush(): void { |
|
|
|
protected onBrush(): void { |
|
|
|
const selection = this.d3.event.selection; |
|
|
|
const selection = d3.event.selection; |
|
|
|
if(this.options.zoomEvents.mouse.zoom.orientation !== BrushOrientation.SQUARE || selection === null) { |
|
|
|
if(this.options.zoomEvents.mouse.zoom.orientation !== BrushOrientation.SQUARE || selection === null) { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
@ -816,15 +810,15 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
protected onBrushStart(): void { |
|
|
|
protected onBrushStart(): void { |
|
|
|
// TODO: move to state
|
|
|
|
// TODO: move to state
|
|
|
|
this.isBrushing === true; |
|
|
|
this.isBrushing === true; |
|
|
|
const selection = this.d3.event.selection; |
|
|
|
const selection = d3.event.selection; |
|
|
|
if(selection !== null && selection.length > 0) { |
|
|
|
if(selection !== null && selection.length > 0) { |
|
|
|
this.brushStartSelection = this.d3.event.selection[0]; |
|
|
|
this.brushStartSelection = d3.event.selection[0]; |
|
|
|
} |
|
|
|
} |
|
|
|
this.onMouseOut(); |
|
|
|
this.onMouseOut(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
protected onBrushEnd(): void { |
|
|
|
protected onBrushEnd(): void { |
|
|
|
const extent = this.d3.event.selection; |
|
|
|
const extent = d3.event.selection; |
|
|
|
this.isBrushing === false; |
|
|
|
this.isBrushing === false; |
|
|
|
if(extent === undefined || extent === null || extent.length < 2) { |
|
|
|
if(extent === undefined || extent === null || extent.length < 2) { |
|
|
|
console.warn('Chartwerk Core: skip brush end (no extent)'); |
|
|
|
console.warn('Chartwerk Core: skip brush end (no extent)'); |
|
|
@ -902,14 +896,14 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
// TODO: move to State
|
|
|
|
// TODO: move to State
|
|
|
|
get absXScale(): d3.ScaleLinear<number, number> { |
|
|
|
get absXScale(): d3.ScaleLinear<number, number> { |
|
|
|
const domain = [0, Math.abs(this.state.getMaxValueX() - this.state.getMinValueX())]; |
|
|
|
const domain = [0, Math.abs(this.state.getMaxValueX() - this.state.getMinValueX())]; |
|
|
|
return this.d3.scaleLinear() |
|
|
|
return d3.scaleLinear() |
|
|
|
.domain(domain) |
|
|
|
.domain(domain) |
|
|
|
.range([0, this.width]); |
|
|
|
.range([0, this.width]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
get absYScale(): d3.ScaleLinear<number, number> { |
|
|
|
get absYScale(): d3.ScaleLinear<number, number> { |
|
|
|
const domain = [0, Math.abs(this.state.getMaxValueY() - this.state.getMinValueY())]; |
|
|
|
const domain = [0, Math.abs(this.state.getMaxValueY() - this.state.getMinValueY())]; |
|
|
|
return this.d3.scaleLinear() |
|
|
|
return d3.scaleLinear() |
|
|
|
.domain(domain) |
|
|
|
.domain(domain) |
|
|
|
.range([0, this.height]); |
|
|
|
.range([0, this.height]); |
|
|
|
} |
|
|
|
} |
|
|
@ -928,23 +922,23 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
|
|
|
|
|
|
|
|
getd3TimeRangeEvery(count: number): d3.TimeInterval { |
|
|
|
getd3TimeRangeEvery(count: number): d3.TimeInterval { |
|
|
|
if(this.options.timeInterval === undefined || this.options.timeInterval.timeFormat === undefined) { |
|
|
|
if(this.options.timeInterval === undefined || this.options.timeInterval.timeFormat === undefined) { |
|
|
|
return this.d3.timeMinute.every(count); |
|
|
|
return d3.timeMinute.every(count); |
|
|
|
} |
|
|
|
} |
|
|
|
switch(this.options.timeInterval.timeFormat) { |
|
|
|
switch(this.options.timeInterval.timeFormat) { |
|
|
|
case TimeFormat.SECOND: |
|
|
|
case TimeFormat.SECOND: |
|
|
|
return this.d3.utcSecond.every(count); |
|
|
|
return d3.utcSecond.every(count); |
|
|
|
case TimeFormat.MINUTE: |
|
|
|
case TimeFormat.MINUTE: |
|
|
|
return this.d3.utcMinute.every(count); |
|
|
|
return d3.utcMinute.every(count); |
|
|
|
case TimeFormat.HOUR: |
|
|
|
case TimeFormat.HOUR: |
|
|
|
return this.d3.utcHour.every(count); |
|
|
|
return d3.utcHour.every(count); |
|
|
|
case TimeFormat.DAY: |
|
|
|
case TimeFormat.DAY: |
|
|
|
return this.d3.utcDay.every(count); |
|
|
|
return d3.utcDay.every(count); |
|
|
|
case TimeFormat.MONTH: |
|
|
|
case TimeFormat.MONTH: |
|
|
|
return this.d3.utcMonth.every(count); |
|
|
|
return d3.utcMonth.every(count); |
|
|
|
case TimeFormat.YEAR: |
|
|
|
case TimeFormat.YEAR: |
|
|
|
return this.d3.utcYear.every(count); |
|
|
|
return d3.utcYear.every(count); |
|
|
|
default: |
|
|
|
default: |
|
|
|
return this.d3.utcMinute.every(count); |
|
|
|
return d3.utcMinute.every(count); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -965,7 +959,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
switch(axisOptions.format) { |
|
|
|
switch(axisOptions.format) { |
|
|
|
case AxisFormat.TIME: |
|
|
|
case AxisFormat.TIME: |
|
|
|
// TODO: customize time format?
|
|
|
|
// TODO: customize time format?
|
|
|
|
return this.d3.timeFormat('%m/%d %H:%M'); |
|
|
|
return d3.timeFormat('%m/%d %H:%M'); |
|
|
|
case AxisFormat.NUMERIC: |
|
|
|
case AxisFormat.NUMERIC: |
|
|
|
return (d) => d; |
|
|
|
return (d) => d; |
|
|
|
case AxisFormat.STRING: |
|
|
|
case AxisFormat.STRING: |
|
|
@ -1114,7 +1108,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
isOutOfChart(): boolean { |
|
|
|
isOutOfChart(): boolean { |
|
|
|
const event = this.d3.mouse(this.chartContainer.node()); |
|
|
|
const event = d3.mouse(this.chartContainer.node()); |
|
|
|
const eventX = event[0]; |
|
|
|
const eventX = event[0]; |
|
|
|
const eventY = event[1]; |
|
|
|
const eventY = event[1]; |
|
|
|
if( |
|
|
|
if( |
|
|
|