Skip to content

Commit

Permalink
WIP: boxplot component
Browse files Browse the repository at this point in the history
  • Loading branch information
mvdbeek committed Oct 24, 2024
1 parent 164f1bf commit 31404dd
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 156 deletions.
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"dom-to-image": "^2.6.0",
"dompurify": "^3.0.6",
"dumpmeta-webpack-plugin": "^0.2.0",
"echarts": "^5.5.1",
"elkjs": "^0.8.2",
"file-saver": "^2.0.5",
"flush-promises": "^1.0.2",
Expand Down Expand Up @@ -101,6 +102,7 @@
"underscore": "^1.13.6",
"util": "^0.12.5",
"vue": "^2.7.14",
"vue-echarts": "^7.0.3",
"vue-infinite-scroll": "^2.0.2",
"vue-multiselect": "^2.1.7",
"vue-observe-visibility": "^1.0.0",
Expand Down
288 changes: 132 additions & 156 deletions client/src/components/WorkflowInvocationState/MetricsBoxPlots.vue
Original file line number Diff line number Diff line change
@@ -1,169 +1,145 @@
<template>
<div ref="boxPlot"></div>
<v-chart class="chart" :option="option" autoresize />
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as d3 from 'd3';
import { isTerminal } from './util';
interface JobData {
title: string;
value: string;
plugin: string;
name: string;
raw_value: string;
tool_id: string;
import * as echarts from 'echarts/core';
import {
DatasetComponent,
type DatasetComponentOption,
TitleComponent,
type TitleComponentOption,
TooltipComponent,
type TooltipComponentOption,
GridComponent,
type GridComponentOption,
LegendComponent,
type LegendComponentOption,
DataZoomComponent,
type DataZoomComponentOption,
TransformComponent
} from 'echarts/components';
import { BoxplotChart, type BoxplotSeriesOption } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';
import VChart from 'vue-echarts';
echarts.use([
DatasetComponent,
TitleComponent,
TooltipComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
TransformComponent,
BoxplotChart,
CanvasRenderer
]);
type EChartsOption = echarts.ComposeOption<
| DatasetComponentOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| DataZoomComponentOption
| BoxplotSeriesOption
>;
// Generate data.
function makeData() {
let data = [];
for (let i = 0; i < 18; i++) {
let cate = [];
for (let j = 0; j < 100; j++) {
cate.push(Math.random() * 200);
}
data.push(cate);
}
return data;
}
const boxPlot = ref<HTMLElement | null>(null);
// Define the job data (this can come from props or API in real scenario// Extract runtime seconds data
// Define props
const props = defineProps<{
jobData: JobData[];
}>();
// Function to render box plots for different tool_ids on the same graph
const renderBoxPlots = () => {
const margin = { top: 10, right: 30, bottom: 50, left: 40 },
width = 800 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// Remove any previous content
d3.select(boxPlot.value).selectAll('*').remove();
// Group data by tool_id
const groupedData = d3.group(props.jobData, d => d.tool_id);
// Extract the tool_ids for the x-axis
const toolIds = Array.from(groupedData.keys());
// Create SVG
const svg = d3
.select(boxPlot.value)
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// X Scale: tool_ids on the x-axis
const x = d3.scaleBand()
.range([0, width])
.domain(toolIds)
.padding(0.4);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x));
// Y Scale: common for all box plots
const allRuntimes = Array.from(props.jobData)
.filter(d => d.name === "runtime_seconds")
.map(d => parseFloat(d.raw_value));
const y = d3.scaleLinear()
.domain([0, d3.max(allRuntimes) as number])
.range([height, 0]);
svg.append('g').call(d3.axisLeft(y));
// Tooltip for individual dots
const tooltip = d3.select(boxPlot.value)
.append("div")
.style("position", "absolute")
.style("background", "lightgray")
.style("padding", "5px")
.style("border-radius", "5px")
.style("pointer-events", "none")
.style("visibility", "hidden");
// Render box plots for each tool_id
groupedData.forEach((data, toolId) => {
const runtimeData = data.map(d => parseFloat(d.raw_value));
const center = x(toolId) as number + x.bandwidth() / 2;
const boxWidth = x.bandwidth() / 2;
const q1 = d3.quantile(runtimeData.sort(d3.ascending), 0.25) as number;
const median = d3.quantile(runtimeData.sort(d3.ascending), 0.5) as number;
const q3 = d3.quantile(runtimeData.sort(d3.ascending), 0.75) as number;
const interQuantileRange = q3 - q1;
const min = Math.max(d3.min(runtimeData) as number, q1 - 1.5 * interQuantileRange);
const max = Math.min(d3.max(runtimeData) as number, q3 + 1.5 * interQuantileRange);
// Box
svg.append('rect')
.attr('x', center - boxWidth / 2)
.attr('y', y(q3))
.attr('height', y(q1) - y(q3))
.attr('width', boxWidth)
.attr('stroke', 'black')
.style('fill', '#69b3a2');
// Median, min, max lines
svg.selectAll('line')
.data([min, median, max])
.enter()
.append('line')
.attr('x1', center - boxWidth / 2)
.attr('x2', center + boxWidth / 2)
.attr('y1', d => y(d))
.attr('y2', d => y(d))
.attr('stroke', 'black');
// Vertical lines for min-max
svg.append('line')
.attr('x1', center)
.attr('x2', center)
.attr('y1', y(min))
.attr('y2', y(q1))
.attr('stroke', 'black');
svg.append('line')
.attr('x1', center)
.attr('x2', center)
.attr('y1', y(max))
.attr('y2', y(q3))
.attr('stroke', 'black');
// Add individual data points (dots)
svg.selectAll("circle")
.data(runtimeData)
.enter()
.append("circle")
.attr("cx", center) // Align horizontally to the center of the group
.attr("cy", d => y(d)) // Map the data value to the y-axis
.attr("r", 5) // Radius of the circle
.style("fill", "#ff5722") // Fill color for the points
.attr("stroke", "black")
.on("mouseover", function (event, d) {
tooltip.style("visibility", "visible")
.text(`Value: ${d}`);
d3.select(this).style("fill", "yellow");
})
.on("mousemove", function (event) {
tooltip.style("top", `${event.pageY - 10}px`)
.style("left", `${event.pageX + 10}px`);
})
.on("mouseout", function () {
tooltip.style("visibility", "hidden");
d3.select(this).style("fill", "#ff5722");
});
});
const data0 = makeData();
const option: EChartsOption = {
title: {
text: 'Multiple Categories',
left: 'center'
},
dataset: [
{
source: data0
},
{
fromDatasetIndex: 0,
transform: { type: 'boxplot' }
}
],
legend: {
top: '10%'
},
tooltip: {
trigger: 'item',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '10%',
top: '20%',
right: '10%',
bottom: '15%'
},
xAxis: {
type: 'category',
boundaryGap: true,
nameGap: 30,
splitArea: {
show: true
},
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
name: 'Value',
min: -400,
max: 600,
splitArea: {
show: false
}
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 20
},
{
show: true,
type: 'slider',
top: '90%',
xAxisIndex: [0],
start: 0,
end: 20
}
],
series: [
{
name: 'category0',
type: 'boxplot',
datasetIndex: 0
}
]
};
// Lifecycle hook to render the plot once the component is mounted
onMounted(() => {
renderBoxPlots();
});
</script>

