@deck.gl-community/timeline-layers 9.2.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +20 -0
- package/dist/index.cjs +538 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer-uniforms.d.ts +23 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer-uniforms.d.ts.map +1 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer-uniforms.js +33 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer-uniforms.js.map +1 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.d.ts +38 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.d.ts.map +1 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.fs.d.ts +3 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.fs.d.ts.map +1 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.fs.js +53 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.fs.js.map +1 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.js +138 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.js.map +1 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.vs.d.ts +3 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.vs.d.ts.map +1 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.vs.js +24 -0
- package/dist/layers/horizon-graph-layer/horizon-graph-layer.vs.js.map +1 -0
- package/dist/layers/horizon-graph-layer/multi-horizon-graph-layer.d.ts +23 -0
- package/dist/layers/horizon-graph-layer/multi-horizon-graph-layer.d.ts.map +1 -0
- package/dist/layers/horizon-graph-layer/multi-horizon-graph-layer.js +100 -0
- package/dist/layers/horizon-graph-layer/multi-horizon-graph-layer.js.map +1 -0
- package/dist/layers/time-axis-layer.d.ts +56 -0
- package/dist/layers/time-axis-layer.d.ts.map +1 -0
- package/dist/layers/time-axis-layer.js +78 -0
- package/dist/layers/time-axis-layer.js.map +1 -0
- package/dist/layers/vertical-grid-layer.d.ts +41 -0
- package/dist/layers/vertical-grid-layer.d.ts.map +1 -0
- package/dist/layers/vertical-grid-layer.js +43 -0
- package/dist/layers/vertical-grid-layer.js.map +1 -0
- package/dist/utils/format-utils.d.ts +7 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/format-utils.js +75 -0
- package/dist/utils/format-utils.js.map +1 -0
- package/dist/utils/tick-utils.d.ts +10 -0
- package/dist/utils/tick-utils.d.ts.map +1 -0
- package/dist/utils/tick-utils.js +32 -0
- package/dist/utils/tick-utils.js.map +1 -0
- package/package.json +48 -0
- package/src/index.ts +13 -0
- package/src/layers/horizon-graph-layer/horizon-graph-layer-uniforms.ts +47 -0
- package/src/layers/horizon-graph-layer/horizon-graph-layer.fs.ts +53 -0
- package/src/layers/horizon-graph-layer/horizon-graph-layer.ts +202 -0
- package/src/layers/horizon-graph-layer/horizon-graph-layer.vs.ts +24 -0
- package/src/layers/horizon-graph-layer/multi-horizon-graph-layer.ts +164 -0
- package/src/layers/time-axis-layer.ts +102 -0
- package/src/layers/vertical-grid-layer.ts +69 -0
- package/src/utils/format-utils.ts +80 -0
- package/src/utils/tick-utils.ts +41 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {DefaultProps, LayerProps, Color, LayerContext, UpdateParameters} from '@deck.gl/core';
|
|
6
|
+
import {Layer, project32} from '@deck.gl/core';
|
|
7
|
+
import {Model, Geometry} from '@luma.gl/engine';
|
|
8
|
+
import vs from './horizon-graph-layer.vs';
|
|
9
|
+
import fs from './horizon-graph-layer.fs';
|
|
10
|
+
import {Texture} from '@luma.gl/core';
|
|
11
|
+
import {horizonLayerUniforms} from './horizon-graph-layer-uniforms';
|
|
12
|
+
|
|
13
|
+
export type _HorizonGraphLayerProps = {
|
|
14
|
+
data: number[] | Float32Array;
|
|
15
|
+
|
|
16
|
+
yAxisScale?: number;
|
|
17
|
+
|
|
18
|
+
bands?: number;
|
|
19
|
+
|
|
20
|
+
positiveColor?: Color;
|
|
21
|
+
negativeColor?: Color;
|
|
22
|
+
|
|
23
|
+
x?: number;
|
|
24
|
+
y?: number;
|
|
25
|
+
width?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type HorizonGraphLayerProps = _HorizonGraphLayerProps & LayerProps;
|
|
30
|
+
|
|
31
|
+
const defaultProps: DefaultProps<HorizonGraphLayerProps> = {
|
|
32
|
+
yAxisScale: {type: 'number', value: 1000},
|
|
33
|
+
|
|
34
|
+
bands: {type: 'number', value: 2},
|
|
35
|
+
|
|
36
|
+
positiveColor: {type: 'color', value: [0, 128, 0]},
|
|
37
|
+
negativeColor: {type: 'color', value: [0, 0, 255]},
|
|
38
|
+
|
|
39
|
+
x: {type: 'number', value: 0},
|
|
40
|
+
y: {type: 'number', value: 0},
|
|
41
|
+
width: {type: 'number', value: 800},
|
|
42
|
+
height: {type: 'number', value: 300}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export class HorizonGraphLayer<ExtraProps extends {} = {}> extends Layer<
|
|
46
|
+
ExtraProps & Required<_HorizonGraphLayerProps>
|
|
47
|
+
> {
|
|
48
|
+
static layerName = 'HorizonGraphLayer';
|
|
49
|
+
static defaultProps = defaultProps;
|
|
50
|
+
|
|
51
|
+
state: {
|
|
52
|
+
model?: Model;
|
|
53
|
+
dataTexture?: Texture;
|
|
54
|
+
dataTextureSize?: number;
|
|
55
|
+
dataTextureCount?: number;
|
|
56
|
+
} = {};
|
|
57
|
+
|
|
58
|
+
initializeState() {
|
|
59
|
+
this.state = {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getShaders() {
|
|
63
|
+
return super.getShaders({
|
|
64
|
+
vs,
|
|
65
|
+
fs,
|
|
66
|
+
modules: [project32, horizonLayerUniforms]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
_createDataTexture(seriesData: Float32Array | number[]): {
|
|
71
|
+
dataTexture: Texture;
|
|
72
|
+
dataTextureSize: number;
|
|
73
|
+
dataTextureCount: number;
|
|
74
|
+
} {
|
|
75
|
+
const _data = seriesData instanceof Float32Array ? seriesData : new Float32Array(seriesData);
|
|
76
|
+
|
|
77
|
+
const {device} = this.context;
|
|
78
|
+
const count = _data.length;
|
|
79
|
+
|
|
80
|
+
let dataTextureSize = 32;
|
|
81
|
+
while (count > dataTextureSize * dataTextureSize) {
|
|
82
|
+
dataTextureSize *= 2;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// TODO: use the right way to only submit the minimum amount of data
|
|
86
|
+
const data = new Float32Array(dataTextureSize * dataTextureSize);
|
|
87
|
+
data.set(_data, 0);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
dataTexture: device.createTexture({
|
|
91
|
+
data,
|
|
92
|
+
format: 'r32float',
|
|
93
|
+
dimension: '2d',
|
|
94
|
+
width: dataTextureSize,
|
|
95
|
+
height: dataTextureSize,
|
|
96
|
+
sampler: {
|
|
97
|
+
minFilter: 'nearest',
|
|
98
|
+
magFilter: 'nearest',
|
|
99
|
+
addressModeU: 'clamp-to-edge',
|
|
100
|
+
addressModeV: 'clamp-to-edge'
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
dataTextureSize,
|
|
104
|
+
dataTextureCount: count
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_createModel() {
|
|
109
|
+
const {x, y, width, height} = this.props;
|
|
110
|
+
|
|
111
|
+
// Create a rectangle using triangle strip (4 vertices)
|
|
112
|
+
// Order: bottom-left, bottom-right, top-left, top-right
|
|
113
|
+
const positions = [
|
|
114
|
+
x,
|
|
115
|
+
y,
|
|
116
|
+
0.0,
|
|
117
|
+
|
|
118
|
+
x + width,
|
|
119
|
+
y,
|
|
120
|
+
0.0,
|
|
121
|
+
|
|
122
|
+
x,
|
|
123
|
+
y + height,
|
|
124
|
+
0.0,
|
|
125
|
+
|
|
126
|
+
x + width,
|
|
127
|
+
y + height,
|
|
128
|
+
0.0
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const uv = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0];
|
|
132
|
+
|
|
133
|
+
const geometry = new Geometry({
|
|
134
|
+
topology: 'triangle-strip',
|
|
135
|
+
attributes: {
|
|
136
|
+
positions: {value: new Float32Array(positions), size: 3},
|
|
137
|
+
uv: {value: new Float32Array(uv), size: 2}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return new Model(this.context.device, {
|
|
142
|
+
...this.getShaders(),
|
|
143
|
+
geometry,
|
|
144
|
+
bufferLayout: this.getAttributeManager().getBufferLayouts()
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
updateState(params: UpdateParameters<Layer<ExtraProps & Required<_HorizonGraphLayerProps>>>) {
|
|
149
|
+
super.updateState(params);
|
|
150
|
+
|
|
151
|
+
const {changeFlags} = params;
|
|
152
|
+
|
|
153
|
+
if (changeFlags.dataChanged) {
|
|
154
|
+
this.state.dataTexture?.destroy();
|
|
155
|
+
this.setState(this._createDataTexture(this.props.data));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (changeFlags.extensionsChanged || changeFlags.propsChanged) {
|
|
159
|
+
this.state.model?.destroy();
|
|
160
|
+
this.setState({model: this._createModel()});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
draw() {
|
|
165
|
+
const {model, dataTexture} = this.state;
|
|
166
|
+
|
|
167
|
+
if (!model) {
|
|
168
|
+
this.setState({model: this._createModel()});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!dataTexture) {
|
|
173
|
+
this.setState(this._createDataTexture(this.props.data));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const {bands, yAxisScale, positiveColor, negativeColor} = this.props;
|
|
178
|
+
|
|
179
|
+
model.shaderInputs.setProps({
|
|
180
|
+
horizonLayer: {
|
|
181
|
+
dataTexture: this.state.dataTexture,
|
|
182
|
+
dataTextureSize: this.state.dataTextureSize,
|
|
183
|
+
dataTextureSizeInv: 1.0 / this.state.dataTextureSize,
|
|
184
|
+
dataTextureCount: this.state.dataTextureCount,
|
|
185
|
+
|
|
186
|
+
bands,
|
|
187
|
+
bandsInv: 1.0 / bands,
|
|
188
|
+
yAxisScaleInv: 1.0 / yAxisScale,
|
|
189
|
+
|
|
190
|
+
positiveColor: positiveColor.map((c) => c / 255),
|
|
191
|
+
negativeColor: negativeColor.map((c) => c / 255)
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
model.draw(this.context.renderPass);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
finalizeState(context: LayerContext): void {
|
|
198
|
+
this.state.model?.destroy();
|
|
199
|
+
this.state.dataTexture?.destroy();
|
|
200
|
+
super.finalizeState(context);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
export default `#version 300 es
|
|
6
|
+
#define SHADER_NAME horizon-graph-layer-vertex-shader
|
|
7
|
+
|
|
8
|
+
in vec3 positions;
|
|
9
|
+
in vec2 uv;
|
|
10
|
+
|
|
11
|
+
out vec2 v_uv;
|
|
12
|
+
|
|
13
|
+
void main(void) {
|
|
14
|
+
geometry.worldPosition = positions;
|
|
15
|
+
|
|
16
|
+
vec4 position_commonspace = project_position(vec4(positions.xy, 0.0, 1.0));
|
|
17
|
+
gl_Position = project_common_position_to_clipspace(position_commonspace);
|
|
18
|
+
geometry.position = position_commonspace;
|
|
19
|
+
|
|
20
|
+
DECKGL_FILTER_GL_POSITION(gl_Position, geometry);
|
|
21
|
+
|
|
22
|
+
v_uv = uv;
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {DefaultProps, LayerProps, Color, LayerDataSource, Accessor} from '@deck.gl/core';
|
|
6
|
+
import {CompositeLayer} from '@deck.gl/core';
|
|
7
|
+
import {SolidPolygonLayer} from '@deck.gl/layers';
|
|
8
|
+
import {HorizonGraphLayer} from './horizon-graph-layer';
|
|
9
|
+
|
|
10
|
+
export type _MultiHorizonGraphLayerProps<DataT> = {
|
|
11
|
+
data: LayerDataSource<DataT>;
|
|
12
|
+
getSeries: Accessor<DataT, number[] | Float32Array>;
|
|
13
|
+
getScale: Accessor<DataT, number>;
|
|
14
|
+
|
|
15
|
+
bands?: number;
|
|
16
|
+
|
|
17
|
+
positiveColor?: Color;
|
|
18
|
+
negativeColor?: Color;
|
|
19
|
+
|
|
20
|
+
dividerColor?: Color;
|
|
21
|
+
dividerWidth?: number;
|
|
22
|
+
|
|
23
|
+
x?: number;
|
|
24
|
+
y?: number;
|
|
25
|
+
width?: number;
|
|
26
|
+
height?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type MultiHorizonGraphLayerProps<DataT = unknown> = _MultiHorizonGraphLayerProps<DataT> &
|
|
30
|
+
LayerProps;
|
|
31
|
+
|
|
32
|
+
const defaultProps: DefaultProps<MultiHorizonGraphLayerProps> = {
|
|
33
|
+
getSeries: {type: 'accessor', value: (series: any) => series.values},
|
|
34
|
+
getScale: {type: 'accessor', value: (series: any) => series.scale},
|
|
35
|
+
|
|
36
|
+
bands: {type: 'number', value: 2},
|
|
37
|
+
|
|
38
|
+
positiveColor: {type: 'color', value: [0, 128, 0]},
|
|
39
|
+
negativeColor: {type: 'color', value: [0, 0, 255]},
|
|
40
|
+
|
|
41
|
+
dividerColor: {type: 'color', value: [0, 0, 0]},
|
|
42
|
+
dividerWidth: {type: 'number', value: 2},
|
|
43
|
+
|
|
44
|
+
x: {type: 'number', value: 0},
|
|
45
|
+
y: {type: 'number', value: 0},
|
|
46
|
+
width: {type: 'number', value: 800},
|
|
47
|
+
height: {type: 'number', value: 300}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class MultiHorizonGraphLayer<DataT = any, ExtraProps extends {} = {}> extends CompositeLayer<
|
|
51
|
+
ExtraProps & Required<_MultiHorizonGraphLayerProps<DataT>>
|
|
52
|
+
> {
|
|
53
|
+
static layerName = 'MultiHorizonGraphLayer';
|
|
54
|
+
static defaultProps = defaultProps;
|
|
55
|
+
|
|
56
|
+
renderLayers() {
|
|
57
|
+
const {
|
|
58
|
+
data,
|
|
59
|
+
getSeries,
|
|
60
|
+
getScale,
|
|
61
|
+
bands,
|
|
62
|
+
positiveColor,
|
|
63
|
+
negativeColor,
|
|
64
|
+
dividerColor,
|
|
65
|
+
dividerWidth,
|
|
66
|
+
x,
|
|
67
|
+
y,
|
|
68
|
+
width,
|
|
69
|
+
height
|
|
70
|
+
} = this.props;
|
|
71
|
+
|
|
72
|
+
const seriesCount = (data as any).length;
|
|
73
|
+
|
|
74
|
+
if (!seriesCount) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Calculate layout dimensions
|
|
79
|
+
const totalDividerSpace = dividerWidth * (seriesCount + 1);
|
|
80
|
+
const availableHeight = height - totalDividerSpace;
|
|
81
|
+
const seriesHeight = availableHeight / seriesCount;
|
|
82
|
+
|
|
83
|
+
const layers = [];
|
|
84
|
+
|
|
85
|
+
// Create divider rectangles
|
|
86
|
+
if (dividerWidth > 0) {
|
|
87
|
+
const dividerData = [];
|
|
88
|
+
|
|
89
|
+
// Top divider
|
|
90
|
+
dividerData.push({
|
|
91
|
+
polygon: [
|
|
92
|
+
[x, y],
|
|
93
|
+
[x + width, y],
|
|
94
|
+
[x + width, y + dividerWidth],
|
|
95
|
+
[x, y + dividerWidth]
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Dividers between series
|
|
100
|
+
for (let i = 0; i < seriesCount - 1; i++) {
|
|
101
|
+
const dividerY = y + dividerWidth + (i + 1) * seriesHeight + i * dividerWidth;
|
|
102
|
+
dividerData.push({
|
|
103
|
+
polygon: [
|
|
104
|
+
[x, dividerY],
|
|
105
|
+
[x + width, dividerY],
|
|
106
|
+
[x + width, dividerY + dividerWidth],
|
|
107
|
+
[x, dividerY + dividerWidth]
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Bottom divider
|
|
113
|
+
const bottomDividerY = y + height - dividerWidth;
|
|
114
|
+
dividerData.push({
|
|
115
|
+
polygon: [
|
|
116
|
+
[x, bottomDividerY],
|
|
117
|
+
[x + width, bottomDividerY],
|
|
118
|
+
[x + width, y + height],
|
|
119
|
+
[x, y + height]
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
layers.push(
|
|
124
|
+
new SolidPolygonLayer({
|
|
125
|
+
id: `${this.props.id}-dividers`,
|
|
126
|
+
data: dividerData,
|
|
127
|
+
getPolygon: (d: any) => d.polygon,
|
|
128
|
+
getFillColor: dividerColor,
|
|
129
|
+
pickable: false
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create horizon graph layers for each series
|
|
135
|
+
(data as any).forEach((series, index) => {
|
|
136
|
+
const seriesData = (getSeries as any)(series);
|
|
137
|
+
|
|
138
|
+
if (!seriesData || seriesData.length === 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const seriesY = y + dividerWidth + index * (seriesHeight + dividerWidth);
|
|
143
|
+
|
|
144
|
+
const yAxisScale = (getScale as any)(series);
|
|
145
|
+
|
|
146
|
+
layers.push(
|
|
147
|
+
new HorizonGraphLayer({
|
|
148
|
+
id: `${this.props.id}-series-${index}`,
|
|
149
|
+
data: seriesData,
|
|
150
|
+
yAxisScale,
|
|
151
|
+
bands,
|
|
152
|
+
positiveColor,
|
|
153
|
+
negativeColor,
|
|
154
|
+
x,
|
|
155
|
+
y: seriesY,
|
|
156
|
+
width,
|
|
157
|
+
height: seriesHeight
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return layers;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {CompositeLayer, type CompositeLayerProps, type UpdateParameters} from '@deck.gl/core';
|
|
6
|
+
import {LineLayer, TextLayer} from '@deck.gl/layers';
|
|
7
|
+
|
|
8
|
+
import {formatTimeMs} from '../utils/format-utils';
|
|
9
|
+
import {getPrettyTicks, getZoomedRange} from '../utils/tick-utils';
|
|
10
|
+
|
|
11
|
+
export type TimeAxisLayerProps = CompositeLayerProps & {
|
|
12
|
+
unit: 'timestamp' | 'milliseconds';
|
|
13
|
+
/** Start time in milliseconds since epoch */
|
|
14
|
+
startTimeMs: number;
|
|
15
|
+
/** End time in milliseconds since epoch */
|
|
16
|
+
endTimeMs: number;
|
|
17
|
+
/** Optional: Number of tick marks (default: 5) */
|
|
18
|
+
tickCount?: number;
|
|
19
|
+
/** Optional: Y-coordinate for the axis line (default: 0) */
|
|
20
|
+
y?: number;
|
|
21
|
+
/** Optional: RGBA color for axis and ticks (default: [0, 0, 0, 255]) */
|
|
22
|
+
color?: [number, number, number, number];
|
|
23
|
+
/** Optional: Bounds for the axis line (default: viewport bounds) */
|
|
24
|
+
bounds?: [number, number, number, number];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class TimeAxisLayer extends CompositeLayer<TimeAxisLayerProps> {
|
|
28
|
+
static override layerName = 'TimeAxisLayer';
|
|
29
|
+
static override defaultProps: Required<Omit<TimeAxisLayerProps, keyof CompositeLayerProps>> = {
|
|
30
|
+
startTimeMs: 0,
|
|
31
|
+
endTimeMs: 100,
|
|
32
|
+
tickCount: 5,
|
|
33
|
+
y: 0,
|
|
34
|
+
color: [0, 0, 0, 255],
|
|
35
|
+
unit: 'timestamp',
|
|
36
|
+
bounds: undefined!
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Called whenever props/data/viewports change
|
|
40
|
+
override shouldUpdateState(params: UpdateParameters<TimeAxisLayer>): boolean {
|
|
41
|
+
return params.changeFlags.viewportChanged || super.shouldUpdateState(params);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override renderLayers() {
|
|
45
|
+
const {startTimeMs, endTimeMs, tickCount = 10, y = 0, color = [0, 0, 0, 255]} = this.props;
|
|
46
|
+
|
|
47
|
+
let bounds: [number, number, number, number];
|
|
48
|
+
try {
|
|
49
|
+
bounds = this.context.viewport.getBounds();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// eslint-disable-next-line no-console
|
|
52
|
+
console.log('Error getting bounds from viewport:', error);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const [startTimeZoomed, endTimeZoomed] = getZoomedRange(startTimeMs, endTimeMs, bounds);
|
|
56
|
+
// Generate tick positions and labels
|
|
57
|
+
const ticks = getPrettyTicks(startTimeZoomed, endTimeZoomed, tickCount);
|
|
58
|
+
|
|
59
|
+
const tickLines = ticks.map((x) => ({
|
|
60
|
+
sourcePosition: [x, y - 5],
|
|
61
|
+
targetPosition: [x, y + 5]
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
const tickLabels = ticks.map((x) => ({
|
|
65
|
+
position: [x, y - 10],
|
|
66
|
+
text:
|
|
67
|
+
this.props.unit === 'timestamp' ? new Date(x).toLocaleTimeString() : formatTimeMs(x, false)
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
// Axis line
|
|
72
|
+
new LineLayer({
|
|
73
|
+
id: 'axis-line',
|
|
74
|
+
data: [{sourcePosition: [startTimeZoomed, y], targetPosition: [endTimeZoomed, y]}],
|
|
75
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
76
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
77
|
+
getColor: color,
|
|
78
|
+
getWidth: 2
|
|
79
|
+
}),
|
|
80
|
+
// Tick marks
|
|
81
|
+
new LineLayer({
|
|
82
|
+
id: 'tick-marks',
|
|
83
|
+
data: tickLines,
|
|
84
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
85
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
86
|
+
getColor: color,
|
|
87
|
+
getWidth: 1
|
|
88
|
+
}),
|
|
89
|
+
// Tick labels
|
|
90
|
+
new TextLayer({
|
|
91
|
+
id: 'tick-labels',
|
|
92
|
+
data: tickLabels,
|
|
93
|
+
getPosition: (d) => d.position,
|
|
94
|
+
getText: (d) => d.text,
|
|
95
|
+
getSize: 12,
|
|
96
|
+
getColor: color,
|
|
97
|
+
getTextAnchor: 'middle',
|
|
98
|
+
getAlignmentBaseline: 'top'
|
|
99
|
+
})
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {CompositeLayer, type CompositeLayerProps, type UpdateParameters} from '@deck.gl/core';
|
|
6
|
+
import {LineLayer} from '@deck.gl/layers';
|
|
7
|
+
|
|
8
|
+
import {getPrettyTicks, getZoomedRange} from '../utils/tick-utils';
|
|
9
|
+
|
|
10
|
+
export type VerticalGridLayerProps = CompositeLayerProps & {
|
|
11
|
+
/** Start time in milliseconds since epoch */
|
|
12
|
+
xMin: number;
|
|
13
|
+
/** End time in milliseconds since epoch */
|
|
14
|
+
xMax: number;
|
|
15
|
+
/** Optional: Number of tick marks (default: 5) */
|
|
16
|
+
tickCount?: number;
|
|
17
|
+
/** Minimum Y-coordinate for grid lines */
|
|
18
|
+
yMin?: number;
|
|
19
|
+
/** Maximum Y-coordinate for grid lines */
|
|
20
|
+
yMax?: number;
|
|
21
|
+
/** Optional: Width of the grid lines (default: 1) */
|
|
22
|
+
width?: number;
|
|
23
|
+
/** Optional: RGBA color for grid lines (default: [200, 200, 200, 255]) */
|
|
24
|
+
color?: [number, number, number, number];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class VerticalGridLayer extends CompositeLayer<VerticalGridLayerProps> {
|
|
28
|
+
static override layerName = 'VerticalGridLayer';
|
|
29
|
+
static override defaultProps = {
|
|
30
|
+
yMin: -1e6,
|
|
31
|
+
yMax: 1e6,
|
|
32
|
+
tickCount: 5,
|
|
33
|
+
width: 1,
|
|
34
|
+
color: [200, 200, 200, 255]
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
override shouldUpdateState(params: UpdateParameters<VerticalGridLayer>): boolean {
|
|
38
|
+
return params.changeFlags.viewportChanged || super.shouldUpdateState(params);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override renderLayers() {
|
|
42
|
+
const {xMin, xMax, tickCount = 5, yMin, yMax, color} = this.props;
|
|
43
|
+
|
|
44
|
+
// Access the current viewport
|
|
45
|
+
const viewport = this.context.viewport;
|
|
46
|
+
const bounds = viewport.getBounds(); // Returns [minX, minY, maxX, maxY]
|
|
47
|
+
|
|
48
|
+
// Calculate the visible time range based on the viewport bounds
|
|
49
|
+
const [startTimeZoomed, endTimeZoomed] = getZoomedRange(xMin, xMax, bounds);
|
|
50
|
+
|
|
51
|
+
// Generate tick positions
|
|
52
|
+
const tickPositions = getPrettyTicks(startTimeZoomed, endTimeZoomed, tickCount);
|
|
53
|
+
|
|
54
|
+
// Create vertical grid lines at each tick position
|
|
55
|
+
const gridLines = tickPositions.map((x) => ({
|
|
56
|
+
sourcePosition: [x, yMin],
|
|
57
|
+
targetPosition: [x, yMax]
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
return new LineLayer({
|
|
61
|
+
id: `${this.props.id}-lines`,
|
|
62
|
+
data: gridLines,
|
|
63
|
+
getSourcePosition: (d) => d.sourcePosition,
|
|
64
|
+
getTargetPosition: (d) => d.targetPosition,
|
|
65
|
+
getColor: color,
|
|
66
|
+
getWidth: 1
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert a time in microseconds to a human-readable string
|
|
7
|
+
* @param us Time in microseconds
|
|
8
|
+
*/
|
|
9
|
+
export function formatTimeMs(timeMs: number, space: boolean = true): string {
|
|
10
|
+
const sep = space ? ' ' : '';
|
|
11
|
+
const us = timeMs * 1000;
|
|
12
|
+
if (us === 0) {
|
|
13
|
+
return '0s';
|
|
14
|
+
}
|
|
15
|
+
if (Math.abs(us) < 1000) {
|
|
16
|
+
return `${floatToStr(us)}${sep}µs`;
|
|
17
|
+
}
|
|
18
|
+
const ms = us / 1000;
|
|
19
|
+
if (Math.abs(ms) < 1000) {
|
|
20
|
+
return `${floatToStr(ms)}${sep} ms`;
|
|
21
|
+
}
|
|
22
|
+
const s = ms / 1000;
|
|
23
|
+
if (Math.abs(s) < 60) {
|
|
24
|
+
return `${floatToStr(s)}${sep} s`;
|
|
25
|
+
}
|
|
26
|
+
const m = s / 60;
|
|
27
|
+
if (Math.abs(m) < 60) {
|
|
28
|
+
return `${floatToStr(m)}${sep} min`;
|
|
29
|
+
}
|
|
30
|
+
const h = m / 60;
|
|
31
|
+
if (Math.abs(h) < 24) {
|
|
32
|
+
return `${floatToStr(h)}${sep} hrs`;
|
|
33
|
+
}
|
|
34
|
+
const d = h / 24;
|
|
35
|
+
return `${floatToStr(d)}${sep} days`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatTimeRangeMs(startMs: number, endMs: number): string {
|
|
39
|
+
return `${formatTimeMs(startMs)} - ${formatTimeMs(endMs)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert a float to a string
|
|
44
|
+
*/
|
|
45
|
+
function floatToStr(f: number, roundDigits: number = 5): string {
|
|
46
|
+
if (Number.isInteger(f)) {
|
|
47
|
+
return f.toString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let i = 1; i < roundDigits - 1; i++) {
|
|
51
|
+
const rounded = parseFloat(f.toPrecision(i));
|
|
52
|
+
if (rounded === f) {
|
|
53
|
+
return rounded.toPrecision(i);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return f.toPrecision(roundDigits);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// export function formatTimesUs(ticks: number[]): string {
|
|
61
|
+
// // Try from 0 up to a reasonable max (e.g. 20)
|
|
62
|
+
// for (let d = 0; d <= 20; d++) {
|
|
63
|
+
// const seen = new Set<string>();
|
|
64
|
+
// let allDistinct = true;
|
|
65
|
+
// for (const t of ticks) {
|
|
66
|
+
// // Format each tick with d decimals
|
|
67
|
+
// const str = t.toFixed(d);
|
|
68
|
+
// if (seen.has(str)) {
|
|
69
|
+
// allDistinct = false;
|
|
70
|
+
// break;
|
|
71
|
+
// }
|
|
72
|
+
// seen.add(str);
|
|
73
|
+
// }
|
|
74
|
+
// if (allDistinct) {
|
|
75
|
+
// return d;
|
|
76
|
+
// }
|
|
77
|
+
// }
|
|
78
|
+
// // Fallback if somehow not distinct even at 20 decimals
|
|
79
|
+
// return 20;
|
|
80
|
+
// }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
export function getZoomedRange(
|
|
6
|
+
startTime: number,
|
|
7
|
+
endTime: number,
|
|
8
|
+
bounds: [number, number, number, number]
|
|
9
|
+
) {
|
|
10
|
+
const [startTimeZoomed, , endTimeZoomed] = bounds;
|
|
11
|
+
// console.log(`startTimeZoomed: ${startTimeZoomed}, endTimeZoomed: ${endTimeZoomed}, tickInterval: ${tickInterval} tickCountZoomed: ${tickCountZoomed}`);
|
|
12
|
+
return [Math.max(startTime, startTimeZoomed), Math.min(endTime, endTimeZoomed)];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get nicely rounded tick close to the natural spacing
|
|
17
|
+
* @param startTime
|
|
18
|
+
* @param endTime
|
|
19
|
+
* @param tickCount
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
export function getPrettyTicks(startTime: number, endTime: number, tickCount: number = 5) {
|
|
23
|
+
const range = endTime - startTime;
|
|
24
|
+
const roughStep = range / (tickCount - 1);
|
|
25
|
+
const exponent = Math.floor(Math.log10(roughStep));
|
|
26
|
+
const base = Math.pow(10, exponent);
|
|
27
|
+
const multiples = [1, 2, 5, 10];
|
|
28
|
+
|
|
29
|
+
// Find the smallest multiple that is greater than or equal to roughStep
|
|
30
|
+
const niceStep = multiples.find((m) => base * m >= roughStep) * base;
|
|
31
|
+
|
|
32
|
+
const niceStart = Math.ceil(startTime / niceStep) * niceStep;
|
|
33
|
+
const niceEnd = Math.floor(endTime / niceStep) * niceStep;
|
|
34
|
+
|
|
35
|
+
const ticks = [];
|
|
36
|
+
for (let t = niceStart; t <= niceEnd; t += niceStep) {
|
|
37
|
+
ticks.push(t);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return ticks;
|
|
41
|
+
}
|