Files
telemetry_visualization/frontend/src/components/GraphAxis.vue

309 lines
10 KiB
Vue

<script setup lang="ts">
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];
side?: GraphSide;
type?: AxisType;
}>();
const minor_tick_length = computed(() => 4);
const major_tick_length = computed(() => 8);
const text_offset = computed(() => 5);
const side = computed(() => props.side || GraphSide.Right);
const type = computed(() => props.type || AxisType.Linear);
const ticker_locations = [Math.log10(1), Math.log10(2), Math.log10(5)];
ticker_locations.reverse();
const graph_data = inject<GraphData>(GRAPH_DATA)!;
const min_y = ref(Infinity);
const max_y = ref(-Infinity);
const raw_min_y = ref(Infinity);
const raw_max_y = ref(-Infinity);
const axis_update_watch = ref(0);
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;
raw_min_y.value = Infinity;
raw_max_y.value = -Infinity;
});
function update_min_y(y: number) {
if (y < min_y.value) {
min_y.value = y;
}
if (y < raw_min_y.value) {
raw_min_y.value = y;
}
}
function update_max_y(y: number) {
if (y > max_y.value) {
max_y.value = y;
}
if (y > raw_max_y.value) {
raw_max_y.value = y;
}
}
const min_y_value = computed(() => {
if (props.y_limits) {
return props.y_limits[0];
}
if (type.value == AxisType.Linear) {
if (max_y.value > min_y.value) {
const half_diff_y_value = (max_y.value - min_y.value) / 2.0;
const average_y_value = min_y.value + half_diff_y_value;
return average_y_value - half_diff_y_value * 1.05;
} else {
return -1.05;
}
} else {
if (max_y.value > min_y.value) {
const half_diff_y_value =
(Math.log(max_y.value) - Math.log(min_y.value)) / 2.0;
const average_y_value = Math.log(min_y.value) + half_diff_y_value;
return Math.exp(average_y_value - half_diff_y_value * 1.05);
} else {
return 0.01;
}
}
});
const max_y_value = computed(() => {
if (props.y_limits) {
return props.y_limits[1];
}
if (type.value == AxisType.Linear) {
if (max_y.value > min_y.value) {
const half_diff_y_value = (max_y.value - min_y.value) / 2.0;
const average_y_value = min_y.value + half_diff_y_value;
return average_y_value + half_diff_y_value * 1.05;
} else {
return 1.05;
}
} else {
if (max_y.value > min_y.value) {
const half_diff_y_value =
(Math.log(max_y.value) - Math.log(min_y.value)) / 2.0;
const average_y_value = Math.log(min_y.value) + half_diff_y_value;
return Math.exp(average_y_value + half_diff_y_value * 1.05);
} else {
return 1.05;
}
}
});
const y_map = (y: number) => {
const height = toValue(graph_data.height);
const border_top = toValue(graph_data.border_top);
let max_value = toValue(max_y_value);
let min_value = toValue(min_y_value);
let y_value = y;
if (type.value == AxisType.Logarithmic) {
max_value = Math.log(max_value);
min_value = Math.log(min_value);
y_value = Math.log(y_value);
}
const diff_y = max_value - min_value;
return height * (1 - (y_value - min_value) / diff_y) + border_top;
};
provide<AxisData>(AXIS_DATA, {
axis_update_watch: axis_update_watch,
min_y_callback: update_min_y,
max_y_callback: update_max_y,
y_map: y_map,
});
const lines = computed(() => {
const diff_y_val = (max_y_value.value - min_y_value.value) / 2;
const diff_log10 = Math.log10(diff_y_val);
const diff_log10_fraction = diff_log10 - Math.floor(diff_log10);
const ticker_location = ticker_locations.find(
(location) => location <= diff_log10_fraction,
)!;
const grid_spread = Math.pow(10, Math.floor(diff_log10) + ticker_location);
const major_spread = grid_spread / 2;
const minor_spread = major_spread / 2;
const minor_ticks = [];
const major_ticks = [];
const grid_lines = [];
for (
let i = Math.floor(min_y_value.value / grid_spread);
i <= Math.ceil(max_y_value.value / grid_spread);
i++
) {
const y = i * grid_spread;
grid_lines.push(y);
}
for (
let i = Math.floor(min_y_value.value / major_spread);
i <= Math.ceil(max_y_value.value / major_spread);
i++
) {
const y = i * major_spread;
if (grid_lines.indexOf(y) < 0) {
major_ticks.push(y);
}
}
for (
let i = Math.floor(min_y_value.value / minor_spread);
i <= Math.ceil(max_y_value.value / minor_spread);
i++
) {
const y = i * minor_spread;
if (grid_lines.indexOf(y) < 0 && major_ticks.indexOf(y) < 0) {
minor_ticks.push(y);
}
}
return [minor_ticks, major_ticks, grid_lines];
});
</script>
<template>
<template v-if="side == GraphSide.Right">
<g class="minor_tick" clip-path="url(#y_ticker)">
<template v-for="tick of lines[0]" :key="tick">
<polyline
:points="`${graph_data.x_map(toValue(graph_data.max_x)) - minor_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
></polyline>
<text
class="right_edge middle_text"
:x="
graph_data.x_map(toValue(graph_data.max_x)) +
text_offset
"
:y="y_map(tick)"
><NumericText :value="tick" :max_width="7"></NumericText
></text>
</template>
</g>
<g class="major_tick" clip-path="url(#y_ticker)">
<template v-for="tick of lines[1]" :key="tick">
<polyline
:points="`${graph_data.x_map(toValue(graph_data.max_x)) - major_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
></polyline>
<text
class="right_edge middle_text"
:x="
graph_data.x_map(toValue(graph_data.max_x)) +
text_offset
"
:y="y_map(tick)"
><NumericText :value="tick" :max_width="6"></NumericText
></text>
</template>
</g>
<g class="grid_tick" clip-path="url(#y_ticker)">
<template v-for="tick of lines[2]" :key="tick">
<polyline
:points="`${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
></polyline>
<text
class="right_edge middle_text"
:x="
graph_data.x_map(toValue(graph_data.max_x)) +
text_offset
"
:y="y_map(tick)"
>
<NumericText :value="tick" :max_width="5"></NumericText>
</text>
</template>
</g>
</template>
<template v-if="side == GraphSide.Left">
<g class="minor_tick" clip-path="url(#y_ticker)">
<template v-for="tick of lines[0]" :key="tick">
<polyline
:points="`${graph_data.x_map(toValue(graph_data.min_x)) + minor_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)}`"
></polyline>
<text
class="left_edge middle_text"
:x="
graph_data.x_map(toValue(graph_data.min_x)) -
text_offset
"
:y="y_map(tick)"
><NumericText :value="tick" :max_width="7"></NumericText
></text>
</template>
</g>
<g class="major_tick" clip-path="url(#y_ticker)">
<template v-for="tick of lines[1]" :key="tick">
<polyline
:points="`${graph_data.x_map(toValue(graph_data.min_x)) + major_tick_length},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)}`"
></polyline>
<text
class="left_edge middle_text"
:x="
graph_data.x_map(toValue(graph_data.min_x)) -
text_offset
"
:y="y_map(tick)"
><NumericText :value="tick" :max_width="6"></NumericText
></text>
</template>
</g>
<g class="grid_tick" clip-path="url(#y_ticker)">
<template v-for="tick of lines[2]" :key="tick">
<polyline
:points="`${graph_data.x_map(toValue(graph_data.min_x))},${y_map(tick)} ${graph_data.x_map(toValue(graph_data.max_x))},${y_map(tick)}`"
></polyline>
<text
class="left_edge middle_text"
:x="
graph_data.x_map(toValue(graph_data.min_x)) -
text_offset
"
:y="y_map(tick)"
><NumericText :value="tick" :max_width="5"></NumericText
></text>
</template>
</g>
</template>
<slot></slot>
</template>
<style scoped lang="scss">
@use '@/assets/variables';
.right_edge {
text-anchor: start;
}
.left_edge {
text-anchor: end;
}
.middle_text {
dominant-baseline: middle;
font-family: variables.$text-font;
}
.minor_tick {
stroke: variables.$minor-tick;
fill: variables.$minor-tick;
}
.major_tick {
stroke: variables.$major-tick;
fill: variables.$major-tick;
}
.grid_tick {
fill: variables.$grid-line;
stroke: variables.$grid-line;
}
</style>