UNPKG

@seasketch/geoprocessing

Version:

Geoprocessing and reporting framework for SeaSketch 2.0

240 lines • 14 kB
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