Skip to content

Commit

Permalink
Table component to display tabular data (#597)
Browse files Browse the repository at this point in the history
  • Loading branch information
hanbyul-here authored Aug 4, 2023
2 parents 1ced268 + 0984789 commit b82dffc
Show file tree
Hide file tree
Showing 11 changed files with 519 additions and 4 deletions.
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: 'Story 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

0 comments on commit b82dffc

Please sign in to comment.