add graph cursor
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
|||||||
@@ -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)">
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user