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

Table component to display tabular data #597

Merged
merged 11 commits into from
Aug 4, 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
13 changes: 13 additions & 0 deletions app/scripts/components/common/blocks/lazy-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import LazyLoad from 'react-lazyload';
import Chart from '$components/common/chart/block';
import { chartMaxHeight } from '$components/common/chart/constant';

import Table, { tableHeight } from '$components/common/table';
import CompareImage from '$components/common/blocks/images/compare';

import Map, { mapHeight } from '$components/common/blocks/block-map';
Expand Down Expand Up @@ -59,3 +60,15 @@ export function LazyCompareImage(props) {
</LazyLoad>
);
}

export function LazyTable(props) {
return (
<LazyLoad
placeholder={<LoadingSkeleton height={tableHeight} />}
offset={50}
once
>
<Table {...props} />
</LazyLoad>
);
}
6 changes: 4 additions & 2 deletions app/scripts/components/common/mdx-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
LazyChart,
LazyCompareImage,
LazyScrollyTelling,
LazyMap
LazyMap,
LazyTable
} from '$components/common/blocks/lazy-components';
import { NotebookConnectCalloutBlock } from '$components/common/notebook-connect';
import SmartLink from '$components/common/smart-link';
Expand All @@ -41,7 +42,8 @@ function MdxContent(props) {
Chart: LazyChart,
CompareImage: LazyCompareImage,
NotebookConnectCallout: NotebookConnectCalloutBlock,
Link: SmartLink
Link: SmartLink,
Table: LazyTable
}}
>
<pageMdx.MdxContent {...(props.throughProps || {})} />
Expand Down
261 changes: 261 additions & 0 deletions app/scripts/components/common/table/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import React, { useRef } from 'react';
import {
flexRender,
getCoreRowModel,
useReactTable,
ColumnDef,
getSortedRowModel,
SortingState,
Row,
SortDirection
} from '@tanstack/react-table';
import { useVirtual } from 'react-virtual';
import { Sheet2JSONOpts } from 'xlsx';
import {
CollecticonSortAsc,
CollecticonSortDesc,
CollecticonSortNone
} from '@devseed-ui/collecticons';
import { Table } from '@devseed-ui/typography';
import styled from 'styled-components';
import { themeVal } from '@devseed-ui/theme-provider';

import useLoadFile from '$utils/use-load-file';

export interface ExcelOption {
sheetNumber?: number;
parseOption?: Sheet2JSONOpts;
}

interface TablecomponentProps {
dataPath: string;
excelOption?: ExcelOption;
columnToSort?: string[];
}

export const tableHeight = '400';

const PlaceHolderWrapper = styled.div`
display: flex;
height: ${tableHeight}px;
align-items: center;
justify-content: center;
font-weight: bold;
`;

const TableWrapper = styled.div`
display: flex;
max-width: 100%;
max-height: ${tableHeight}px;
overflow: auto;
`;

const StyledTable = styled(Table)`
thead {
position: sticky;
top: 0;
z-index: 1;
border-bottom: 2px solid ${themeVal('color.base-200')};
background: ${themeVal('color.surface')};
box-shadow: 0 0 0 1px ${themeVal('color.base-200a')};

th {
vertical-align: middle;
}

.th-inner {
display: flex;
min-width: 8rem;
gap: 0.5rem;
align-items: center;
}

button {
flex: 0 0 auto;
}
}
`;

