diff --git a/package.json b/package.json index b5916491..b66455e4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.57", "@types/date-arithmetic": "^4.1.1", + "@upsetjs/react": "^1.9.1", "@visx/gradient": "^1.0.0", "@visx/group": "^1.0.0", "@visx/hierarchy": "^1.0.0", diff --git a/src/plots/UpSet.tsx b/src/plots/UpSet.tsx new file mode 100644 index 00000000..e4a5030c --- /dev/null +++ b/src/plots/UpSet.tsx @@ -0,0 +1,69 @@ +import React, { lazy, Suspense, useMemo } from 'react'; +import { PlotProps } from './PlotlyPlot'; +import { UpSetData } from '../types/plots'; +import { extractFromExpression } from '@upsetjs/react'; +import Spinner from '../components/Spinner'; + +export interface UpSetProps extends PlotProps { + /** label for intersection size axis */ + intersectionSizeAxisLabel?: string; + /** label for set size axis */ + setSizeAxisLabel?: string; +} +const EmptyUpSetData: UpSetData = { intersections: [] }; + +const UpSetJS = lazy(() => import('@upsetjs/react')); + +export default function UpSet(props: UpSetProps) { + const { + data = EmptyUpSetData, + intersectionSizeAxisLabel, + setSizeAxisLabel, + // all the props below would normally be handled by PlotlyPlot, so we need to handle them here instead + title, + displayLegend = true, + containerStyles = { width: '100%', height: '400px' }, + interactive = false, + displayLibraryControls, + legendOptions, + legendTitle, + spacingOptions, + showSpinner, + } = props; + + // convert `data` into UpSetJS-friendly `sets` and `combinations` + + // as a placeholder - just use demo data from + // https://github.com/upsetjs/upsetjs/blob/main/examples/staticData/src/App.tsx#L42 + const { sets, combinations } = useMemo( + () => + extractFromExpression( + [ + { sets: ['A'], cardinality: 10 }, + { sets: ['B'], cardinality: 7 }, + { sets: ['A', 'B'], cardinality: 5 }, + ], + { + // ExtractFromExpressionOptions: + type: 'distinctIntersection', // this makes the "Set Size" totals correct + } + ), + [] + ); + + // width and height will need some extra work if we can't specify '100%' + return ( + +
+ + {showSpinner && } +
+
+ ); +} diff --git a/src/stories/plots/UpSet.stories.tsx b/src/stories/plots/UpSet.stories.tsx new file mode 100644 index 00000000..5daffc27 --- /dev/null +++ b/src/stories/plots/UpSet.stories.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Meta, Story } from '@storybook/react'; +import UpSet, { UpSetProps } from '../../plots/UpSet'; + +export default { + title: 'Plots/UpSet', + component: UpSet, +} as Meta; + +const Template = (args: UpSetProps) => ; + +export const Basic: Story = Template.bind({}); +Basic.args = { + title: 'amazing upset plot', + // data: ... + // intersectionSizeAxisLabel: 'Intersection size', + // setSizeAxisLabel: 'Set size', +}; diff --git a/src/types/plots/index.ts b/src/types/plots/index.ts index 66c74997..3bd94003 100644 --- a/src/types/plots/index.ts +++ b/src/types/plots/index.ts @@ -9,6 +9,7 @@ import { XYPlotData } from './xyplot'; import { BarplotData } from './barplot'; import { HeatmapData } from './heatmap'; import { MosaicData } from './mosaic'; +import { UpSetData } from './upset'; // Commonly used type definitions for plots. @@ -23,7 +24,8 @@ export type UnionOfPlotDataTypes = | XYPlotData | BarplotData | HeatmapData - | MosaicData; + | MosaicData + | UpSetData; export * from './addOns'; @@ -35,3 +37,4 @@ export * from './xyplot'; export * from './barplot'; export * from './heatmap'; export * from './mosaic'; +export * from './upset'; diff --git a/src/types/plots/upset.ts b/src/types/plots/upset.ts new file mode 100644 index 00000000..a56ec9bc --- /dev/null +++ b/src/types/plots/upset.ts @@ -0,0 +1,8 @@ +type SetIntersection = { + sets: string[]; // e.g. [ 'height', 'weight', 'country' ] + count: number; // == cardinality in UpSet +}; + +export type UpSetData = { + intersections: SetIntersection[]; +}; diff --git a/yarn.lock b/yarn.lock index 6c4cf225..23d75108 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2602,6 +2602,11 @@ resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-1.25.0.tgz#3d6fe591fac874f48dd225cb5660b2b785a21a05" integrity sha512-iIJp2CP6C32gVqI08HIYnzqj55tlLnodIBMCcMf28q9ckqMfMzocCmIzd9JWI/ALLPMUiTkCu1JGv3FFtu6t3g== +"@types/lz-string@^1.3.34": + version "1.3.34" + resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.34.tgz#69bfadde419314b4a374bf2c8e58659c035ed0a5" + integrity sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow== + "@types/markdown-to-jsx@^6.11.0": version "6.11.3" resolved "https://registry.yarnpkg.com/@types/markdown-to-jsx/-/markdown-to-jsx-6.11.3.tgz#cdd1619308fecbc8be7e6a26f3751260249b020e" @@ -2768,6 +2773,15 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@^17.0.1": + version "17.0.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.14.tgz#f0629761ca02945c4e8fea99b8177f4c5c61fb0f" + integrity sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -2775,6 +2789,11 @@ dependencies: "@types/react" "*" +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -2845,6 +2864,21 @@ dependencies: "@types/yargs-parser" "*" +"@upsetjs/model@~1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@upsetjs/model/-/model-1.9.1.tgz#86be17fd45fecec372d75a6d6f5d8fb493a7559c" + integrity sha512-CI34OPv4jsRAieZBILL+dx9/tnwfv/XwmBaOKkI0Z1iyPjOyMA5YkLXUhcidjlXKRwZ1U1siD73IinC73pCJHQ== + +"@upsetjs/react@^1.9.1": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@upsetjs/react/-/react-1.9.1.tgz#5e9d76dfa55c4953e8efd6dcefad8caa79243781" + integrity sha512-fyjlyDAXDS14+S360Huj8lmfTOiPwviFlvEQzA00DHJwA7LVuLiOO1TUUizAgt468QuIcSVXIQARkxQVX32Wiw== + dependencies: + "@types/lz-string" "^1.3.34" + "@types/react" "^17.0.1" + "@upsetjs/model" "~1.9.1" + lz-string "^1.4.4" + "@veupathdb/browserslist-config@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@veupathdb/browserslist-config/-/browserslist-config-1.0.0.tgz#90ca79640ffbb195a87d4d65cd4b2f92342f8b76" @@ -9154,6 +9188,11 @@ luxon@^1.25.0: resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.25.0.tgz#d86219e90bc0102c0eb299d65b2f5e95efe1fe72" integrity sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ== +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"