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. 64
      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. 42
      src/VueChartwerkPodMixin.ts
  20. 57
      src/components/grid.ts
  21. 14
      src/css/style.css
  22. 644
      src/index.ts
  23. 252
      src/models/options.ts
  24. 182
      src/models/series.ts
  25. 149
      src/state.ts
  26. 187
      src/types.ts
  27. 10
      tsconfig.json
  28. 1266
      yarn.lock

2
.gitignore vendored

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

64
README.md

@ -3,51 +3,18 @@
# 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.
Everything can be overwritted.
## Plugin contains:
- SVG container.
- Series and Options models with defaults.
- State model to control charts changes.
- Overlay container to handle all events.
- Zoom events controller.
## Plugin renders:
- SVG container with:
- Axes, with ticks and labels.
- Grid, with separate behavior from axis.
- Grid, which scales using specified time interval.
- 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 is a config working for whole chart and metrics.
All options are optional.
Options are not mandatory:
- `margin` — chart container positioning;
```js
@ -55,7 +22,7 @@ margin = {
top: number,
right: number,
bottom: number,
left: number,
left: number
}
```
@ -64,6 +31,23 @@ margin = {
['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.
```
{

1
build/webpack.base.conf.js

@ -8,7 +8,6 @@ module.exports = {
context: resolve('src'),
entry: './index.ts',
plugins: [],
externals: ['lodash', 'd3'],
module: {
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",
"version": "0.6.26",
"version": "0.3.4",
"description": "Chartwerk core",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"build": "webpack --config build/webpack.prod.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"
},
"repository": {
"type": "git",
"url": "http://code.corpglory.net/chartwerk/core.git"
"url": "https://gitlab.com/chartwerk/core.git"
},
"author": "CorpGlory Inc.",
"license": "ISC",
"dependencies": {
"d3": "^5.16.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"css-loader": "^6.8.1",
"style-loader": "^3.3.3",
"ts-loader": "^9.4.3",
"typescript": "^5.1.3",
"webpack": "^5.87.0",
"webpack-cli": "^5.1.4"
"@types/d3": "^5.7.2",
"@types/lodash": "^4.14.149",
"css-loader": "^3.4.2",
"lodash": "^4.17.15",
"style-loader": "^1.1.3",
"ts-loader": "^6.2.1",
"typescript": "^3.8.3",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
}
}

42
src/VueChartwerkPodMixin.ts

@ -32,9 +32,7 @@ export default {
this.renderChart();
},
destroyed() {
if(this.pod) {
this.pod.removeEventListeners();
}
this.pod = undefined;
},
methods: {
@ -50,48 +48,38 @@ export default {
this.render();
},
appendEvents() {
if(this.options.events === undefined) {
if(this.options.eventsCallbacks !== undefined) {
this.options.events = this.options.eventsCallbacks
} else {
this.options.events = {};
}
if(this.options.eventsCallbacks === undefined) {
this.options.eventsCallbacks = {}
}
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')) {
this.options.events.zoomOut = this.zoomOut.bind(this);
}
if(has(this.$listeners, 'mouseOver')) {
this.options.events.mouseOver = this.mouseOver.bind(this);
this.options.eventsCallbacks.zoomOut = this.zoomOut.bind(this);
}
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')) {
this.options.events.mouseOut = this.mouseOut.bind(this);
this.options.eventsCallbacks.mouseOut = this.mouseOut.bind(this);
}
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')) {
this.options.events.panningEnd = this.panningEnd.bind(this);
this.options.eventsCallbacks.panningEnd = this.panningEnd.bind(this);
}
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')) {
this.options.events.contextMenu = this.contextMenu.bind(this);
this.options.eventsCallbacks.contextMenu = this.contextMenu.bind(this);
}
if(has(this.$listeners, 'sharedCrosshairMove')) {
this.options.events.sharedCrosshairMove = this.sharedCrosshairMove.bind(this);
}
if(has(this.$listeners, 'renderStart')) {
this.options.events.renderStart = this.renderStart.bind(this);
this.options.eventsCallbacks.sharedCrosshairMove = this.sharedCrosshairMove.bind(this);
}
if(has(this.$listeners, 'renderEnd')) {
this.options.events.renderEnd = this.renderEnd.bind(this);
this.options.eventsCallbacks.renderEnd = this.renderEnd.bind(this);
}
},
zoomIn(range) {
@ -100,9 +88,6 @@ export default {
zoomOut(centers) {
this.$emit('zoomOut', centers);
},
mouseOver() {
this.$emit('mouseOver');
},
mouseMove(evt) {
this.$emit('mouseMove', evt);
},
@ -124,9 +109,6 @@ export default {
sharedCrosshairMove(event) {
this.$emit('sharedCrosshairMove', event);
},
renderStart() {
this.$emit('renderStart');
},
renderEnd() {
this.$emit('renderEnd');
},

57
src/components/grid.ts

@ -1,30 +1,56 @@
import { GridOptions, SvgElParams } from '../types';
// we import only d3 types here
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)
// 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.
// 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.
// We have a general problem with passing d3 as argument everywhere. Fix it, and remove from arg in constructor here.
export class Grid {
protected gridOptions: GridOptions;
constructor(
private _d3: typeof d3,
private _svgEl: d3.Selection<SVGElement, unknown, null, undefined>,
private _svgElParams: SvgElParams,
protected gridOptions: GridOptions,
) {}
public render(): void {
this.clear();
_gridOptions: GridOptions,
) {
this.gridOptions = this.setOptionDefaults(_gridOptions);
}
this.renderGridLinesX();
this.renderGridLinesY();
protected setOptionDefaults(gridOptions: GridOptions): GridOptions {
return defaultsDeep(gridOptions, DEFAULT_GRID_OPTIONS);
}
clear(): void {
public render(): void {
// TODO: temporary. Move out of here
this._svgEl.selectAll('.grid').remove();
this.renderGridLinesX();
this.renderGridLinesY();
this.updateStylesOfTicks();
}
renderGridLinesX(): void {
@ -37,7 +63,7 @@ export class Grid {
.attr('class', 'grid x-grid')
.style('pointer-events', 'none')
.call(
d3.axisBottom(this._svgElParams.xScale)
this._d3.axisBottom(this._svgElParams.xScale)
.ticks(this.gridOptions.x.ticksCount)
.tickSize(-this._svgElParams.height)
.tickFormat(() => '')
@ -53,10 +79,21 @@ export class Grid {
.attr('class', 'grid y-grid')
.style('pointer-events', 'none')
.call(
d3.axisLeft(this._svgElParams.yScale)
this._d3.axisLeft(this._svgElParams.yScale)
.ticks(this.gridOptions.y.ticksCount)
.tickSize(-this._svgElParams.width)
.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;
}

644
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 { CoreSeries } from './series';
import { CoreOptions } from './options';
import { TimeSerie, Options, yAxisOrientation } from './types';
// we import only d3 types here
import * as d3 from 'd3';
import cloneDeep from 'lodash/cloneDeep';
import min from 'lodash/min';
import minBy from 'lodash/minBy';
import max from 'lodash/max';
import maxBy from 'lodash/maxBy';
import sortBy from 'lodash/sortBy';
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: 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.
export class PodState<T extends Serie, O extends Options> {
export class PodState<T extends TimeSerie, O extends Options> {
private _xValueRange: [number, number];
private _yValueRange: [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>;
constructor(
protected _d3: typeof d3,
protected boxParams: { height: number, width: number },
protected coreSeries: CoreSeries<T>,
protected coreOptions: CoreOptions<O>,
protected series: T[],
protected options: O,
) {
this.setInitialRanges();
this.initScales();
@ -54,20 +57,17 @@ export class PodState<T extends Serie, O extends Options> {
protected setYScale(): void {
let domain = this._yValueRange;
domain = sortBy(domain) as [number, number];
if(this.coreOptions.axis.y.invert === true) {
if(this.options.axis.y.invert === true) {
domain = reverse(domain);
}
this._yScale = d3.scaleLinear()
this._yScale = this._d3.scaleLinear()
.domain(domain)
.range([this.boxParams.height, 0]); // inversed, because d3 y-axis goes from top to bottom;
}
protected setXScale(): void {
let domain = this._xValueRange;
if(this.coreOptions.axis.x.invert === true) {
domain = reverse(domain);
}
this._xScale = d3.scaleLinear()
const domain = this._xValueRange;
this._xScale = this._d3.scaleLinear()
.domain(domain)
.range([0, this.boxParams.width]);
}
@ -75,10 +75,10 @@ export class PodState<T extends Serie, O extends Options> {
protected setY1Scale(): void {
let domain = this._y1ValueRange;
domain = sortBy(domain) as [number, number];
if(this.coreOptions.axis.y1.invert === true) {
if(this.options.axis.y1.invert === true) {
domain = reverse(domain);
}
this._y1Scale = d3.scaleLinear()
this._y1Scale = this._d3.scaleLinear()
.domain(domain)
.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 };
}
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> {
return this._yScale;
}
@ -148,79 +139,117 @@ export class PodState<T extends Serie, O extends Options> {
}
public getMinValueY(): number {
if(!this.coreSeries.areSeriesForYExist) {
if(this.isSeriesUnavailable) {
return DEFAULT_AXIS_RANGE[0];
}
if(this.coreOptions.axis.y.range !== undefined) {
return min(this.coreOptions.axis.y.range);
if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) {
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 {
if(!this.coreSeries.areSeriesForYExist) {
if(this.isSeriesUnavailable) {
return DEFAULT_AXIS_RANGE[1];
}
if(this.coreOptions.axis.y.range !== undefined) {
return max(this.coreOptions.axis.y.range);
if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) {
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 {
if(!this.coreSeries.isSeriesAvailable) {
if(this.isSeriesUnavailable) {
return DEFAULT_AXIS_RANGE[0];
}
if(this.coreOptions.axis.x.range !== undefined) {
return min(this.coreOptions.axis.x.range);
if(this.options.axis.x !== undefined && this.options.axis.x.range !== undefined) {
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 {
if(!this.coreSeries.isSeriesAvailable) {
if(this.isSeriesUnavailable) {
return DEFAULT_AXIS_RANGE[1];
}
if(this.coreOptions.axis.x.range !== undefined) {
return max(this.coreOptions.axis.x.range);
if(this.options.axis.x !== undefined && this.options.axis.x.range !== undefined) {
return max(this.options.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 {
if(!this.coreSeries.areSeriesForY1Exist) {
if(this.isSeriesUnavailable || this.options.axis.y1 === undefined || this.options.axis.y1.isActive === false) {
return DEFAULT_AXIS_RANGE[0];
}
if(this.coreOptions.axis.y1.range !== undefined) {
return min(this.coreOptions.axis.y1.range);
if(this.options.axis.y1.range !== undefined) {
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 {
if(!this.coreSeries.areSeriesForY1Exist) {
if(this.isSeriesUnavailable || this.options.axis.y1 === undefined || this.options.axis.y1.isActive === false) {
return DEFAULT_AXIS_RANGE[1];
}
if(this.coreOptions.axis.y1.range !== undefined) {
return max(this.coreOptions.axis.y1.range);
if(this.options.axis.y1 !== undefined && this.options.axis.y1.range !== undefined) {
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 absXScale(): d3.ScaleLinear<number, number> {
const domain = [0, Math.abs(this.getMaxValueX() - this.getMinValueX())];
return d3.scaleLinear()
.domain(domain)
.range([0, this.boxParams.width]);
get isSeriesUnavailable(): boolean {
return this.series === undefined || this.series.length === 0 ||
max(this.series.map(serie => serie.datapoints.length)) === 0;
}
get absYScale(): d3.ScaleLinear<number, number> {
const domain = [0, Math.abs(this.getMaxValueY() - this.getMinValueY())];
return d3.scaleLinear()
.domain(domain)
.range([0, this.boxParams.height]);
protected filterSerieByYAxisOrientation(serie: T, orientation: yAxisOrientation): boolean {
if(serie.yOrientation === undefined || serie.yOrientation === yAxisOrientation.BOTH) {
return true;
}
return serie.yOrientation === orientation;
}
}

187
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 Timestamp = number;
export type Serie = {
// TODO: Pods can render not only "time" series
export type TimeSerie = {
target: string,
datapoints: [Timestamp, number][],
idx?: number,
alias?: string,
visible?: boolean,
color?: string,
class?: string,
yOrientation?: yAxisOrientation,
};
// TODO: move some options to line-chart
export type Events = {
export 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,
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 = {
margin?: Margin;
// obsolete property, use events instead
eventsCallbacks?: Events;
events?: Events;
axis?: AxesOptions;
};
axis?: {
x?: AxisOption,
y?: AxisOption,
y1?: AxisOption
};
grid?: GridOptions;
crosshair?: CrosshairOptions;
zoomEvents?: ZoomEvents;
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?: { // 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;
renderLegend?: boolean;
};
export type GridOptions = {
color?: string,
opacity?: number,
strokeWidth?: number,
x?: {
enabled?: boolean;
ticksCount?: number;
@ -56,32 +103,22 @@ export type GridOptions = {
ticksCount?: number;
},
}
export type AxesOptions = {
x?: AxisOption,
y?: AxisOption,
y1?: AxisOption
}
export type AxisOption = {
isActive?: boolean;
ticksCount?: number;
format?: AxisFormat;
range?: [number, number];
invert?: boolean;
label?: string;
valueFormatter?: (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 VueOptions = Omit<Options, 'eventsCallbacks'>;
export enum TickOrientation {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
DIAGONAL = 'diagonal'
}
export enum TimeFormat {
SECOND = 'second',
MINUTE = 'minute',
@ -90,56 +127,42 @@ export enum TimeFormat {
MONTH = 'month',
YEAR = 'year'
}
export enum BrushOrientation {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
RECTANGLE = 'rectangle',
SQUARE = 'square'
}
export enum PanOrientation {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
BOTH = 'both',
}
export enum ScrollPanOrientation {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
}
export enum ScrollPanDirection {
FORWARD = 'forward',
BACKWARD = 'backward',
BOTH = 'both',
}
export enum AxisFormat {
TIME = 'time',
NUMERIC = 'numeric',
STRING = 'string',
CUSTOM = 'custom'
}
export enum CrosshairOrientation {
VERTICAL = 'vertical',
HORIZONTAL = 'horizontal',
BOTH = 'both'
}
export type SvgElementAttributes = {
x: number,
y: number,
width: number,
height: number
}
export enum KeyEvent {
MAIN = 'main',
SHIFT = 'shift'
}
// allow series values to affect a specific axis
export enum xAxisOrientation {
TOP = 'top',
@ -149,63 +172,11 @@ export enum xAxisOrientation {
export enum yAxisOrientation {
LEFT = 'left',
RIGHT = 'right',
BOTH = 'both'
}
export type SvgElParams = {
height: number;
width: number;
xScale: 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',
height: number,
width: number,
xScale: d3.ScaleLinear<number, number>,
yScale: d3.ScaleLinear<number, number>,
}

10
tsconfig.json

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

1266
yarn.lock

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