adds charts panel
This commit is contained in:
0
frontend/src/assets/icons.scss
Normal file
0
frontend/src/assets/icons.scss
Normal file
@@ -37,11 +37,21 @@
|
|||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content.full {
|
||||||
|
margin: 5vh 5vw;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
.screen.content {
|
.screen.content {
|
||||||
width: calc(100% - 20vw);
|
width: calc(100% - 20vw);
|
||||||
height: calc(100vh - 40vh);
|
height: calc(100vh - 40vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.screen.content.full {
|
||||||
|
width: calc(100% - 10vw);
|
||||||
|
height: calc(100vh - 10vh);
|
||||||
|
}
|
||||||
|
|
||||||
.stretch {
|
.stretch {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
@@ -54,6 +64,10 @@
|
|||||||
flex-grow: 2;
|
flex-grow: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.no-basis {
|
.no-basis {
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ body {
|
|||||||
align-content: center;
|
align-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
stroke-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@@ -28,10 +29,13 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
stroke-width: 0;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
polyline {
|
polyline {
|
||||||
stroke-width: 1px;
|
stroke-width: 1px;
|
||||||
}
|
}
|
||||||
|
|||||||
50
frontend/src/components/InlineIcon.vue
Normal file
50
frontend/src/components/InlineIcon.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
icon: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<i :class="`icon ${icon}`"></i>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '@/assets/variables';
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:before {
|
||||||
|
display: inline-block;
|
||||||
|
content: ' ';
|
||||||
|
background-color: variables.$text-color;
|
||||||
|
mask-size: cover;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.hamburger.menu:before {
|
||||||
|
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9IjAgMCAxMCAxMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlV2lkdGg9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIxIiB4Mj0iMTAiIHkyPSIxIj48L2xpbmU+PGxpbmUgeDE9IjAiIHkxPSI1IiB4Mj0iMTAiIHkyPSI1Ij48L2xpbmU+PGxpbmUgeDE9IjAiIHkxPSI5IiB4Mj0iMTAiIHkyPSI5Ij48L2xpbmU+PC9zdmc+IA==');
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.close:before {
|
||||||
|
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9IjAgMCAxMCAxMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlV2lkdGg9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIwIiB4Mj0iMTAiIHkyPSIxMCI+PC9saW5lPjxsaW5lIHgxPSIxMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMTAiPjwvbGluZT48L3N2Zz4g');
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.up.arrow:before {
|
||||||
|
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9IjAgMCAxMCAxMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlV2lkdGg9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIxMCIgeDI9IjUiIHkyPSIwIj48L2xpbmU+PGxpbmUgeDE9IjUiIHkxPSIwIiB4Mj0iMTAiIHkyPSIxMCI+PC9saW5lPjwvc3ZnPg==');
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon.down.arrow:before {
|
||||||
|
mask-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9IjAgMCAxMCAxMCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlV2lkdGg9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIwIiB4Mj0iNSIgeTI9IjEwIj48L2xpbmU+PGxpbmUgeDE9IjUiIHkxPSIxMCIgeDI9IjEwIiB5Mj0iMCI+PC9saW5lPjwvc3ZnPg==');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox="0 0 10 10" stroke="white" strokeWidth="1px">
|
||||||
|
<line x1="0" y1="0" x2="5" y2="10"></line>
|
||||||
|
<line x1="5" y1="10" x2="10" y2="0"></line>
|
||||||
|
</svg>
|
||||||
|
*/
|
||||||
|
</style>
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
getDurationString,
|
getDurationString,
|
||||||
parseDurationString,
|
parseDurationString,
|
||||||
} from '@/datetime';
|
} from '@/datetime';
|
||||||
|
import { onDocumentShown } from '@/composables/document.ts';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initial_duration?: number;
|
initial_duration?: number;
|
||||||
@@ -32,7 +33,7 @@ const props = defineProps<{
|
|||||||
const divRef = useTemplateRef<HTMLDivElement>('graph-div');
|
const divRef = useTemplateRef<HTMLDivElement>('graph-div');
|
||||||
|
|
||||||
const width = ref(0);
|
const width = ref(0);
|
||||||
const height = ref(0);
|
const raw_height = ref(0);
|
||||||
|
|
||||||
const controls_height = 32;
|
const controls_height = 32;
|
||||||
const min_time_label_separation = 250;
|
const min_time_label_separation = 250;
|
||||||
@@ -41,13 +42,18 @@ const resize_observer = new ResizeObserver((elements) => {
|
|||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
if (element.target == divRef.value) {
|
if (element.target == divRef.value) {
|
||||||
width.value = element.contentBoxSize[0].inlineSize;
|
width.value = element.contentBoxSize[0].inlineSize;
|
||||||
height.value =
|
raw_height.value = element.contentBoxSize[0].blockSize;
|
||||||
element.contentBoxSize[0].blockSize -
|
|
||||||
(props.include_controls ? controls_height : 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const height = computed(() => {
|
||||||
|
return Math.max(
|
||||||
|
raw_height.value - (props.include_controls ? controls_height : 0),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
watch([divRef], ([divRef]) => {
|
watch([divRef], ([divRef]) => {
|
||||||
if (divRef) {
|
if (divRef) {
|
||||||
resize_observer.observe(divRef);
|
resize_observer.observe(divRef);
|
||||||
@@ -132,6 +138,10 @@ watch([now], ([now_value]) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDocumentShown(() => {
|
||||||
|
fetch_history.value++;
|
||||||
|
});
|
||||||
|
|
||||||
const max_x_text = computed({
|
const max_x_text = computed({
|
||||||
// getter
|
// getter
|
||||||
get() {
|
get() {
|
||||||
@@ -193,8 +203,12 @@ 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);
|
||||||
|
|
||||||
|
const graph_width = computed(() => {
|
||||||
|
return Math.max(width.value - border_left.value - border_right.value, 1);
|
||||||
|
});
|
||||||
|
|
||||||
const line_duration = computed(() => {
|
const line_duration = computed(() => {
|
||||||
const width_px = width.value;
|
const width_px = graph_width.value;
|
||||||
const diff_x = max_x.value - min_x.value;
|
const diff_x = max_x.value - min_x.value;
|
||||||
return time_lines.find((duration) => {
|
return time_lines.find((duration) => {
|
||||||
const line_count = diff_x / duration;
|
const line_count = diff_x / duration;
|
||||||
@@ -263,8 +277,7 @@ provide<GraphData>(GRAPH_DATA, {
|
|||||||
max_temporal_resolution: max_temporal_resolution,
|
max_temporal_resolution: max_temporal_resolution,
|
||||||
live: live,
|
live: live,
|
||||||
fetch_history: fetch_history,
|
fetch_history: fetch_history,
|
||||||
width: () =>
|
width: graph_width,
|
||||||
Math.max(width.value - border_left.value - border_right.value, 0),
|
|
||||||
height: () =>
|
height: () =>
|
||||||
Math.max(height.value - border_top.value - border_bottom.value, 0),
|
Math.max(height.value - border_top.value - border_bottom.value, 0),
|
||||||
x_map: x_map,
|
x_map: x_map,
|
||||||
|
|||||||
@@ -6,17 +6,35 @@ import Axis from '@/components/GraphAxis.vue';
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
telemetry_definition: TelemetryDefinition | null;
|
selection: TelemetryDefinition | null;
|
||||||
secondary: TelemetryDefinition | null;
|
mouseover: TelemetryDefinition | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const telemetry_definition = computed(() => {
|
||||||
|
if (props.mouseover) {
|
||||||
|
return props.mouseover;
|
||||||
|
}
|
||||||
|
return props.selection;
|
||||||
|
});
|
||||||
|
|
||||||
|
const secondary = computed(() => {
|
||||||
|
if (props.mouseover) {
|
||||||
|
if (props.selection) {
|
||||||
|
if (props.selection.uuid != props.mouseover.uuid) {
|
||||||
|
return props.selection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const lines = computed(() => {
|
const lines = computed(() => {
|
||||||
const result = [];
|
const result = [];
|
||||||
if (props.secondary) {
|
if (secondary.value) {
|
||||||
result.push(props.secondary);
|
result.push(secondary.value);
|
||||||
}
|
}
|
||||||
if (props.telemetry_definition) {
|
if (telemetry_definition.value) {
|
||||||
result.push(props.telemetry_definition);
|
result.push(telemetry_definition.value);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ function onMouseExit(event: MouseEvent) {
|
|||||||
</g>
|
</g>
|
||||||
<ValueLabel
|
<ValueLabel
|
||||||
v-if="current_data_point"
|
v-if="current_data_point"
|
||||||
class="fade_other_selected"
|
class="fade_other_selected label"
|
||||||
: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_data_point.y)"
|
:y="axis_data.y_map(current_data_point.y)"
|
||||||
:value="current_data_point.y"
|
:value="current_data_point.y"
|
||||||
@@ -473,6 +473,10 @@ function onMouseExit(event: MouseEvent) {
|
|||||||
opacity: 25%;
|
opacity: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade .fade_other_selected.label {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.fade .no-fade .fade_other_selected {
|
.fade .no-fade .fade_other_selected {
|
||||||
opacity: 100%;
|
opacity: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ const props = defineProps<{
|
|||||||
search?: string;
|
search?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const selected = defineModel<TelemetryDefinition | null>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(
|
(e: 'mouseover', tlm_entry: TelemetryDefinition | null): void;
|
||||||
e: 'select',
|
|
||||||
tlm_entry: [TelemetryDefinition | null, TelemetryDefinition | null],
|
|
||||||
): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const search_value = computed(() => (props.search || '').toLowerCase());
|
const search_value = computed(() => (props.search || '').toLowerCase());
|
||||||
@@ -33,7 +32,6 @@ const sorted_tlm_data = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mousedover = ref<TelemetryDefinition | null>(null);
|
const mousedover = ref<TelemetryDefinition | null>(null);
|
||||||
const selected = ref<TelemetryDefinition | null>(null);
|
|
||||||
|
|
||||||
function onMouseover(tlm_entry: TelemetryDefinition) {
|
function onMouseover(tlm_entry: TelemetryDefinition) {
|
||||||
mousedover.value = tlm_entry;
|
mousedover.value = tlm_entry;
|
||||||
@@ -47,17 +45,8 @@ function onClick(tlm_entry: TelemetryDefinition) {
|
|||||||
selected.value = tlm_entry;
|
selected.value = tlm_entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([mousedover, selected], ([mousedover_val, selected_val]) => {
|
watch([mousedover], ([mousedover_val]) => {
|
||||||
if (mousedover_val) {
|
emit('mouseover', mousedover_val);
|
||||||
emit('select', [
|
|
||||||
mousedover_val,
|
|
||||||
mousedover_val.uuid != selected_val?.uuid ? selected_val : null,
|
|
||||||
]);
|
|
||||||
} else if (selected_val) {
|
|
||||||
emit('select', [selected_val, null]);
|
|
||||||
} else {
|
|
||||||
emit('select', [null, null]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
24
frontend/src/composables/document.ts
Normal file
24
frontend/src/composables/document.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
|
export function onDocumentVisibilityChange(
|
||||||
|
handler: (visible: boolean) => void,
|
||||||
|
) {
|
||||||
|
const handlerRef = ref(() => {
|
||||||
|
handler(!document.hidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('visibilitychange', handlerRef.value);
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('visibilitychange', handlerRef.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onDocumentShown(handler: () => void) {
|
||||||
|
onDocumentVisibilityChange((visible) => {
|
||||||
|
if (visible) {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
} from 'vue';
|
} from 'vue';
|
||||||
import type { TelemetryDefinition } from '@/composables/telemetry';
|
import type { TelemetryDefinition } from '@/composables/telemetry';
|
||||||
|
import { onDocumentVisibilityChange } from '@/composables/document.ts';
|
||||||
|
|
||||||
export interface TelemetryDataItem {
|
export interface TelemetryDataItem {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -27,12 +28,14 @@ export class WebsocketHandle {
|
|||||||
websocket: WebSocket | null;
|
websocket: WebSocket | null;
|
||||||
should_be_connected: boolean;
|
should_be_connected: boolean;
|
||||||
connected: Ref<boolean>;
|
connected: Ref<boolean>;
|
||||||
|
enabled: Ref<boolean>;
|
||||||
on_telem_value: Map<string, Array<(value: TelemetryDataItem) => void>>;
|
on_telem_value: Map<string, Array<(value: TelemetryDataItem) => void>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.websocket = null;
|
this.websocket = null;
|
||||||
this.should_be_connected = false;
|
this.should_be_connected = false;
|
||||||
this.connected = ref(false);
|
this.connected = ref(false);
|
||||||
|
this.enabled = ref(true);
|
||||||
this.on_telem_value = new Map();
|
this.on_telem_value = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,9 +122,9 @@ export class WebsocketHandle {
|
|||||||
const is_live = computed(() => toValue(live));
|
const is_live = computed(() => toValue(live));
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[uuid, this.connected, minimum_separation, is_live],
|
[uuid, this.connected, this.enabled, minimum_separation, is_live],
|
||||||
([uuid_value, connected, min_sep, live_value]) => {
|
([uuid_value, connected, enabled, min_sep, live_value]) => {
|
||||||
if (connected && uuid_value && live_value) {
|
if (connected && enabled && uuid_value && live_value) {
|
||||||
this.websocket?.send(
|
this.websocket?.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
RegisterTlmListener: {
|
RegisterTlmListener: {
|
||||||
@@ -160,6 +163,14 @@ export class WebsocketHandle {
|
|||||||
|
|
||||||
return value_result;
|
return value_result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
this.enabled.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resume() {
|
||||||
|
this.enabled.value = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WEBSOCKET_SYMBOL = Symbol();
|
export const WEBSOCKET_SYMBOL = Symbol();
|
||||||
@@ -170,6 +181,13 @@ export function useWebsocket() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
handle.value.connect();
|
handle.value.connect();
|
||||||
});
|
});
|
||||||
|
onDocumentVisibilityChange((visible) => {
|
||||||
|
if (visible) {
|
||||||
|
handle.value.resume();
|
||||||
|
} else {
|
||||||
|
handle.value.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
handle.value.disconnect();
|
handle.value.disconnect();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { RouteLocationRaw } from 'vue-router';
|
import type { RouteLocationRaw } from 'vue-router';
|
||||||
|
|
||||||
export enum PanelHeirarchyType {
|
export enum PanelHeirarchyType {
|
||||||
LEAF,
|
LEAF = 'leaf',
|
||||||
FOLDER,
|
FOLDER = 'folder',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PanelHeirarchyLeaf = {
|
export type PanelHeirarchyLeaf = {
|
||||||
@@ -37,6 +37,12 @@ export function getPanelHeirarchy(): PanelHeirarchyChildren {
|
|||||||
type: PanelHeirarchyType.LEAF,
|
type: PanelHeirarchyType.LEAF,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
name: 'Chart',
|
||||||
|
to: { name: 'chart' },
|
||||||
|
type: PanelHeirarchyType.LEAF,
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ const router = createRouter({
|
|||||||
name: 'list',
|
name: 'list',
|
||||||
component: () => import('../views/TelemetryListView.vue'),
|
component: () => import('../views/TelemetryListView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/chart',
|
||||||
|
name: 'chart',
|
||||||
|
component: () => import('../views/ChartView.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
309
frontend/src/views/ChartDefinitionView.vue
Normal file
309
frontend/src/views/ChartDefinitionView.vue
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import FlexDivider from '@/components/FlexDivider.vue';
|
||||||
|
import TelemetryList from '@/components/TelemetryList.vue';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import TextInput from '@/components/TextInput.vue';
|
||||||
|
import type { TelemetryDefinition } from '@/composables/telemetry.ts';
|
||||||
|
import InlineIcon from '@/components/InlineIcon.vue';
|
||||||
|
|
||||||
|
const model = defineModel<TelemetryDefinition[][][]>({
|
||||||
|
required: true,
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const selected = ref(0);
|
||||||
|
const selected_cell_x = ref(0);
|
||||||
|
const selected_cell_y = ref(0);
|
||||||
|
|
||||||
|
const searchValue = ref('');
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
[1, 1],
|
||||||
|
[2, 1],
|
||||||
|
[1, 2],
|
||||||
|
[2, 2],
|
||||||
|
[3, 2],
|
||||||
|
];
|
||||||
|
|
||||||
|
function selectOption(i: number) {
|
||||||
|
selected.value = i;
|
||||||
|
if (selected_cell_x.value >= options[i][0]) {
|
||||||
|
selected_cell_x.value = 0;
|
||||||
|
}
|
||||||
|
if (selected_cell_y.value >= options[i][1]) {
|
||||||
|
selected_cell_y.value = 0;
|
||||||
|
}
|
||||||
|
const initial_cells = model.value;
|
||||||
|
const result_cells: TelemetryDefinition[][][] = [];
|
||||||
|
for (let x = 0; x < options[i][0]; x++) {
|
||||||
|
result_cells.push([]);
|
||||||
|
for (let y = 0; y < options[i][1]; y++) {
|
||||||
|
if (x < initial_cells.length && y < initial_cells[x].length) {
|
||||||
|
result_cells[x].push(initial_cells[x][y]);
|
||||||
|
} else {
|
||||||
|
result_cells[x].push([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.value = result_cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const model_value = model.value;
|
||||||
|
let s = 0;
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
// X length correct
|
||||||
|
if (options[i][0] == model_value.length) {
|
||||||
|
// Y length correct
|
||||||
|
if (options[i][1] == model_value[0].length) {
|
||||||
|
// selectOption(i);
|
||||||
|
s = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to option 0
|
||||||
|
selectOption(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectCell(x: number, y: number) {
|
||||||
|
selected_cell_x.value = x;
|
||||||
|
selected_cell_y.value = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTelemetry(telemetry: TelemetryDefinition | null) {
|
||||||
|
if (telemetry != null) {
|
||||||
|
if (
|
||||||
|
!model.value[selected_cell_x.value][selected_cell_y.value].includes(
|
||||||
|
telemetry,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
model.value[selected_cell_x.value][selected_cell_y.value].push(
|
||||||
|
telemetry,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUp(index: number) {
|
||||||
|
if (index > 0) {
|
||||||
|
const removed = model.value[selected_cell_x.value][
|
||||||
|
selected_cell_y.value
|
||||||
|
].splice(index - 1, 2);
|
||||||
|
removed.reverse();
|
||||||
|
model.value[selected_cell_x.value][selected_cell_y.value].splice(
|
||||||
|
index - 1,
|
||||||
|
0,
|
||||||
|
...removed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDown(index: number) {
|
||||||
|
if (
|
||||||
|
index + 1 <
|
||||||
|
model.value[selected_cell_x.value][selected_cell_y.value].length
|
||||||
|
) {
|
||||||
|
const removed = model.value[selected_cell_x.value][
|
||||||
|
selected_cell_y.value
|
||||||
|
].splice(index, 2);
|
||||||
|
removed.reverse();
|
||||||
|
model.value[selected_cell_x.value][selected_cell_y.value].splice(
|
||||||
|
index,
|
||||||
|
0,
|
||||||
|
...removed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTelemetry(index: number) {
|
||||||
|
model.value[selected_cell_x.value][selected_cell_y.value].splice(index, 1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row grow stretch">
|
||||||
|
<div class="column grow stretch no-basis">
|
||||||
|
<div class="grow no-basis type_grid no-min-height scroll">
|
||||||
|
<svg
|
||||||
|
v-for="(option, i) in options"
|
||||||
|
:key="i"
|
||||||
|
:class="`layout ${selected == i ? 'selected' : ''}`"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
@click="selectOption(i)"
|
||||||
|
>
|
||||||
|
<template v-for="x in option[0]" :key="x">
|
||||||
|
<template v-for="y in option[1]" :key="y">
|
||||||
|
<rect
|
||||||
|
:x="1 + ((x - 1) * 98) / option[0]"
|
||||||
|
:y="1 + ((y - 1) * 98) / option[1]"
|
||||||
|
:width="98 / option[0]"
|
||||||
|
:height="98 / option[1]"
|
||||||
|
></rect>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<FlexDivider class="horizontal_divider_margin"></FlexDivider>
|
||||||
|
<div class="row grow center no-basis">
|
||||||
|
<div
|
||||||
|
class="selected_grid grow"
|
||||||
|
:style="`grid-template: repeat(${options[selected][1]}, 1fr) / repeat(${options[selected][0]}, 1fr);`"
|
||||||
|
>
|
||||||
|
<template v-for="y in options[selected][1]" :key="y">
|
||||||
|
<template v-for="x in options[selected][0]" :key="x">
|
||||||
|
<div
|
||||||
|
:class="`cell ${selected_cell_x == x - 1 && selected_cell_y == y - 1 ? 'selected' : ''}`"
|
||||||
|
@click="selectCell(x - 1, y - 1)"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FlexDivider></FlexDivider>
|
||||||
|
<div class="column grow2 stretch no-basis">
|
||||||
|
<div class="row grow stretch no-basis">
|
||||||
|
<div class="column grow stretch">
|
||||||
|
<template v-if="model.length > 0">
|
||||||
|
<div
|
||||||
|
class="row chosen"
|
||||||
|
v-for="(selected, i) in model[selected_cell_x][
|
||||||
|
selected_cell_y
|
||||||
|
]"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{{ selected.name }}
|
||||||
|
</span>
|
||||||
|
<span class="grow"></span>
|
||||||
|
<div class="column tiny_text">
|
||||||
|
<InlineIcon
|
||||||
|
icon="up arrow"
|
||||||
|
:class="`${i == 0 ? 'hidden' : 'button'}`"
|
||||||
|
@click="moveUp(i)"
|
||||||
|
></InlineIcon>
|
||||||
|
<InlineIcon
|
||||||
|
icon="down arrow"
|
||||||
|
:class="`${i == model[selected_cell_x][selected_cell_y].length - 1 ? 'hidden' : 'button'}`"
|
||||||
|
@click="moveDown(i)"
|
||||||
|
></InlineIcon>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="close icon button"
|
||||||
|
@click="removeTelemetry(i)"
|
||||||
|
>
|
||||||
|
<InlineIcon icon="close"></InlineIcon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FlexDivider class="horizontal_divider_margin"></FlexDivider>
|
||||||
|
<div class="row grow stretch no-basis">
|
||||||
|
<div class="column grow stretch no-basis">
|
||||||
|
<div class="row">
|
||||||
|
<TextInput
|
||||||
|
autofocus
|
||||||
|
class="grow"
|
||||||
|
v-model="searchValue"
|
||||||
|
placeholder="Search"
|
||||||
|
></TextInput>
|
||||||
|
</div>
|
||||||
|
<div class="row scroll grow no-min-height no-basis">
|
||||||
|
<div class="column grow stretch">
|
||||||
|
<TelemetryList
|
||||||
|
:search="searchValue"
|
||||||
|
:model-value="null"
|
||||||
|
@update:model-value="
|
||||||
|
(value) => selectTelemetry(value)
|
||||||
|
"
|
||||||
|
></TelemetryList>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '@/assets/variables';
|
||||||
|
|
||||||
|
.type_grid {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
grid-auto-rows: max-content;
|
||||||
|
gap: 1em;
|
||||||
|
justify-items: stretch;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type_grid > * {
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout {
|
||||||
|
stroke: variables.$grid-line;
|
||||||
|
stroke-width: 1px;
|
||||||
|
fill: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: variables.$light-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected_grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1em;
|
||||||
|
justify-items: stretch;
|
||||||
|
align-self: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
align-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
border: variables.$grid-line 1px solid;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal_divider_margin {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chosen {
|
||||||
|
padding: 0.3em;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: variables.$gray-3 solid 1px;
|
||||||
|
border-top: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chosen:first-child {
|
||||||
|
border-top: variables.$gray-3 solid 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chosen:hover {
|
||||||
|
background-color: variables.$light2-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close.icon.button {
|
||||||
|
height: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.icon.button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column.tiny_text {
|
||||||
|
font-size: variables.$normal-text-size / 2;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
51
frontend/src/views/ChartRenderView.vue
Normal file
51
frontend/src/views/ChartRenderView.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TelemetryDefinition } from '@/composables/telemetry.ts';
|
||||||
|
import SvgGraph from '@/components/SvgGraph.vue';
|
||||||
|
import { GraphSide } from '@/graph/graph.ts';
|
||||||
|
import GraphAxis from '@/components/GraphAxis.vue';
|
||||||
|
import TelemetryLine from '@/components/TelemetryLine.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
charts: TelemetryDefinition[][][];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="chart grid grow"
|
||||||
|
:style="`grid-template: repeat(${charts[0].length} , 1fr) / repeat(${charts.length}, 1fr);`"
|
||||||
|
v-if="charts.length > 0"
|
||||||
|
>
|
||||||
|
<template v-for="y in charts[0].length" :key="y">
|
||||||
|
<template v-for="x in charts.length" :key="x">
|
||||||
|
<div class="no-min-height">
|
||||||
|
<SvgGraph
|
||||||
|
right_axis
|
||||||
|
cursor
|
||||||
|
:legend="GraphSide.Left"
|
||||||
|
include_controls
|
||||||
|
>
|
||||||
|
<GraphAxis>
|
||||||
|
<TelemetryLine
|
||||||
|
v-for="tlm in charts[x - 1][y - 1]"
|
||||||
|
:key="tlm.uuid"
|
||||||
|
:data="tlm.name"
|
||||||
|
></TelemetryLine>
|
||||||
|
</GraphAxis>
|
||||||
|
</SvgGraph>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '@/assets/variables';
|
||||||
|
|
||||||
|
.chart.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
place-content: stretch;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
frontend/src/views/ChartView.vue
Normal file
50
frontend/src/views/ChartView.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import InlineIcon from '@/components/InlineIcon.vue';
|
||||||
|
import ChartDefinitionView from '@/views/ChartDefinitionView.vue';
|
||||||
|
import type { TelemetryDefinition } from '@/composables/telemetry.ts';
|
||||||
|
import ChartRenderView from '@/views/ChartRenderView.vue';
|
||||||
|
|
||||||
|
const settingsOpen = ref(true);
|
||||||
|
const settings = ref<TelemetryDefinition[][][]>([]);
|
||||||
|
|
||||||
|
function openSettings() {
|
||||||
|
settingsOpen.value = true;
|
||||||
|
}
|
||||||
|
function closeSettings() {
|
||||||
|
settingsOpen.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="column stretch screen content full center divider">
|
||||||
|
<template v-if="!settingsOpen">
|
||||||
|
<div class="settings column center" @click="openSettings">
|
||||||
|
<InlineIcon icon="hamburger menu"></InlineIcon>
|
||||||
|
</div>
|
||||||
|
<ChartRenderView :charts="settings"></ChartRenderView>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="settings column center" @click="closeSettings">
|
||||||
|
<InlineIcon icon="close"></InlineIcon>
|
||||||
|
</div>
|
||||||
|
<ChartDefinitionView v-model="settings"></ChartDefinitionView>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use '@/assets/variables';
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: variables.$dark-background-color;
|
||||||
|
border: 1px solid variables.$light2-background-color;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 1px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,9 +8,8 @@ import FlexDivider from '@/components/FlexDivider.vue';
|
|||||||
|
|
||||||
const searchValue = ref('');
|
const searchValue = ref('');
|
||||||
|
|
||||||
const selectValue = ref<
|
const selected = ref<TelemetryDefinition | null>(null);
|
||||||
[TelemetryDefinition | null, TelemetryDefinition | null]
|
const mousedover = ref<TelemetryDefinition | null>(null);
|
||||||
>([null, null]);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -29,7 +28,11 @@ const selectValue = ref<
|
|||||||
<div class="column grow stretch">
|
<div class="column grow stretch">
|
||||||
<TelemetryList
|
<TelemetryList
|
||||||
:search="searchValue"
|
:search="searchValue"
|
||||||
@select="(selection) => (selectValue = selection)"
|
v-model="selected"
|
||||||
|
@mouseover="
|
||||||
|
(mousedover_value) =>
|
||||||
|
(mousedover = mousedover_value)
|
||||||
|
"
|
||||||
></TelemetryList>
|
></TelemetryList>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,8 +40,8 @@ const selectValue = ref<
|
|||||||
<FlexDivider></FlexDivider>
|
<FlexDivider></FlexDivider>
|
||||||
<div class="column grow stretch no-basis">
|
<div class="column grow stretch no-basis">
|
||||||
<TelemetryInfo
|
<TelemetryInfo
|
||||||
:telemetry_definition="selectValue[0]"
|
:mouseover="mousedover"
|
||||||
:secondary="selectValue[1]"
|
:selection="selected"
|
||||||
></TelemetryInfo>
|
></TelemetryInfo>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user