merge: Components package initial setup + Components extraction
**Issue:** https://github.com/datopian/portaljs/issues/812 ## Changes - Renamed old package to "components-old" - Created a Vite project based on https://dev.to/nicolaserny/create-a-react-component-library-with-vite-and-typescript-1ih9 and https://zach.codes/build-your-own-flexible-component-library-using-tsdx-typescript-tailwind-css-headless-ui/ - Implemented tailwind on it - Extracted components - LineChart - Table - Vega - VegaLite - Created stories for the extracted components
This commit is contained in:
1424
package-lock.json
generated
1424
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,10 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@nrwl/cypress": "15.9.2",
|
"@nrwl/cypress": "15.9.2",
|
||||||
"@nrwl/eslint-plugin-nx": "15.9.2",
|
"@nrwl/eslint-plugin-nx": "^16.0.2",
|
||||||
"@nrwl/jest": "15.9.2",
|
"@nrwl/jest": "15.9.2",
|
||||||
"@nrwl/js": "15.9.2",
|
"@nrwl/js": "15.9.2",
|
||||||
"@nrwl/linter": "15.9.2",
|
"@nrwl/linter": "15.9.2",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 627 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
16
packages/components/.eslintrc.cjs
Normal file
16
packages/components/.eslintrc.cjs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2020: true
|
||||||
|
},
|
||||||
|
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'warn'
|
||||||
|
}
|
||||||
|
};
|
||||||
24
packages/components/.gitignore
vendored
Normal file
24
packages/components/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
17
packages/components/.storybook/main.ts
Normal file
17
packages/components/.storybook/main.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
'@storybook/addon-interactions',
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
17
packages/components/.storybook/preview.ts
Normal file
17
packages/components/.storybook/preview.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import 'tailwindcss/tailwind.css'
|
||||||
|
|
||||||
|
import type { Preview } from '@storybook/react';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
32
packages/components/README.md
Normal file
32
packages/components/README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# `components` package
|
||||||
|
|
||||||
|
**See live:** https://storybook.portaljs.org
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
|
||||||
|
Use Storybook to work on components by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn storybook
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
To build this project, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
TODO: command to install the package
|
||||||
|
|
||||||
|
### Next.js
|
||||||
|
|
||||||
|
Add this line at the start of `_app.tsx`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// pages/_app.tsx
|
||||||
|
import "dist/styles.css";
|
||||||
|
```
|
||||||
70
packages/components/package.json
Normal file
70
packages/components/package.json
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"name": "components",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "yarn storybook",
|
||||||
|
"build": "tsc && vite build && yarn build-tailwind",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"prepack": "json -f package.json -I -e \"delete this.devDependencies; delete this.dependencies\"",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build",
|
||||||
|
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/styles.css --minify"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.0.17",
|
||||||
|
"next-mdx-remote": "^4.4.1",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-vega": "^7.6.0",
|
||||||
|
"vega": "5.20.2",
|
||||||
|
"vega-lite": "5.1.0",
|
||||||
|
"@tanstack/react-table": "^8.8.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@storybook/addon-essentials": "^7.0.7",
|
||||||
|
"@storybook/addon-interactions": "^7.0.7",
|
||||||
|
"@storybook/addon-links": "^7.0.7",
|
||||||
|
"@storybook/blocks": "^7.0.7",
|
||||||
|
"@storybook/react": "^7.0.7",
|
||||||
|
"@storybook/react-vite": "^7.0.7",
|
||||||
|
"@storybook/testing-library": "^0.0.14-next.2",
|
||||||
|
"@types/papaparse": "^5.3.7",
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||||
|
"@typescript-eslint/parser": "^5.57.1",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
|
"eslint-plugin-storybook": "^0.6.11",
|
||||||
|
"json": "^11.0.0",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"storybook": "^7.0.7",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.3.2",
|
||||||
|
"vite-plugin-dts": "^2.3.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"main": "./dist/components.umd.js",
|
||||||
|
"module": "./dist/components.es.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/components.es.js",
|
||||||
|
"require": "./dist/components.umd.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/components/postcss.config.js
Normal file
6
packages/components/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
32
packages/components/src/components/DebouncedInput.tsx
Normal file
32
packages/components/src/components/DebouncedInput.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const DebouncedInput = ({
|
||||||
|
value: initialValue,
|
||||||
|
onChange,
|
||||||
|
debounce = 500,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(initialValue);
|
||||||
|
}, [initialValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
onChange(value);
|
||||||
|
}, debounce);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DebouncedInput;
|
||||||
63
packages/components/src/components/LineChart.tsx
Normal file
63
packages/components/src/components/LineChart.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { VegaLite } from './VegaLite';
|
||||||
|
|
||||||
|
export type LineChartProps = {
|
||||||
|
data: Array<Array<string | number>> | string | { x: string; y: number }[];
|
||||||
|
title?: string;
|
||||||
|
xAxis?: string;
|
||||||
|
yAxis?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LineChart({
|
||||||
|
data = [],
|
||||||
|
fullWidth = false,
|
||||||
|
title = '',
|
||||||
|
xAxis = 'x',
|
||||||
|
yAxis = 'y',
|
||||||
|
}: LineChartProps) {
|
||||||
|
var tmp = data;
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
tmp = data.map((r, i) => {
|
||||||
|
return { x: r[0], y: r[1] };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const vegaData = { table: tmp };
|
||||||
|
const spec = {
|
||||||
|
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
|
||||||
|
title,
|
||||||
|
width: 'container',
|
||||||
|
height: 300,
|
||||||
|
mark: {
|
||||||
|
type: 'line',
|
||||||
|
color: 'black',
|
||||||
|
strokeWidth: 1,
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: 'table',
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
grid: {
|
||||||
|
type: 'interval',
|
||||||
|
bind: 'scales',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
encoding: {
|
||||||
|
x: {
|
||||||
|
field: xAxis,
|
||||||
|
timeUnit: 'year',
|
||||||
|
type: 'temporal',
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
field: yAxis,
|
||||||
|
type: 'quantitative',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
spec.data = { url: data } as any;
|
||||||
|
return <VegaLite fullWidth={fullWidth} spec={spec} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <VegaLite fullWidth={fullWidth} data={vegaData} spec={spec} />;
|
||||||
|
}
|
||||||
195
packages/components/src/components/Table.tsx
Normal file
195
packages/components/src/components/Table.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import {
|
||||||
|
createColumnHelper,
|
||||||
|
FilterFn,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
ChevronDoubleLeftIcon,
|
||||||
|
ChevronDoubleRightIcon,
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
} from '@heroicons/react/24/solid';
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import parseCsv from '../lib/parseCsv';
|
||||||
|
import DebouncedInput from './DebouncedInput';
|
||||||
|
import loadData from '../lib/loadData';
|
||||||
|
|
||||||
|
export type TableProps = {
|
||||||
|
data?: Array<{ [key: string]: number | string }>;
|
||||||
|
cols?: Array<{ [key: string]: string }>;
|
||||||
|
csv?: string;
|
||||||
|
url?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Table = ({
|
||||||
|
data: ogData = [],
|
||||||
|
cols: ogCols = [],
|
||||||
|
csv = '',
|
||||||
|
url = '',
|
||||||
|
fullWidth = false,
|
||||||
|
}: TableProps) => {
|
||||||
|
if (csv) {
|
||||||
|
const out = parseCsv(csv);
|
||||||
|
ogData = out.rows;
|
||||||
|
ogCols = out.fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, setData] = React.useState(ogData);
|
||||||
|
const [cols, setCols] = React.useState(ogCols);
|
||||||
|
// const [error, setError] = React.useState(""); // TODO: add error handling
|
||||||
|
|
||||||
|
const tableCols = useMemo(() => {
|
||||||
|
const columnHelper = createColumnHelper();
|
||||||
|
return cols.map((c) =>
|
||||||
|
columnHelper.accessor<any, string>(c.key, {
|
||||||
|
header: () => c.name,
|
||||||
|
cell: (info) => info.getValue(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [data, cols]);
|
||||||
|
|
||||||
|
const [globalFilter, setGlobalFilter] = useState('');
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns: tableCols,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
state: {
|
||||||
|
globalFilter,
|
||||||
|
},
|
||||||
|
globalFilterFn: globalFilterFn,
|
||||||
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (url) {
|
||||||
|
loadData(url).then((data) => {
|
||||||
|
const { rows, fields } = parseCsv(data);
|
||||||
|
setData(rows);
|
||||||
|
setCols(fields);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full'}`}>
|
||||||
|
<DebouncedInput
|
||||||
|
value={globalFilter ?? ''}
|
||||||
|
onChange={(value: any) => setGlobalFilter(String(value))}
|
||||||
|
className="p-2 text-sm shadow border border-block"
|
||||||
|
placeholder="Search all columns..."
|
||||||
|
/>
|
||||||
|
<table className="w-full mt-10">
|
||||||
|
<thead className="text-left border-b border-b-slate-300">
|
||||||
|
{table.getHeaderGroups().map((hg) => (
|
||||||
|
<tr key={hg.id}>
|
||||||
|
{hg.headers.map((h) => (
|
||||||
|
<th key={h.id} className="pr-2 pb-2">
|
||||||
|
<div
|
||||||
|
{...{
|
||||||
|
className: h.column.getCanSort()
|
||||||
|
? 'cursor-pointer select-none'
|
||||||
|
: '',
|
||||||
|
onClick: h.column.getToggleSortingHandler(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(h.column.columnDef.header, h.getContext())}
|
||||||
|
{{
|
||||||
|
asc: (
|
||||||
|
<ArrowUpIcon className="inline-block ml-2 h-4 w-4" />
|
||||||
|
),
|
||||||
|
desc: (
|
||||||
|
<ArrowDownIcon className="inline-block ml-2 h-4 w-4" />
|
||||||
|
),
|
||||||
|
}[h.column.getIsSorted() as string] ?? (
|
||||||
|
<div className="inline-block ml-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{table.getRowModel().rows.map((r) => (
|
||||||
|
<tr key={r.id} className="border-b border-b-slate-200">
|
||||||
|
{r.getVisibleCells().map((c) => (
|
||||||
|
<td key={c.id} className="py-2">
|
||||||
|
{flexRender(c.column.columnDef.cell, c.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="flex gap-2 items-center justify-center mt-10">
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronDoubleLeftIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<div>Page</div>
|
||||||
|
<strong>
|
||||||
|
{table.getState().pagination.pageIndex + 1} of{' '}
|
||||||
|
{table.getPageCount()}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`w-6 h-6 ${
|
||||||
|
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
|
||||||
|
}`}
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<ChevronDoubleRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const globalFilterFn: FilterFn<any> = (row, columnId, filterValue: string) => {
|
||||||
|
const search = filterValue.toLowerCase();
|
||||||
|
|
||||||
|
let value = row.getValue(columnId) as string;
|
||||||
|
if (typeof value === 'number') value = String(value);
|
||||||
|
|
||||||
|
return value?.toLowerCase().includes(search);
|
||||||
|
};
|
||||||
6
packages/components/src/components/Vega.tsx
Normal file
6
packages/components/src/components/Vega.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Wrapper for the Vega component
|
||||||
|
import { Vega as VegaOg } from "react-vega";
|
||||||
|
|
||||||
|
export function Vega(props) {
|
||||||
|
return <VegaOg {...props} />;
|
||||||
|
}
|
||||||
9
packages/components/src/components/VegaLite.tsx
Normal file
9
packages/components/src/components/VegaLite.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Wrapper for the Vega Lite component
|
||||||
|
import { VegaLite as VegaLiteOg } from "react-vega";
|
||||||
|
import applyFullWidthDirective from "../lib/applyFullWidthDirective";
|
||||||
|
|
||||||
|
export function VegaLite(props) {
|
||||||
|
const Component = applyFullWidthDirective({ Component: VegaLiteOg });
|
||||||
|
|
||||||
|
return <Component {...props} />;
|
||||||
|
}
|
||||||
3
packages/components/src/index.css
Normal file
3
packages/components/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
5
packages/components/src/index.ts
Normal file
5
packages/components/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./components/Table";
|
||||||
|
export * from "./components/LineChart";
|
||||||
|
export * from "./components/Vega";
|
||||||
|
export * from "./components/VegaLite";
|
||||||
|
export * from "./components/DataRichDocument";
|
||||||
21
packages/components/src/lib/applyFullWidthDirective.tsx
Normal file
21
packages/components/src/lib/applyFullWidthDirective.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export default function applyFullWidthDirective({
|
||||||
|
Component,
|
||||||
|
defaultWFull = true,
|
||||||
|
}) {
|
||||||
|
return (props) => {
|
||||||
|
const newProps = { ...props };
|
||||||
|
|
||||||
|
let newClassName = newProps.className || "";
|
||||||
|
if (newProps.fullWidth === true) {
|
||||||
|
newClassName += " w-[90vw] ml-[calc(50%-45vw)] max-w-none";
|
||||||
|
} else if (defaultWFull) {
|
||||||
|
// So that charts and tables will have the
|
||||||
|
// same width as the text content, but images
|
||||||
|
// can have its width set using the width prop
|
||||||
|
newClassName += " w-full";
|
||||||
|
}
|
||||||
|
newProps.className = newClassName;
|
||||||
|
|
||||||
|
return <Component {...newProps} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
5
packages/components/src/lib/loadData.tsx
Normal file
5
packages/components/src/lib/loadData.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default async function loadData(url: string) {
|
||||||
|
const response = await fetch(url)
|
||||||
|
const data = await response.text()
|
||||||
|
return data
|
||||||
|
}
|
||||||
20
packages/components/src/lib/parseCsv.ts
Normal file
20
packages/components/src/lib/parseCsv.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import papa from "papaparse";
|
||||||
|
|
||||||
|
const parseCsv = (csv: string) => {
|
||||||
|
csv = csv.trim();
|
||||||
|
const rawdata = papa.parse(csv, { header: true });
|
||||||
|
|
||||||
|
let cols: any[] = [];
|
||||||
|
if(rawdata.meta.fields) {
|
||||||
|
cols = rawdata.meta.fields.map((r: string) => {
|
||||||
|
return { key: r, name: r };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: rawdata.data as any,
|
||||||
|
fields: cols,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parseCsv;
|
||||||
1
packages/components/src/vite-env.d.ts
vendored
Normal file
1
packages/components/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
9
packages/components/stories/Introduction.mdx
Normal file
9
packages/components/stories/Introduction.mdx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Meta } from '@storybook/blocks';
|
||||||
|
|
||||||
|
<Meta title="Components/Introduction" />
|
||||||
|
|
||||||
|
# Welcome to the PortalJS components guide
|
||||||
|
|
||||||
|
**Official Website:** [portaljs.org](https://portaljs.org)
|
||||||
|
**Docs:** [portaljs.org/docs](https://portaljs.org/docs)
|
||||||
|
**GitHub:** [github.com/datopian/portaljs](https://github.com/datopian/portaljs)
|
||||||
59
packages/components/stories/LineChart.stories.ts
Normal file
59
packages/components/stories/LineChart.stories.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { LineChart, LineChartProps } from '../src/components/LineChart';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/LineChart',
|
||||||
|
component: LineChart,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
data: {
|
||||||
|
description:
|
||||||
|
'Data to be displayed.\n\n E.g.: [["1990", 1], ["1991", 2]] \n\nOR\n\n "https://url.to/data.csv"',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
description: 'Title to display on the chart. Optional.',
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
description:
|
||||||
|
'Name of the X axis on the data. Required when the "data" parameter is an URL.',
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
description:
|
||||||
|
'Name of the Y axis on the data. Required when the "data" parameter is an URL.',
|
||||||
|
},
|
||||||
|
fullWidth: {
|
||||||
|
description:
|
||||||
|
'Whether the component should be rendered as full bleed or not',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<LineChartProps>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
export const FromDataPoints: Story = {
|
||||||
|
name: 'Line chart from array of data points',
|
||||||
|
args: {
|
||||||
|
data: [
|
||||||
|
['1850', -0.41765878],
|
||||||
|
['1851', -0.2333498],
|
||||||
|
['1852', -0.22939907],
|
||||||
|
['1853', -0.27035445],
|
||||||
|
['1854', -0.29163003],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromURL: Story = {
|
||||||
|
name: 'Line chart from URL',
|
||||||
|
args: {
|
||||||
|
title: 'Oil Price x Year',
|
||||||
|
data: 'https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv',
|
||||||
|
xAxis: 'Date',
|
||||||
|
yAxis: 'Price',
|
||||||
|
},
|
||||||
|
};
|
||||||
69
packages/components/stories/Table.stories.ts
Normal file
69
packages/components/stories/Table.stories.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Table, TableProps } from '../src/components/Table';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/Table',
|
||||||
|
component: Table,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
data: {
|
||||||
|
description: "Data to be displayed in the table, must also set \"cols\" to work."
|
||||||
|
},
|
||||||
|
cols: {
|
||||||
|
description: "Columns to be displayed in the table, must also set \"data\" to work."
|
||||||
|
},
|
||||||
|
csv: {
|
||||||
|
description: "CSV data as string.",
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
description: "Fetch the data from a CSV file remotely."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<TableProps>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
export const FromColumnsAndData: Story = {
|
||||||
|
name: "Table from columns and data",
|
||||||
|
args: {
|
||||||
|
data: [
|
||||||
|
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||||
|
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
|
||||||
|
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
|
||||||
|
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
|
||||||
|
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
|
||||||
|
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
|
||||||
|
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
|
||||||
|
],
|
||||||
|
cols: [
|
||||||
|
{ key: 'id', name: 'ID' },
|
||||||
|
{ key: 'firstName', name: 'First name' },
|
||||||
|
{ key: 'lastName', name: 'Last name' },
|
||||||
|
{ key: 'age', name: 'Age' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromRawCSV: Story = {
|
||||||
|
name: "Table from raw CSV",
|
||||||
|
args: {
|
||||||
|
csv: `
|
||||||
|
Year,Temp Anomaly
|
||||||
|
1850,-0.418
|
||||||
|
2020,0.923
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FromURL: Story = {
|
||||||
|
name: "Table from URL",
|
||||||
|
args: {
|
||||||
|
url: "https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
50
packages/components/stories/Vega.stories.ts
Normal file
50
packages/components/stories/Vega.stories.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { Vega } from '../src/components/Vega';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/Vega',
|
||||||
|
component: Vega,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<any>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
export const Primary: Story = {
|
||||||
|
name: 'Chart built with Vega',
|
||||||
|
args: {
|
||||||
|
data: {
|
||||||
|
table: [
|
||||||
|
{
|
||||||
|
y: -0.418,
|
||||||
|
x: 1850,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
y: 0.923,
|
||||||
|
x: 2020,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
|
||||||
|
mark: 'bar',
|
||||||
|
data: {
|
||||||
|
name: 'table',
|
||||||
|
},
|
||||||
|
encoding: {
|
||||||
|
x: {
|
||||||
|
field: 'x',
|
||||||
|
type: 'ordinal',
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
field: 'y',
|
||||||
|
type: 'quantitative',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
60
packages/components/stories/VegaLite.stories.ts
Normal file
60
packages/components/stories/VegaLite.stories.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { VegaLite } from '../src/components/VegaLite';
|
||||||
|
|
||||||
|
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'Components/VegaLite',
|
||||||
|
component: VegaLite,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
argTypes: {
|
||||||
|
data: {
|
||||||
|
description:
|
||||||
|
'Data to be used by Vega Lite. See the Vega Lite docs: https://vega.github.io/vega-lite/docs/data.html.',
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
description:
|
||||||
|
'Spec to be used by Vega Lite. See the Vega Lite docs: https://vega.github.io/vega-lite/docs/spec.html.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<any>;
|
||||||
|
|
||||||
|
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||||
|
export const Primary: Story = {
|
||||||
|
name: 'Chart built with Vega Lite',
|
||||||
|
args: {
|
||||||
|
data: {
|
||||||
|
table: [
|
||||||
|
{
|
||||||
|
y: -0.418,
|
||||||
|
x: 1850,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
y: 0.923,
|
||||||
|
x: 2020,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
|
||||||
|
mark: 'bar',
|
||||||
|
data: {
|
||||||
|
name: 'table',
|
||||||
|
},
|
||||||
|
encoding: {
|
||||||
|
x: {
|
||||||
|
field: 'x',
|
||||||
|
type: 'ordinal',
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
field: 'y',
|
||||||
|
type: 'quantitative',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
8
packages/components/tailwind.config.js
Normal file
8
packages/components/tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
28
packages/components/tsconfig.json
Normal file
28
packages/components/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
// "strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }],
|
||||||
|
}
|
||||||
10
packages/components/tsconfig.node.json
Normal file
10
packages/components/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
33
packages/components/vite.config.ts
Normal file
33
packages/components/vite.config.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import dts from 'vite-plugin-dts';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
'process.env': {}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
dts({
|
||||||
|
insertTypesEntry: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'src/index.ts'),
|
||||||
|
name: 'components',
|
||||||
|
formats: ['es', 'umd'],
|
||||||
|
fileName: (format) => `components.${format}.js`,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['react', 'react-dom', 'styled-components'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: 'React',
|
||||||
|
'react-dom': 'ReactDOM'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
8947
packages/components/yarn.lock
Normal file
8947
packages/components/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user