Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Waitp 1338 add matrices #4725

Merged
merged 8 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions packages/tupaia-web/src/api/queries/useReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/tupaia-web/src/components/DateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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%;
}
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
`;

/**
Expand Down Expand Up @@ -128,6 +136,7 @@ export const EnlargedDashboardItem = () => {
/>
)}
</TitleWrapper>
{currentReport?.description && <Subheading>{currentReport?.description}</Subheading>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good pick up on this

<DashboardItemContent
isLoading={isLoadingReportData}
error={error}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const ExpandableButton = styled(Button).attrs({
border: none;
top: 0;
left: 0;
z-index: 1;
opacity: 0;
margin-top: 0;
&:hover,
Expand Down
113 changes: 113 additions & 0 deletions packages/tupaia-web/src/features/Matrix.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Tupaia
* Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import {
MatrixColumnType,
MatrixRowType,
getIsUsingDots,
Matrix as MatrixComponent,
Alert,
} from '@tupaia/ui-components';
import { MatrixDataColumn, MatrixDataRow, MatrixViewContent } from '../types';
import { ConditionalPresentationOptions } from '@tupaia/types';
import styled from 'styled-components';

const NoDataMessage = styled(Alert).attrs({
severity: 'info',
})`
width: 100%;
margin: 1rem auto;
max-width: 24rem;
`;

// This is a recursive function that parses the rows of the matrix into a format that the Matrix component can use.
const parseRows = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot going on here but separating it out into parseRows and parseColumns and all the comments help clarify it a lot.

rows: MatrixDataRow[],
categoryId?: MatrixDataRow['categoryId'],
): MatrixRowType[] => {
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 <img src={placeholderImage} alt="Matrix Placeholder" />;

const parsedRows = parseRows(rows);
const parsedColumns = parseColumns(columns);

if (!parsedRows.length) return <NoDataMessage>No data available</NoDataMessage>;

return <MatrixComponent {...config} rows={parsedRows} columns={parsedColumns} />;
};
26 changes: 24 additions & 2 deletions packages/tupaia-web/src/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Project> & {
hasAccess: boolean;
Expand Down Expand Up @@ -63,7 +65,6 @@ export type TupaiaUrlParams = {
dashboardCode?: DashboardCode;
};

export type ReportDisplayProps = ViewContent & DashboardItemType;
export type DashboardName = DashboardResponse['dashboardName'];

export type SingleMapOverlayItem = KeysToCamelCase<
Expand Down Expand Up @@ -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<string, any> & {
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[];
};
1 change: 1 addition & 0 deletions packages/ui-components/src/components/Matrix/Matrix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/ui-components/src/components/Matrix/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
*/

export { Matrix } from './Matrix';
export * from './utils';