diff --git a/packages/tupaia-web/public/images/matrix-placeholder-dot-only.png b/packages/tupaia-web/public/images/matrix-placeholder-dot-only.png
new file mode 100644
index 0000000000..983325bbc5
Binary files /dev/null and b/packages/tupaia-web/public/images/matrix-placeholder-dot-only.png differ
diff --git a/packages/tupaia-web/public/images/matrix-placeholder-mix.png b/packages/tupaia-web/public/images/matrix-placeholder-mix.png
new file mode 100644
index 0000000000..0e32c614ea
Binary files /dev/null and b/packages/tupaia-web/public/images/matrix-placeholder-mix.png differ
diff --git a/packages/tupaia-web/public/images/matrix-placeholder-text-only.png b/packages/tupaia-web/public/images/matrix-placeholder-text-only.png
new file mode 100644
index 0000000000..08a6a319e5
Binary files /dev/null and b/packages/tupaia-web/public/images/matrix-placeholder-text-only.png differ
diff --git a/packages/tupaia-web/src/api/queries/useReport.ts b/packages/tupaia-web/src/api/queries/useReport.ts
index be3f8d2a6b..4488d226de 100644
--- a/packages/tupaia-web/src/api/queries/useReport.ts
+++ b/packages/tupaia-web/src/api/queries/useReport.ts
@@ -24,6 +24,7 @@ export const useReport = (reportCode: DashboardItemType['reportCode'], params: Q
const timeZone = getBrowserTimeZone();
const formattedStartDate = formatDateForApi(startDate, null);
const formattedEndDate = formatDateForApi(endDate, null);
+ const endPoint = legacy ? 'legacyDashboardReport' : 'report';
return useQuery(
[
'report',
@@ -36,10 +37,9 @@ export const useReport = (reportCode: DashboardItemType['reportCode'], params: Q
formattedEndDate,
],
() =>
- get(`report/${reportCode}`, {
+ get(`${endPoint}/${reportCode}`, {
params: {
dashboardCode,
- legacy,
itemCode,
projectCode,
organisationUnitCode: entityCode,
diff --git a/packages/tupaia-web/src/components/DateRangePicker.tsx b/packages/tupaia-web/src/components/DateRangePicker.tsx
index b06102b87e..ed503f8724 100644
--- a/packages/tupaia-web/src/components/DateRangePicker.tsx
+++ b/packages/tupaia-web/src/components/DateRangePicker.tsx
@@ -23,6 +23,7 @@ const Wrapper = styled.div`
padding: 0.2rem;
text-transform: none;
font-size: 0.875rem;
+ min-width: 0;
color: ${({ theme }) => theme.palette.text.primary};
svg {
height: 1.3rem;
diff --git a/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx b/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx
index c7d93f8da5..225dd6b32f 100644
--- a/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx
+++ b/packages/tupaia-web/src/features/DashboardItem/DashboardItemContent.tsx
@@ -7,17 +7,18 @@ import React from 'react';
import styled from 'styled-components';
import { UseQueryResult } from 'react-query';
import { Alert as BaseAlert, TextButton } from '@tupaia/ui-components';
+import { ViewContent as ChartViewContent } from '@tupaia/ui-chart-components';
import { Typography, Link, CircularProgress } from '@material-ui/core';
-import { ReportDisplayProps } from '../../types';
import { Chart } from '../Chart';
import { ExpandItemButton } from './ExpandItemButton';
+import { Matrix } from '../Matrix';
+import { DashboardItemType, MatrixViewContent } from '../../types';
const ErrorLink = styled(Link)`
color: inherit;
text-decoration: underline;
font-weight: ${({ theme }) => theme.typography.fontWeightBold};
`;
-
const RetryButton = styled(TextButton)`
margin: 0;
padding: 0;
@@ -30,6 +31,7 @@ const RetryButton = styled(TextButton)`
`;
const Alert = styled(BaseAlert)`
+ overflow: hidden; // this is to stop any extra long text from overflowing the alert and causing a horizontal scroll on the dashboard
.MuiAlert-message {
max-width: 100%;
}
@@ -49,13 +51,13 @@ const LoadingContainer = styled.div`
padding: 1rem;
`;
-// Eventually matrix etc will be added here
const DisplayComponents = {
chart: Chart,
+ matrix: Matrix,
};
interface DashboardItemContentProps {
- viewContent: ReportDisplayProps;
+ viewContent: DashboardItemType & (ChartViewContent | MatrixViewContent);
isEnlarged?: boolean;
isLoading: boolean;
error: UseQueryResult['error'] | null;
diff --git a/packages/tupaia-web/src/features/DashboardItem/EnlargedDashboardItem.tsx b/packages/tupaia-web/src/features/DashboardItem/EnlargedDashboardItem.tsx
index 59508d7cec..3ea93a37e5 100644
--- a/packages/tupaia-web/src/features/DashboardItem/EnlargedDashboardItem.tsx
+++ b/packages/tupaia-web/src/features/DashboardItem/EnlargedDashboardItem.tsx
@@ -46,6 +46,14 @@ const Title = styled(Typography).attrs({
const TitleWrapper = styled(FlexColumn)`
align-items: center;
+ margin-bottom: 1rem;
+`;
+
+const Subheading = styled(Typography).attrs({
+ variant: 'h3',
+})`
+ font-size: 1rem;
+ margin-bottom: 1rem;
`;
/**
@@ -128,6 +136,7 @@ export const EnlargedDashboardItem = () => {
/>
)}
+ {currentReport?.description && {currentReport?.description}}
{
+ let topLevelRows = [];
+ // if a categoryId is not passed in, then we need to find the top level rows
+ if (!categoryId) {
+ // get the highest level rows, which are the ones that have a category but no categoryId
+ const highestLevel = rows.filter(row => row.category && !row.categoryId);
+ // if there are no highest level rows, then the top level rows are just all of the rows
+ topLevelRows = highestLevel.length ? highestLevel : rows;
+ } else {
+ // otherwise, the top level rows are the ones that have the categoryId that was passed in
+ topLevelRows = rows.filter(row => row.categoryId === categoryId);
+ }
+
+ // loop through the topLevelRows, and parse them into the format that the Matrix component can use
+ return topLevelRows.map(row => {
+ const { dataElement = '', category, ...rest } = row;
+ // if the row has a category, then it has children, so we need to parse them using this same function
+ if (category) {
+ return {
+ title: category,
+ ...rest,
+ children: parseRows(rows, category),
+ };
+ }
+ // otherwise, handle as a regular row
+ return {
+ title: dataElement,
+ ...rest,
+ };
+ });
+};
+
+// This is a recursive function that parses the columns of the matrix into a format that the Matrix component can use.
+const parseColumns = (columns: MatrixDataColumn[]): MatrixColumnType[] => {
+ return columns.map(column => {
+ const { category, key, title, columns: children } = column;
+ // if a column has a category, then it has children, so we need to parse them using this same function
+ if (category)
+ return {
+ title: category,
+ key: category,
+ children: parseColumns(children!),
+ };
+ // otherwise, handle as a regular column
+ return {
+ title,
+ key,
+ };
+ });
+};
+
+const getPlaceholderImage = ({ presentationOptions = {}, categoryPresentationOptions = {} }) => {
+ // if the matrix is not using any dots, show a text-only placeholder
+ if (!getIsUsingDots(presentationOptions) && !getIsUsingDots(categoryPresentationOptions))
+ return '/images/matrix-placeholder-text-only.png';
+ // if the matrix has applyLocation.columnIndexes, show a mix placeholder, because this means it is a mix of dots and text
+ if ((presentationOptions as ConditionalPresentationOptions)?.applyLocation?.columnIndexes)
+ return '/images/matrix-placeholder-mix.png';
+ // otherwise, show a dot-only placeholder
+ return '/images/matrix-placeholder-dot-only.png';
+};
+
+/**
+ * This is the component that is used to display a matrix. It handles the parsing of the data into the format that the Matrix component can use, as well as placeholder images. It shows a message when there are no rows available to display.
+ */
+
+interface MatrixProps {
+ viewContent: MatrixViewContent;
+ isEnlarged?: boolean;
+}
+export const Matrix = ({ viewContent, isEnlarged = false }: MatrixProps) => {
+ const { columns, rows, ...config } = viewContent;
+
+ const placeholderImage = getPlaceholderImage(config);
+ // in the dashboard, show a placeholder image
+ if (!isEnlarged) return ;
+
+ const parsedRows = parseRows(rows);
+ const parsedColumns = parseColumns(columns);
+
+ if (!parsedRows.length) return No data available;
+
+ return ;
+};
diff --git a/packages/tupaia-web/src/types/types.d.ts b/packages/tupaia-web/src/types/types.d.ts
index 7e29031bc6..07409ed0b0 100644
--- a/packages/tupaia-web/src/types/types.d.ts
+++ b/packages/tupaia-web/src/types/types.d.ts
@@ -9,12 +9,14 @@ import {
MapOverlay,
MapOverlayGroupRelation,
EntityType,
+ MatrixConfig,
} from '@tupaia/types';
import { ActivePolygonProps, LeafletMapProps } from '@tupaia/ui-map-components';
-import { ViewContent } from '@tupaia/ui-chart-components';
+import { ViewContent as ChartViewContent } from '@tupaia/ui-chart-components';
import { Position } from 'geojson';
import { KeysToCamelCase } from './helpers';
import { GRANULARITY_CONFIG } from '@tupaia/utils';
+import { MatrixColumnType, MatrixRowType } from '@tupaia/ui-components';
export type SingleProject = KeysToCamelCase & {
hasAccess: boolean;
@@ -63,7 +65,6 @@ export type TupaiaUrlParams = {
dashboardCode?: DashboardCode;
};
-export type ReportDisplayProps = ViewContent & DashboardItemType;
export type DashboardName = DashboardResponse['dashboardName'];
export type SingleMapOverlayItem = KeysToCamelCase<
@@ -101,3 +102,24 @@ export type EntityResponse = Entity & {
photoUrl?: string;
children?: Entity[];
};
+
+// This is the row type in the response from the report endpoint when the report is a matrix. It will contain data for each column, keyed by the column key, as well as dataElement, categoryId and category
+export type MatrixDataRow = Record & {
+ dataElement?: string; // this is the data to display in the row header cell
+ categoryId?: string; // this means the row is a child of a grouped row
+ category?: string; // this means the row is a grouped row
+};
+
+// This is the column type in the response from the report endpoint when the report is a matrix
+export type MatrixDataColumn = {
+ title: string;
+ key: string;
+ category?: string; // this means the column is a grouped column
+ columns?: MatrixDataColumn[]; // these are the child columns of a grouped column
+};
+
+// The 'ViewContent' is the data that is passed to the matrix view component
+export type MatrixViewContent = MatrixConfig & {
+ rows: MatrixDataRow[];
+ columns: MatrixDataColumn[];
+};
diff --git a/packages/ui-components/src/components/Matrix/Matrix.tsx b/packages/ui-components/src/components/Matrix/Matrix.tsx
index df11977f90..a6fe220533 100644
--- a/packages/ui-components/src/components/Matrix/Matrix.tsx
+++ b/packages/ui-components/src/components/Matrix/Matrix.tsx
@@ -20,6 +20,7 @@ const MatrixTable = styled.table`
border-collapse: collapse;
border: 1px solid ${({ theme }) => getFullHex(theme.palette.text.primary)}33;
color: ${({ theme }) => theme.palette.text.primary};
+ table-layout: fixed; // this is to allow us to set max-widths on the columns
height: 1px; // this is to make the cell content (eg. buttons) take full height of the cell, and does not actually get applied
td,
th {
diff --git a/packages/ui-components/src/components/Matrix/index.ts b/packages/ui-components/src/components/Matrix/index.ts
index dd73d8877f..32e3eb86e7 100644
--- a/packages/ui-components/src/components/Matrix/index.ts
+++ b/packages/ui-components/src/components/Matrix/index.ts
@@ -4,3 +4,4 @@
*/
export { Matrix } from './Matrix';
+export * from './utils';