export default function TableComponent({
dataPath,
excelOption,
columnToSort
}: TablecomponentProps) {
const tableContainerRef = useRef<HTMLDivElement>(null);

const { data, dataLoading, dataError } = useLoadFile(dataPath, excelOption);
const [sorting, setSorting] = React.useState<SortingState>([]);
const dataLoaded = !dataLoading && !dataError;

const columns: ColumnDef<object>[] = data.length
? Object.keys(data[0]).map((key) => {
return {
accessorKey: key,
enableSorting: columnToSort?.includes(key) ? true : false
};
})
: [];

const table = useReactTable({
data,
columns,
state: {
sorting
},
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel()
});

const { rows } = table.getRowModel();
const rowVirtualizer = useVirtual({
parentRef: tableContainerRef,
size: rows.length,
overscan: 50
});
const { virtualItems: virtualRows, totalSize } = rowVirtualizer;

const paddingTop = virtualRows.length > 0 ? virtualRows[0]?.start || 0 : 0;
const paddingBottom =
virtualRows.length > 0
? totalSize - (virtualRows[virtualRows.length - 1]?.end || 0)
: 0;

return (
<>
{dataLoading && (
<PlaceHolderWrapper>
<p>Loading Data...</p>
</PlaceHolderWrapper>
)}
{dataError && (
<PlaceHolderWrapper>
<p>Something went wrong while loading the data. Please try later. </p>
</PlaceHolderWrapper>
)}
{dataLoaded && (
<TableWrapper ref={tableContainerRef}>
<StyledTable>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} colSpan={header.colSpan}>
<SortableTh
isSortable={header.column.getCanSort()}
sortDirection={header.column.getIsSorted()}
onSortClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</SortableTh>
</th>
))}
</tr>
))}
</thead>
<tbody>
{paddingTop > 0 && (
<tr>
<td style={{ height: `${paddingTop}px` }} />
</tr>
)}
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index] as Row<unknown>;
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
);
})}
</tr>
);
})}
{paddingBottom > 0 && (
<tr>
<td style={{ height: `${paddingBottom}px` }} />
</tr>
)}
</tbody>
</StyledTable>
</TableWrapper>
)}
</>
);
}

const SortableLink = styled.a`
display: inline-flex;
gap: 0.25rem;
align-items: center;
transition: opacity 0.16s ease-in-out;

&,
&:visited {
color: inherit;
text-decoration: none;
}

&:hover {
opacity: 0.8;
}

svg {
flex-shrink: 0;
}
`;

interface SortableThProps {
children: React.ReactNode;
isSortable: boolean;
sortDirection: false | SortDirection;
onSortClick: ((event: unknown) => void) | undefined;
}

function SortableTh(props: SortableThProps) {
const { children, isSortable, sortDirection, onSortClick } = props;

return (
<div className='th-inner'>
{isSortable ? (
<SortableLink
href='#'
onClick={(e) => {
e.preventDefault();
onSortClick?.(e);
}}
>
<span>{children}</span>
{sortDirection === 'asc' && (
<CollecticonSortAsc
meaningful={true}
title='Sorted in ascending order'
/>
)}
{sortDirection === 'desc' && (
<CollecticonSortDesc
meaningful={true}
title='Sorted in descending order'
/>
)}
{!sortDirection && (
<CollecticonSortNone
meaningful={true}
title={`Sort the rows with this column's value`}
/>
)}
</SortableLink>
) : (
children
)}
</div>
);
}
6 changes: 6 additions & 0 deletions app/scripts/components/sandbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SandboxAnalysisChart from './analysis-chart';
import SandboxRequest from './request';
import SandboxColors from './colors';
import SandboxMDXEditor from './mdx-editor';
import SandboxTable from './table';
import { resourceNotFound } from '$components/uhoh';
import { Card, CardList } from '$components/common/card';
import { Fold, FoldHeader, FoldTitle } from '$components/common/fold';
Expand Down Expand Up @@ -98,6 +99,11 @@ const pages = [
id: 'mdxeditor',
name: 'Discovery Editor ⚠️EXPERIMENTAL',
component: SandboxMDXEditor
},
{
id: 'sandboxtable',
name: 'Table',
component: SandboxTable
}
];

Expand Down
10 changes: 10 additions & 0 deletions app/scripts/components/sandbox/table/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React, { lazy } from 'react';

const MdxContent = lazy(() => import('$components/common/mdx-content'));
const pageLoader = () => import('./table.mdx');

function SandboxMDXPage() {
return <MdxContent loader={pageLoader} />;
}

export default SandboxMDXPage;
27 changes: 27 additions & 0 deletions app/scripts/components/sandbox/table/table.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Block>
<Figure>
<Table
dataPath='/public/2021_data_summary_spreadsheets/ghgp_data_by_year.xlsx'
excelOption={{ parseOption: { range: 3 } }}
columnToSort={['Facility Id', 'Zip Code']}
/>
<Caption> Table example</Caption>
</Figure>
</Block>

<Block type='wide'>
<Figure>
<Table
dataPath='/public/2021_data_summary_spreadsheets/ghgp_data_by_year.xlsx'
excelOption={{ sheetNumber: 0, parseOption: { range: 3 } }}
/>
<Caption> Wide block Table example</Caption>
</Figure>
</Block>

<Block>
<Figure>
<Table dataPath='/public/example.csv' columnToSort={['New Positives']} />
<Caption> Table example from csv</Caption>
</Figure>
</Block>
Loading
Loading