From 208c9f80c04408104184b4b04e0f9d3fa344d6e1 Mon Sep 17 00:00:00 2001 From: joaquinvanschoren Date: Sat, 27 Jan 2024 00:54:22 +0100 Subject: [PATCH] Even nicer distribution charts with nice tooltips. --- app/src/components/data/FeatureTable.js | 241 ++++++++++++++++++------ 1 file changed, 185 insertions(+), 56 deletions(-) diff --git a/app/src/components/data/FeatureTable.js b/app/src/components/data/FeatureTable.js index bac4ef4..fe65d0a 100644 --- a/app/src/components/data/FeatureTable.js +++ b/app/src/components/data/FeatureTable.js @@ -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"; @@ -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; @@ -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, })); @@ -48,6 +162,7 @@ function StackedBarChart({ data, chartId }) { scales: { x: { stacked: true, + display: showX, }, y: { stacked: true, @@ -58,6 +173,11 @@ function StackedBarChart({ data, chartId }) { legend: { display: false, }, + tooltip: { + enabled: false, + position: "nearest", + external: externalTooltipHandler, + }, }, responsive: true, maintainAspectRatio: false, @@ -69,62 +189,18 @@ function StackedBarChart({ data, chartId }) { }; }, [data]); - return ; + return ; } -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 {params.value}; - }, - }, - { - 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 ( - - - - ); - }, - }, -]; - 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) => { @@ -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 {params.value}; + }, + }, + { + 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 ( + + + + ); + }, + }, + ]; + return ( @@ -145,7 +275,6 @@ const FeatureTable = ({ data }) => { rows={rows} columns={columns} getRowId={(row) => row.index} - getRowHeight={(row) => 100} sortModel={[ { field: "id",