-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed issues with boxplots, improved styling, reorganized code for ea…
…sier reuse
- Loading branch information
1 parent
5d8eb2c
commit 9b3935c
Showing
9 changed files
with
342 additions
and
293 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
}; |
Oops, something went wrong.