Compare commits

..

2 Commits

Author SHA1 Message Date
vargburz 46910fa3da grid styles 3 years ago
vargburz 81b66f971b chart background 3 years ago
  1. 2
      .gitignore
  2. 68
      README.md
  3. 1
      build/webpack.base.conf.js
  4. 40
      build/webpack.demo.conf.js
  5. 16
      demo.html
  6. 5
      demo/README.md
  7. 38
      demo/demo.html
  8. 56
      demo/demo_pod.ts
  9. 47
      dist/VueChartwerkPodMixin.d.ts
  10. 1
      dist/colors.d.ts
  11. 14
      dist/components/grid.d.ts
  12. 111
      dist/index.d.ts
  13. 1
      dist/index.js
  14. 55
      dist/state.d.ts
  15. 189
      dist/types.d.ts
  16. 1
      dist/utils.d.ts
  17. 4323
      package-lock.json
  18. 27
      package.json
  19. 50
      src/VueChartwerkPodMixin.ts
  20. 57
      src/components/grid.ts
  21. 14
      src/css/style.css
  22. 708
      src/index.ts
  23. 252
      src/models/options.ts
  24. 182
      src/models/series.ts
  25. 149
      src/state.ts
  26. 205
      src/types.ts
  27. 10
      tsconfig.json
  28. 1266
      yarn.lock

2
.gitignore vendored

@ -1,4 +1,2 @@
node_modules node_modules
.vscode .vscode
dist

68
README.md