<style scoped>
svg {
font-family: Arial, sans-serif;
.chart {
height: 100vh;
}
</style>
32 changes: 32 additions & 0 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5327,6 +5327,14 @@ eastasianwidth@^0.2.0:
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==

echarts@^5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.5.1.tgz#8dc9c68d0c548934bedcb5f633db07ed1dd2101c"
integrity sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==
dependencies:
tslib "2.3.0"
zrender "5.6.0"

editorconfig@^0.15.3:
version "0.15.3"
resolved "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz"
Expand Down Expand Up @@ -11468,6 +11476,11 @@ tsconfig@^7.0.0:
strip-bom "^3.0.0"
strip-json-comments "^2.0.0"

[email protected]:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==

tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
Expand Down Expand Up @@ -11895,6 +11908,18 @@ vue-demi@>=0.14.7:
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.7.tgz#8317536b3ef74c5b09f268f7782e70194567d8f2"
integrity sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==

vue-demi@^0.13.11:
version "0.13.11"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"
integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==

vue-echarts@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/vue-echarts/-/vue-echarts-7.0.3.tgz#bf79f7ee0144bbdc6aee5610e8443fed91f6abbe"
integrity sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==
dependencies:
vue-demi "^0.13.11"

vue-eslint-parser@^9.0.1:
version "9.0.3"
resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.0.3.tgz"
Expand Down Expand Up @@ -12508,3 +12533,10 @@ yoctocolors-cjs@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz#f4b905a840a37506813a7acaa28febe97767a242"
integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==

[email protected]:
version "5.6.0"
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.6.0.tgz#01325b0bb38332dd5e87a8dbee7336cafc0f4a5b"
integrity sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==
dependencies:
tslib "2.3.0"

0 comments on commit 31404dd

Please sign in to comment.