add graph cursor
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
@use 'sass:color';
|
||||
|
||||
$gray-0: oklch(100% 0 0);
|
||||
$gray-1: oklch(90% 0 0);
|
||||
$gray-2: oklch(80% 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%);
|
||||
$dark-background-color: color.adjust($background-color, $lightness: -5%);
|
||||
|
||||
$cursor-tick: $gray-0;
|
||||
$time-tick: $gray-1;
|
||||
$grid-line: $gray-1;
|
||||
$major-tick: $gray-4;
|
||||
|
||||
@@ -21,6 +21,7 @@ const props = defineProps<{
|
||||
hide_time_ticks?: boolean;
|
||||
include_controls?: boolean;
|
||||
legend?: GraphSide;
|
||||
cursor?: boolean;
|
||||
}>();
|
||||
|
||||
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 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 diff_x = max_x.value - min_x.value;
|
||||
return (
|
||||
((width.value - border_left.value - border_right.value) *
|
||||
(x - min_x.value)) /
|
||||
diff_x +
|
||||
diff_x.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([]);
|
||||
|
||||
@@ -132,25 +141,6 @@ const legend_x_stride = computed(() => 0);
|
||||
const legend_y_stride = computed(() => 16);
|
||||
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 diff_x = max_x.value - min_x.value;
|
||||
return time_lines.find((duration) => diff_x / duration >= 2)!;
|
||||
@@ -168,6 +158,65 @@ const lines = computed(() => {
|
||||
}
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -182,7 +231,14 @@ const lines = computed(() => {
|
||||
<span>Duration Dropdown</span>
|
||||
</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>
|
||||
<clipPath id="content">
|
||||
<rect
|
||||
@@ -231,6 +287,27 @@ const lines = computed(() => {
|
||||
</template>
|
||||
</g>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -249,6 +326,16 @@ const lines = computed(() => {
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -152,8 +152,8 @@ watch([graph_data.min_x, graph_data.max_x], ([min_x, max_x]) => {
|
||||
|
||||
if (min_x) {
|
||||
while (
|
||||
memo.value.data.length > 2 &&
|
||||
memo.value.data[1].x < toValue(min_x)
|
||||
memo.value.data.length > 1 &&
|
||||
memo.value.data[0].x < toValue(min_x)
|
||||
) {
|
||||
memo.value.data.shift();
|
||||
memo_changed = true;
|
||||
@@ -161,8 +161,8 @@ watch([graph_data.min_x, graph_data.max_x], ([min_x, max_x]) => {
|
||||
}
|
||||
if (max_x) {
|
||||
while (
|
||||
memo.value.data.length > 2 &&
|
||||
memo.value.data[memo.value.data.length - 2].x > toValue(max_x)
|
||||
memo.value.data.length > 1 &&
|
||||
memo.value.data[memo.value.data.length - 1].x > toValue(max_x)
|
||||
) {
|
||||
memo.value.data.pop();
|
||||
memo_changed = true;
|
||||
@@ -221,10 +221,9 @@ watch([recompute_points], () => {
|
||||
return '';
|
||||
}
|
||||
|
||||
const future_number =
|
||||
toValue(graph_data.max_x) + toValue(graph_data.max_update_rate);
|
||||
|
||||
let last_x = graph_data.x_map(future_number) + smoothing_distance_x;
|
||||
let last_x = graph_data.x_map(
|
||||
memo.value.data[memo.value.data.length - 1].x,
|
||||
);
|
||||
old_max.value = toValue(graph_data.max_x);
|
||||
|
||||
for (let i = memo.value.data.length - 1; i >= 0; i--) {
|
||||
@@ -247,12 +246,22 @@ watch([recompute_points], () => {
|
||||
points.value = new_points;
|
||||
});
|
||||
|
||||
const current_value = computed(() => {
|
||||
const val = value.value;
|
||||
if (val) {
|
||||
return val.value[telemetry_data.value!.data_type] as number;
|
||||
const current_data_point = computed(() => {
|
||||
if (memo.value.data.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
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(() => {
|
||||
@@ -319,12 +328,13 @@ function onCloseLegend() {
|
||||
:transform="group_transform"
|
||||
:points="points"
|
||||
></polyline>
|
||||
<polyline fill="none" :points="current_data_point_line"> </polyline>
|
||||
</g>
|
||||
<ValueLabel
|
||||
v-if="current_value"
|
||||
v-if="current_data_point"
|
||||
:x="graph_data.x_map(toValue(graph_data.max_x)) + text_offset"
|
||||
:y="axis_data.y_map(current_value)"
|
||||
:value="current_value"
|
||||
:y="axis_data.y_map(current_data_point.y)"
|
||||
:value="current_data_point.y"
|
||||
>
|
||||
</ValueLabel>
|
||||
<template v-if="toValue(graph_data.legend_enabled)">
|
||||
|
||||
@@ -23,4 +23,5 @@ export interface GraphData {
|
||||
legend_y: MaybeRefOrGetter<number>;
|
||||
legend_y_stride: MaybeRefOrGetter<number>;
|
||||
legend_width: MaybeRefOrGetter<number>;
|
||||
cursor_time: MaybeRefOrGetter<number>;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ provide(WEBSOCKET_SYMBOL, websocket);
|
||||
|
||||
<template>
|
||||
<div style="width: 100vw; height: 50vh">
|
||||
<SvgGraph :legend="GraphSide.Left" right_axis include_controls>
|
||||
<SvgGraph :legend="GraphSide.Left" right_axis include_controls cursor>
|
||||
<Axis>
|
||||
<Line data="simple_producer/time_offset"></Line>
|
||||
<Line data="simple_producer/publish_offset"></Line>
|
||||
|
||||
Reference in New Issue
Block a user