feat: create line, area, heatmap components

This commit is contained in:
Qiuxia Fan 2021-12-30 21:26:47 +08:00
parent 09ab161183
commit a58ad5e8f0
7 changed files with 400 additions and 127 deletions

View File

@ -26,23 +26,30 @@ import {
} from "vue"; } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
const dom = ref<any>(null); // eslint-disable-next-line no-undef
const state = reactive<any>({ const dom = ref<Nullable<HTMLElement>>(null);
const state = reactive<{ instanceChart: any }>({
instanceChart: null, instanceChart: null,
}); });
const props = defineProps({ const props = defineProps({
clickEvent: { type: Function as PropType<(param: unknown) => void> }, clickEvent: { type: Function as PropType<(param: unknown) => void> },
height: { type: Number, default: 100 }, height: { type: String, default: "100%" },
width: { type: Number, default: 300 }, width: { type: String, default: "100%" },
option: { type: Object as PropType<any>, default: () => ({}) }, option: {
type: Object as PropType<{ [key: string]: unknown }>,
default: () => ({}),
},
}); });
onMounted(() => { onMounted(() => {
drawEcharts(); drawEcharts();
window.addEventListener("resize", state.instanceChart.resize); window.addEventListener("resize", state.instanceChart.resize);
}); });
function drawEcharts(): void { function drawEcharts(): void {
if (!dom.value) {
return;
}
state.instanceChart = echarts.init(dom.value, ""); state.instanceChart = echarts.init(dom.value, "");
state.instanceChart.setOption(state.option); state.instanceChart.setOption(props.option);
state.instanceChart.on("click", (params: any) => { state.instanceChart.on("click", (params: any) => {
if (!props.clickEvent) { if (!props.clickEvent) {
return; return;

View File

@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="widget-config"> <div class="widget-config">
<div class="graph">No Data</div> <div class="graph" style="height: 350px; width: 100%">
<Bar :data="source" :intervalTime="appStoreWithOut.intervalTime" />
<span v-show="!source">No Data</span>
</div>
<div class="config"> <div class="config">
<div class="metrics item"> <div class="metrics item">
<label>Graph your metric</label> <label>Graph your metric</label>
@ -75,10 +78,12 @@ limitations under the License. -->
import { reactive } from "vue"; import { reactive } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { ElMessage, ElButton } from "element-plus"; import { ElMessage, ElButton } from "element-plus";
import { ValuesTypes, MetricQueryTypes, ChartTypes } from "../data"; import { ValuesTypes, MetricQueryTypes, ChartTypes } from "../data";
import { Option } from "@/types/app"; import { Option } from "@/types/app";
import Loading from "@/utils/loading"; import Loading from "@/utils/loading";
import Bar from "../graphs/Bar.vue";
const states = reactive<{ const states = reactive<{
metrics: string; metrics: string;
@ -95,6 +100,7 @@ const states = reactive<{
}); });
const { t } = useI18n(); const { t } = useI18n();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const appStoreWithOut = useAppStoreWithOut();
const { loading } = Loading(); const { loading } = Loading();
async function changeMetrics(val: Option[]) { async function changeMetrics(val: Option[]) {
if (!val.length) { if (!val.length) {
@ -129,10 +135,14 @@ const metricOpts = [
{ value: "service_mq_consume_latency", label: "service_mq_consume_latency" }, { value: "service_mq_consume_latency", label: "service_mq_consume_latency" },
{ value: "service_mq_consume_count", label: "service_mq_consume_count" }, { value: "service_mq_consume_count", label: "service_mq_consume_count" },
]; ];
const source = {
count: [1, 2, 3, 4, 5, 6, 7, 3, 4, 5, 2, 1, 6, 9],
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.widget-config { .widget-config {
position: relative; position: relative;
width: 100%;
} }
.item { .item {
@ -140,9 +150,9 @@ const metricOpts = [
} }
.graph { .graph {
width: 100%;
height: 350px;
min-width: 1280px; min-width: 1280px;
border: 1px solid #eee;
padding: 10px;
} }
label { label {

View File

@ -0,0 +1,31 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<Line :data="data" :intervalTime="intervalTime" :type="'areaChart'" />
</template>
<script lang="ts" setup>
import { defineProps } from "vue";
import type { PropType } from "vue";
import Line from "./Line.vue";
defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
</script>

View File

@ -25,15 +25,15 @@ const props = defineProps({
type: Object as PropType<{ [key: string]: number[] }>, type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}), default: () => ({}),
}, },
intervalTime: { type: Function as any }, intervalTime: { type: Array as PropType<string[]>, default: () => [] },
theme: { type: String, default: "dark" }, theme: { type: String, default: "dark" },
itemEvents: { type: Array as PropType<Event[]>, default: () => [] }, itemEvents: { type: Array as PropType<Event[]>, default: () => [] },
}); });
const chart = ref<any>(null); const chart = ref<any>(null);
const option = computed(() => getOption()); const option = computed(() => getOption());
// function resize() { function resize() {
// chart.value.myChart.resize(); chart.value.myChart.resize();
// } }
function getOption() { function getOption() {
const keys = Object.keys(props.data || {}).filter( const keys = Object.keys(props.data || {}).filter(
(i: any) => Array.isArray(props.data[i]) && props.data[i].length (i: any) => Array.isArray(props.data[i]) && props.data[i].length

View File

@ -0,0 +1,156 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<Graph ref="chart" :option="option" :autoResize="true" />
</template>
<script lang="ts" setup>
import { defineProps, ref, computed } from "vue";
import type { PropType } from "vue";
import { Event } from "@/types/events";
const props = defineProps({
data: {
type: Object as PropType<{ nodes: number[][]; buckets: number[][] }>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
theme: { type: String, default: "dark" },
itemEvents: { type: Array as PropType<Event[]>, default: () => [] },
itemConfig: {
type: Object as PropType<{ unit: string }>,
default: () => ({}),
},
});
const chart = ref<any>(null);
const option = computed(() => getOption());
function getOption() {
const source = (props.data.nodes || []).map((d: number[]) => d[2]);
const maxItem = Math.max(...source);
const minItem = Math.min(...source);
const colorBox = [
"#fff",
"#FDF0F0",
"#FAE2E2",
"#F8D3D3",
"#F6C4C4",
"#F4B5B5",
"#F1A7A7",
"#EF9898",
"#E86C6C",
"#E44E4E",
"#E23F3F",
"#DF3131",
"#DD2222",
"#CE2020",
"#C01D1D",
"#B11B1B",
"#A21919",
"#851414",
"#761212",
"#671010",
];
return {
tooltip: {
position: "top",
formatter: (a: any) =>
`${a.data[1] * 100}${props.itemConfig.unit} [ ${a.data[2]} ]`,
textStyle: {
fontSize: 13,
color: "#ccc",
},
},
grid: {
top: 15,
left: 0,
right: 10,
bottom: 5,
containLabel: true,
},
xAxis: {
type: "category",
data: props.intervalTime,
axisTick: {
lineStyle: { color: "#c1c5ca" },
alignWithLabel: true,
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
visualMap: [
{
min: minItem,
max: maxItem,
show: false,
type: "piecewise",
calculable: true,
pieces: generatePieces(maxItem, colorBox, minItem),
},
],
yAxis: {
type: "category",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca", type: "dashed" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
data: props.data.buckets,
},
series: [
{
type: "heatmap",
data: props.data.nodes || [],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: "rgba(0, 0, 0, 0.5)",
},
},
},
],
};
}
function generatePieces(maxValue: number, colorBox: string[], minItem: number) {
if (maxValue < minItem) {
return [];
}
const pieces = [];
let quotient = 1;
let temp = {} as { min: number; max: number; color: string };
temp.max = minItem;
temp.min = minItem;
temp.color = colorBox[0];
pieces.push(temp);
if (maxValue && maxValue >= 19) {
quotient = Math.floor(maxValue / 19);
for (let i = 1; i < 20; i++) {
temp = {} as any;
if (i === 1) {
temp.min = minItem;
} else {
temp.min = quotient * (i - 1);
}
temp.max = quotient * i;
temp.color = colorBox[i];
pieces.push(temp);
}
}
const length = pieces.length;
if (length) {
const item = pieces[length - 1];
item.max = maxValue;
}
return pieces;
}
</script>

View File

@ -0,0 +1,183 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<Graph ref="chart" :option="option" :autoResize="true" />
</template>
<script lang="ts" setup>
import { defineProps, ref, computed } from "vue";
import type { PropType } from "vue";
import { Event } from "@/types/events";
const props = defineProps({
data: {
type: Object as PropType<{ [key: string]: number[] }>,
default: () => ({}),
},
type: { type: String, default: "" },
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
theme: { type: String, default: "dark" },
itemEvents: { type: Array as PropType<Event[]>, default: () => [] },
});
const chart = ref<any>(null);
const option = computed(() => getOption());
// function resize() {
// chart.value.myChart.resize();
// }
function getOption() {
const keys = Object.keys(props.data || {}).filter(
(i: any) => Array.isArray(props.data[i]) && props.data[i].length
);
const startP = keys.length > 1 ? 50 : 15;
const diff = 10;
const markAreas = (props.itemEvents || []).map(
(event: Event, index: number) => {
return [
{
name: `${event.name}:${event.type}`,
xAxis: event.startTime,
y: startP + diff * index,
itemStyle: {
borderWidth: 2,
borderColor: event.type === "Normal" ? "#5dc859" : "#FF0087",
color: event.type === "Normal" ? "#5dc859" : "#FF0087",
},
},
{
name: event.message,
xAxis: event.endTime,
y: startP + diff * (index + 1),
},
];
}
);
const temp = keys.map((i: any, index: number) => {
const serie: any = {
data: props.data[i].map((item: any, itemIndex: number) => [
props.intervalTime[itemIndex],
item,
]),
name: i,
type: "line",
symbol: "none",
barMaxWidth: 10,
lineStyle: {
width: 1.5,
type: "solid",
},
markArea:
index === 0
? {
silent: false,
data: markAreas,
label: {
show: false,
width: 60,
},
emphasis: {
label: {
position: "bottom",
show: true,
},
},
}
: undefined,
};
if (props.type === "areaChart") {
serie.areaStyle = {
opacity: 0.4,
};
}
return serie;
});
let color: string[] = [];
switch (keys.length) {
case 2:
color = ["#FF6A84", "#a0b1e6"];
break;
case 1:
color = ["#3f96e3"];
break;
default:
color = [
"#30A4EB",
"#45BFC0",
"#FFCC55",
"#FF6A84",
"#a0a7e6",
"#c23531",
"#2f4554",
"#61a0a8",
"#d48265",
"#91c7ae",
"#749f83",
"#ca8622",
"#bda29a",
"#6e7074",
"#546570",
"#c4ccd3",
];
break;
}
return {
color,
tooltip: {
trigger: "axis",
backgroundColor: "rgb(50,50,50)",
textStyle: {
fontSize: 13,
color: "#ccc",
},
enterable: true,
extraCssText: "max-height: 300px; overflow: auto;",
},
legend: {
type: "scroll",
show: keys.length === 1 ? false : true,
icon: "circle",
top: 0,
left: 0,
itemWidth: 12,
textStyle: {
color: props.theme === "dark" ? "#fff" : "#333",
},
},
grid: {
top: keys.length === 1 ? 15 : 55,
left: 0,
right: 10,
bottom: 5,
containLabel: true,
},
xAxis: {
type: "category",
axisTick: {
lineStyle: { color: "#c1c5ca41" },
alignWithLabel: true,
},
splitLine: { show: false },
axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
yAxis: {
type: "value",
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
axisLabel: { color: "#9da5b2", fontSize: "11" },
},
series: temp,
};
}
</script>

114
type.d.ts vendored
View File

@ -1,114 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {
ComponentRenderProxy,
VNode,
VNodeChild,
ComponentPublicInstance,
FunctionalComponent,
PropType as VuePropType,
} from "vue";
declare module "three";
declare module "three-orbit-controls";
declare module "element-plus";
declare global {
const __APP_INFO__: {
pkg: {
name: string;
version: string;
dependencies: Recordable<string>;
devDependencies: Recordable<string>;
};
lastBuildTime: string;
};
// vue
declare type PropType<T> = VuePropType<T>;
declare type VueNode = VNodeChild | JSX.Element;
export type Writable<T> = {
-readonly [P in keyof T]: T[P];
};
declare type Nullable<T> = T | null;
declare type NonNullable<T> = T extends null | undefined ? never : T;
declare type Recordable<T = any> = Record<string, T>;
declare type ReadonlyRecordable<T = any> = {
readonly [key: string]: T;
};
declare type Indexable<T = any> = {
[key: string]: T;
};
declare type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
declare type TimeoutHandle = ReturnType<typeof setTimeout>;
declare type IntervalHandle = ReturnType<typeof setInterval>;
declare interface ChangeEvent extends Event {
target: HTMLInputElement;
}
declare interface WheelEvent {
path?: EventTarget[];
}
interface ImportMetaEnv extends ViteEnv {
__: unknown;
}
declare interface ViteEnv {
VITE_PORT: number;
VITE_USE_MOCK: boolean;
VITE_USE_PWA: boolean;
VITE_PUBLIC_PATH: string;
VITE_PROXY: [string, string][];
VITE_GLOB_APP_TITLE: string;
VITE_GLOB_APP_SHORT_NAME: string;
VITE_USE_CDN: boolean;
VITE_DROP_CONSOLE: boolean;
VITE_BUILD_COMPRESS: "gzip" | "brotli" | "none";
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
VITE_LEGACY: boolean;
VITE_USE_IMAGEMIN: boolean;
VITE_GENERATE_UI: string;
}
declare function parseInt(s: string | number, radix?: number): number;
declare function parseFloat(string: string | number): number;
namespace JSX {
type Element = VNode;
type ElementClass = ComponentRenderProxy;
interface ElementAttributesProperty {
$props: any;
}
interface IntrinsicElements {
[elem: string]: any;
}
interface IntrinsicAttributes {
[elem: string]: any;
}
}
}
declare module "vue" {
export type JSXComponent<Props = any> =
| { new (): ComponentPublicInstance<Props> }
| FunctionalComponent<Props>;
}