allows scrolling backwards through history
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed, inject, provide, ref, toValue, watch } from 'vue';
|
||||
import { AXIS_DATA, type AxisData, AxisType } from '@/graph/axis';
|
||||
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
|
||||
import NumericText from '@/components/NumericText.vue';
|
||||
import { useNow } from '@/composables/ticker';
|
||||
|
||||
const props = defineProps<{
|
||||
y_limits?: [number, number];
|
||||
@@ -28,7 +29,9 @@ const raw_max_y = ref(-Infinity);
|
||||
|
||||
const axis_update_watch = ref(0);
|
||||
|
||||
watch([graph_data.min_x, graph_data.max_x], () => {
|
||||
const axis_update_ticker = useNow(50);
|
||||
|
||||
watch([graph_data.min_x, graph_data.max_x, axis_update_ticker], () => {
|
||||
axis_update_watch.value++;
|
||||
min_y.value = raw_min_y.value;
|
||||
max_y.value = raw_max_y.value;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { useNow } from '@/composables/ticker';
|
||||
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
|
||||
import TimeText from '@/components/TimeText.vue';
|
||||
import { getDateString } from '@/datetime';
|
||||
|
||||
const props = defineProps<{
|
||||
duration?: number;
|
||||
@@ -107,11 +108,47 @@ const border_right = computed(
|
||||
const border_top = computed(() => 6);
|
||||
const border_bottom = computed(() => (props.hide_time_labels ? 6 : 24));
|
||||
|
||||
const max_x = now;
|
||||
const max_x = ref(now.value);
|
||||
const min_x = computed(() => max_x.value - window_duration.value);
|
||||
|
||||
const fetch_history = ref(0);
|
||||
|
||||
const diff_x = computed(() => max_x.value - min_x.value);
|
||||
|
||||
const live = ref(true);
|
||||
watch([live], ([live_value]) => {
|
||||
if (live_value) {
|
||||
fetch_history.value++;
|
||||
}
|
||||
});
|
||||
const valid_max_x_text = ref(true);
|
||||
watch([now], ([now_value]) => {
|
||||
if (live.value) {
|
||||
max_x.value = now_value;
|
||||
valid_max_x_text.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const max_x_text = computed({
|
||||
// getter
|
||||
get() {
|
||||
return getDateString(new Date(max_x.value), props.utc, true);
|
||||
},
|
||||
// setter
|
||||
set(newValue) {
|
||||
const new_max_x = Date.parse(
|
||||
newValue.replace('/', '-').replace('/', '-').replace(' ', 'T'),
|
||||
);
|
||||
if (!Number.isNaN(new_max_x)) {
|
||||
fetch_history.value++;
|
||||
max_x.value = new_max_x;
|
||||
valid_max_x_text.value = true;
|
||||
} else {
|
||||
valid_max_x_text.value = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const x_map = (x: number) => {
|
||||
return (
|
||||
((width.value - border_left.value - border_right.value) *
|
||||
@@ -203,7 +240,9 @@ const should_fade = ref(false);
|
||||
provide<GraphData>(GRAPH_DATA, {
|
||||
border_top: border_top,
|
||||
min_x: min_x,
|
||||
max_x: now,
|
||||
max_x: max_x,
|
||||
live: live,
|
||||
fetch_history: fetch_history,
|
||||
width: () =>
|
||||
Math.max(width.value - border_left.value - border_right.value, 0),
|
||||
height: () =>
|
||||
@@ -231,7 +270,17 @@ provide<GraphData>(GRAPH_DATA, {
|
||||
>
|
||||
<div class="grow"></div>
|
||||
<div>
|
||||
<span>Duration Dropdown</span>
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="live"
|
||||
:size="max_x_text.length"
|
||||
v-model="max_x_text"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" v-model="live" />
|
||||
<label>Live</label>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
|
||||
@@ -28,7 +28,6 @@ const props = defineProps<{
|
||||
data: string;
|
||||
minimum_separation?: number;
|
||||
class?: string;
|
||||
fetch_history?: number;
|
||||
}>();
|
||||
|
||||
const smoothing_distance_x = 5;
|
||||
@@ -42,13 +41,17 @@ const min_sep = computed(() =>
|
||||
Math.min(props.minimum_separation || 0, maximum_minimum_separation_live),
|
||||
);
|
||||
|
||||
const { data: telemetry_data } = useTelemetry(() => props.data);
|
||||
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
|
||||
const value = websocket.value.listen_to_telemetry(telemetry_data, min_sep);
|
||||
|
||||
const graph_data = inject<GraphData>(GRAPH_DATA)!;
|
||||
const axis_data = inject<AxisData>(AXIS_DATA)!;
|
||||
|
||||
const { data: telemetry_data } = useTelemetry(() => props.data);
|
||||
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
|
||||
const value = websocket.value.listen_to_telemetry(
|
||||
telemetry_data,
|
||||
min_sep,
|
||||
graph_data.live,
|
||||
);
|
||||
|
||||
const min = ref(Infinity);
|
||||
const max = ref(-Infinity);
|
||||
|
||||
@@ -104,8 +107,9 @@ watch([value], ([val]) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
const recompute_bounds = ref(0);
|
||||
watch(
|
||||
[telemetry_data, () => props.fetch_history],
|
||||
[telemetry_data, () => toValue(graph_data.fetch_history)],
|
||||
async ([data]) => {
|
||||
if (data) {
|
||||
const uuid = data.uuid;
|
||||
@@ -137,9 +141,10 @@ watch(
|
||||
);
|
||||
triggerRef(memo);
|
||||
debounced_recompute();
|
||||
recompute_bounds.value++;
|
||||
} catch (e) {
|
||||
// TODO: Response?
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -170,19 +175,22 @@ watch([graph_data.min_x, graph_data.max_x], ([min_x, max_x]) => {
|
||||
}
|
||||
}
|
||||
if (memo_changed) {
|
||||
let min_val = Infinity;
|
||||
let max_val = -Infinity;
|
||||
for (let i = 1; i < memo.value.data.length; i++) {
|
||||
const item_val = memo.value.data[i].y;
|
||||
min_val = Math.min(min_val, item_val);
|
||||
max_val = Math.max(max_val, item_val);
|
||||
}
|
||||
triggerRef(memo);
|
||||
debounced_recompute();
|
||||
max.value = max_val;
|
||||
min.value = min_val;
|
||||
recompute_bounds.value++;
|
||||
}
|
||||
});
|
||||
watch([recompute_bounds], () => {
|
||||
let min_val = Infinity;
|
||||
let max_val = -Infinity;
|
||||
for (let i = 1; i < memo.value.data.length; i++) {
|
||||
const item_val = memo.value.data[i].y;
|
||||
min_val = Math.min(min_val, item_val);
|
||||
max_val = Math.max(max_val, item_val);
|
||||
}
|
||||
triggerRef(memo);
|
||||
debounced_recompute();
|
||||
max.value = max_val;
|
||||
min.value = min_val;
|
||||
});
|
||||
|
||||
watch(
|
||||
[min, axis_data.axis_update_watch],
|
||||
@@ -354,7 +362,7 @@ function onMouseExit(event: MouseEvent) {
|
||||
:cx="marker_radius"
|
||||
:cy="marker_radius"
|
||||
:r="marker_radius"
|
||||
:class="`indexed-color color-${index}`"
|
||||
:class="`indexed-color color-${index} marker`"
|
||||
/>
|
||||
</marker>
|
||||
</defs>
|
||||
@@ -446,6 +454,10 @@ function onMouseExit(event: MouseEvent) {
|
||||
<style lang="scss">
|
||||
@use '@/assets/variables';
|
||||
|
||||
.fade rect.fade_other_selected {
|
||||
opacity: 10%;
|
||||
}
|
||||
|
||||
.fade .fade_other_selected {
|
||||
opacity: 25%;
|
||||
}
|
||||
@@ -463,6 +475,11 @@ function onMouseExit(event: MouseEvent) {
|
||||
fill: var(--indexed-color);
|
||||
}
|
||||
|
||||
circle.marker {
|
||||
stroke: variables.$background-color;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
polyline {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { getDateString } from '@/datetime';
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
@@ -9,57 +10,12 @@ const props = defineProps<{
|
||||
show_millis?: boolean;
|
||||
}>();
|
||||
|
||||
// This function is slow
|
||||
function getDateString(date: Date) {
|
||||
const year = props.utc ? date.getUTCFullYear() : date.getFullYear();
|
||||
const month = (
|
||||
(props.utc ? date.getMonth() : date.getMonth()) + 1
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const day = (props.utc ? date.getUTCDate() : date.getDate()).toLocaleString(
|
||||
'en-US',
|
||||
{
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
},
|
||||
);
|
||||
const hour = (
|
||||
props.utc ? date.getUTCHours() : date.getHours()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const minute = (
|
||||
props.utc ? date.getUTCMinutes() : date.getMinutes()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const second = (
|
||||
props.utc ? date.getUTCSeconds() : date.getSeconds()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const milliseconds = (
|
||||
props.utc ? date.getUTCMilliseconds() : date.getMilliseconds()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 3,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
return `${year}/${month}/${day} ${hour}:${minute}:${second}${props.show_millis ? `.${milliseconds}` : ''}${props.utc ? 'Z' : ''}`;
|
||||
}
|
||||
|
||||
const timetext = computed(() => {
|
||||
return getDateString(new Date(props.timestamp));
|
||||
return getDateString(
|
||||
new Date(props.timestamp),
|
||||
props.utc,
|
||||
props.show_millis,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type MaybeRefOrGetter,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
onWatcherCleanup,
|
||||
ref,
|
||||
type Ref,
|
||||
shallowRef,
|
||||
@@ -95,6 +96,7 @@ export class WebsocketHandle {
|
||||
listen_to_telemetry(
|
||||
telemetry: MaybeRefOrGetter<TelemetryDefinition | null>,
|
||||
minimum_separation_ms: MaybeRefOrGetter<number> | undefined,
|
||||
live: MaybeRefOrGetter<boolean>,
|
||||
) {
|
||||
const value_result = ref<TelemetryDataItem | null>(null);
|
||||
|
||||
@@ -114,10 +116,12 @@ export class WebsocketHandle {
|
||||
return 0;
|
||||
});
|
||||
|
||||
const is_live = computed(() => toValue(live));
|
||||
|
||||
watch(
|
||||
[uuid, this.connected, minimum_separation],
|
||||
([uuid_value, connected, min_sep]) => {
|
||||
if (connected && uuid_value) {
|
||||
[uuid, this.connected, minimum_separation, is_live],
|
||||
([uuid_value, connected, min_sep, live_value]) => {
|
||||
if (connected && uuid_value && live_value) {
|
||||
this.websocket?.send(
|
||||
JSON.stringify({
|
||||
RegisterTlmListener: {
|
||||
@@ -129,8 +133,26 @@ export class WebsocketHandle {
|
||||
if (!this.on_telem_value.has(uuid_value)) {
|
||||
this.on_telem_value.set(uuid_value, []);
|
||||
}
|
||||
this.on_telem_value.get(uuid_value)?.push((value) => {
|
||||
const callback_fn = (value: TelemetryDataItem) => {
|
||||
value_result.value = value;
|
||||
};
|
||||
this.on_telem_value.get(uuid_value)?.push(callback_fn);
|
||||
onWatcherCleanup(() => {
|
||||
this.websocket?.send(
|
||||
JSON.stringify({
|
||||
UnregisterTlmListener: {
|
||||
uuid: uuid_value,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const index = this.on_telem_value
|
||||
.get(uuid_value)
|
||||
?.indexOf(callback_fn);
|
||||
if (index !== undefined && index >= 0) {
|
||||
this.on_telem_value
|
||||
.get(uuid_value)
|
||||
?.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
49
frontend/src/datetime.ts
Normal file
49
frontend/src/datetime.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// This function is slow
|
||||
export function getDateString(date: Date, utc: boolean, millis: boolean) {
|
||||
const year = utc ? date.getUTCFullYear() : date.getFullYear();
|
||||
const month = (
|
||||
(utc ? date.getMonth() : date.getMonth()) + 1
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const day = (utc ? date.getUTCDate() : date.getDate()).toLocaleString(
|
||||
'en-US',
|
||||
{
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
},
|
||||
);
|
||||
const hour = (utc ? date.getUTCHours() : date.getHours()).toLocaleString(
|
||||
'en-US',
|
||||
{
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
},
|
||||
);
|
||||
const minute = (
|
||||
utc ? date.getUTCMinutes() : date.getMinutes()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const second = (
|
||||
utc ? date.getUTCSeconds() : date.getSeconds()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
const milliseconds = (
|
||||
utc ? date.getUTCMilliseconds() : date.getMilliseconds()
|
||||
).toLocaleString('en-US', {
|
||||
minimumIntegerDigits: 3,
|
||||
useGrouping: false,
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
return `${year}/${month}/${day} ${hour}:${minute}:${second}${millis ? `.${milliseconds}` : ''}${utc ? 'Z' : ''}`;
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export interface GraphData {
|
||||
border_top: MaybeRefOrGetter<number>;
|
||||
min_x: MaybeRefOrGetter<number>;
|
||||
max_x: MaybeRefOrGetter<number>;
|
||||
live: MaybeRefOrGetter<boolean>;
|
||||
fetch_history: MaybeRefOrGetter<number>;
|
||||
width: MaybeRefOrGetter<number>;
|
||||
height: MaybeRefOrGetter<number>;
|
||||
x_map: (x: number) => number;
|
||||
|
||||
Reference in New Issue
Block a user