@ -3,59 +3,26 @@
# Chartwerk Core # Chartwerk Core
Repo contains the core code of chartwerk project: abstract classes, rendering system, basic components. Repo contains the core code of chartwerk project: abstrac classes and rendreing of grid together with crosshair! :)
See ChartwerkPod to see what is parent for all chartwerk pods and get involved into development. See ChartwerkPod to see what is parent for all chartwerk pods and get involved into development.
Everything can be overwritted.
## Plugin contains: ## Plugin renders:
- SVG container. - SVG container with:
- Series and Options models with defaults. - Axes, with ticks and labels.
- State model to control charts changes. - Grid, which scales using specified time interval.
- Overlay container to handle all events. - Legend, which can hide metrics.
- Zoom events controller.
- Axes, with ticks and labels.
- Grid, with separate behavior from axis.
- Legend, which can hide metrics.
- Crosshair.
## Declare
```js
const pod = new ChartwerkPod(
document.getElementById('chart-div'),
series,
options,
);
pod.render();
```
## Series
Series is a list of metrics with datapoints and specific config working for each serie.
series = Serie[]
- `datapoints` - metric data for rendering.
```js
datapoints = [number, number][]; // 0 index for X, 1 index for Y
```
- `target` - id of metric. Required param, should be unique.
```js
target: string;
```
## Options: ## Options:
Options is a config working for whole chart and metrics. Options are not mandatory:
All options are optional.
- `margin` — chart container positioning; - `margin` — chart container positioning;
```js ```js
margin = { margin={
top: number, top: number,
right: number, right: number,
bottom: number, bottom: number,
left: number, left: number
} }
``` ```
@ -64,6 +31,23 @@ margin = {
['red', 'blue', 'green'] ['red', 'blue', 'green']
``` ```
- `timeInterval`: interval in minutes (max value = 60) affecting grid and x-axis ticks.
- `tickFormat`: config to control the axes ticks format.
```js
{
xAxis: string; // x-axis time format (see [d3-time-format](https://github.com/d3/d3-time-format#locale_format) }
xTickOrientation: TickOrientation; // horizontal, diagonal or vertical orientation
}
```
for example:
```js
{
xAxis: '%Y-%m-%d %H:%M',
xTickOrientation: TickOrientation.DIAGONAL
}
```
- `labelFormat`: labels for axes. - `labelFormat`: labels for axes.
``` ```
{ {

1
build/webpack.base.conf.js

@ -8,7 +8,6 @@ module.exports = {
context: resolve('src'), context: resolve('src'),
entry: './index.ts', entry: './index.ts',
plugins: [], plugins: [],
externals: ['lodash', 'd3'],
module: { module: {
rules: [ rules: [
{ {

40
build/webpack.demo.conf.js

@ -1,40 +0,0 @@
const path = require('path');
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: resolve('demo'),
entry: './demo_pod.ts',
plugins: [],
devtool: 'inline-source-map',
watch: true,
mode: 'development',
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
{ loader: 'style-loader', options: { injectType: 'lazyStyleTag' } },
'css-loader',
],
exclude: /node_modules/
}
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'demo.js',
path: resolve('demo/dist'),
libraryTarget: 'umd',
umdNamedDefine: true
}
};

16
demo.html

@ -0,0 +1,16 @@
<!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.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 500px; height: 500px;"></div>
<script type="text/javascript">
new ChartwerkPod(document.getElementById('chart'))
</script>
</body>
</html>

5
demo/README.md

@ -1,5 +0,0 @@
### HOW TO RUN
run `yarn run dev` and `yarn run demo` in separate terminals simultaneously.
open `demo.html` in your browser.

38
demo/demo.html

@ -1,38 +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/demo.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const arrayLength = 20;
const data1 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 40)]);
let options = {
renderLegend: false,
axis: {
y: { range: [0, 350] },
x: { format: 'time' }
},
zoomEvents: {
mouse: { zoom: { isActive: false }, pan: { isActive: false } },
scroll: { zoom: { isActive: false } }
},
}
var pod = new DemoPod(
document.getElementById('chart'),
[
{ target: 'test1', datapoints: data1, color: 'green' },
],
options
);
pod.render();
</script>
</body>
</html>

56
demo/demo_pod.ts

@ -1,56 +0,0 @@
import {
ChartwerkPod,
Serie,
Options
} from '../dist/index';
import * as d3 from 'd3';
import * as _ from 'lodash';
class DemoPod extends ChartwerkPod<Serie, Options> {
lineGenerator = null;
constructor(
_el: HTMLElement,
_series: Serie[] = [],
_options: Options = {},
) {
super(_el, _series, _options);
}
override renderMetrics(): void {
this.clearAllMetrics();
this.initLineGenerator();
for(const serie of this.series.visibleSeries) {
this.renderLine(serie);
}
}
clearAllMetrics(): void {
// TODO: temporary hack before it will be implemented in core.
this.chartContainer.selectAll('.metric-el').remove();
}
initLineGenerator(): void {
this.lineGenerator = d3.line()
.x(d => this.state.xScale(d[0]))
.y(d => this.state.yScale(d[1]));
}
renderLine(serie: Serie): void {
this.metricContainer
.append('path')
.datum(serie.datapoints)
.attr('class', `metric-path-${serie.idx} metric-el ${serie.class}`)
.attr('fill-opacity', 0)
.attr('stroke', serie.color)
.attr('stroke-width', 1)
.attr('stroke-opacity', 0.7)
.attr('pointer-events', 'none')
.attr('d', this.lineGenerator);
}
}
export { DemoPod };

47
dist/VueChartwerkPodMixin.d.ts vendored

@ -0,0 +1,47 @@
declare const _default: {
props: {
id: {
type: StringConstructor;
required: boolean;
};
series: {
type: ArrayConstructor;
required: boolean;
default: () => any[];
};
options: {
type: ObjectConstructor;
required: boolean;
default: () => {};
};
};
watch: {
id(): void;
series(): void;
options(): void;
};
mounted(): void;
destroyed(): void;
methods: {
render(): void;
renderSharedCrosshair(values: {
x?: number;
y?: number;
}): void;
hideSharedCrosshair(): void;
onPanningRescale(event: any): void;
renderChart(): void;
appendEvents(): void;
zoomIn(range: any): void;
zoomOut(centers: any): void;
mouseMove(evt: any): void;
mouseOut(): void;
onLegendClick(idx: any): void;
panningEnd(range: any): void;
panning(range: any): void;
contextMenu(evt: any): void;
sharedCrosshairMove(event: any): void;
renderEnd(): void;
};
};
export default _default;

1
dist/colors.d.ts vendored

@ -0,0 +1 @@
export declare const palette: string[];

14
dist/components/grid.d.ts vendored

@ -0,0 +1,14 @@
import { GridOptions, SvgElParams } from '../types';
import * as d3 from 'd3';
export declare class Grid {
private _d3;
private _svgEl;
private _svgElParams;
protected gridOptions: GridOptions;
constructor(_d3: typeof d3, _svgEl: d3.Selection<SVGElement, unknown, null, undefined>, _svgElParams: SvgElParams, _gridOptions: GridOptions);
protected setOptionDefaults(gridOptions: GridOptions): GridOptions;
render(): void;
renderGridLinesX(): void;
renderGridLinesY(): void;
updateStylesOfTicks(): void;
}

111
dist/index.d.ts vendored

@ -0,0 +1,111 @@
/// <reference types="lodash" />
import VueChartwerkPodMixin from './VueChartwerkPodMixin';
import { PodState } from './state';
import { Grid } from './components/grid';
import { Margin, TimeSerie, Options, TickOrientation, TimeFormat, BrushOrientation, AxisFormat, CrosshairOrientation, SvgElementAttributes, KeyEvent, PanOrientation, yAxisOrientation, ScrollPanOrientation, AxisOption } from './types';
import { palette } from './colors';
import * as d3 from 'd3';
declare abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
protected readonly el: HTMLElement;
protected d3Node?: d3.Selection<HTMLElement, unknown, null, undefined>;
protected customOverlay?: d3.Selection<SVGRectElement, unknown, null, undefined>;
protected crosshair?: d3.Selection<SVGGElement, unknown, null, undefined>;
protected brush?: d3.BrushBehavior<unknown>;
protected zoom?: any;
protected svg?: d3.Selection<SVGElement, unknown, null, undefined>;
protected state: PodState<T, O>;
protected pan?: d3.ZoomBehavior<Element, unknown>;
protected clipPath?: any;
protected isPanning: boolean;
protected isBrushing: boolean;
protected brushStartSelection: [number, number] | null;
protected initScaleX?: d3.ScaleLinear<any, any>;
protected initScaleY?: d3.ScaleLinear<any, any>;
protected initScaleY1?: d3.ScaleLinear<any, any>;
protected xAxisElement?: d3.Selection<SVGGElement, unknown, null, undefined>;
protected yAxisElement?: d3.Selection<SVGGElement, unknown, null, undefined>;
protected y1AxisElement?: d3.Selection<SVGGElement, unknown, null, undefined>;
protected yAxisTicksColors?: string[];
private _clipPathUID;
protected series: T[];
protected options: O;
protected readonly d3: typeof d3;
protected deltaYTransform: number;
protected debouncedRender: import("lodash").DebouncedFunc<any>;
protected chartContainer: d3.Selection<SVGGElement, unknown, null, undefined>;
protected metricContainer: d3.Selection<SVGGElement, unknown, null, undefined>;
protected grid: Grid;
constructor(_d3: typeof d3, el: HTMLElement, _series: T[], _options: O);
protected addEventListeners(): void;
protected removeEventListeners(): void;
render(): void;
updateData(series?: T[], options?: O, shouldRerender?: boolean): void;
forceRerender(): void;
protected updateOptions(newOptions: O): void;
protected updateSeries(newSeries: T[]): void;
protected abstract renderMetrics(): void;
protected abstract onMouseOver(): void;
protected abstract onMouseOut(): void;
protected abstract onMouseMove(): void;
abstract renderSharedCrosshair(values: {
x?: number;
y?: number;
}): void;
abstract hideSharedCrosshair(): void;
protected initPodState(): void;
protected initComponents(): void;
protected renderMetricsContainer(): void;
protected createSvg(): void;
protected renderGrid(): void;
protected renderAxes(): void;
protected renderXAxis(): void;
protected renderYAxis(): void;
protected renderY1Axis(): void;
protected renderCrosshair(): void;
updateBackground(): void;
protected addEvents(): void;
protected initBrush(): void;
protected filterByKeyEvent(key: KeyEvent): () => boolean;
protected isD3EventKeyEqualOption(event: d3.D3ZoomEvent<any, any>, optionsKeyEvent: KeyEvent): boolean;
protected initPan(): void;
protected renderClipPath(): void;
protected renderLegend(): void;
protected renderYLabel(): void;
protected renderXLabel(): void;
protected renderNoDataPointsMessage(): void;
protected onPanning(): void;
rescaleMetricAndAxis(event: d3.D3ZoomEvent<any, any>): void;
protected onPanningRescale(event: d3.D3ZoomEvent<any, any>): void;
rescaleAxisX(transformX: number): void;
rescaleAxisY(transformY: number): void;
protected onScrollPanningRescale(event: d3.D3ZoomEvent<any, any>): void;
protected onPanningEnd(): void;
protected onBrush(): void;
protected getSelectionAttrs(selection: number[][]): SvgElementAttributes | undefined;
protected onBrushStart(): void;
protected onBrushEnd(): void;
protected zoomOut(): void;
get absXScale(): d3.ScaleLinear<number, number>;
get absYScale(): d3.ScaleLinear<number, number>;
get xScale(): d3.ScaleLinear<number, number>;
get yScale(): d3.ScaleLinear<number, number>;
protected get y1Scale(): d3.ScaleLinear<number, number>;
getd3TimeRangeEvery(count: number): d3.TimeInterval;
get serieTimestampRange(): number | undefined;
getAxisTicksFormatter(axisOptions: AxisOption): (d: any, i: number) => any;
get timeInterval(): number;
get xTickTransform(): string;
get extraMargin(): Margin;
get width(): number;
get height(): number;
get legendRowPositionY(): number;
get margin(): Margin;
formattedBound(alias: string, target: string): string;
protected clearState(): void;
protected getSerieColor(idx: number): string;
protected get seriesTargetsWithBounds(): any[];
protected get visibleSeries(): any[];
protected get rectClipId(): string;
isOutOfChart(): boolean;
}
export { ChartwerkPod, VueChartwerkPodMixin, Margin, TimeSerie, Options, TickOrientation, TimeFormat, BrushOrientation, PanOrientation, AxisFormat, yAxisOrientation, CrosshairOrientation, ScrollPanOrientation, KeyEvent, palette };

1
dist/index.js vendored

File diff suppressed because one or more lines are too long

55
dist/state.d.ts vendored

@ -0,0 +1,55 @@
import { TimeSerie, Options, yAxisOrientation } from './types';
import * as d3 from 'd3';
export declare class PodState<T extends TimeSerie, O extends Options> {
protected _d3: typeof d3;
protected boxParams: {
height: number;
width: number;
};
protected series: T[];
protected options: O;
private _xValueRange;
private _yValueRange;
private _y1ValueRange;
private _transform;
private _xScale;
private _yScale;
private _y1Scale;
constructor(_d3: typeof d3, boxParams: {
height: number;
width: number;
}, series: T[], options: O);
protected setInitialRanges(): void;
protected initScales(): void;
protected setYScale(): void;
protected setXScale(): void;
protected setY1Scale(): void;
clearState(): void;
get yScale(): d3.ScaleLinear<number, number>;
get xScale(): d3.ScaleLinear<number, number>;
get y1Scale(): d3.ScaleLinear<number, number>;
get xValueRange(): [number, number] | undefined;
get yValueRange(): [number, number] | undefined;
get y1ValueRange(): [number, number] | undefined;
get transform(): {
x?: number;
y?: number;
k?: number | string;
};
set xValueRange(range: [number, number]);
set yValueRange(range: [number, number]);
set y1ValueRange(range: [number, number]);
set transform(transform: {
x?: number;
y?: number;
k?: number | string;
});
getMinValueY(): number;
getMaxValueY(): number;
getMinValueX(): number;
getMaxValueX(): number;
getMinValueY1(): number;
getMaxValueY1(): number;
get isSeriesUnavailable(): boolean;
protected filterSerieByYAxisOrientation(serie: T, orientation: yAxisOrientation): boolean;
}

189
dist/types.d.ts vendored

@ -0,0 +1,189 @@
export declare type Margin = {
top: number;
right: number;
bottom: number;
left: number;
};
export declare type Timestamp = number;
export declare type TimeSerie = {
target: string;
datapoints: [Timestamp, number][];
alias?: string;
visible?: boolean;
color?: string;
yOrientation?: yAxisOrientation;
};
export declare type Options = {
margin?: Margin;
confidence?: number;
eventsCallbacks?: {
zoomIn?: (range: AxisRange[]) => void;
panning?: (event: {
ranges: AxisRange[];
d3Event: any;
}) => void;
panningEnd?: (range: AxisRange[]) => void;
zoomOut?: (centers: {
x: number;
y: number;
}) => void;
mouseMove?: (evt: any) => void;
mouseOut?: () => void;
onLegendClick?: (idx: number) => void;
onLegendLabelClick?: (idx: number) => void;
contextMenu?: (evt: any) => void;
sharedCrosshairMove?: (event: any) => void;
renderEnd?: () => void;
};
axis?: {
x?: AxisOption;
y?: AxisOption;
y1?: AxisOption;
};
grid?: GridOptions;
crosshair?: {
orientation?: CrosshairOrientation;
color?: string;
};
background?: {
color?: string;
};
timeInterval?: {
timeFormat?: TimeFormat;
count?: number;
};
tickFormat?: {
xAxis?: string;
xTickOrientation?: TickOrientation;
};
labelFormat?: {
xAxis?: string;
yAxis?: string;
};
bounds?: {
upper: string;
lower: string;
};
timeRange?: {
from: number;
to: number;
};
zoomEvents?: {
mouse?: {
zoom?: {
isActive: boolean;
keyEvent?: KeyEvent;
orientation?: BrushOrientation;
};
pan?: {
isActive: boolean;
keyEvent?: KeyEvent;
orientation?: PanOrientation;
};
};
scroll?: {
zoom?: {
isActive: boolean;
keyEvent?: KeyEvent;
orientation?: PanOrientation;
};
pan?: {
isActive: boolean;
keyEvent?: KeyEvent;
panStep?: number;
orientation?: ScrollPanOrientation;
};
};
};
renderTicksfromTimestamps?: boolean;
renderLegend?: boolean;
};
export declare type GridOptions = {
color?: string;
opacity?: number;
strokeWidth?: number;
x?: {
enabled?: boolean;
ticksCount?: number;
};
y?: {
enabled?: boolean;
ticksCount?: number;
};
};
export declare type AxisOption = {
isActive?: boolean;
ticksCount?: number;
format?: AxisFormat;
range?: [number, number];
invert?: boolean;
valueFormatter?: (value: number, i: number) => string;
colorFormatter?: (value: number, i: number) => string;
};
export declare type AxisRange = [number, number] | undefined;
export declare type VueOptions = Omit<Options, 'eventsCallbacks'>;
export declare enum TickOrientation {
VERTICAL = "vertical",
HORIZONTAL = "horizontal",
DIAGONAL = "diagonal"
}
export declare enum TimeFormat {
SECOND = "second",
MINUTE = "minute",
HOUR = "hour",
DAY = "day",
MONTH = "month",
YEAR = "year"
}
export declare enum BrushOrientation {
VERTICAL = "vertical",
HORIZONTAL = "horizontal",
RECTANGLE = "rectangle",
SQUARE = "square"
}
export declare enum PanOrientation {
VERTICAL = "vertical",
HORIZONTAL = "horizontal",
BOTH = "both"
}
export declare enum ScrollPanOrientation {
VERTICAL = "vertical",
HORIZONTAL = "horizontal"
}
export declare enum AxisFormat {
TIME = "time",
NUMERIC = "numeric",
STRING = "string",
CUSTOM = "custom"
}
export declare enum CrosshairOrientation {
VERTICAL = "vertical",
HORIZONTAL = "horizontal",
BOTH = "both"
}
export declare type SvgElementAttributes = {
x: number;
y: number;
width: number;
height: number;
};
export declare enum KeyEvent {
MAIN = "main",
SHIFT = "shift"
}
export declare enum xAxisOrientation {
TOP = "top",
BOTTOM = "bottom",
BOTH = "both"
}
export declare enum yAxisOrientation {
LEFT = "left",
RIGHT = "right",
BOTH = "both"
}
export declare type SvgElParams = {
height: number;
width: number;
xScale: d3.ScaleLinear<number, number>;
yScale: d3.ScaleLinear<number, number>;
};

1
dist/utils.d.ts vendored

@ -0,0 +1 @@
export declare function uid(): string;

4323
package-lock.json generated

File diff suppressed because it is too large Load Diff

27
package.json

@ -1,34 +1,29 @@
{ {
"name": "@chartwerk/core", "name": "@chartwerk/core",
"version": "0.6.26", "version": "0.3.4",
"description": "Chartwerk core", "description": "Chartwerk core",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"files": [
"/dist"
],
"scripts": { "scripts": {
"build": "webpack --config build/webpack.prod.conf.js", "build": "webpack --config build/webpack.prod.conf.js",
"dev": "webpack --config build/webpack.dev.conf.js", "dev": "webpack --config build/webpack.dev.conf.js",
"demo": "webpack --config build/webpack.demo.conf.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "http://code.corpglory.net/chartwerk/core.git" "url": "https://gitlab.com/chartwerk/core.git"
}, },
"author": "CorpGlory Inc.", "author": "CorpGlory Inc.",
"license": "ISC", "license": "ISC",
"dependencies": {
"d3": "^5.16.0",
"lodash": "^4.17.21"
},
"devDependencies": { "devDependencies": {
"css-loader": "^6.8.1", "@types/d3": "^5.7.2",
"style-loader": "^3.3.3", "@types/lodash": "^4.14.149",
"ts-loader": "^9.4.3", "css-loader": "^3.4.2",
"typescript": "^5.1.3", "lodash": "^4.17.15",
"webpack": "^5.87.0", "style-loader": "^1.1.3",
"webpack-cli": "^5.1.4" "ts-loader": "^6.2.1",
"typescript": "^3.8.3",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
} }
} }

50
src/VueChartwerkPodMixin.ts

@ -32,16 +32,14 @@ export default {
this.renderChart(); this.renderChart();
}, },
destroyed() { destroyed() {
if(this.pod) { this.pod.removeEventListeners();
this.pod.removeEventListeners();
}
this.pod = undefined; this.pod = undefined;
}, },
methods: { methods: {
// it's "abstract" method. "children" components should override it // it's "abstract" method. "children" components should override it
render() {}, render() { },
renderSharedCrosshair(values: { x?: number, y?: number }) {}, renderSharedCrosshair(values: { x?: number, y?: number }) { },
hideSharedCrosshair() {}, hideSharedCrosshair() { },
onPanningRescale(event) { onPanningRescale(event) {
this.pod.rescaleMetricAndAxis(event); this.pod.rescaleMetricAndAxis(event);
}, },
@ -50,48 +48,38 @@ export default {
this.render(); this.render();
}, },
appendEvents() { appendEvents() {
if(this.options.events === undefined) { if(this.options.eventsCallbacks === undefined) {
if(this.options.eventsCallbacks !== undefined) { this.options.eventsCallbacks = {}
this.options.events = this.options.eventsCallbacks
} else {
this.options.events = {};
}
} }
if(has(this.$listeners, 'zoomIn')) { if(has(this.$listeners, 'zoomIn')) {
this.options.events.zoomIn = this.zoomIn.bind(this); this.options.eventsCallbacks.zoomIn = this.zoomIn.bind(this);
} }
if(has(this.$listeners, 'zoomOut')) { if(has(this.$listeners, 'zoomOut')) {
this.options.events.zoomOut = this.zoomOut.bind(this); this.options.eventsCallbacks.zoomOut = this.zoomOut.bind(this);
}
if(has(this.$listeners, 'mouseOver')) {
this.options.events.mouseOver = this.mouseOver.bind(this);
} }
if(has(this.$listeners, 'mouseMove')) { if(has(this.$listeners, 'mouseMove')) {
this.options.events.mouseMove = this.mouseMove.bind(this); this.options.eventsCallbacks.mouseMove = this.mouseMove.bind(this);
} }
if(has(this.$listeners, 'mouseOut')) { if(has(this.$listeners, 'mouseOut')) {
this.options.events.mouseOut = this.mouseOut.bind(this); this.options.eventsCallbacks.mouseOut = this.mouseOut.bind(this);
} }
if(has(this.$listeners, 'onLegendClick')) { if(has(this.$listeners, 'onLegendClick')) {
this.options.events.onLegendClick = this.onLegendClick.bind(this); this.options.eventsCallbacks.onLegendClick = this.onLegendClick.bind(this);
} }
if(has(this.$listeners, 'panningEnd')) { if(has(this.$listeners, 'panningEnd')) {
this.options.events.panningEnd = this.panningEnd.bind(this); this.options.eventsCallbacks.panningEnd = this.panningEnd.bind(this);
} }
if(has(this.$listeners, 'panning')) { if(has(this.$listeners, 'panning')) {
this.options.events.panning = this.panning.bind(this); this.options.eventsCallbacks.panning = this.panning.bind(this);
} }
if(has(this.$listeners, 'contextMenu')) { if(has(this.$listeners, 'contextMenu')) {
this.options.events.contextMenu = this.contextMenu.bind(this); this.options.eventsCallbacks.contextMenu = this.contextMenu.bind(this);
} }
if(has(this.$listeners, 'sharedCrosshairMove')) { if(has(this.$listeners, 'sharedCrosshairMove')) {
this.options.events.sharedCrosshairMove = this.sharedCrosshairMove.bind(this); this.options.eventsCallbacks.sharedCrosshairMove = this.sharedCrosshairMove.bind(this);
}
if(has(this.$listeners, 'renderStart')) {
this.options.events.renderStart = this.renderStart.bind(this);
} }
if(has(this.$listeners, 'renderEnd')) { if(has(this.$listeners, 'renderEnd')) {
this.options.events.renderEnd = this.renderEnd.bind(this); this.options.eventsCallbacks.renderEnd = this.renderEnd.bind(this);
} }
}, },
zoomIn(range) { zoomIn(range) {
@ -100,9 +88,6 @@ export default {
zoomOut(centers) { zoomOut(centers) {
this.$emit('zoomOut', centers); this.$emit('zoomOut', centers);
}, },
mouseOver() {
this.$emit('mouseOver');
},
mouseMove(evt) { mouseMove(evt) {
this.$emit('mouseMove', evt); this.$emit('mouseMove', evt);
}, },
@ -124,9 +109,6 @@ export default {
sharedCrosshairMove(event) { sharedCrosshairMove(event) {
this.$emit('sharedCrosshairMove', event); this.$emit('sharedCrosshairMove', event);
}, },
renderStart() {
this.$emit('renderStart');
},
renderEnd() { renderEnd() {
this.$emit('renderEnd'); this.$emit('renderEnd');
}, },

57
src/components/grid.ts

@ -1,30 +1,56 @@
import { GridOptions, SvgElParams } from '../types'; import { GridOptions, SvgElParams } from '../types';
// we import only d3 types here
import * as d3 from 'd3'; import * as d3 from 'd3';
import defaultsDeep from 'lodash/defaultsDeep';
const DEFAULT_GRID_TICK_COUNT = 5;
const DEFAULT_GRID_OPTIONS: GridOptions = {
color: 'lightgray',
opacity: 0.7,
strokeWidth: 0.5,
x: {
enabled: true,
ticksCount: DEFAULT_GRID_TICK_COUNT,
},
y: {
enabled: true,
ticksCount: DEFAULT_GRID_TICK_COUNT,
},
}
// Grid Class - is a core component, which can be a separate Pod in the future. (but not in current Pod terminology) // Grid Class - is a core component, which can be a separate Pod in the future. (but not in current Pod terminology)
// All components have construcor with required args: svg element which will be filled with this component and options for it. // All components have construcor with required args: svg element which will be filled with this component and options for it.
// All compoтents have a reqiured method "render", which will be called in core costructor. <- this solution is temporary. // All compoтents have a reqiured method "render", which will be called in core costructor. <- this solution is temporary.
// Each component has its own default options.
// svgElement should be a separate class with its own height, width, xScale, yScale params to avoid SvgElParams as argument. // svgElement should be a separate class with its own height, width, xScale, yScale params to avoid SvgElParams as argument.
// We have a general problem with passing d3 as argument everywhere. Fix it, and remove from arg in constructor here. // We have a general problem with passing d3 as argument everywhere. Fix it, and remove from arg in constructor here.
export class Grid { export class Grid {
protected gridOptions: GridOptions;
constructor( constructor(
private _d3: typeof d3,
private _svgEl: d3.Selection<SVGElement, unknown, null, undefined>, private _svgEl: d3.Selection<SVGElement, unknown, null, undefined>,
private _svgElParams: SvgElParams, private _svgElParams: SvgElParams,
protected gridOptions: GridOptions, _gridOptions: GridOptions,
) {} ) {
this.gridOptions = this.setOptionDefaults(_gridOptions);
public render(): void { }
this.clear();
this.renderGridLinesX(); protected setOptionDefaults(gridOptions: GridOptions): GridOptions {
this.renderGridLinesY(); return defaultsDeep(gridOptions, DEFAULT_GRID_OPTIONS);
} }
clear(): void { public render(): void {
// TODO: temporary. Move out of here // TODO: temporary. Move out of here
this._svgEl.selectAll('.grid').remove(); this._svgEl.selectAll('.grid').remove();
this.renderGridLinesX();
this.renderGridLinesY();
this.updateStylesOfTicks();
} }
renderGridLinesX(): void { renderGridLinesX(): void {
@ -37,7 +63,7 @@ export class Grid {
.attr('class', 'grid x-grid') .attr('class', 'grid x-grid')
.style('pointer-events', 'none') .style('pointer-events', 'none')
.call( .call(
d3.axisBottom(this._svgElParams.xScale) this._d3.axisBottom(this._svgElParams.xScale)
.ticks(this.gridOptions.x.ticksCount) .ticks(this.gridOptions.x.ticksCount)
.tickSize(-this._svgElParams.height) .tickSize(-this._svgElParams.height)
.tickFormat(() => '') .tickFormat(() => '')
@ -53,10 +79,21 @@ export class Grid {
.attr('class', 'grid y-grid') .attr('class', 'grid y-grid')
.style('pointer-events', 'none') .style('pointer-events', 'none')
.call( .call(
d3.axisLeft(this._svgElParams.yScale) this._d3.axisLeft(this._svgElParams.yScale)
.ticks(this.gridOptions.y.ticksCount) .ticks(this.gridOptions.y.ticksCount)
.tickSize(-this._svgElParams.width) .tickSize(-this._svgElParams.width)
.tickFormat(() => '') .tickFormat(() => '')
); );
} }
updateStylesOfTicks(): void {
// TODO: add options for these actions
this._svgEl.selectAll('.grid').selectAll('.tick').select('line')
.attr('stroke', this.gridOptions.color)
.attr('stroke-opacity', this.gridOptions.opacity)
.style('stroke-width', this.gridOptions.strokeWidth + 'px');
this._svgEl.selectAll('.grid').select('.domain')
.style('pointer-events', 'none')
.style('stroke-width', 0);
}
} }

14
src/css/style.css

@ -1,14 +0,0 @@
.grid path {
stroke-width: 0;
}
.grid line {
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}
.grid .tick {
opacity: 0.5;
}
.grid .domain {
pointer-events: none;
}

708
src/index.ts

File diff suppressed because it is too large Load Diff

252
src/models/options.ts

@ -1,252 +0,0 @@
import {
Options,
GridOptions, AxesOptions, AxisFormat,
CrosshairOptions, CrosshairOrientation,
ZoomEvents, MouseZoomEvent, MousePanEvent, DoubleClickEvent, ScrollZoomEvent, ScrollPanEvent,
ScrollPanOrientation, ScrollPanDirection, PanOrientation, KeyEvent, BrushOrientation,
Margin, TimeFormat, AxisRange, RenderComponent,
} from '../types';
import lodashDefaultsDeep from 'lodash/defaultsDeep';
import lodashCloneDeep from 'lodash/cloneDeep';
import has from 'lodash/has';
const DEFAULT_TICK_COUNT = 4;
const DEFAULT_SCROLL_PAN_STEP = 50;
const DEFAULT_GRID_TICK_COUNT = 5;
const DEFAULT_GRID_OPTIONS: GridOptions = {
x: {
enabled: true,
ticksCount: DEFAULT_GRID_TICK_COUNT,
},
y: {
enabled: true,
ticksCount: DEFAULT_GRID_TICK_COUNT,
},
}
const DEFAULT_AXES_OPTIONS: AxesOptions = {
x: {
isActive: true,
ticksCount: DEFAULT_TICK_COUNT,
format: AxisFormat.TIME
},
y: {
isActive: true,
ticksCount: DEFAULT_TICK_COUNT,
format: AxisFormat.NUMERIC
},
y1: {
isActive: false,
ticksCount: DEFAULT_TICK_COUNT,
format: AxisFormat.NUMERIC
}
}
const DEFAULT_CROSSHAIR_OPTIONS = {
orientation: CrosshairOrientation.VERTICAL,
color: 'gray',
}
const DEFAULT_MARGIN: Margin = { top: 30, right: 20, bottom: 20, left: 30 };
export const CORE_DEFAULT_OPTIONS: Options = {
zoomEvents: {
mouse: {
zoom: {
isActive: true,
keyEvent: KeyEvent.MAIN,
orientation: BrushOrientation.HORIZONTAL
},
pan: {
isActive: true,
keyEvent: KeyEvent.SHIFT,
orientation: PanOrientation.HORIZONTAL
},
doubleClick: {
isActive: true,
keyEvent: KeyEvent.MAIN,
},
},
scroll: {
zoom: {
isActive: true,
keyEvent: KeyEvent.MAIN,
orientation: PanOrientation.BOTH,
},
pan: {
isActive: false,
keyEvent: KeyEvent.SHIFT,
panStep: DEFAULT_SCROLL_PAN_STEP,
orientation: ScrollPanOrientation.HORIZONTAL,
direction: ScrollPanDirection.BOTH,
},
},
},
axis: DEFAULT_AXES_OPTIONS,
grid: DEFAULT_GRID_OPTIONS,
crosshair: DEFAULT_CROSSHAIR_OPTIONS,
renderLegend: true,
margin: DEFAULT_MARGIN,
}
export class CoreOptions<O extends Options> {
_options: O;
constructor(options: O, private _podDefaults?: Partial<O>) {
this.setOptions(options);
}
public updateOptions(options: O): void {
this.setOptions(options);
}
protected setOptions(options: O): void {
this._options = lodashDefaultsDeep(lodashCloneDeep(options), this.getDefaults());
if(this._options.eventsCallbacks !== undefined) {
if(this._options.events !== undefined) {
throw new Error('events and eventsCallbacks are mutually exclusive');
}
this._options.events = this._options.eventsCallbacks;
}
// also bakward compatibility for clients who use "eventsCallbacks" instead of "events"
this._options.eventsCallbacks = this._options.events;
}
// this getter can be overrited in Pod
protected getDefaults(): Partial<O> {
return lodashDefaultsDeep(this._podDefaults, CORE_DEFAULT_OPTIONS);
}
get allOptions(): O {
return this._options;
}
get grid(): GridOptions {
return this._options.grid;
}
get axis(): AxesOptions {
return this._options.axis;
}
get crosshair(): CrosshairOptions {
return this._options.crosshair;
}
get margin(): Margin {
return this._options.margin;
}
// events
get allEvents(): ZoomEvents {
return this._options.zoomEvents;
}
get mouseZoomEvent(): MouseZoomEvent {
return this._options.zoomEvents.mouse.zoom;
}
get mousePanEvent(): MousePanEvent {
return this._options.zoomEvents.mouse.pan;
}
get doubleClickEvent(): DoubleClickEvent {
return this._options.zoomEvents.mouse.doubleClick;
}
get scrollZoomEvent(): ScrollZoomEvent {
return this._options.zoomEvents.scroll.zoom;
}
get scrollPanEvent(): ScrollPanEvent {
return this._options.zoomEvents.scroll.pan;
}
// event callbacks
callbackRenderStart(): void {
if(has(this._options.events, 'renderStart')) {
this._options.events.renderStart();
}
}
callbackRenderEnd(): void {
if(has(this._options.events, 'renderEnd')) {
this._options.events.renderEnd();
}
}
callbackComponentRenderEnd(part: RenderComponent): void {
if(has(this._options.events, 'componentRenderEnd')) {
this._options.events.componentRenderEnd(part);
}
}
callbackLegendClick(idx: number): void {
if(has(this._options.events, 'onLegendClick')) {
this._options.events.onLegendClick(idx);
}
}
callbackLegendLabelClick(idx: number): void {
if(has(this._options.events, 'onLegendLabelClick')) {
this._options.events.onLegendLabelClick(idx);
}
}
callbackPanning(event: { ranges: AxisRange[], d3Event: any }): void {
if(has(this._options.events, 'panning')) {
this._options.events.panning(event);
}
}
callbackPanningEnd(ranges: AxisRange[]): void {
if(has(this._options.events, 'panningEnd')) {
this._options.events.panningEnd(ranges);
}
}
callbackZoomIn(ranges: AxisRange[]): void {
if(has(this._options.events, 'zoomIn')) {
this._options.events.zoomIn(ranges);
}
}
callbackZoomOut(centers: { x: number, y: number }): void {
if(has(this._options.events, 'zoomOut')) {
this._options.events.zoomOut(centers);
}
}
callbackSharedCrosshairMove(event: { datapoints, eventX, eventY }): void {
if(has(this._options.events, 'sharedCrosshairMove')) {
this._options.events.sharedCrosshairMove(event);
}
}
callbackMouseOver(): void {
if(has(this._options.events, 'mouseOver')) {
this._options.events.mouseOver();
}
}
callbackMouseMove(event): void {
if(has(this._options.events, 'mouseMove')) {
this._options.events.mouseMove(event);
}
}
callbackMouseOut(): void {
if(has(this._options.events, 'mouseOut')) {
this._options.events.mouseOut();
}
}
callbackMouseClick(event): void {
if(has(this._options.events, 'mouseClick')) {
this._options.events.mouseClick(event);
}
}
}

182
src/models/series.ts

@ -1,182 +0,0 @@
import { Serie, yAxisOrientation } from '../types';
import { palette } from '../colors';
import lodashDefaultsDeep from 'lodash/defaultsDeep';
import lodashMap from 'lodash/map';
import lodashCloneDeep from 'lodash/cloneDeep';
import lodashUniq from 'lodash/uniq';
import lodashMin from 'lodash/min';
import lodashMinBy from 'lodash/minBy';
import lodashMax from 'lodash/max';
import lodashMaxBy from 'lodash/maxBy';
import lodashIsNil from 'lodash/isNil';
import lodashIncludes from 'lodash/includes';
export const CORE_SERIE_DEFAULTS = {
alias: '',
target: '',
visible: true,
yOrientation: yAxisOrientation.LEFT,
datapoints: [],
class: '',
// fields below will be set in "fillDefaults" method
idx: undefined,
color: undefined,
};
export enum Extremum {
MIN = 'min',
MAX = 'max',
}
export class CoreSeries<T extends Serie> {
_series: Array<T> = [];
constructor(series: T[], private _podDefaults?: Partial<T>) {
// TODO: create separate Serie class, and store instances in this._series
this.setSeries(series);
}
public updateSeries(series: T[]): void {
this.setSeries(series);
}
protected setSeries(series: T[]): void {
this._series = lodashMap(series, (serie, serieIdx) => this.fillDefaults(lodashCloneDeep(serie), serieIdx));
this.ensureSeriesValid(this._series);
}
ensureSeriesValid(series: T[]): void {
const targets = lodashMap(series, serie => serie.target);
const uniqTargets = lodashUniq(targets);
if(uniqTargets.length !== series.length) {
throw new Error(`All serie.target should be uniq`);
}
}
protected fillDefaults(serie: T, idx: number): T {
let defaults = lodashCloneDeep(this.getDefaults());
defaults.color = palette[idx % palette.length];
defaults.idx = idx;
lodashDefaultsDeep(serie, defaults);
return serie;
}
// this getter can be overrited in Pod
protected getDefaults(): Partial<T> {
return lodashDefaultsDeep(this._podDefaults, CORE_SERIE_DEFAULTS);
}
private _isSerieEmpty(serie: T): boolean {
if(serie.datapoints.length > 0) {
for(const datapoint of serie.datapoints) {
// TODO: axis-related
if(!lodashIsNil(datapoint[1])) {
return false;
}
}
}
return true;
}
get isSeriesAvailable(): boolean {
if(this.visibleSeries.length > 0) {
const seriesEmptiness = lodashMap(this.visibleSeries, this._isSerieEmpty.bind(this));
return lodashIncludes(seriesEmptiness, false);
}
return false;
}
get visibleSeries(): Array<T> {
return this._series.filter(serie => serie.visible);
}
get allSeries(): Array<T> {
return this._series;
}
get leftYRelatedSeries(): Array<T> {
return this.visibleSeries.filter(serie => serie.yOrientation === yAxisOrientation.LEFT);
}
get rightYRelatedSeries(): Array<T> {
return this.visibleSeries.filter(serie => serie.yOrientation === yAxisOrientation.RIGHT);
}
get areSeriesForY1Exist(): boolean {
return this.rightYRelatedSeries.length > 0;
}
get areSeriesForYExist(): boolean {
return this.leftYRelatedSeries.length > 0;
}
get minValueY(): number | undefined {
return lodashMin(
this.leftYRelatedSeries.map(
serie => {
const mins = lodashMinBy<number[]>(serie.datapoints, dp => dp[1]);
return !lodashIsNil(mins) ? mins[1] : undefined;
}
)
);
}
get maxValueY(): number | undefined {
return lodashMax(
this.leftYRelatedSeries.map(
serie => {
const maxs = lodashMaxBy<number[]>(serie.datapoints, dp => dp[1]);
return !lodashIsNil(maxs) ? maxs[1] : undefined;
}
)
);
}
get minValueX(): number | undefined {
return lodashMin(
this.visibleSeries.map(
serie => {
const mins = lodashMinBy<number[]>(serie.datapoints, dp => dp[0]);
return !lodashIsNil(mins) ? mins[0] : undefined;
}
)
);
}
get maxValueX(): number | undefined {
return lodashMax(
this.visibleSeries.map(
serie => {
const maxs = lodashMaxBy<number[]>(serie.datapoints, dp => dp[0]);
return !lodashIsNil(maxs) ? maxs[0] : undefined;
}
)
);
}
get minValueY1(): number | undefined {
return lodashMin(
this.rightYRelatedSeries.map(
serie => {
const mins = lodashMinBy<number[]>(serie.datapoints, dp => dp[1]);
return !lodashIsNil(mins) ? mins[1] : undefined;
}
)
);
}
get maxValueY1(): number | undefined {
return lodashMax(
this.rightYRelatedSeries.map(
serie => {
const maxs = lodashMaxBy<number[]>(serie.datapoints, dp => dp[1]);
return !lodashIsNil(maxs) ? maxs[1] : undefined;
}
)
);
}
}

149
src/models/state.ts → src/state.ts

@ -1,12 +1,13 @@
import { Serie, Options, yAxisOrientation } from '../types'; import { TimeSerie, Options, yAxisOrientation } from './types';
import { CoreSeries } from './series';
import { CoreOptions } from './options';
// we import only d3 types here
import * as d3 from 'd3'; import * as d3 from 'd3';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import min from 'lodash/min'; import min from 'lodash/min';
import minBy from 'lodash/minBy';
import max from 'lodash/max'; import max from 'lodash/max';
import maxBy from 'lodash/maxBy';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import reverse from 'lodash/reverse'; import reverse from 'lodash/reverse';
@ -20,8 +21,9 @@ const DEFAULT_TRANSFORM = {
// TODO: replace all getters with fields. Because getters will be recalculated on each call. Use scales as example. // TODO: replace all getters with fields. Because getters will be recalculated on each call. Use scales as example.
// TODO: remove duplicates in max/min values. // TODO: remove duplicates in max/min values.
// TODO: PodState can be divided in two classes, but it is hard now.
// TODO: PodState.transform has conflicts with d3.zoom.event.transform. It should be synchronized. // TODO: PodState.transform has conflicts with d3.zoom.event.transform. It should be synchronized.
export class PodState<T extends Serie, O extends Options> { export class PodState<T extends TimeSerie, O extends Options> {
private _xValueRange: [number, number]; private _xValueRange: [number, number];
private _yValueRange: [number, number]; private _yValueRange: [number, number];
private _y1ValueRange: [number, number]; private _y1ValueRange: [number, number];
@ -31,9 +33,10 @@ export class PodState<T extends Serie, O extends Options> {
private _y1Scale: d3.ScaleLinear<number, number>; private _y1Scale: d3.ScaleLinear<number, number>;
constructor( constructor(
protected _d3: typeof d3,
protected boxParams: { height: number, width: number }, protected boxParams: { height: number, width: number },
protected coreSeries: CoreSeries<T>, protected series: T[],
protected coreOptions: CoreOptions<O>, protected options: O,
) { ) {
this.setInitialRanges(); this.setInitialRanges();
this.initScales(); this.initScales();
@ -54,20 +57,17 @@ export class PodState<T extends Serie, O extends Options> {
protected setYScale(): void { protected setYScale(): void {
let domain = this._yValueRange; let domain = this._yValueRange;
domain = sortBy(domain) as [number, number]; domain = sortBy(domain) as [number, number];
if(this.coreOptions.axis.y.invert === true) { if(this.options.axis.y.invert === true) {
domain = reverse(domain); domain = reverse(domain);
} }
this._yScale = d3.scaleLinear() this._yScale = this._d3.scaleLinear()
.domain(domain) .domain(domain)
.range([this.boxParams.height, 0]); // inversed, because d3 y-axis goes from top to bottom; .range([this.boxParams.height, 0]); // inversed, because d3 y-axis goes from top to bottom;
} }
protected setXScale(): void { protected setXScale(): void {
let domain = this._xValueRange; const domain = this._xValueRange;
if(this.coreOptions.axis.x.invert === true) { this._xScale = this._d3.scaleLinear()
domain = reverse(domain);
}
this._xScale = d3.scaleLinear()
.domain(domain) .domain(domain)
.range([0, this.boxParams.width]); .range([0, this.boxParams.width]);
} }
@ -75,10 +75,10 @@ export class PodState<T extends Serie, O extends Options> {
protected setY1Scale(): void { protected setY1Scale(): void {
let domain = this._y1ValueRange; let domain = this._y1ValueRange;
domain = sortBy(domain) as [number, number]; domain = sortBy(domain) as [number, number];
if(this.coreOptions.axis.y1.invert === true) { if(this.options.axis.y1.invert === true) {
domain = reverse(domain); domain = reverse(domain);
} }
this._y1Scale = d3.scaleLinear() this._y1Scale = this._d3.scaleLinear()
.domain(domain) .domain(domain)
.range([this.boxParams.height, 0]); // inversed, because d3 y-axis goes from top to bottom .range([this.boxParams.height, 0]); // inversed, because d3 y-axis goes from top to bottom
} }
@ -89,15 +89,6 @@ export class PodState<T extends Serie, O extends Options> {
this._transform = { x: 0, y: 0, k: 1 }; this._transform = { x: 0, y: 0, k: 1 };
} }
getYScaleByOrientation(orientation?: yAxisOrientation): d3.ScaleLinear<number, number> {
// TODO: we set defaults in Series class, so we don't expect `undefined` here
// we can remove this check when we implement Serie class (see TODO in `series.ts`)
if(orientation === undefined) {
return this._yScale;
}
return orientation === yAxisOrientation.LEFT ? this._yScale : this._y1Scale;
}
get yScale(): d3.ScaleLinear<number, number> { get yScale(): d3.ScaleLinear<number, number> {
return this._yScale; return this._yScale;
} }
@ -148,79 +139,117 @@ export class PodState<T extends Serie, O extends Options> {
} }
public getMinValueY(): number { public getMinValueY(): number {
if(!this.coreSeries.areSeriesForYExist) { if(this.isSeriesUnavailable) {
return DEFAULT_AXIS_RANGE[0]; return DEFAULT_AXIS_RANGE[0];
} }
if(this.coreOptions.axis.y.range !== undefined) { if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) {
return min(this.coreOptions.axis.y.range); return min(this.options.axis.y.range);
} }
return this.coreSeries.minValueY; const minValue = min(
this.series
.filter(serie => serie.visible !== false && this.filterSerieByYAxisOrientation(serie, yAxisOrientation.LEFT))
.map(
serie => minBy<number[]>(serie.datapoints, dp => dp[1])[1]
)
);
return minValue;
} }
public getMaxValueY(): number { public getMaxValueY(): number {
if(!this.coreSeries.areSeriesForYExist) { if(this.isSeriesUnavailable) {
return DEFAULT_AXIS_RANGE[1]; return DEFAULT_AXIS_RANGE[1];
} }
if(this.coreOptions.axis.y.range !== undefined) { if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) {
return max(this.coreOptions.axis.y.range); return max(this.options.axis.y.range);
} }
return this.coreSeries.maxValueY; const maxValue = max(
this.series
.filter(serie => serie.visible !== false && this.filterSerieByYAxisOrientation(serie, yAxisOrientation.LEFT))
.map(
serie => maxBy<number[]>(serie.datapoints, dp => dp[1])[1]
)
);
return maxValue;
} }
public getMinValueX(): number { public getMinValueX(): number {
if(!this.coreSeries.isSeriesAvailable) { if(this.isSeriesUnavailable) {
return DEFAULT_AXIS_RANGE[0]; return DEFAULT_AXIS_RANGE[0];
} }
if(this.coreOptions.axis.x.range !== undefined) { if(this.options.axis.x !== undefined && this.options.axis.x.range !== undefined) {
return min(this.coreOptions.axis.x.range); return min(this.options.axis.x.range);
} }
return this.coreSeries.minValueX; const minValue = min(
this.series
.filter(serie => serie.visible !== false)
.map(
serie => minBy<number[]>(serie.datapoints, dp => dp[0])[0]
)
);
return minValue;
} }
public getMaxValueX(): number { public getMaxValueX(): number {
if(!this.coreSeries.isSeriesAvailable) { if(this.isSeriesUnavailable) {
return DEFAULT_AXIS_RANGE[1]; return DEFAULT_AXIS_RANGE[1];
} }
if(this.options.axis.x !== undefined && this.options.axis.x.range !== undefined) {
if(this.coreOptions.axis.x.range !== undefined) { return max(this.options.axis.x.range)
return max(this.coreOptions.axis.x.range);
} }
return this.coreSeries.maxValueX; const maxValue = max(
this.series
.filter(serie => serie.visible !== false)
.map(
serie => maxBy<number[]>(serie.datapoints, dp => dp[0])[0]
)
);
return maxValue;
} }
public getMinValueY1(): number { public getMinValueY1(): number {
if(!this.coreSeries.areSeriesForY1Exist) { if(this.isSeriesUnavailable || this.options.axis.y1 === undefined || this.options.axis.y1.isActive === false) {
return DEFAULT_AXIS_RANGE[0]; return DEFAULT_AXIS_RANGE[0];
} }
if(this.coreOptions.axis.y1.range !== undefined) { if(this.options.axis.y1.range !== undefined) {
return min(this.coreOptions.axis.y1.range); return min(this.options.axis.y1.range);
} }
return this.coreSeries.minValueY1; const minValue = min(
this.series
.filter(serie => serie.visible !== false && this.filterSerieByYAxisOrientation(serie, yAxisOrientation.RIGHT))
.map(
serie => minBy<number[]>(serie.datapoints, dp => dp[1])[1]
)
);
return minValue;
} }
public getMaxValueY1(): number { public getMaxValueY1(): number {
if(!this.coreSeries.areSeriesForY1Exist) { if(this.isSeriesUnavailable || this.options.axis.y1 === undefined || this.options.axis.y1.isActive === false) {
return DEFAULT_AXIS_RANGE[1]; return DEFAULT_AXIS_RANGE[1];
} }
if(this.coreOptions.axis.y1.range !== undefined) { if(this.options.axis.y1 !== undefined && this.options.axis.y1.range !== undefined) {
return max(this.coreOptions.axis.y1.range); return max(this.options.axis.y1.range);
} }
return this.coreSeries.maxValueY1; const maxValue = max(
this.series
.filter(serie => serie.visible !== false && this.filterSerieByYAxisOrientation(serie, yAxisOrientation.RIGHT))
.map(
serie => maxBy<number[]>(serie.datapoints, dp => dp[1])[1]
)
);
return maxValue;
} }
// getters for correct transform get isSeriesUnavailable(): boolean {
get absXScale(): d3.ScaleLinear<number, number> { return this.series === undefined || this.series.length === 0 ||
const domain = [0, Math.abs(this.getMaxValueX() - this.getMinValueX())]; max(this.series.map(serie => serie.datapoints.length)) === 0;
return d3.scaleLinear()
.domain(domain)
.range([0, this.boxParams.width]);
} }
get absYScale(): d3.ScaleLinear<number, number> { protected filterSerieByYAxisOrientation(serie: T, orientation: yAxisOrientation): boolean {
const domain = [0, Math.abs(this.getMaxValueY() - this.getMinValueY())]; if(serie.yOrientation === undefined || serie.yOrientation === yAxisOrientation.BOTH) {
return d3.scaleLinear() return true;
.domain(domain) }
.range([0, this.boxParams.height]); return serie.yOrientation === orientation;
} }
} }

205
src/types.ts

@ -1,52 +1,99 @@
import * as d3 from 'd3';
export type Margin = { top: number, right: number, bottom: number, left: number }; export type Margin = { top: number, right: number, bottom: number, left: number };
export type Timestamp = number; export type Timestamp = number;
export type Serie = { // TODO: Pods can render not only "time" series
export type TimeSerie = {
target: string, target: string,
datapoints: [Timestamp, number][], datapoints: [Timestamp, number][],
idx?: number,
alias?: string, alias?: string,
visible?: boolean, visible?: boolean,
color?: string, color?: string,
class?: string,
yOrientation?: yAxisOrientation, yOrientation?: yAxisOrientation,
}; };
// TODO: move some options to line-chart // TODO: move some options to line-chart
export type Events = {
zoomIn?: (range: AxisRange[]) => void,
panning?: (event: { ranges: AxisRange[], d3Event: any }) => void,
panningEnd?: (range: AxisRange[]) => void,
zoomOut?: (centers: {x: number, y: number}) => void,
mouseOver?: () => void,
mouseMove?: (evt: any) => void,
mouseClick?: (evt: any) => void,
mouseOut?: () => void,
onLegendClick?: (idx: number) => void,
onLegendLabelClick?: (idx: number) => void,
contextMenu?: (evt: any) => void, // the same name as in d3.events
sharedCrosshairMove?: (event: any) => void,
renderStart?: () => void,
renderEnd?: () => void,
componentRenderEnd?: (part: RenderComponent) => void,
}
export type Options = { export type Options = {
margin?: Margin; margin?: Margin;
// obsolete property, use events instead confidence?: number;
eventsCallbacks?: Events; eventsCallbacks?: {
events?: Events; zoomIn?: (range: AxisRange[]) => void,
axis?: AxesOptions; panning?: (event: { ranges: AxisRange[], d3Event: any }) => void,
panningEnd?: (range: AxisRange[]) => void,
zoomOut?: (centers: {x: number, y: number}) => void,
mouseMove?: (evt: any) => void,
mouseOut?: () => void,
onLegendClick?: (idx: number) => void,
onLegendLabelClick?: (idx: number) => void,
contextMenu?: (evt: any) => void, // the same name as in d3.events
sharedCrosshairMove?: (event: any) => void,
renderEnd?: () => void,
};
axis?: {
x?: AxisOption,
y?: AxisOption,
y1?: AxisOption
};
grid?: GridOptions; grid?: GridOptions;
crosshair?: CrosshairOptions; crosshair?: {
zoomEvents?: ZoomEvents; orientation?: CrosshairOrientation;
color?: string;
}
background?: {
color?: string;
};
timeInterval?: {
timeFormat?: TimeFormat;
count?: number;
};
tickFormat?: {
xAxis?: string;
xTickOrientation?: TickOrientation;
};
labelFormat?: {
xAxis?: string;
yAxis?: string;
};
bounds?: {
upper: string;
lower: string;
};
timeRange?: {
from: number,
to: number
};
zoomEvents?: {
mouse?: {
zoom?: { // same as brush
isActive: boolean;
keyEvent?: KeyEvent; // main(or base, or smth) / shift / alt / etc
orientation?: BrushOrientation; // to BrushOrientation: vertical, horizaontal, square, rectange
},
pan?: {
isActive: boolean;
keyEvent?: KeyEvent; // main(or base, or smth) / shift / alt / etc
orientation?: PanOrientation;
},
},
scroll?: {
zoom?: {
isActive: boolean;
keyEvent?: KeyEvent;
orientation?: PanOrientation; // TODO: rename
},
pan?: {
isActive: boolean;
keyEvent?: KeyEvent;
panStep?: number;
orientation?: ScrollPanOrientation;
},
},
}
renderTicksfromTimestamps?: boolean; renderTicksfromTimestamps?: boolean;
renderLegend?: boolean; renderLegend?: boolean;
}; };
export type GridOptions = { export type GridOptions = {
color?: string,
opacity?: number,
strokeWidth?: number,
x?: { x?: {
enabled?: boolean; enabled?: boolean;
ticksCount?: number; ticksCount?: number;
@ -56,32 +103,22 @@ export type GridOptions = {
ticksCount?: number; ticksCount?: number;
}, },
} }
export type AxesOptions = {
x?: AxisOption,
y?: AxisOption,
y1?: AxisOption
}
export type AxisOption = { export type AxisOption = {
isActive?: boolean; isActive?: boolean;
ticksCount?: number; ticksCount?: number;
format?: AxisFormat; format?: AxisFormat;
range?: [number, number]; range?: [number, number];
invert?: boolean; invert?: boolean;
label?: string;
valueFormatter?: (value: number, i: number) => string; valueFormatter?: (value: number, i: number) => string;
colorFormatter?: (value: number, i: number) => string; colorFormatter?: (value: number, i: number) => string;
} }
export type CrosshairOptions = {
orientation?: CrosshairOrientation;
color?: string;
}
export type AxisRange = [number, number] | undefined; export type AxisRange = [number, number] | undefined;
export type VueOptions = Omit<Options, 'eventsCallbacks'>; export type VueOptions = Omit<Options, 'eventsCallbacks'>;
export enum TickOrientation {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
DIAGONAL = 'diagonal'
}
export enum TimeFormat { export enum TimeFormat {
SECOND = 'second', SECOND = 'second',
MINUTE = 'minute', MINUTE = 'minute',
@ -90,56 +127,42 @@ export enum TimeFormat {
MONTH = 'month', MONTH = 'month',
YEAR = 'year' YEAR = 'year'
} }
export enum BrushOrientation { export enum BrushOrientation {
VERTICAL = 'vertical', VERTICAL = 'vertical',
HORIZONTAL = 'horizontal', HORIZONTAL = 'horizontal',
RECTANGLE = 'rectangle', RECTANGLE = 'rectangle',
SQUARE = 'square' SQUARE = 'square'
} }
export enum PanOrientation { export enum PanOrientation {
VERTICAL = 'vertical', VERTICAL = 'vertical',
HORIZONTAL = 'horizontal', HORIZONTAL = 'horizontal',
BOTH = 'both', BOTH = 'both',
} }
export enum ScrollPanOrientation { export enum ScrollPanOrientation {
VERTICAL = 'vertical', VERTICAL = 'vertical',
HORIZONTAL = 'horizontal', HORIZONTAL = 'horizontal',
} }
export enum ScrollPanDirection {
FORWARD = 'forward',
BACKWARD = 'backward',
BOTH = 'both',
}
export enum AxisFormat { export enum AxisFormat {
TIME = 'time', TIME = 'time',
NUMERIC = 'numeric', NUMERIC = 'numeric',
STRING = 'string', STRING = 'string',
CUSTOM = 'custom' CUSTOM = 'custom'
} }
export enum CrosshairOrientation { export enum CrosshairOrientation {
VERTICAL = 'vertical', VERTICAL = 'vertical',
HORIZONTAL = 'horizontal', HORIZONTAL = 'horizontal',
BOTH = 'both' BOTH = 'both'
} }
export type SvgElementAttributes = { export type SvgElementAttributes = {
x: number, x: number,
y: number, y: number,
width: number, width: number,
height: number height: number
} }
export enum KeyEvent { export enum KeyEvent {
MAIN = 'main', MAIN = 'main',
SHIFT = 'shift' SHIFT = 'shift'
} }
// allow series values to affect a specific axis // allow series values to affect a specific axis
export enum xAxisOrientation { export enum xAxisOrientation {
TOP = 'top', TOP = 'top',
@ -149,63 +172,11 @@ export enum xAxisOrientation {
export enum yAxisOrientation { export enum yAxisOrientation {
LEFT = 'left', LEFT = 'left',
RIGHT = 'right', RIGHT = 'right',
BOTH = 'both'
} }
export type SvgElParams = { export type SvgElParams = {
height: number; height: number,
width: number; width: number,
xScale: d3.ScaleLinear<number, number>; xScale: d3.ScaleLinear<number, number>,
yScale: d3.ScaleLinear<number, number>; yScale: d3.ScaleLinear<number, number>,
}
export type ZoomEvents = {
mouse?: {
zoom?: MouseZoomEvent;
pan?: MousePanEvent;
doubleClick?: DoubleClickEvent;
},
scroll?: {
zoom?: ScrollZoomEvent;
pan?: ScrollPanEvent;
}
}
export type MouseZoomEvent = { // same as brush
isActive?: boolean;
keyEvent?: KeyEvent; // main(or base, or smth) / shift / alt / etc
orientation?: BrushOrientation; // to BrushOrientation: vertical, horizaontal, square, rectange
}
export type MousePanEvent = { // same as brush
isActive?: boolean;
keyEvent?: KeyEvent; // main(or base, or smth) / shift / alt / etc
orientation?: PanOrientation;
}
export type DoubleClickEvent = {
isActive: boolean;
keyEvent?: KeyEvent;
}
export type ScrollZoomEvent = {
isActive?: boolean;
keyEvent?: KeyEvent;
orientation?: PanOrientation; // TODO: rename
}
export type ScrollPanEvent = {
isActive?: boolean;
keyEvent?: KeyEvent;
panStep?: number;
orientation?: ScrollPanOrientation;
direction?: ScrollPanDirection;
}
export enum RenderComponent {
CLIP_PATH = 'clipPath',
OVERLAY = 'overlay',
AXES = 'axes',
GRID = 'grid',
CROSSHAIR = 'crosshair',
METRICS_CONTAINER = 'metricsContainer',
LEGEND = 'legend',
} }

10
tsconfig.json

@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"rootDir": "./src",
"module": "esnext", "module": "esnext",
"declaration": true, "declaration": true,
"declarationDir": "dist", "declarationDir": "dist",
@ -15,11 +16,6 @@
"noImplicitUseStrict": false, "noImplicitUseStrict": false,
"noImplicitAny": false, "noImplicitAny": false,
"noUnusedLocals": false, "noUnusedLocals": false,
"moduleResolution": "node", "baseUrl": "./src"
}, }
"exclude": [
"node_modules",
"dist",
"demo"
]
} }

1266
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save