@seasketch/geoprocessing
Version:
Geoprocessing and reporting framework for SeaSketch 2.0
240 lines • 14 kB
JavaScript
import React from "react";
import { useTranslation } from "react-i18next";
import { nestMetrics } from "../../metrics/helpers.js";
import { percentWithEdge, keyBy, getObjectiveById, } from "../../helpers/index.js";
import { Table } from "../table/Table.js";
import { LayerToggle } from "../LayerToggle.js";
import { CheckCircleFill, InfoCircleFill } from "@styled-icons/bootstrap";
import { HorizontalStackedBar, } from "../chart/HorizontalStackedBar.js";
import { valueFormatter, } from "../../helpers/valueFormatter.js";
import { ReportTableStyled } from "../table/ReportTableStyled.js";
import { styled } from "styled-components";
import { getMetricGroupObjectiveId } from "../../helpers/metricGroup.js";
import { Tooltip } from "../Tooltip.js";
export const ClassTableStyled = styled(ReportTableStyled) `
.styled {
font-size: 13px;
td {
padding: 6px 5px;
}
}
`;
/**
* Table displaying class metrics, one class per table row. Having more than one metric per class may yield unexpected results
* Returns 0 value in table when faced with a 'missing' metric instead of erroring
* Handles "class has no value" NaN situation (common when sketch doesn't overlap with a geography) by overwriting with 0 and adding information circle
*/
export const ClassTable = ({ rows, columnConfig, metricGroup, objective, }) => {
const { t } = useTranslation();
const classesByName = keyBy(metricGroup.classes, (curClass) => curClass.classId);
// group metrics by class ID, then metric ID, for easy lookup
const metricsByClassByMetric = nestMetrics(rows, ["classId", "metricId"]);
// Use sketch ID for each table row, use index to lookup into nested metrics
const tableRows = Object.keys(metricsByClassByMetric).map((classId) => ({
classId,
}));
const genColumns = (colConfigs) => {
const defaultWidth = 100 / colConfigs.length;
const defaultClassLabel = t("Class");
const defaultMapLabel = t("Map");
const defaultTargetLabel = t("Target");
const defaultGoalLabel = t("Goal");
const defaultValueLabel = t("Value");
// Transform column configs into Columns
const colz = colConfigs.map((colConfig) => {
const style = {
width: `${colConfig.width || defaultWidth}%`,
...(colConfig.colStyle ? colConfig.colStyle : {}),
};
switch (colConfig.type) {
case "class": {
return {
Header: colConfig.columnLabel || defaultClassLabel,
accessor: (row) => {
/* i18next-extract-disable-next-line */
const transString = t(classesByName[row.classId || "missing"]?.display);
return transString || "missing";
},
style,
};
}
case "metricValue": {
return {
Header: colConfig.columnLabel || defaultValueLabel,
accessor: (row) => {
if (!colConfig.metricId)
throw new Error("Missing metricId in column config");
// Return 0 when faced with a 'missing' metric
// Return 0 with a Tooltip when faced with a 'NaN' metric value
const value = (() => {
if (metricsByClassByMetric[row.classId] &&
metricsByClassByMetric[row.classId][colConfig.metricId]) {
return metricsByClassByMetric[row.classId][colConfig.metricId][0].value;
}
else {
return 0;
}
})();
const suffix = (() => {
if (Number.isNaN(value)) {
const tooltipText = (classesByName[row.classId || "missing"]?.display ||
"This feature class") +
" not found in the selected planning area";
return (React.createElement(Tooltip, { text: tooltipText, placement: "bottom", offset: { horizontal: 0, vertical: 5 } },
React.createElement(InfoCircleFill, { size: 14, style: {
color: "#83C6E6",
} })));
}
else {
return React.createElement(React.Fragment, null);
}
})();
const formattedValue = (() => {
const finalValue = Number.isNaN(value) ? 0 : value;
return colConfig.valueFormatter
? valueFormatter(finalValue, colConfig.valueFormatter)
: finalValue;
})();
return (React.createElement(React.Fragment, null,
formattedValue,
colConfig.valueLabel ? ` ${colConfig.valueLabel}` : "",
suffix));
},
style,
};
}
case "metricChart": {
return {
Header: colConfig.columnLabel || " ",
style: { textAlign: "center", ...style },
accessor: (row, rowIndex) => {
if (!colConfig.metricId)
throw new Error("Missing metricId in column config");
// Return 0 when faced with a 'missing' metric
const value = (() => {
if (metricsByClassByMetric[row.classId] &&
metricsByClassByMetric[row.classId][colConfig.metricId]) {
return metricsByClassByMetric[row.classId][colConfig.metricId][0].value;
}
else {
return 0;
}
})();
const target = (() => {
if (!objective)
return 0;
if (Array.isArray(objective)) {
// Multi-objective - need to find by class ID
const objectiveId = getMetricGroupObjectiveId(metricGroup, row.classId);
const theObj = Array.isArray(objective)
? getObjectiveById(objectiveId, objective)
: objective;
if (colConfig.valueFormatter === "percent") {
return theObj.target * 100;
}
else {
return theObj.target;
}
}
else {
// single objective, just grab the target
if (colConfig.valueFormatter === "percent") {
return objective.target * 100;
}
else {
return objective.target;
}
}
})();
const tooltipText = (classesByName[row.classId || "missing"]?.display ||
"This feature class") +
" not found in the selected planning area";
const chartProps = {
...(colConfig.chartOptions ? colConfig.chartOptions : {}),
rows: [
[
[
colConfig.valueFormatter === "percent"
? value * 100
: value,
],
],
],
rowConfigs: [
{
title: (value) => (React.createElement(React.Fragment, null,
Number.isNaN(value) ? (React.createElement(Tooltip, { text: tooltipText, placement: "bottom", offset: { horizontal: 0, vertical: 5 } },
React.createElement(InfoCircleFill, { size: 14, style: {
color: "#83C6E6",
} }))) : target && value >= target ? (React.createElement(CheckCircleFill, { size: 14, style: { color: "#78c679", paddingRight: 5 } })) : (React.createElement(React.Fragment, null)),
percentWithEdge(Number.isNaN(value) ? 0 : value / 100))),
},
],
max: 100,
};
let targetValueFormatter;
if (typeof colConfig.targetValueFormatter === "function") {
targetValueFormatter = colConfig.targetValueFormatter(target, rowIndex, tableRows.length);
}
else {
targetValueFormatter = (targetValue) => rowIndex === tableRows.length - 1
? `${defaultTargetLabel} - ${valueFormatter(targetValue / 100, "percent0dig")}`
: "";
}
return (React.createElement("div", { style: { display: "flex", alignItems: "center" } },
React.createElement("div", { style: { flex: 1 } },
React.createElement(HorizontalStackedBar, { blockGroupNames: ["foo"], blockGroupStyles: [{ backgroundColor: "#ACD0DE" }], showTitle: true, showLegend: false, showTargetLabel: true, targetLabelPosition: "bottom", showTotalLabel: false, barHeight: 12, target: target || undefined, targetValueFormatter: targetValueFormatter, ...chartProps }))));
},
};
}
case "metricGoal": {
return {
Header: colConfig.columnLabel || defaultGoalLabel,
style,
accessor: (row) => {
const objectiveId = getMetricGroupObjectiveId(metricGroup, row.classId);
const theObj = Array.isArray(objective)
? getObjectiveById(objectiveId, objective)
: objective;
if (!theObj)
throw new Error(`Missing objective for objectiveId ${objectiveId}`);
return colConfig.valueFormatter
? valueFormatter(theObj.target, colConfig.valueFormatter)
: `${theObj.target}${colConfig.valueLabel ? ` ${colConfig.valueLabel}` : ""}`;
},
};
}
case "layerToggle": {
return {
Header: colConfig.columnLabel || defaultMapLabel,
style: { textAlign: "center", ...style },
accessor: (row, index) => {
const isSimpleGroup = metricGroup.layerId ? false : true;
const layerId = metricGroup.layerId || classesByName[row.classId].layerId;
if (isSimpleGroup && layerId) {
return (React.createElement(LayerToggle, { simple: true, size: "small", layerId: layerId, style: {
marginTop: 0,
justifyContent: "center",
} }));
}
else if (!isSimpleGroup && layerId && index === 0) {
return (React.createElement(LayerToggle, { simple: true, size: "small", layerId: layerId, style: { marginTop: 0, justifyContent: "center" } }));
}
else {
return React.createElement(React.Fragment, null);
}
},
};
}
default: {
throw new Error(`Unexpected ClassTableColumnConfig type ${colConfig.type}`);
}
}
});
return colz;
};
const columns = genColumns(columnConfig);
return (React.createElement(ClassTableStyled, null,
React.createElement(Table, { className: "styled", columns: columns, data: tableRows })));
};
//# sourceMappingURL=ClassTable.js.map