From 26ce19c9229fe118e266f0965faed88d7dbfdfee Mon Sep 17 00:00:00 2001 From: Zirui Cai <74649535+Feudalman@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:58:23 +0800 Subject: [PATCH] Feature/keyboard (#389) * feat: exit by esc * feat: turn page by keyboard * fix: delete experiment * restore create_experiment.py * Update create_experiment.py * Update create_experiment.py * feat: click to switch images * fix: same file name * feat: turn page by arrow in audio chart modal --------- Co-authored-by: KAAANG --- .vscode/settings.json | 9 ++- swanlab/server/controller/experiment.py | 7 ++- test/create_experiment.py | 68 +++++++++++++++++--- vue/src/charts/components/SlideBar.vue | 28 ++++++++- vue/src/charts/modules/TextDetail.vue | 8 ++- vue/src/charts/modules/TextModule.vue | 17 ++++- vue/src/charts/package/AudioChart.vue | 3 +- vue/src/charts/package/ImageChart.vue | 82 ++++++++++++++++++++++--- vue/src/charts/package/LineChart.vue | 2 +- vue/src/charts/package/TextChart.vue | 13 +++- vue/src/components/SLModal.vue | 33 +++++++++- 11 files changed, 235 insertions(+), 35 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 23e3a10a5..acc4ae4b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -56,7 +56,8 @@ "todo-tree.general.tags": [ "TODO", // 待办 "FIXME", // 待修复 - "COMPAT" // 兼容性问题 + "COMPAT", // 兼容性问题 + "WARNING" // 警告 ], "todo-tree.highlights.customHighlight": { "TODO": { @@ -65,6 +66,12 @@ "foreground": "#ffff00", "iconColour": "#ffff" }, + "WARNING": { + "icon": "alert", + "type": "tag", + "foreground": "#ff0000", + "iconColour": "#ff0000" + }, "FIXME": { "icon": "flame", "type": "tag", diff --git a/swanlab/server/controller/experiment.py b/swanlab/server/controller/experiment.py index 19126937b..97b52ac1a 100644 --- a/swanlab/server/controller/experiment.py +++ b/swanlab/server/controller/experiment.py @@ -560,6 +560,7 @@ def delete_experiment(experiment_id: int): tag_names = [tag["name"] for tag in __to_list(tags)] # 检查多实验图表是否有需要删除的 project_id = Experiment.get_by_id(experiment_id).project_id.id + # 找到属于多实验且与该实验相关的图表 charts = Chart.filter(Chart.project_id == project_id, Chart.name.in_(tag_names)) db = connect() @@ -567,9 +568,11 @@ def delete_experiment(experiment_id: int): # 必须先清除数据库中的实验数据 Experiment.delete().where(Experiment.id == experiment_id).execute() # 图表无 source 的需要删除 + del_list = [] for chart in charts: - if len(chart.sources) == 0: - chart.delete().execute() + if chart.sources.count() == 0: + del_list.append(chart.id) + Chart.delete().where(Chart.id.in_(del_list)).execute() db.commit() return SUCCESS_200({"experiment_id": experiment_id}) diff --git a/test/create_experiment.py b/test/create_experiment.py index 5892a6fa7..2ddf98673 100644 --- a/test/create_experiment.py +++ b/test/create_experiment.py @@ -1,19 +1,67 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +r""" +@DATE: 2024-03-04 13:26:59 +@File: test/create_experiment.py +@IDE: vscode +@Description: + 创建一个文件,作为测试用例 + WARNING 请勿随意修改此文件,以免影响测试效果 +""" import swanlab +import time import random +import numpy as np +epochs = 50 +lr = 0.01 offset = random.random() / 5 - -run = swanlab.init( - experiment_name="Example", - description="这是一个机器学习模拟实验", +# 初始化 +swanlab.init( + log_level="debug", config={ - "learning_rate": 0.01, - "epochs": 20, + "epochs": epochs, + "learning_rate": lr, + "test": 1, + "debug": "这是一串" + "很长" * 100 + "的字符串", + "verbose": 1, }, + logggings=True, ) - -# 模拟机器学习训练过程 -for epoch in range(2, run.config.epochs): +# 模拟训练 +for epoch in range(2, epochs): acc = 1 - 2**-epoch - random.random() / epoch - offset loss = 2**-epoch + random.random() / epoch + offset - swanlab.log({"loss": loss, "accuracy": acc}) + loss2 = 3**-epoch + random.random() / epoch + offset * 3 + print(f"epoch={epoch}, accuracy={acc}, loss={loss}") + if epoch % 10 == 0: + # 测试audio + sample_rate = 44100 + test_audio_arr = np.random.randn(2, 100000) + swanlab.log( + { + "test/audio": [swanlab.Audio(test_audio_arr, sample_rate, caption="test")] * (epoch // 10), + }, + step=epoch, + ) + # 测试image + test_image = np.random.randint(0, 255, (100, 100, 3)) + swanlab.log( + { + "test/image": [swanlab.Image(test_image, caption="test")] * (epoch // 10), + }, + step=epoch, + ) + # 测试text + swanlab.log( + { + "text": swanlab.Text("这是一段测试文本", caption="test"), + }, + step=epoch, + ) + # 测试折线图 + swanlab.log({"t/accuracy": acc, "loss": loss, "loss2": loss2}) + else: + # 测试折线图 + swanlab.log({"t/accuracy": acc, "loss": loss, "loss2": loss2}) + time.sleep(0.5) diff --git a/vue/src/charts/components/SlideBar.vue b/vue/src/charts/components/SlideBar.vue index 0cf27c358..98f8a4a8d 100644 --- a/vue/src/charts/components/SlideBar.vue +++ b/vue/src/charts/components/SlideBar.vue @@ -23,7 +23,7 @@ * @file: SlideBar.vue * @since: 2024-01-30 16:18:31 **/ -import { computed, ref } from 'vue' +import { computed, ref, onUnmounted } from 'vue' const props = defineProps({ max: { @@ -45,6 +45,11 @@ const props = defineProps({ reference: { type: String, default: 'Step' + }, + // 通过方向键切换 + turnByArrow: { + type: Boolean, + default: false } }) @@ -94,6 +99,27 @@ const handleChange = (e) => { if (e.target.value == _modelValue.value) return _modelValue.value = e.target.value } + +// ---------------------------------- 键盘左右切换 ---------------------------------- + +const handleKeydown = (e) => { + if (!props.turnByArrow) return + if (e.key == 'ArrowLeft') { + handleClickDown() + } else if (e.key == 'ArrowRight') { + handleClickUp() + } +} + +if (props.turnByArrow) { + document.addEventListener('keydown', handleKeydown) +} + +onUnmounted(() => { + if (props.turnByArrow) { + document.removeEventListener('keydown', handleKeydown) + } +}) + diff --git a/vue/src/charts/modules/TextModule.vue b/vue/src/charts/modules/TextModule.vue index 689f70ae4..e9ac865f0 100644 --- a/vue/src/charts/modules/TextModule.vue +++ b/vue/src/charts/modules/TextModule.vue @@ -44,9 +44,10 @@ @turn="clickToTurn" :key="pages.maxIndex" v-if="data.list.length > 1" + :turn-by-arrow="modal && !isZoom" /> - + @@ -58,7 +59,7 @@ * @file: TextModule.vue * @since: 2024-02-20 20:06:45 **/ -import { ref, inject, computed } from 'vue' +import { ref, inject, computed, watch } from 'vue' import SLModal from '@swanlab-vue/components/SLModal.vue' import TextDetail from './TextDetail.vue' import SlideBar from '../components/SlideBar.vue' @@ -78,10 +79,13 @@ const props = defineProps({ }, modal: { type: Boolean + }, + modelValue: { + type: Boolean } }) -const emits = defineEmits(['getText']) +const emits = defineEmits(['getText', 'update:modelValue']) const color = inject('colors')[0] const skeleton = ref(false) @@ -217,6 +221,13 @@ const zoom = (text, i) => { } isZoom.value = true } + +watch( + () => isZoom.value, + (v) => { + emits('update:modelValue', v) + } +) diff --git a/vue/src/charts/package/LineChart.vue b/vue/src/charts/package/LineChart.vue index 58bcd4778..e647dca54 100644 --- a/vue/src/charts/package/LineChart.vue +++ b/vue/src/charts/package/LineChart.vue @@ -21,7 +21,7 @@ - +

{{ title }}

- + { // 是否放大 const isZoom = ref(false) +// 是否展示数据详情弹窗 +const isDetailZoom = ref(false) // 放大数据 const zoom = () => { isZoom.value = true } +// 通过按键退出放大弹窗 +const exitByEsc = () => { + // 如果放大弹窗中有数据详情弹窗,则不关闭放大弹窗,直接关闭数据详情弹窗(在 TextModel.vue 中关闭) + if (isDetailZoom.value) return + isZoom.value = false +} + // ---------------------------------- 暴露api ---------------------------------- defineExpose({ render, diff --git a/vue/src/components/SLModal.vue b/vue/src/components/SLModal.vue index 85e27327f..5b3be8e4a 100644 --- a/vue/src/components/SLModal.vue +++ b/vue/src/components/SLModal.vue @@ -18,7 +18,7 @@