add graph cursor

This commit is contained in:
2025-01-04 11:45:09 -05:00
parent 35603c98a4
commit c69022448f
5 changed files with 139 additions and 39 deletions

View File

@@ -1,5 +1,6 @@
@use 'sass:color'; @use 'sass:color';
$gray-0: oklch(100% 0 0);
$gray-1: oklch(90% 0 0); $gray-1: oklch(90% 0 0);
$gray-2: oklch(80% 0 0); $gray-2: oklch(80% 0 0);
$gray-3: oklch(70% 0 0); $gray-3: oklch(70% 0 0);
@@ -29,6 +30,7 @@ $background-color: $gray-7;
$light-background-color: color.adjust($background-color, $lightness: 5%); $light-background-color: color.adjust($background-color, $lightness: 5%);
$dark-background-color: color.adjust($background-color, $lightness: -5%); $dark-background-color: color.adjust($background-color, $lightness: -5%);
$cursor-tick: $gray-0;
$time-tick: $gray-1; $time-tick: $gray-1;
$grid-line: $gray-1; $grid-line: $gray-1;
$major-tick: $gray-4; $major-tick: $gray-4;

View File

@@ -21,6 +21,7 @@ const props = defineProps<{
hide_time_ticks?: boolean; hide_time_ticks?: boolean;
include_controls?: boolean; include_controls?: boolean;
legend?: GraphSide; legend?: GraphSide;
cursor?: boolean;
}>(); }>();
const divRef = useTemplateRef<HTMLDivElement>('graph-div'); const divRef = useTemplateRef<HTMLDivElement>('graph-div');
@@ -109,15 +110,23 @@ const border_bottom = computed(() => (props.hide_time_labels ? 6 : 24));
const max_x = now; const max_x = now;
const min_x = computed(() => max_x.value - window_duration.value); const min_x = computed(() => max_x.value - window_duration.value);
const diff_x = computed(() => max_x.value - min_x.value);
const x_map = (x: number) => { const x_map = (x: number) => {
const diff_x = max_x.value - min_x.value;
return ( return (
((width.value - border_left.value - border_right.value) * ((width.value - border_left.value - border_right.value) *
(x - min_x.value)) / (x - min_x.value)) /
diff_x + diff_x.value +
border_left.value border_left.value
); );
}; };
const inv_x_map = (x: number) => {
return (
((x - border_left.value) * diff_x.value) /
(width.value - border_left.value - border_right.value) +
min_x.value
);
};
const telemetry_lines = ref([]); const telemetry_lines = ref([]);
@@ -132,25 +141,6 @@ const legend_x_stride = computed(() => 0);
const legend_y_stride = computed(() => 16); const legend_y_stride = computed(() => 16);
const legend_width_output = computed(() => legend_width - 8); const legend_width_output = computed(() => legend_width - 8);
provide<GraphData>(GRAPH_DATA, {
border_top: border_top,
min_x: min_x,
max_x: now,
width: () =>
Math.max(width.value - border_left.value - border_right.value, 0),
height: () =>
Math.max(height.value - border_top.value - border_bottom.value, 0),
x_map: x_map,
lines: telemetry_lines,
max_update_rate: 1000 / 10,
legend_enabled: legend_enabled,
legend_x: legend_x,
legend_y: legend_y,
legend_x_stride: legend_x_stride,
legend_y_stride: legend_y_stride,
legend_width: legend_width_output,
});
const line_duration = computed(() => { const line_duration = computed(() => {
const diff_x = max_x.value - min_x.value; const diff_x = max_x.value - min_x.value;
return time_lines.find((duration) => diff_x / duration >= 2)!; return time_lines.find((duration) => diff_x / duration >= 2)!;
@@ -168,6 +158,65 @@ const lines = computed(() => {
} }
return result; return result;
}); });
const mouse_x = ref<number | null>(null);
function onMouseMove(event: MouseEvent) {
if (props.cursor) {
const new_x =
event.clientX -
(event.currentTarget as Element).getBoundingClientRect().left;
if (new_x == 0) {
console.log(event);
}
mouse_x.value = new_x;
}
}
function onMouseOut() {
mouse_x.value = null;
}
const mouse_t = computed(() => {
if (mouse_x.value === null) {
return null;
}
const t = inv_x_map(mouse_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 t;
});
const show_data_at_time = computed(() => {
if (mouse_t.value === null) {
return max_x.value;
} else {
return mouse_t.value;
}
});
provide<GraphData>(GRAPH_DATA, {
border_top: border_top,
min_x: min_x,
max_x: now,
width: () =>
Math.max(width.value - border_left.value - border_right.value, 0),
height: () =>
Math.max(height.value - border_top.value - border_bottom.value, 0),
x_map: x_map,
lines: telemetry_lines,
max_update_rate: 1000 / 10,
legend_enabled: legend_enabled,
legend_x: legend_x,
legend_y: legend_y,
legend_x_stride: legend_x_stride,
legend_y_stride: legend_y_stride,
legend_width: legend_width_output,
cursor_time: show_data_at_time,
});
</script> </script>
<template> <template>
@@ -182,7 +231,14 @@ const lines = computed(() => {
<span>Duration Dropdown</span> <span>Duration Dropdown</span>
</div> </div>
</div> </div>
<svg ref="svg_graph" class="graph" :width="width" :height="height"> <svg
ref="svg_graph"
class="graph"
:width="width"
:height="height"
@mousemove="onMouseMove"
@mouseleave="onMouseOut"
>
<defs> <defs>
<clipPath id="content"> <clipPath id="content">
<rect <rect
@@ -231,6 +287,27 @@ const lines = computed(() => {
</template> </template>
</g> </g>
<slot></slot> <slot></slot>
<g class="cursor_tick" v-if="mouse_t && cursor">
<rect
:x="x_map(mouse_t) - 100"
:y="height - border_bottom"
width="200"
height="20"
></rect>
<polyline
:points="`${x_map(mouse_t)},${border_top} ${x_map(mouse_t)},${height - border_bottom}`"
></polyline>
<TimeText
v-if="mouse_t"
id="cursor_text"
class="bottom_edge"
:x="x_map(mouse_t)"
:y="height - border_bottom + text_offset"
:timestamp="mouse_t"
:utc="props.utc"
show_millis
></TimeText>
</g>
</svg> </svg>
</div> </div>
</template> </template>
@@ -249,6 +326,16 @@ const lines = computed(() => {
fill: variables.$time-tick; fill: variables.$time-tick;
} }
.cursor_tick {
stroke: variables.$cursor-tick;
fill: variables.$cursor-tick;
}
.cursor_tick > rect {
fill: variables.$background-color;
opacity: 66%;
}
div.full-size { div.full-size {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -152,8 +152,8 @@ watch([graph_data.min_x, graph_data.max_x], ([min_x, max_x]) => {
if (min_x) { if (min_x) {
while ( while (
memo.value.data.length > 2 && memo.value.data.length > 1 &&
memo.value.data[1].x < toValue(min_x) memo.value.data[0].x < toValue(min_x)
) { ) {
memo.value.data.shift(); memo.value.data.shift();
memo_changed = true; memo_changed = true;
@@ -161,8 +161,8 @@ watch([graph_data.min_x, graph_data.max_x], ([min_x, max_x]) => {
} }
if (max_x) { if (max_x) {
while ( while (
memo.value.data.length > 2 && memo.value.data.length > 1 &&
memo.value.data[memo.value.data.length - 2].x > toValue(max_x) memo.value.data[memo.value.data.length - 1].x > toValue(max_x)
) { ) {
memo.value.data.pop(); memo.value.data.pop();
memo_changed = true; memo_changed = true;
@@ -221,10 +221,9 @@ watch([recompute_points], () => {
return ''; return '';
} }
const future_number = let last_x = graph_data.x_map(
toValue(graph_data.max_x) + toValue(graph_data.max_update_rate); memo.value.data[memo.value.data.length - 1].x,
);
let last_x = graph_data.x_map(future_number) + smoothing_distance_x;
old_max.value = toValue(graph_data.max_x); old_max.value = toValue(graph_data.max_x);
for (let i = memo.value.data.length - 1; i >= 0; i--) { for (let i = memo.value.data.length - 1; i >= 0; i--) {
@@ -247,12 +246,22 @@ watch([recompute_points], () => {
points.value = new_points; points.value = new_points;
}); });
const current_value = computed(() => { const current_data_point = computed(() => {
const val = value.value; if (memo.value.data.length == 0) {
if (val) { return undefined;
return val.value[telemetry_data.value!.data_type] as number;
} }
return undefined; const cursor_time = toValue(graph_data.cursor_time);
const index = Math.max(memo.value.find_index(cursor_time) - 1, 0);
return memo.value.data[index];
});
const current_data_point_line = computed(() => {
if (!current_data_point.value) {
return '';
}
const x = current_data_point.value.x;
const y = axis_data.y_map(current_data_point.value.y);
return `${graph_data.x_map(x)},${y} ${graph_data.x_map(toValue(graph_data.max_x))},${y}`;
}); });
const legend_x = computed(() => { const legend_x = computed(() => {
@@ -319,12 +328,13 @@ function onCloseLegend() {
:transform="group_transform" :transform="group_transform"
:points="points" :points="points"
></polyline> ></polyline>
<polyline fill="none" :points="current_data_point_line"> </polyline>
</g> </g>
<ValueLabel <ValueLabel
v-if="current_value" v-if="current_data_point"
:x="graph_data.x_map(toValue(graph_data.max_x)) + text_offset" :x="graph_data.x_map(toValue(graph_data.max_x)) + text_offset"
:y="axis_data.y_map(current_value)" :y="axis_data.y_map(current_data_point.y)"
:value="current_value" :value="current_data_point.y"
> >
</ValueLabel> </ValueLabel>
<template v-if="toValue(graph_data.legend_enabled)"> <template v-if="toValue(graph_data.legend_enabled)">

View File

@@ -23,4 +23,5 @@ export interface GraphData {
legend_y: MaybeRefOrGetter<number>; legend_y: MaybeRefOrGetter<number>;
legend_y_stride: MaybeRefOrGetter<number>; legend_y_stride: MaybeRefOrGetter<number>;
legend_width: MaybeRefOrGetter<number>; legend_width: MaybeRefOrGetter<number>;
cursor_time: MaybeRefOrGetter<number>;
} }

View File

@@ -12,7 +12,7 @@ provide(WEBSOCKET_SYMBOL, websocket);
<template> <template>
<div style="width: 100vw; height: 50vh"> <div style="width: 100vw; height: 50vh">
<SvgGraph :legend="GraphSide.Left" right_axis include_controls> <SvgGraph :legend="GraphSide.Left" right_axis include_controls cursor>
<Axis> <Axis>
<Line data="simple_producer/time_offset"></Line> <Line data="simple_producer/time_offset"></Line>
<Line data="simple_producer/publish_offset"></Line> <Line data="simple_producer/publish_offset"></Line>