AggregatedStatsChart - clustered column chart

GitOrigin-RevId: dc350197d17854d002ffd6fa1661a50d37a8b679
This commit is contained in:
Vladimir Krivosheev
2019-10-02 10:07:03 +00:00
committed by intellij-monorepo-bot
parent 2d72c69170
commit 41b30868fa
10 changed files with 202 additions and 72 deletions
@@ -1,6 +1,5 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import {Metrics} from "@/aggregatedStats/model"
import {InfoResponse} from "@/aggregatedStats/ChartSettings"
import {InfoResponse, Metrics} from "@/aggregatedStats/model"
const hiddenMetricsByDefault = new Set(["moduleLoading", "pluginDescriptorLoading"])
@@ -1,17 +1,18 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import {Component, Vue, Watch} from "vue-property-decorator"
import {AggregatedStatsChartManager} from "./AggregatedStatsChartManager"
import {LineChartManager} from "./LineChartManager"
import {AggregatedDataManager} from "@/aggregatedStats/AggregatedDataManager"
import {AppStateModule} from "@/state/state"
import {getModule} from "vuex-module-decorators"
import {InfoResponse} from "@/aggregatedStats/ChartSettings"
import {loadJson} from "@/httpUtil"
import {Metrics} from "@/aggregatedStats/model"
import {GroupedMetricResponse, InfoResponse, Metrics} from "@/aggregatedStats/model"
import {ClusteredChartManager} from "@/aggregatedStats/ClusteredChartManager"
@Component
export default class AggregatedStatsChart extends Vue {
export default class AggregatedStatsPage extends Vue {
private readonly dataModule = getModule(AppStateModule, this.$store)
private readonly chartManagers: Array<AggregatedStatsChartManager> = []
private readonly lineChartManagers: Array<LineChartManager> = []
private readonly clusteredChartManagers: Array<ClusteredChartManager> = []
isFetching: boolean = false
@@ -30,7 +31,7 @@ export default class AggregatedStatsChart extends Vue {
}
isShowScrollbarXPreviewChanged(_value: boolean) {
this.chartManagers.forEach(it => it.scrollbarXPreviewOptionChanged())
this.lineChartManagers.forEach(it => it.scrollbarXPreviewOptionChanged())
this.dataModule.updateChartSettings(this.chartSettings)
}
@@ -97,23 +98,47 @@ export default class AggregatedStatsChart extends Vue {
return
}
this.loadClusteredChartData(product, machine)
this.loadLineChartData(product, machine)
}
loadClusteredChartData(product: string, machine: string): void {
const url = new URL(`${this.chartSettings.serverUrl}/groupedMetrics`)
url.searchParams.set("product", product)
url.searchParams.set("machine", machine)
loadJson(url, null, this.$notify)
.then(data => {
if (data != null) {
this.renderClusteredCharts(data)
}
})
.catch(e => {
console.error(e)
})
}
// noinspection DuplicatedCode
loadLineChartData(product: string, machine: string): void {
const url = new URL(`${this.chartSettings.serverUrl}/metrics`)
url.searchParams.set("product", product)
url.searchParams.set("machine", machine)
const infoResponse = this.lastInfoResponse!!
loadJson(url, () => {}, this.$notify)
loadJson(url, null, this.$notify)
.then(data => {
if (data != null) {
this.renderData(data, infoResponse)
this.renderLineCharts(data, infoResponse)
}
})
.catch(e => {
console.error(e)
})
}
private renderData(data: Array<Metrics>, infoResponse: InfoResponse): void {
let chartManagers = this.chartManagers
private renderLineCharts(data: Array<Metrics>, infoResponse: InfoResponse): void {
let chartManagers = this.lineChartManagers
if (chartManagers.length === 0) {
chartManagers.push(new AggregatedStatsChartManager(this.$refs.durationEventChartContainer as HTMLElement, this.chartSettings, false))
chartManagers.push(new AggregatedStatsChartManager(this.$refs.instantEventChartContainer as HTMLElement, this.chartSettings, true))
chartManagers.push(new LineChartManager(this.$refs.durationEventChartContainer as HTMLElement, this.chartSettings, false))
chartManagers.push(new LineChartManager(this.$refs.instantEventChartContainer as HTMLElement, this.chartSettings, true))
}
const dataManager = new AggregatedDataManager(data, infoResponse)
@@ -127,6 +152,22 @@ export default class AggregatedStatsChart extends Vue {
}
}
private renderClusteredCharts(data: GroupedMetricResponse): void {
let chartManagers = this.clusteredChartManagers
if (chartManagers.length === 0) {
chartManagers.push(new ClusteredChartManager(this.$refs.clusteredChartContainer as HTMLElement))
}
for (const chartManager of chartManagers) {
try {
chartManager.render(data)
}
catch (e) {
console.error(e)
}
}
}
mounted() {
const chartSettings = this.chartSettings
if (!isEmpty(chartSettings.serverUrl)) {
@@ -135,10 +176,15 @@ export default class AggregatedStatsChart extends Vue {
}
beforeDestroy() {
for (const chartManager of this.chartManagers) {
for (const chartManager of this.clusteredChartManagers) {
chartManager.dispose()
}
this.chartManagers.length = 0
this.clusteredChartManagers.length = 0
for (const chartManager of this.lineChartManagers) {
chartManager.dispose()
}
this.lineChartManagers.length = 0
}
}
@@ -49,10 +49,14 @@
</el-form-item>
</el-form>
<div class="aggregatedChart" ref="clusteredChartContainer"></div>
<h2>Duration Events</h2>
<div class="aggregatedChart" ref="durationEventChartContainer"></div>
<h2>Instant Events</h2>
<div class="aggregatedChart" ref="instantEventChartContainer"></div>
<el-row>
<el-col>
<ul>
@@ -65,4 +69,4 @@
</div>
</template>
<script lang="ts" src="./AggregatedStatsChart.ts"></script>
<script lang="ts" src="./AggregatedStatsPage.ts"></script>
@@ -6,12 +6,4 @@ export class ChartSettings {
selectedMachine: string = ""
showScrollbarXPreview: boolean = false
}
export interface InfoResponse {
readonly productNames: Array<string>
readonly productToMachineNames: { [key: string]: Array<string>; }
readonly durationMetricsNames: Array<string>
readonly instantMetricsNames: Array<string>
}
@@ -0,0 +1,85 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
import * as am4charts from "@amcharts/amcharts4/charts"
import * as am4core from "@amcharts/amcharts4/core"
import {addExportMenu} from "@/charts/ChartManager"
import {GroupedMetricResponse} from "@/aggregatedStats/model"
// todo https://www.amcharts.com/demos/variance-indicators/
export class ClusteredChartManager {
private readonly chart: am4charts.XYChart
constructor(container: HTMLElement) {
this.chart = am4core.create(container, am4charts.XYChart)
const chart = this.chart
chart.legend = new am4charts.Legend()
addExportMenu(chart)
const categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis())
categoryAxis.dataFields.category = "metric"
categoryAxis.renderer.grid.template.location = 0
categoryAxis.renderer.minGridDistance = 20
categoryAxis.renderer.cellStartLocation = 0.1
categoryAxis.renderer.cellEndLocation = 0.9
const valueAxis = chart.yAxes.push(new am4charts.DurationAxis())
valueAxis.durationFormatter.baseUnit = "millisecond"
valueAxis.durationFormatter.durationFormat = "S"
const cursor = new am4charts.XYCursor()
cursor.behavior = "zoomXY"
cursor.lineX.disabled = true
cursor.lineY.disabled = true
// cursor.xAxis = dateAxis
// cursor.snapToSeries = series
chart.cursor = cursor
}
render(data: GroupedMetricResponse): void {
const chart = this.chart
const oldSeries = new Map<string, am4charts.ColumnSeries>()
for (const series of chart.series) {
oldSeries.set(series.name, series as am4charts.ColumnSeries)
}
for (const groupName of data.groupNames) {
let series = oldSeries.get(groupName)
if (series == null) {
series = new am4charts.ColumnSeries()
this.configureSeries(groupName, series)
chart.series.push(series)
}
else {
oldSeries.delete(groupName)
}
}
oldSeries.forEach(value => {
chart.series.removeIndex(chart.series.indexOf(value))
value.dispose()
})
chart.data = data.data
}
private configureSeries(groupName: string, series: am4charts.ColumnSeries) {
series.name = groupName
series.dataFields.valueY = groupName
series.dataFields.categoryX = "metric"
series.columns.template.width = am4core.percent(95)
// series.columns.template.tooltipText = `${groupName}: {valueY} ms`
let valueLabel = series.bullets.push(new am4charts.LabelBullet())
valueLabel.label.text = "{valueY.formatDuration('S')}"
valueLabel.label.verticalCenter = "bottom"
// valueLabel.label.dx = 10
valueLabel.label.hideOversized = false
valueLabel.label.truncate = false
}
dispose(): void {
this.chart.dispose()
}
}
@@ -6,7 +6,7 @@ import {ChartSettings} from "@/aggregatedStats/ChartSettings"
import {addExportMenu} from "@/charts/ChartManager"
interface ChartConfigurator {
configureXAxis(chart: am4charts.XYChart): void
configureXAxis(chart: am4charts.XYChart): void
configureSeries(series: am4charts.LineSeries): void
}
@@ -34,7 +34,7 @@ class SortedByCategory implements ChartConfigurator {
}
}
export class AggregatedStatsChartManager {
export class LineChartManager {
private readonly chart: am4charts.XYChart
constructor(container: HTMLElement, private readonly chartSettings: ChartSettings, private readonly isInstantEvents: boolean, private readonly configurator: ChartConfigurator = new SortedByCategory()) {
@@ -87,7 +87,6 @@ export class AggregatedStatsChartManager {
scrollbarXPreviewOptionChanged() {
// no need to dispose old scrollbar explicit - will be disposed automatically on set
const chart = this.chart
console.log("scrollbarXPreviewOptionChanged: " + this.chartSettings.showScrollbarXPreview)
if (this.chartSettings.showScrollbarXPreview) {
const scrollbarX = this.configureScrollbarXWithPreview()
chart.series.each(it => {
@@ -134,10 +133,6 @@ export class AggregatedStatsChartManager {
})
chart.data = dataManager.metrics
if (this.isInstantEvents) {
(window as any).chart = chart
}
console.log(dataManager.metrics)
}
private configureLineSeries(metric: MetricDescriptor, series: am4charts.LineSeries) {
@@ -1,2 +1,15 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
export type Metrics = { [key: string]: number; }
export type Metrics = { [key: string]: number; }
export interface InfoResponse {
readonly productNames: Array<string>
readonly productToMachineNames: { [key: string]: Array<string>; }
readonly durationMetricsNames: Array<string>
readonly instantMetricsNames: Array<string>
}
export interface GroupedMetricResponse {
readonly groupNames: Array<string>
readonly data: Array<{ [key: string]: Array<string | number>; }>
}
@@ -1,5 +1,5 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
export function loadJson(url: URL, processed: () => void, notificationManager: any): Promise<any> {
export function loadJson(url: URL, processed: (() => void) | null, notificationManager: any): Promise<any> {
function showError(reason: string) {
notificationManager.error({
title: "Error",
@@ -21,7 +21,9 @@ export function loadJson(url: URL, processed: () => void, notificationManager: a
.then(data => {
clearTimeout(timeoutId)
processed()
if (processed != null) {
processed()
}
if (data == null) {
showError("Server returns empty result")
@@ -33,7 +35,9 @@ export function loadJson(url: URL, processed: () => void, notificationManager: a
.catch(e => {
clearTimeout(timeoutId)
processed()
if (processed != null) {
processed()
}
if (!isCancelledByTimeout) {
showError(e.toString())
@@ -30,7 +30,7 @@ const routes: Array<RouteConfig> = [
{
path: `/aggregatedStats`,
name: "Aggregated Stats",
component: () => import("@/aggregatedStats/AggregatedStatsChart.vue"),
component: () => import("@/aggregatedStats/AggregatedStatsPage.vue"),
},
{
path: "/",
@@ -118,9 +118,9 @@
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*":
version "12.7.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.8.tgz#cb1bf6800238898bc2ff6ffa5702c3cadd350708"
integrity sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==
version "12.7.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.9.tgz#da0210f91096aa67138cf5afd04c4d629f8a406a"
integrity sha512-P57oKTJ/vYivL2BCfxCC5tQjlS8qW31pbOL6qt99Yrjm95YdHgNZwjrTTjMBh+C2/y6PXIX4oz253+jUzxKKfQ==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@@ -830,9 +830,9 @@ binary-extensions@^1.0.0:
integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
bluebird@^3.1.1, bluebird@^3.5.1, bluebird@^3.5.5:
version "3.5.5"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
version "3.7.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.0.tgz#56a6a886e03f6ae577cffedeb524f8f2450293cf"
integrity sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
@@ -1175,9 +1175,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000989:
version "1.0.30000997"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000997.tgz#ba44a606804f8680894b7042612c2c7f65685b7e"
integrity sha512-BQLFPIdj2ntgBNWp9Q64LGUIEmvhKkzzHhUHR3CD5A9Lb7ZKF20/+sgadhFap69lk5XmK1fTUleDclaRFvgVUA==
version "1.0.30000998"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000998.tgz#7227a8046841e7d01e156ae7227a504d065f6744"
integrity sha512-8Tj5sPZR9kMHeDD9SZXIVr5m9ofufLLCG2Y4QwQrH18GIwG+kCc+zYdlR036ZRkuKjVVetyxeAgGA1xF7XdmzQ==
canvg@^2.0.0:
version "2.0.0"
@@ -2259,9 +2259,9 @@ ejs@^2.6.1:
integrity sha512-kS/gEPzZs3Y1rRsbGX4UOSjtP/CeJP0CxSNZHYxGfVM/VgLcv0ZqM7C45YyTj2DI2g7+P9Dd24C+IMIg6D0nYQ==
electron-to-chromium@^1.3.247:
version "1.3.269"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.269.tgz#3e00cc9266a0123fc2e7b4f290899e257200e6e3"
integrity sha512-t2ZTfo07HxkxTOUbIwMmqHBSnJsC9heqJUm7LwQu2iSk0wNhG4H5cMREtb8XxeCrQABDZ6IqQKY3yZq+NfAqwg==
version "1.3.273"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.273.tgz#94872d6823219f2812f2e35a2ce2a7d03c1eaa3f"
integrity sha512-0kUppiHQvHEENHh+nTtvTt4eXMwcPyWmMaj73GPrSEm3ldKhmmHuOH6IjrmuW6YmyS/fpXcLvMQLNVpqRhpNWw==
element-ui@^2.12.0:
version "2.12.0"
@@ -4356,15 +4356,7 @@ minimist@^1.1.3, minimist@^1.2.0:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
minipass@^2.6.0, minipass@^2.8.6:
version "2.8.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.8.6.tgz#620d889ace26356391d010ecb9458749df9b6db5"
integrity sha512-lFG7d6g3+/UaFDCOtqPiKAC9zngWWsQZl1g5q6gaONqrjq61SX2xFqXMleQiFVyDpYwa018E9hmlAFY22PCb+A==
dependencies:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minipass@^2.9.0:
minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
@@ -4373,9 +4365,9 @@ minipass@^2.9.0:
yallist "^3.0.0"
minizlib@^1.2.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.2.tgz#5d24764998f98112586f7e566bd4c0999769dad4"
integrity sha512-lsNFqSHdJ21EwKzCp12HHJGxSMtHkCW1EMA9cceG3MkMNARjuWotZnMe3NKNshAvFXpm4loZqmYsCmRwhS2JMw==
version "1.3.3"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
dependencies:
minipass "^2.9.0"
@@ -4533,10 +4525,10 @@ no-case@^2.2.0:
dependencies:
lower-case "^1.1.1"
node-forge@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.8.2.tgz#b4bcc59fb12ce77a8825fc6a783dfe3182499c5a"
integrity sha512-mXQ9GBq1N3uDCyV1pdSzgIguwgtVpM7f5/5J4ipz12PKWElmPpVWLDuWl8iXmhysr21+WmX/OJ5UKx82wjomgg==
node-forge@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
node-ipc@^9.1.1:
version "9.1.1"
@@ -6083,11 +6075,11 @@ select-hose@^2.0.0:
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
selfsigned@^1.10.6:
version "1.10.6"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.6.tgz#7b3cd37ed9c2034261a173af1a1aae27d8169b67"
integrity sha512-i3+CeqxL7DpAazgVpAGdKMwHuL63B5nhJMh9NQ7xmChGkA3jNFflq6Jyo1LLJYcr3idWiNOPWHCrm4zMayLG4w==
version "1.10.7"
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b"
integrity sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==
dependencies:
node-forge "0.8.2"
node-forge "0.9.0"
"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
@@ -7531,9 +7523,9 @@ yallist@^2.1.2:
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.0.tgz#906cc2100972dc2625ae78f566a2577230a1d6f7"
integrity sha512-6gpP93MR+VOOehKbCPchro3wFZNSNmek8A2kbkOAZLIZAYx1KP/zAqwO0sOHi3xJEb+UBz8NaYt/17UNit1Q9w==
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yargs-parser@^11.1.1:
version "11.1.1"