Skip to content

Commit

Permalink
Even nicer distribution charts with nice tooltips.
Browse files Browse the repository at this point in the history
  • Loading branch information
joaquinvanschoren committed Jan 26, 2024
1 parent aff99d5 commit 208c9f8
Showing 1 changed file with 185 additions and 56 deletions.
241 changes: 185 additions & 56 deletions app/src/components/data/FeatureTable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, React } from "react";
import Box from "@mui/material/Box";
import { DataGrid } from "@mui/x-data-grid";
import { DataGrid as MuiDataGrid } from "@mui/x-data-grid";
import { Card, CardContent, Typography } from "@mui/material";
import styled from "@emotion/styled";

Expand All @@ -12,11 +12,125 @@ const CellContent = styled.span`
`;

const ChartBox = styled.div`
width: 400px;
height: 100px;
width: 200px;
height: 50px;
`;

function StackedBarChart({ data, chartId }) {
const DataGrid = styled(MuiDataGrid)`
& .MuiDataGrid-row > .MuiDataGrid-cell {
overflow: visible !important;
}
`;

// External tooltip handler
// See 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;
};

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;

const text = document.createTextNode(body);

td.appendChild(span);
td.appendChild(text);
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";
};

function StackedBarChart({ data, chartId, showX, targets }) {
useEffect(() => {
if (!data) {
return;
Expand All @@ -33,7 +147,7 @@ function StackedBarChart({ data, chartId }) {
);

const datasets = transposedData.map((data, index) => ({
label: `Class ${index + 1}`,
label: targets[index],
data: data,
borderWidth: 1,
}));
Expand All @@ -48,6 +162,7 @@ function StackedBarChart({ data, chartId }) {
scales: {
x: {
stacked: true,
display: showX,
},
y: {
stacked: true,
Expand All @@ -58,6 +173,11 @@ function StackedBarChart({ data, chartId }) {
legend: {
display: false,
},
tooltip: {
enabled: false,
position: "nearest",
external: externalTooltipHandler,
},
},
responsive: true,
maintainAspectRatio: false,
Expand All @@ -69,62 +189,18 @@ function StackedBarChart({ data, chartId }) {
};
}, [data]);

return <canvas id={chartId} width="400" height="400"></canvas>;
return <canvas id={chartId}></canvas>;
}

const columns = [
{ field: "id", headerName: "Index", type: "number", width: 90 },
{
field: "name",
headerName: "Feature Name",
width: 200,
editable: true,
valueGetter: (params) =>
`${params.row.name} ${params.row.target === "1" ? "(target)" : ""}`,
renderCell: (params) => {
const isBold = params.row.target === "1";
return <CellContent isBold={isBold}>{params.value}</CellContent>;
},
},
{
field: "type",
headerName: "Type",
width: 100,
editable: true,
},
{
field: "distinct",
headerName: "Distinct values",
type: "number",
width: 110,
editable: true,
},
{
field: "missing",
headerName: "Missing values",
type: "number",
width: 110,
editable: true,
},
{
field: "distr",
headerName: "Distribution",
width: 400,
renderCell: (params) => {
const chartId = `chart-${params.row.id}`; // Assuming each row has a unique 'id'
return (
<ChartBox>
<StackedBarChart data={params.value} chartId={chartId} />
</ChartBox>
);
},
},
];

const FeatureTable = ({ data }) => {
// Check for targets
let targets = [];
// Define the rows for the grid
const rows = data.map((feature) => {
const id = feature.index; // Rename index to id
if (feature.target === "1") {
targets = feature.distr[0];
}
return {
id,
...Object.keys(feature).reduce((acc, key) => {
Expand All @@ -134,6 +210,60 @@ const FeatureTable = ({ data }) => {
};
});

const columns = [
{ field: "id", headerName: "Index", type: "number", width: 90 },
{
field: "name",
headerName: "Feature Name",
width: 200,
editable: true,
valueGetter: (params) =>
`${params.row.name} ${params.row.target === "1" ? "(target)" : ""}`,
renderCell: (params) => {
const isBold = params.row.target === "1";
return <CellContent isBold={isBold}>{params.value}</CellContent>;
},
},
{
field: "type",
headerName: "Type",
width: 100,
editable: true,
},
{
field: "distinct",
headerName: "Distinct values",
type: "number",
width: 110,
editable: true,
},
{
field: "missing",
headerName: "Missing values",
type: "number",
width: 110,
editable: true,
},
{
field: "distr",
headerName: "Distribution",
width: 200,
renderCell: (params) => {
const chartId = `chart-${params.row.id}`; // Assuming each row has a unique 'id'
return (
<ChartBox>
<StackedBarChart
data={params.value}
chartId={chartId}
showX={params.row.target === "1"}
targets={targets}
/>
</ChartBox>
);
},
},
];

return (
<Card>
<CardContent>
Expand All @@ -145,7 +275,6 @@ const FeatureTable = ({ data }) => {
rows={rows}
columns={columns}
getRowId={(row) => row.index}
getRowHeight={(row) => 100}
sortModel={[
{
field: "id",
Expand Down

0 comments on commit 208c9f8

Please sign in to comment.