improve numeric labeling

This commit is contained in:
2025-01-01 14:28:05 -05:00
parent ac2014d27d
commit 6a8e076ee7
7 changed files with 124 additions and 57 deletions

View File

@@ -1,18 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject, provide, ref, toValue, watch } from 'vue'; import { computed, inject, provide, ref, toValue, watch } from 'vue';
import { AXIS_DATA, type AxisData, AxisSide, AxisType } from '@/graph/axis'; import { AXIS_DATA, type AxisData, AxisType } from '@/graph/axis';
import { GRAPH_DATA, type GraphData } from '@/graph/graph'; import { GRAPH_DATA, type GraphData, GraphSide } from '@/graph/graph';
import NumericText from '@/components/NumericText.vue';
const props = defineProps<{ const props = defineProps<{
y_limits?: [number, number]; y_limits?: [number, number];
side?: AxisSide; side?: GraphSide;
type?: AxisType; type?: AxisType;
}>(); }>();
const minor_tick_length = computed(() => 4); const minor_tick_length = computed(() => 4);
const major_tick_length = computed(() => 8); const major_tick_length = computed(() => 8);
const text_offset = computed(() => 5); const text_offset = computed(() => 5);
const side = computed(() => props.side || AxisSide.Right); const side = computed(() => props.side || GraphSide.Right);
const type = computed(() => props.type || AxisType.Linear); const type = computed(() => props.type || AxisType.Linear);
const ticker_locations = [Math.log10(1), Math.log10(2), Math.log10(5)]; const ticker_locations = [Math.log10(1), Math.log10(2), Math.log10(5)];
@@ -101,7 +102,7 @@ const max_y_value = computed(() => {
const y_map = (y: number) => { const y_map = (y: number) => {
const height = toValue(graph_data.height); const height = toValue(graph_data.height);
const border_top_bottom = toValue(graph_data.border_top_bottom); const border_top = toValue(graph_data.border_top);
let max_value = toValue(max_y_value); let max_value = toValue(max_y_value);
let min_value = toValue(min_y_value); let min_value = toValue(min_y_value);
let y_value = y; let y_value = y;
@@ -111,7 +112,7 @@ const y_map = (y: number) => {
y_value = Math.log(y_value); y_value = Math.log(y_value);
} }
const diff_y = max_value - min_value; const diff_y = max_value - min_value;
return height * (1 - (y_value - min_value) / diff_y) + border_top_bottom; return height * (1 - (y_value - min_value) / diff_y) + border_top;
}; };
provide<AxisData>(AXIS_DATA, { provide<AxisData>(AXIS_DATA, {
@@ -167,7 +168,7 @@ const lines = computed(() => {
</script> </script>
<template> <template>
<template v-if="side == AxisSide.Right"> <template v-if="side == GraphSide.Right">
<g class="minor_tick" clip-path="url(#y_ticker)"> <g class="minor_tick" clip-path="url(#y_ticker)">
<template v-for="tick of lines[0]" :key="tick"> <template v-for="tick of lines[0]" :key="tick">
<polyline <polyline
@@ -180,8 +181,7 @@ const lines = computed(() => {
text_offset text_offset
" "
:y="y_map(tick)" :y="y_map(tick)"
>{{ tick.toFixed(2) }}</text ><NumericText :value="tick" :max_width="7"></NumericText></text>
>
</template> </template>
</g> </g>
<g class="major_tick" clip-path="url(#y_ticker)"> <g class="major_tick" clip-path="url(#y_ticker)">
@@ -196,7 +196,7 @@ const lines = computed(() => {
text_offset text_offset
" "
:y="y_map(tick)" :y="y_map(tick)"
>{{ tick.toFixed(1) }}</text ><NumericText :value="tick" :max_width="6"></NumericText></text
> >
</template> </template>
</g> </g>
@@ -212,12 +212,14 @@ const lines = computed(() => {
text_offset text_offset
" "
:y="y_map(tick)" :y="y_map(tick)"
>{{ tick.toFixed(0) }}</text >
<NumericText :value="tick" :max_width="5"></NumericText>
</text
> >
</template> </template>
</g> </g>
</template> </template>
<template v-if="side == AxisSide.Left"> <template v-if="side == GraphSide.Left">
<g class="minor_tick" clip-path="url(#y_ticker)"> <g class="minor_tick" clip-path="url(#y_ticker)">
<template v-for="tick of lines[0]" :key="tick"> <template v-for="tick of lines[0]" :key="tick">
<polyline <polyline
@@ -230,8 +232,7 @@ const lines = computed(() => {
text_offset text_offset
" "
:y="y_map(tick)" :y="y_map(tick)"
>{{ tick.toFixed(2) }}</text ><NumericText :value="tick" :max_width="7"></NumericText></text>
>
</template> </template>
</g> </g>
<g class="major_tick" clip-path="url(#y_ticker)"> <g class="major_tick" clip-path="url(#y_ticker)">
@@ -246,8 +247,7 @@ const lines = computed(() => {
text_offset text_offset
" "
:y="y_map(tick)" :y="y_map(tick)"
>{{ tick.toFixed(1) }}</text ><NumericText :value="tick" :max_width="6"></NumericText></text>
>
</template> </template>
</g> </g>
<g class="grid_tick" clip-path="url(#y_ticker)"> <g class="grid_tick" clip-path="url(#y_ticker)">
@@ -262,8 +262,7 @@ const lines = computed(() => {
text_offset text_offset
" "
:y="y_map(tick)" :y="y_map(tick)"
>{{ tick.toFixed(0) }}</text ><NumericText :value="tick" :max_width="5"></NumericText></text>
>
</template> </template>
</g> </g>
</template> </template>

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import { computed, type Ref, watch } from 'vue';
const props = defineProps<{
value: number;
max_width: number;
}>();
const emit = defineEmits<{
(e: 'update', value: string): void
}>()
const display_value = computed(() => {
if (props.value == 0) {
return "0";
}
let precision = props.value.toPrecision(props.max_width - 3);
// Chop off the last character as long as it is a 0
while (precision.length > 0 && precision.charAt(precision.length - 1) == '0') {
precision = precision.substring(0, precision.length - 1);
}
if (precision.length > 0 && precision.charAt(precision.length - 1) == '.') {
precision = precision.substring(0, precision.length - 1);
}
if (precision.includes("e")) {
let fixed = props.value.toFixed(props.max_width - 4);
// Chop off the last character as long as it is a 0
while (fixed.length > 0 && fixed.charAt(fixed.length - 1) == '0') {
fixed = fixed.substring(0, fixed.length - 1);
}
if (fixed.length > 0 && fixed.charAt(fixed.length - 1) == '.') {
fixed = fixed.substring(0, fixed.length - 1);
}
if (fixed.length <= props.max_width) {
return fixed;
}
}
if (precision.length > props.max_width) {
const initial_exponential = props.value.toExponential(props.max_width - 4);
const parts = initial_exponential.split('e');
let left = parts[0];
// Chop off the last character as long as it is a 0
while (left.length > 0 && left.charAt(left.length - 1) == '0') {
left = left.substring(0, left.length - 1);
}
if (left.length > 0 && left.charAt(left.length - 1) == '.') {
left = left.substring(0, left.length - 1);
}
let right = parts[1];
return left + "e" + right;
}
return precision;
});
watch([display_value], ([display_str]) => {
emit('update', display_str);
});
</script>
<template>
{{ display_value }}
</template>
<style scoped lang="scss">
</style>

View File

@@ -8,9 +8,11 @@ const props = defineProps<{
width: number; width: number;
height: number; height: number;
duration?: number; duration?: number;
border_left_right?: number;
border_top_bottom?: number;
utc?: boolean; utc?: boolean;
left_axis?: boolean;
right_axis?: boolean;
hide_time_labels?: boolean;
hide_time_ticks?: boolean;
}>(); }>();
const width = computed(() => { const width = computed(() => {
@@ -60,8 +62,10 @@ const time_lines = [
time_lines.reverse(); time_lines.reverse();
const text_offset = computed(() => 5); const text_offset = computed(() => 5);
const border_left_right = computed(() => props.border_left_right || 0); const border_left = computed(() => props.left_axis ? 96 : 0);
const border_top_bottom = computed(() => props.border_top_bottom || 0); const border_right = computed(() => props.right_axis ? 80 : 0);
const border_top = computed(() => 6);
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);
@@ -69,20 +73,20 @@ const min_x = computed(() => max_x.value - window_duration.value);
const x_map = (x: number) => { const x_map = (x: number) => {
const diff_x = max_x.value - min_x.value; const diff_x = max_x.value - min_x.value;
return ( return (
((width.value - 2 * border_left_right.value) * (x - min_x.value)) / ((width.value - border_left.value - border_right.value) * (x - min_x.value)) /
diff_x + diff_x +
border_left_right.value border_left.value
); );
}; };
const telemetry_lines = ref([]); const telemetry_lines = ref([]);
provide<GraphData>(GRAPH_DATA, { provide<GraphData>(GRAPH_DATA, {
border_top_bottom: border_top_bottom, border_top: border_top,
min_x: min_x, min_x: min_x,
max_x: now, max_x: now,
width: () => width.value - 2 * border_left_right.value, width: () => width.value - border_left.value - border_right.value,
height: () => height.value - 2 * border_top_bottom.value, height: () => height.value - border_top.value - border_bottom.value,
x_map: x_map, x_map: x_map,
lines: telemetry_lines, lines: telemetry_lines,
max_update_rate: 1000 / 10, max_update_rate: 1000 / 10,
@@ -112,25 +116,25 @@ const lines = computed(() => {
<defs> <defs>
<clipPath id="content"> <clipPath id="content">
<rect <rect
:x="border_left_right" :x="border_left"
:y="border_top_bottom" :y="border_top"
:width="width - border_left_right * 2" :width="width - border_left - border_right"
:height="height - border_top_bottom * 2" :height="height - border_top - border_bottom"
></rect> ></rect>
</clipPath> </clipPath>
<clipPath id="y_ticker"> <clipPath id="y_ticker">
<rect <rect
:x="0" :x="0"
:y="border_top_bottom" :y="border_top"
:width="width" :width="width"
:height="height - border_top_bottom * 2" :height="height - border_top - border_bottom"
></rect> ></rect>
</clipPath> </clipPath>
<clipPath id="x_ticker"> <clipPath id="x_ticker">
<rect <rect
:x="border_left_right" :x="border_left"
:y="0" :y="0"
:width="width - border_left_right * 2" :width="width - border_left - border_right"
:height="height" :height="height"
></rect> ></rect>
</clipPath> </clipPath>
@@ -138,12 +142,14 @@ const lines = computed(() => {
<g class="time_tick" clip-path="url(#x_ticker)"> <g class="time_tick" clip-path="url(#x_ticker)">
<template v-for="tick of lines" :key="tick"> <template v-for="tick of lines" :key="tick">
<polyline <polyline
:points="`${x_map(tick)},${border_top_bottom} ${x_map(tick)},${height - border_top_bottom}`" v-if="!hide_time_ticks"
:points="`${x_map(tick)},${border_top} ${x_map(tick)},${height - border_bottom}`"
></polyline> ></polyline>
<TimeText <TimeText
v-if="!hide_time_labels"
class="bottom_edge" class="bottom_edge"
:x="x_map(tick)" :x="x_map(tick)"
:y="height - border_top_bottom + text_offset" :y="height - border_bottom + text_offset"
:timestamp="tick" :timestamp="tick"
:utc="props.utc" :utc="props.utc"
:show_millis="line_duration < 1000" :show_millis="line_duration < 1000"
@@ -167,4 +173,5 @@ const lines = computed(() => {
stroke: variables.$time-tick; stroke: variables.$time-tick;
fill: variables.$time-tick; fill: variables.$time-tick;
} }
</style> </style>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, useTemplateRef, watch } from 'vue'; import { computed, ref, useTemplateRef, watch } from 'vue';
import NumericText from '@/components/NumericText.vue';
const props = defineProps<{ const props = defineProps<{
x: number; x: number;
@@ -12,10 +13,7 @@ const y_offset = computed(() => 9);
const labelRef = useTemplateRef<SVGTextElement>('label-ref'); const labelRef = useTemplateRef<SVGTextElement>('label-ref');
const value_text = computed(() => { const value_text = ref("");
return props.value.toFixed(2);
});
const label_width = ref(0); const label_width = ref(0);
watch( watch(
@@ -27,6 +25,10 @@ watch(
flush: 'post', flush: 'post',
}, },
); );
function update_value_text(text: string) {
value_text.value = text;
}
</script> </script>
<template> <template>
@@ -37,7 +39,7 @@ watch(
:height="16 + background_offset * 2" :height="16 + background_offset * 2"
></rect> ></rect>
<text ref="label-ref" :x="x" :y="y"> <text ref="label-ref" :x="x" :y="y">
{{ value_text }} <NumericText :value="value" :max_width="6" @update="update_value_text"></NumericText>
</text> </text>
</template> </template>

View File

@@ -1,11 +1,5 @@
import type { Ref } from 'vue'; import type { Ref } from 'vue';
export enum AxisSide {
Right,
Left,
Hidden,
}
export enum AxisType { export enum AxisType {
Linear, Linear,
Logarithmic, Logarithmic,

View File

@@ -1,9 +1,15 @@
import type { MaybeRefOrGetter, Ref } from 'vue'; import type { MaybeRefOrGetter, Ref } from 'vue';
export enum GraphSide {
Right,
Left,
Hidden,
}
export const GRAPH_DATA = Symbol(); export const GRAPH_DATA = Symbol();
export interface GraphData { export interface GraphData {
border_top_bottom: MaybeRefOrGetter<number>; border_top: MaybeRefOrGetter<number>;
min_x: MaybeRefOrGetter<number>; min_x: MaybeRefOrGetter<number>;
max_x: MaybeRefOrGetter<number>; max_x: MaybeRefOrGetter<number>;
width: MaybeRefOrGetter<number>; width: MaybeRefOrGetter<number>;

View File

@@ -14,8 +14,7 @@ provide(WEBSOCKET_SYMBOL, websocket);
<Graph <Graph
:width="800" :width="800"
:height="400" :height="400"
:border_top_bottom="24" :right_axis="true"
:border_left_right="128"
> >
<Axis> <Axis>
<Line data="simple_producer/time_offset"></Line> <Line data="simple_producer/time_offset"></Line>
@@ -26,9 +25,8 @@ provide(WEBSOCKET_SYMBOL, websocket);
<Graph <Graph
:width="800" :width="800"
:height="400" :height="400"
:border_top_bottom="24"
:border_left_right="128"
:duration="60 * 1000 * 10" :duration="60 * 1000 * 10"
:right_axis="true"
> >
<Axis> <Axis>
<Line <Line
@@ -68,16 +66,12 @@ provide(WEBSOCKET_SYMBOL, websocket);
<Graph <Graph
:width="800" :width="800"
:height="400" :height="400"
:border_top_bottom="24"
:border_left_right="128"
:duration="5 * 1000" :duration="5 * 1000"
> >
</Graph> </Graph>
<Graph <Graph
:width="800" :width="800"
:height="400" :height="400"
:border_top_bottom="24"
:border_left_right="128"
:duration="2 * 1000" :duration="2 * 1000"
> >
</Graph> </Graph>