diff --git a/app/package.json b/app/package.json
index b1d6009..dad57a1 100644
--- a/app/package.json
+++ b/app/package.json
@@ -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",
diff --git a/app/src/components/charts/HorizontalBoxPlot.js b/app/src/components/charts/HorizontalBoxPlot.js
new file mode 100644
index 0000000..bb3bde8
--- /dev/null
+++ b/app/src/components/charts/HorizontalBoxPlot.js
@@ -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 ;
+};
+
+export default HorizontalBoxPlot;
diff --git a/app/src/components/charts/StackedBarChart.js b/app/src/components/charts/StackedBarChart.js
new file mode 100644
index 0000000..a30653e
--- /dev/null
+++ b/app/src/components/charts/StackedBarChart.js
@@ -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 ;
+};
+
+export default StackedBarChart;
diff --git a/app/src/components/charts/helpers.js b/app/src/components/charts/helpers.js
new file mode 100644
index 0000000..0028d44
--- /dev/null
+++ b/app/src/components/charts/helpers.js
@@ -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";
+};
diff --git a/app/src/components/data/FeatureTable.js b/app/src/components/data/FeatureTable.js
index 77f992a..beff6e4 100644
--- a/app/src/components/data/FeatureTable.js
+++ b/app/src/components/data/FeatureTable.js
@@ -1,18 +1,18 @@
-import { useEffect, React } from "react";
+import React from "react";
import Box from "@mui/material/Box";
import { DataGrid as MuiDataGrid } from "@mui/x-data-grid";
import { Card, CardContent, Typography } from "@mui/material";
import styled from "@emotion/styled";
-import { Chart, registerables } from "chart.js";
-Chart.register(...registerables);
+import StackedBarChart from "../charts/StackedBarChart";
+import HorizontalBoxPlot from "../charts/HorizontalBoxPlot";
const CellContent = styled.span`
font-weight: ${(props) => (props.isBold ? "bold" : "normal")};
`;
const ChartBox = styled.div`
- width: 200px;
+ width: 275px;
height: 50px;
`;
@@ -22,254 +22,13 @@ const DataGrid = styled(MuiDataGrid)`
}
`;
-// 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;
- }
- 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,
- },
- 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 ;
-}
-
-function randomValues(count, min, max) {
- const delta = max - min;
- return Array.from({ length: count }).map(() => Math.random() * delta + min);
-}
-
-function HorizontalBoxPlot({ data, chartId }) {
- useEffect(() => {
- if (!data) {
- return;
- }
-
- const ctx = document.getElementById(chartId).getContext("2d");
-
- // Unpack your data. Assuming data is in the format { min, max, mean, stdev }
- const { min, max, mean, stdev } = data;
-
- // Calculate box plot data
- const lowerQuartile = mean - stdev; // This is a simplification
- const upperQuartile = mean + stdev; // This is a simplification
-
- const chartData = {
- labels: ["Stats"],
- datasets: [
- {
- label: "Box Plot",
- data: [randomValues(100, min, max)],
- barPercentage: 0.5,
- barThickness: 50,
- maxBarThickness: 100,
- minBarLength: 2,
- errorBars: {
- Stats: { plus: max - mean, minus: mean - min },
- },
- },
- ],
- };
-
- const myChart = new Chart(ctx, {
- type: "bar",
- data: chartData,
- options: {
- indexAxis: "y",
- scales: {
- x: {
- display: false,
- },
- y: {
- display: true,
- },
- },
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- },
- },
- responsive: true,
- maintainAspectRatio: false,
- },
- });
-
- return () => {
- myChart.destroy();
- };
- }, [data]);
-
- 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") {
+ if (feature.target === "1" && feature.distr) {
targets = feature.distr[0];
}
return {
@@ -282,7 +41,7 @@ const FeatureTable = ({ data }) => {
});
const columns = [
- { field: "id", headerName: "Index", type: "number", width: 90 },
+ //{ field: "id", headerName: "Index", type: "number", width: 90 },
{
field: "name",
headerName: "Feature Name",
@@ -295,37 +54,17 @@ const FeatureTable = ({ data }) => {
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,
+ width: 280,
renderCell: (params) => {
const chartId = `chart-${params.row.id}`; // Assuming each row has a unique 'id'
const stats = {
- min: params.row.min,
- max: params.row.max,
- mean: params.row.mean,
- stdev: params.row.stdev,
+ min: parseFloat(params.row.min),
+ max: parseFloat(params.row.max),
+ mean: parseFloat(params.row.mean),
+ stdev: parseFloat(params.row.stdev),
};
return (
@@ -343,6 +82,27 @@ const FeatureTable = ({ data }) => {
);
},
},
+ {
+ field: "type",
+ headerName: "Type",
+ width: 90,
+ editable: true,
+ align: "right",
+ },
+ {
+ field: "distinct",
+ headerName: "Distinct values",
+ type: "number",
+ width: 110,
+ editable: true,
+ },
+ {
+ field: "missing",
+ headerName: "Missing values",
+ type: "number",
+ width: 110,
+ editable: true,
+ },
];
return (
diff --git a/app/src/pages/d/[dataId].js b/app/src/pages/d/[dataId].js
index bacc558..641303d 100644
--- a/app/src/pages/d/[dataId].js
+++ b/app/src/pages/d/[dataId].js
@@ -196,22 +196,6 @@ function Dataset({ data, error }) {
value: "v." + data.version,
icon: faCodeBranch,
},
- {
- label: "Data status",
- value: data.status === "active" ? "verified" : data.status,
- color:
- data.status === "active"
- ? "green"
- : data.status === "deactivated"
- ? "red"
- : "orange",
- icon:
- data.status === "active"
- ? faCheckCircle
- : data.status === "deactivated"
- ? faTimes
- : faWrench,
- },
{
label: "Data format",
value: data.format,
@@ -236,6 +220,22 @@ function Dataset({ data, error }) {
url: `/u/${data.uploader_id}`,
avatar: {data.uploader ? data.uploader.charAt(0) : "X"},
},
+ {
+ label: "Data status",
+ value: data.status === "active" ? "verified" : data.status,
+ color:
+ data.status === "active"
+ ? "green"
+ : data.status === "deactivated"
+ ? "red"
+ : "orange",
+ icon:
+ data.status === "active"
+ ? faCheckCircle
+ : data.status === "deactivated"
+ ? faTimes
+ : faWrench,
+ },
{
label: "Data likes",
value: data.nr_of_likes + " likes",
@@ -261,7 +261,7 @@ function Dataset({ data, error }) {
-
+
-
+
-
+
-
+