allows controlling duration on the frontend

This commit is contained in:
2025-01-13 20:36:46 -08:00
parent fdd3f2c128
commit 39ddf45f0d
6 changed files with 115 additions and 60 deletions

View File

@@ -11,10 +11,14 @@ import {
import { useNow } from '@/composables/ticker'; import { useNow } from '@/composables/ticker';
import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph'; import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
import TimeText from '@/components/TimeText.vue'; import TimeText from '@/components/TimeText.vue';
import { getDateString } from '@/datetime'; import {
getDateString,
getDurationString,
parseDurationString,
} from '@/datetime';
const props = defineProps<{ const props = defineProps<{
duration?: number; initial_duration?: number;
utc?: boolean; utc?: boolean;
left_axis?: boolean; left_axis?: boolean;
right_axis?: boolean; right_axis?: boolean;
@@ -56,12 +60,7 @@ onUnmounted(() => {
}); });
const now = useNow(33); const now = useNow(33);
const window_duration = computed(() => { const window_duration = ref(props.initial_duration || 10 * 1000);
if (props.duration) {
return props.duration;
}
return 10 * 1000; // 10 seconds
});
const time_lines = [ const time_lines = [
1, // 1ms 1, // 1ms
@@ -148,6 +147,18 @@ const max_x_text = computed({
} }
}, },
}); });
const duration_text = computed({
get() {
return getDurationString(window_duration.value);
},
set(newValue) {
const new_duration = parseDurationString(newValue);
if (!Number.isNaN(new_duration)) {
window_duration.value = Math.max(new_duration, 1);
fetch_history.value++;
}
},
});
const x_map = (x: number) => { const x_map = (x: number) => {
return ( return (
@@ -200,13 +211,9 @@ const mouse_x = ref<number | null>(null);
function onMouseMove(event: MouseEvent) { function onMouseMove(event: MouseEvent) {
if (props.cursor) { if (props.cursor) {
const new_x = mouse_x.value =
event.clientX - event.clientX -
(event.currentTarget as Element).getBoundingClientRect().left; (event.currentTarget as Element).getBoundingClientRect().left;
if (new_x == 0) {
console.log(event);
}
mouse_x.value = new_x;
} }
} }
@@ -220,8 +227,6 @@ const mouse_t = computed(() => {
} }
const t = inv_x_map(mouse_x.value); const t = inv_x_map(mouse_x.value);
if (t < min_x.value || t > max_x.value) { if (t < min_x.value || t > max_x.value) {
// console.log(`x ${mouse_x.value} t ${t} min ${t < min_x.value} max ${t > max_x.value}`)
// debugger;
return null; return null;
} }
return t; return t;
@@ -237,10 +242,16 @@ const show_data_at_time = computed(() => {
const should_fade = ref(false); const should_fade = ref(false);
const max_temporal_resolution = computed(() => {
const delta_t = window_duration.value;
return Math.floor(delta_t / 1000); // Aim for a maximum of 1000 data points
});
provide<GraphData>(GRAPH_DATA, { provide<GraphData>(GRAPH_DATA, {
border_top: border_top, border_top: border_top,
min_x: min_x, min_x: min_x,
max_x: max_x, max_x: max_x,
max_temporal_resolution: max_temporal_resolution,
live: live, live: live,
fetch_history: fetch_history, fetch_history: fetch_history,
width: () => width: () =>
@@ -269,6 +280,14 @@ provide<GraphData>(GRAPH_DATA, {
:style="`height: ${controls_height}px`" :style="`height: ${controls_height}px`"
> >
<div class="grow"></div> <div class="grow"></div>
<div>
<input
type="text"
autocomplete="off"
:size="15"
v-model="duration_text"
/>
</div>
<div> <div>
<input <input
type="text" type="text"
@@ -401,7 +420,7 @@ div.controls-header {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
align-content: stretch; align-content: stretch;
gap: 1em 0; gap: 0 1em;
margin: 0 1em 0 1em; margin: 0 1em 0 1em;
} }

View File

@@ -37,18 +37,26 @@ const legend_text_offset = 4;
const marker_radius = 3; const marker_radius = 3;
const text_offset = computed(() => 10); const text_offset = computed(() => 10);
const min_sep = computed(() =>
Math.min(props.minimum_separation || 0, maximum_minimum_separation_live),
);
const graph_data = inject<GraphData>(GRAPH_DATA)!; const graph_data = inject<GraphData>(GRAPH_DATA)!;
const axis_data = inject<AxisData>(AXIS_DATA)!; const axis_data = inject<AxisData>(AXIS_DATA)!;
const data_min_sep = computed(() => {
return Math.max(
props.minimum_separation || 0,
toValue(graph_data.max_temporal_resolution),
);
});
const live_min_sep = computed(() =>
Math.min(data_min_sep.value, maximum_minimum_separation_live),
);
const { data: telemetry_data } = useTelemetry(() => props.data); const { data: telemetry_data } = useTelemetry(() => props.data);
const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!; const websocket = inject<ShallowRef<WebsocketHandle>>(WEBSOCKET_SYMBOL)!;
const value = websocket.value.listen_to_telemetry( const value = websocket.value.listen_to_telemetry(
telemetry_data, telemetry_data,
min_sep, live_min_sep,
graph_data.live, graph_data.live,
); );
@@ -95,7 +103,7 @@ watch([value], ([val]) => {
x: val_t, x: val_t,
y: item_val, y: item_val,
} as Point; } as Point;
memo.value.insert(new_item, props.minimum_separation); memo.value.insert(new_item, data_min_sep.value);
if (item_val < min.value) { if (item_val < min.value) {
min.value = item_val; min.value = item_val;
} }
@@ -118,7 +126,7 @@ watch(
const min_x = new Date(toValue(graph_data.min_x)); const min_x = new Date(toValue(graph_data.min_x));
const max_x = new Date(toValue(graph_data.max_x)); const max_x = new Date(toValue(graph_data.max_x));
const res = await fetch( const res = await fetch(
`/api/tlm/history/${uuid}?from=${min_x.toISOString()}&to=${max_x.toISOString()}&resolution=${props.minimum_separation || 0}`, `/api/tlm/history/${uuid}?from=${min_x.toISOString()}&to=${max_x.toISOString()}&resolution=${data_min_sep.value}`,
); );
const response = (await res.json()) as TelemetryDataItem[]; const response = (await res.json()) as TelemetryDataItem[];
for (const data_item of response) { for (const data_item of response) {
@@ -136,9 +144,7 @@ watch(
max.value = item_val; max.value = item_val;
} }
} }
memo.value.reduce_to_maximum_separation( memo.value.reduce_to_maximum_separation(data_min_sep.value);
props.minimum_separation || 0,
);
triggerRef(memo); triggerRef(memo);
debounced_recompute(); debounced_recompute();
recompute_bounds.value++; recompute_bounds.value++;
@@ -438,6 +444,11 @@ function onMouseExit(event: MouseEvent) {
<div class="row"> <div class="row">
<span>{{ telemetry_data?.data_type }}</span> <span>{{ telemetry_data?.data_type }}</span>
</div> </div>
<div class="row">
<span>{{
current_data_point?.y || 'Missing Data'
}}</span>
</div>
</template> </template>
<template v-else> <template v-else>
<div class="row"> <div class="row">

View File

@@ -20,7 +20,7 @@ const height = ref(0);
function update_for_element(element: Element) { function update_for_element(element: Element) {
top.value = element.getBoundingClientRect().top + window.scrollY; top.value = element.getBoundingClientRect().top + window.scrollY;
left.value = element.getBoundingClientRect().left + window.screenX; left.value = element.getBoundingClientRect().left + window.scrollX;
width.value = element.getBoundingClientRect().width; width.value = element.getBoundingClientRect().width;
height.value = element.getBoundingClientRect().height; height.value = element.getBoundingClientRect().height;
} }

View File

@@ -1,5 +1,9 @@
// This function is slow // This function is slow
export function getDateString(date: Date, utc: boolean, millis: boolean) { export function getDateString(
date: Date,
utc: boolean,
millis: boolean,
): string {
const year = utc ? date.getUTCFullYear() : date.getFullYear(); const year = utc ? date.getUTCFullYear() : date.getFullYear();
const month = ( const month = (
(utc ? date.getMonth() : date.getMonth()) + 1 (utc ? date.getMonth() : date.getMonth()) + 1
@@ -47,3 +51,47 @@ export function getDateString(date: Date, utc: boolean, millis: boolean) {
}); });
return `${year}/${month}/${day} ${hour}:${minute}:${second}${millis ? `.${milliseconds}` : ''}${utc ? 'Z' : ''}`; return `${year}/${month}/${day} ${hour}:${minute}:${second}${millis ? `.${milliseconds}` : ''}${utc ? 'Z' : ''}`;
} }
export function getDurationString(duration_millis: number): string {
const millis = duration_millis % 1000;
const duration_secs = Math.floor(duration_millis / 1000);
const seconds = duration_secs % 60;
const duration_minutes = Math.floor(duration_secs / 60);
const minutes = duration_minutes % 60;
const duration_hours = Math.floor(duration_minutes / 60);
const millis_str = millis.toLocaleString('en-US', {
minimumIntegerDigits: 3,
useGrouping: false,
maximumFractionDigits: 0,
});
const seconds_str = seconds.toLocaleString('en-US', {
minimumIntegerDigits: 2,
useGrouping: false,
maximumFractionDigits: 0,
});
const minutes_str = minutes.toLocaleString('en-US', {
minimumIntegerDigits: 2,
useGrouping: false,
maximumFractionDigits: 0,
});
const hours_str = duration_hours.toLocaleString('en-US', {
minimumIntegerDigits: 1,
useGrouping: false,
maximumFractionDigits: 0,
});
return `${hours_str}:${minutes_str}:${seconds_str}.${millis_str}`;
}
export function parseDurationString(duration: string): number {
const parts = duration.split(':');
const seconds_str = parts.length >= 1 ? parts[parts.length - 1] : '0';
const minutes_str = parts.length >= 2 ? parts[parts.length - 2] : '0';
const hours_str = parts.length >= 3 ? parts[parts.length - 3] : '0';
const seconds = Number.parseFloat(seconds_str);
const minutes = Number.parseFloat(minutes_str);
const hours = Number.parseFloat(hours_str);
return Math.floor(((hours * 60 + minutes) * 60 + seconds) * 1000);
}

View File

@@ -12,6 +12,7 @@ export interface GraphData {
border_top: MaybeRefOrGetter<number>; border_top: MaybeRefOrGetter<number>;
min_x: MaybeRefOrGetter<number>; min_x: MaybeRefOrGetter<number>;
max_x: MaybeRefOrGetter<number>; max_x: MaybeRefOrGetter<number>;
max_temporal_resolution: MaybeRefOrGetter<number>;
live: MaybeRefOrGetter<boolean>; live: MaybeRefOrGetter<boolean>;
fetch_history: MaybeRefOrGetter<number>; fetch_history: MaybeRefOrGetter<number>;
width: MaybeRefOrGetter<number>; width: MaybeRefOrGetter<number>;

View File

@@ -22,43 +22,19 @@ provide(WEBSOCKET_SYMBOL, websocket);
</div> </div>
<div style="width: 100vw; height: 50vh"> <div style="width: 100vw; height: 50vh">
<SvgGraph <SvgGraph
:duration="60 * 1000 * 10" :initial_duration="60 * 1000 * 10"
:legend="GraphSide.Right" :legend="GraphSide.Right"
right_axis right_axis
> >
<Axis> <Axis>
<Line <Line data="simple_producer/sin"></Line>
data="simple_producer/sin" <Line data="simple_producer/cos4"></Line>
:minimum_separation="1000" <Line data="simple_producer/sin2"></Line>
></Line> <Line data="simple_producer/cos"></Line>
<Line <Line data="simple_producer/sin3"></Line>
data="simple_producer/cos4" <Line data="simple_producer/cos2"></Line>
:minimum_separation="1000" <Line data="simple_producer/sin4"></Line>
></Line> <Line data="simple_producer/cos3"></Line>
<Line
data="simple_producer/sin2"
:minimum_separation="1000"
></Line>
<Line
data="simple_producer/cos"
:minimum_separation="1000"
></Line>
<Line
data="simple_producer/sin3"
:minimum_separation="1000"
></Line>
<Line
data="simple_producer/cos2"
:minimum_separation="1000"
></Line>
<Line
data="simple_producer/sin4"
:minimum_separation="1000"
></Line>
<Line
data="simple_producer/cos3"
:minimum_separation="1000"
></Line>
</Axis> </Axis>
</SvgGraph> </SvgGraph>
</div> </div>