Skip to content

Commit

Permalink
Fixed issues with boxplots, improved styling, reorganized code for ea…
Browse files Browse the repository at this point in the history
…sier reuse
  • Loading branch information
joaquinvanschoren committed Jan 28, 2024
1 parent 5d8eb2c commit 9b3935c
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 293 deletions.
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@
"@mui/x-date-pickers": "^6.16.2",
"@react-spring/web": "^9.7.3",
"@reduxjs/toolkit": "1.9.7",
"@sgratzl/chartjs-chart-boxplot": "^4.2.8",
"@svgr/webpack": "8.1.0",
"axios": "^1.6.1",
"axios-mock-adapter": "1.22.0",
"chart.js": "4.4.0",
"chart.js": "^4.4.0",
"date-fns": "2.30.0",
"deepmerge": "4.3.1",
"formik": "2.4.5",
Expand Down
89 changes: 89 additions & 0 deletions app/src/components/charts/HorizontalBoxPlot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React, { useEffect } from "react";
import {
BoxPlotController,
BoxAndWiskers,
} from "@sgratzl/chartjs-chart-boxplot";
import { Chart, registerables } from "chart.js";
import { externalTooltipHandler } from "../charts/helpers";
Chart.register(...registerables, BoxPlotController, BoxAndWiskers);

const HorizontalBoxPlot = (props) => {
const { data, chartId } = props;

useEffect(() => {
if (!data) {
return;
}

const ctx = document.getElementById(chartId).getContext("2d");

// Calculate box plot data
// Simplified. Server should return median and quantiles for correct results
const boxplotData = {
min: data.min,
whiskerMin: data.min,
q1: data.mean - data.stdev,
median: data.mean,
mean: data.mean,
q3: data.mean + data.stdev,
max: data.max,
whiskerMax: data.max,
};

const chartData = {
labels: [""],
datasets: [
{
label: "",
data: [boxplotData],
},
],
};

const myChart = new Chart(ctx, {
type: "boxplot",
data: chartData,
options: {
indexAxis: "y",
minStats: "whiskerMin",
maxStats: "whiskerMax",
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: "nearest",
external: externalTooltipHandler,
},
},
scales: {
x: {
min: data.min,
max: data.max,
ticks: {
font: {
size: 10,
},
},
},
y: {
grid: {
display: false,
},
},
},
},
});

return () => {
myChart.destroy();
};
}, [data]);

return <canvas id={chartId}></canvas>;
};

export default HorizontalBoxPlot;
78 changes: 78 additions & 0 deletions app/src/components/charts/StackedBarChart.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useEffect } from "react";
import {
BoxPlotController,
BoxAndWiskers,
} from "@sgratzl/chartjs-chart-boxplot";
import { Chart, registerables } from "chart.js";
import { externalTooltipHandler } from "./helpers";
Chart.register(...registerables, BoxPlotController, BoxAndWiskers);

const StackedBarChart = (props) => {
const { data, chartId, showX, targets } = props;
useEffect(() => {
if (!data || data.length === 0) {
return;
}
const ctx = document.getElementById(chartId).getContext("2d");

// Assuming data is in the format: [categories, [class1Data, class2Data, ...]]
const categories = data[0];
const classData = data[1];

// Transpose classData to get data per category
const transposedData = categories.map((_, ci) =>
classData.map((row) => row[ci]),
);

const datasets = transposedData.map((data, index) => ({
label: targets[index],
data: data,
borderWidth: 1,
}));

const myChart = new Chart(ctx, {
type: "bar",
data: {
labels: categories,
datasets: datasets,
},
options: {
scales: {
x: {
stacked: true,
display: showX || categories.length < 5,
ticks: {
font: {
size: 10,
},
},
},
y: {
stacked: true,
display: false,
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: "nearest",
external: externalTooltipHandler,
},
},
responsive: true,
maintainAspectRatio: false,
},
});

return () => {
myChart.destroy();
};
}, [data]);

return <canvas id={chartId}></canvas>;
};

export default StackedBarChart;
121 changes: 121 additions & 0 deletions app/src/components/charts/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// External tooltip handler
// Based on https://www.chartjs.org/docs/latest/samples/tooltip/html.html
const getOrCreateTooltip = (chart) => {
let tooltipEl = chart.canvas.parentNode.querySelector("div");

if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.style.background = "rgba(0, 0, 0, 0.7)";
tooltipEl.style.borderRadius = "3px";
tooltipEl.style.color = "white";
tooltipEl.style.opacity = 1;
tooltipEl.style.pointerEvents = "none";
tooltipEl.style.position = "absolute";
tooltipEl.style.transform = "translate(-50%, 0)";
tooltipEl.style.transition = "all .1s ease";

const table = document.createElement("table");
table.style.margin = "0px";

tooltipEl.appendChild(table);
chart.canvas.parentNode.appendChild(tooltipEl);
}

return tooltipEl;
};

export const externalTooltipHandler = (context) => {
// Tooltip Element
const { chart, tooltip } = context;
const tooltipEl = getOrCreateTooltip(chart);

// Hide if no tooltip
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}

// Set Text
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map((b) => b.lines);

const tableHead = document.createElement("thead");

titleLines.forEach((title) => {
const tr = document.createElement("tr");
tr.style.borderWidth = 0;

const th = document.createElement("th");
th.style.borderWidth = 0;
const text = document.createTextNode(title);

th.appendChild(text);
tr.appendChild(th);
tableHead.appendChild(tr);
});

const tableBody = document.createElement("tbody");
bodyLines.forEach((body, i) => {
const colors = tooltip.labelColors[i];

const span = document.createElement("span");
span.style.background = colors.backgroundColor;
span.style.borderColor = colors.borderColor;
span.style.borderWidth = "2px";
span.style.marginRight = "10px";
span.style.height = "10px";
span.style.width = "10px";
span.style.display = "inline-block";

const tr = document.createElement("tr");
tr.style.backgroundColor = "inherit";
tr.style.borderWidth = 0;

const td = document.createElement("td");
td.style.borderWidth = 0;

let textLine = body[0];

// Boxplot data
if (textLine && textLine.startsWith("(")) {
textLine = textLine.substring(1, textLine.length - 1);
textLine.split(", ").forEach((item, index) => {
if (index > 0) {
td.appendChild(document.createElement("br"));
}
td.appendChild(document.createTextNode(item));
});
} else {
// Other data
const text = document.createTextNode(textLine);
td.appendChild(text);
}

td.insertBefore(span, td.firstChild);
tr.appendChild(td);
tableBody.appendChild(tr);
});

const tableRoot = tooltipEl.querySelector("table");

// Remove old children
while (tableRoot.firstChild) {
tableRoot.firstChild.remove();
}

// Add new children
tableRoot.appendChild(tableHead);
tableRoot.appendChild(tableBody);
}

const { offsetLeft: positionX, offsetTop: positionY } = chart.canvas;

// Display, position, and set styles for font
tooltipEl.style.opacity = 1;
tooltipEl.style.left = positionX + tooltip.caretX + "px";
tooltipEl.style.top = positionY + tooltip.caretY + "px";
tooltipEl.style.font = tooltip.options.bodyFont.string;
tooltipEl.style.padding =
tooltip.options.padding + "px " + tooltip.options.padding + "px";
};
Loading

0 comments on commit 9b3935c

Please sign in to comment.