diff --git a/package-lock.json b/package-lock.json index 6c11e972..7312558d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trelliscopejs-lib", - "version": "0.7.1", + "version": "0.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trelliscopejs-lib", - "version": "0.7.1", + "version": "0.7.4", "dependencies": { "@dnd-kit/core": "^6.1.0", "@dnd-kit/modifiers": "^7.0.0", @@ -34,6 +34,7 @@ "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "dompurify": "^3.0.8", + "lodash.clonedeep": "^4.5.0", "lodash.difference": "^4.5.0", "lodash.isequal": "^4.5.0", "material-react-table": "^2.11.1", @@ -65,6 +66,7 @@ "@types/d3-array": "^3.2.1", "@types/d3-scale": "^4.0.8", "@types/dompurify": "^3.0.5", + "@types/lodash.clonedeep": "^4.5.9", "@types/lodash.difference": "^4.5.9", "@types/lodash.isequal": "^4.5.8", "@types/node": "^20.11.16", @@ -2367,6 +2369,15 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.difference": { "version": "4.5.9", "resolved": "https://registry.npmjs.org/@types/lodash.difference/-/lodash.difference-4.5.9.tgz", @@ -6288,6 +6299,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", diff --git a/package.json b/package.json index a0379351..f2c06bad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trelliscopejs-lib", - "version": "0.7.3", + "version": "0.7.5", "type": "module", "files": [ "dist" @@ -41,6 +41,7 @@ "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "dompurify": "^3.0.8", + "lodash.clonedeep": "^4.5.0", "lodash.difference": "^4.5.0", "lodash.isequal": "^4.5.0", "material-react-table": "^2.11.1", @@ -92,6 +93,7 @@ "@types/d3-array": "^3.2.1", "@types/d3-scale": "^4.0.8", "@types/dompurify": "^3.0.5", + "@types/lodash.clonedeep": "^4.5.9", "@types/lodash.difference": "^4.5.9", "@types/lodash.isequal": "^4.5.8", "@types/node": "^20.11.16", diff --git a/src/assets/styles/main.css b/src/assets/styles/main.css index 16dada74..8478e482 100644 --- a/src/assets/styles/main.css +++ b/src/assets/styles/main.css @@ -72,6 +72,14 @@ body { color: #ff5252; } +.trelliscope-fullscreen { + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100vw; + height: 100vh; +} + .react-daterange-picker__clear-button, .react-datetimerange-picker__clear-button { display: none !important; diff --git a/src/components/ContentHeader/ContentHeader.module.scss b/src/components/ContentHeader/ContentHeader.module.scss index affccca3..5811952a 100644 --- a/src/components/ContentHeader/ContentHeader.module.scss +++ b/src/components/ContentHeader/ContentHeader.module.scss @@ -45,7 +45,7 @@ z-index: 1001; &Icon { - position: fixed; + position: absolute; cursor: pointer; z-index: 1200; color: #000; diff --git a/src/components/FullscreenButton/FullscreenButton.tsx b/src/components/FullscreenButton/FullscreenButton.tsx index 377824d0..47ddd87b 100644 --- a/src/components/FullscreenButton/FullscreenButton.tsx +++ b/src/components/FullscreenButton/FullscreenButton.tsx @@ -29,12 +29,14 @@ const FullscreenButton: React.FC = () => { document.body.webkitRequestFullscreen(); if (mainEl) { addClass(mainEl, 'trelliscope-spa'); + addClass(mainEl, 'trelliscope-fullscreen'); } dispatch(setFullscreen(true)); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); if (mainEl) { removeClass(mainEl, 'trelliscope-spa'); + removeClass(mainEl, 'trelliscope-fullscreen'); } dispatch(setFullscreen(false)); dispatch(windowResize(originalDims)); @@ -45,12 +47,14 @@ const FullscreenButton: React.FC = () => { document.body.requestFullscreen(); if (mainEl) { addClass(mainEl, 'trelliscope-spa'); + addClass(mainEl, 'trelliscope-fullscreen'); } dispatch(setFullscreen(true)); } else if (document.exitFullscreen) { document.exitFullscreen(); if (mainEl) { removeClass(mainEl, 'trelliscope-spa'); + removeClass(mainEl, 'trelliscope-fullscreen'); } dispatch(setFullscreen(false)); dispatch(windowResize(originalDims)); @@ -61,6 +65,7 @@ const FullscreenButton: React.FC = () => { if (!document.fullscreenElement && !document.webkitFullscreenElement) { if (mainEl) { removeClass(mainEl, 'trelliscope-spa'); + removeClass(mainEl, 'trelliscope-fullscreen'); } dispatch(setFullscreen(false)); dispatch(windowResize(originalDims)); diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index cf4adcb7..b93ea00d 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -57,6 +57,7 @@ const Sidebar: React.FC = () => { className={styles.sidebar} variant="persistent" anchor="left" + PaperProps={{ sx: { position: 'relative !important' }}} > {mutableFilters.length === 0 && showFilterHelpText && ( diff --git a/src/index.tsx b/src/index.tsx index 78737826..c9122b7a 100755 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,175 +1,10 @@ -import React from 'react'; -import * as ReactDOMClient from 'react-dom/client'; -import { Provider } from 'react-redux'; -// latin-only reduces bundle size by 0.5MB but users's might have data with non-latin characters -// import '@fontsource/poppins/latin-300.css'; -// import '@fontsource/poppins/latin-500.css'; -// import '@fontsource/poppins/latin-600.css'; -// import '@fontsource/jost/latin-500.css'; -// import '@fontsource/source-code-pro/latin-300.css'; -// import '@fontsource/source-code-pro/latin-600.css'; -import '@fontsource/poppins/300.css'; -import '@fontsource/poppins/500.css'; -import '@fontsource/poppins/600.css'; -import '@fontsource/jost/latin-500.css'; -import '@fontsource/source-code-pro/300.css'; -import '@fontsource/source-code-pro/600.css'; -import '@fortawesome/fontawesome-svg-core/styles.css'; -import 'react-virtualized/styles.css'; // only needs to be imported once -import { Trelliscope, prepareTrelliscope } from './jsApi'; -import TrelliscopeApp from './TrelliscopeApp'; - -import store from './store'; - -import { addClass } from './classManipulation'; - -import './assets/styles/main.css'; -import './assets/styles/variables.scss'; - -import { setLayout } from './slices/layoutSlice'; -import { windowResize, setAppDims } from './slices/uiSlice'; -// import reducers from './reducers'; -import App from './App'; - -import CrossfilterClient from './CrossfilterClient'; -import type { IDataClient } from './DataClient'; - -// function for populating a div with a trelliscope app -const trelliscopeApp = ( - id: string, - config: string | ITrelliscopeAppSpec, - options: { logger?: boolean; mockData?: boolean } = {} as AppOptions, -) => { - // Sets up msw worker for mocking api calls - /* if (import.meta.env.MODE !== 'production' && options.mockData) { - const worker = await import('./test/__mockData__/worker'); - worker.default.start(import { IDataClient } from './DataClient'; -); - } */ - const crossFilterClient = new CrossfilterClient(); - - const el = document.getElementById(id) as HTMLElement; - const container = document.getElementById(id) as HTMLElement; - const root = ReactDOMClient.createRoot(container); - - addClass(el, 'trelliscope-app'); - addClass(el, 'trelliscope-app-container'); - if (el.style.position !== 'relative') { - el.style.position = 'relative'; - } - if (el.style.overflow !== 'hidden') { - el.style.overflow = 'hidden'; - } - el.style['font-family' as unknown as number] = '"Poppins", sans-serif'; - el.style['font-weight' as unknown as number] = '300'; - el.style['-webkit-tap-highlight-color' as unknown as number] = 'rgba(0,0,0,0)'; - - // if there is only one div in the whole document and it doesn't have dimensions - // then we treat this as a single-page application - const noHeight = el.style.height === undefined || el.style.height === '' || el.style.height === '100%'; - const noWidth = el.style.width === undefined || el.style.width === '' || el.style.width === '100%'; - - let singlePageApp = false; - - if (!el.classList.contains('trelliscope-not-spa') && (noHeight || noWidth)) { - singlePageApp = true; - // el.parentNode.nodeName === 'BODY' - el.style.width = '100%'; - el.style.height = '100%'; - addClass(document.body, 'trelliscope-fullscreen-body'); - addClass(document.getElementsByTagName('html')[0], 'trelliscope-fullscreen-html'); - } else { - const bodyEl = document.createElement('div'); - bodyEl.style.width = '100%'; - bodyEl.style.height = '100%'; - bodyEl.style.display = 'none'; - bodyEl.id = 'trelliscope-fullscreen-div'; - document.getElementsByTagName('body')[0].appendChild(bodyEl); - - if (noHeight) { - const nSiblings = - [].slice - .call(el?.parentNode?.childNodes) - .map((d: { nodeType: number }) => d.nodeType !== 3 && d.nodeType !== 8) - .filter(Boolean).length - 1; - if (nSiblings === 0) { - el.style.height = `${el?.parentNode?.firstElementChild?.clientHeight}px`; - el.style.width = `${el?.parentNode?.firstElementChild?.clientWidth}px`; - } - } - - // give 'el' a new parent so we know where to move div back to after fullscreen - const parent = el.parentNode as ParentNode; - const wrapper = document.createElement('div'); - wrapper.id = `${el.id}-parent`; - parent.replaceChild(wrapper, el); - // set element as child of wrapper - wrapper.appendChild(el); - } - - // need to store original app dims (constant) if it isn't a SPA - // this will only be used in that case, but store it always anyway - const appDims = {} as { width: number; height: number }; - - // set size of app - if (singlePageApp) { - appDims.width = window.innerWidth; - appDims.height = window.innerHeight; - } else { - appDims.width = el.clientWidth; - appDims.height = el.clientHeight; - } - - if (!el.classList.contains('trelliscope-not-spa') && (noHeight || noWidth)) { - singlePageApp = true; - } - - // resize handler only when in fullscreen mode (which is always for SPA) - window.addEventListener('resize', () => { - if (store.getState().app.fullscreen) { - store.dispatch( - windowResize({ - height: window.innerHeight, - width: window.innerWidth, - }), - ); - } - }); - - root.render( - - - , - ); - - return { - resize: (width: number, height: number) => { - el.style.height = `${height}px`; - el.style.width = `${width}px`; - store.dispatch(setAppDims({ width, height })); - store.dispatch(windowResize({ width, height })); - }, - setLayout: (nrow: number, ncol: number) => { - store.dispatch(setLayout({ nrow, ncol })); - }, - currentMeta: () => crossFilterClient.getData(), - }; -}; +import TrelliscopeApp from "./TrelliscopeApp"; +import trelliscopeApp from "./trelliscopeAppFunc"; +import { Trelliscope } from "./jsApi"; window.trelliscopeApp = trelliscopeApp; window.Trelliscope = Trelliscope; -// TODO: should be able to just attach this to window so that it can be loaded in other apps -// by including the js script and then using the component (vs. having to import and bundle it in the app) -// window.TrelliscopeApp = TrelliscopeApp; - // if in development mode, populate div with an example trelliscope app if (import.meta.env.MODE === 'development') { const example = window.__DEV_EXAMPLE__ as unknown as { id: string; name: string; datatype: string }; @@ -185,6 +20,7 @@ if (import.meta.env.MODE === 'development') { document.body.appendChild(div); if (example.name === 'gapminder_js') { + const snakeCase = (str: string) => str.replace(/([^a-zA-Z0-9_])/g, '_'); fetch('_examples/gapminder_js/gapminder.json') .then((response) => response.json()) .then((data) => { @@ -213,11 +49,9 @@ if (import.meta.env.MODE === 'development') { .setPanelFunction({ varname: 'lexp_time', label: 'Life expectancy over time', - aspect: 0.66, - func: (row: Datum) => { - console.log('row::::', row); - return `https://apps.trelliscope.org/gapminder/displays/Life_expectancy/panels/lexp_time_unfacet/${row.country}_${row.continent}.png`; - }, + aspect: 1 / 0.66, + func: (row: Datum) => + `https://apps.trelliscope.org/gapminder/displays/Life_expectancy/panels/lexp_time_unfacet/${snakeCase(row.country as string)}_${row.continent}.png`, }) // FIXME // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -225,19 +59,48 @@ if (import.meta.env.MODE === 'development') { .setPanelFunction({ varname: 'flag', label: 'Country flag', - aspect: 0.66, + aspect: 1 / 0.66, func: (row: Datum) => `https://raw.githubusercontent.com/hafen/countryflags/master/png/512/${row.iso_alpha2}.png`, }) + // FIXME + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore .setPanelFunction({ varname: 'html_panel', label: 'Dummy "plot" to test html panels', - aspect: 2, + aspect: 1 / 2, panelType: 'iframeSrcDoc', func: (row: Datum) => `
${row.country}
`, }); - trelliscopeApp(example.id, appdat); + + const testCase = 'view'; + + if (testCase === 'view') { + div.className = ''; + const testEl = appdat.view({ width: 1200, height: 800 }); + document.getElementById(example.id)?.appendChild(testEl); + } else if (testCase === 'el') { + div.className = ''; + // optionally, you can create a new div pass the div to trelliscopeApp instead of needing a div to already exist + const el = document.createElement('div'); + el.id = 'test'; + el.style.width = '1000px'; + el.style.height = '800px'; + const newEl = trelliscopeApp(el, appdat); + const outer = document.createElement('div'); + outer.style.width = '1000px'; + outer.style.height = '800px'; + outer.style.position = 'fixed'; + outer.style.top = '100px'; + outer.style.left = '100px'; + outer.style.border = '1px solid red'; + outer.appendChild(newEl as HTMLElement); + document.getElementById(example.id)?.appendChild(outer); + } else if (testCase === 'id') { + trelliscopeApp(example.id, appdat); + } }); } else { trelliscopeApp(example.id, `_examples/${example.name}/config.${example.datatype}`, { logger: true }); diff --git a/src/jsApi.ts b/src/jsApi.ts index 701fe43f..36136d43 100644 --- a/src/jsApi.ts +++ b/src/jsApi.ts @@ -1,8 +1,9 @@ /* eslint-disable max-classes-per-file */ /* eslint-disable @typescript-eslint/lines-between-class-members */ -import { max } from 'd3-array'; +// import { max } from 'd3-array'; +import cloneDeep from 'lodash.clonedeep'; +import trelliscopeApp from './trelliscopeAppFunc'; import { metaIndex } from './slices/metaDataAPI'; -import { cloneDeep } from 'lodash'; export class Meta implements IMeta { name: string; // why?? @@ -797,6 +798,21 @@ class TrelliscopeClass implements ITrelliscopeAppSpec { return this; } + view({ + width = 1200, + height = 800, + } : { + width: number; + height: number; + }): HTMLElement { + const div = document.createElement("div") as HTMLElement; + div.id = `trelliscope-app-${Math.random().toString(36).substring(2, 15)}`; + div.style.width = `${width}px`; + div.style.height = `${height}px`; + const newEl = trelliscopeApp(div, this, {}); + return newEl as HTMLElement; + } + // setStringFilter(varname: string, values: string[], regexp: string): ITrelliscopeAppSpec { // const {metas} = this.displays[this.displayList[0].name].displayInfo; // const meta = metas.find((m) => m.varname === varname); diff --git a/src/trelliscopeAppFunc.tsx b/src/trelliscopeAppFunc.tsx new file mode 100644 index 00000000..b07e5a1c --- /dev/null +++ b/src/trelliscopeAppFunc.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import * as ReactDOMClient from 'react-dom/client'; +import { Provider } from 'react-redux'; +// latin-only reduces bundle size by 0.5MB but users's might have data with non-latin characters +// import '@fontsource/poppins/latin-300.css'; +// import '@fontsource/poppins/latin-500.css'; +// import '@fontsource/poppins/latin-600.css'; +// import '@fontsource/jost/latin-500.css'; +// import '@fontsource/source-code-pro/latin-300.css'; +// import '@fontsource/source-code-pro/latin-600.css'; +import '@fontsource/poppins/300.css'; +import '@fontsource/poppins/500.css'; +import '@fontsource/poppins/600.css'; +import '@fontsource/jost/latin-500.css'; +import '@fontsource/source-code-pro/300.css'; +import '@fontsource/source-code-pro/600.css'; +import '@fortawesome/fontawesome-svg-core/styles.css'; +import 'react-virtualized/styles.css'; // only needs to be imported once +import { prepareTrelliscope } from './jsApi'; + +import store from './store'; + +import { addClass } from './classManipulation'; + +import './assets/styles/main.css'; +import './assets/styles/variables.scss'; + +import { setLayout } from './slices/layoutSlice'; +import { windowResize, setAppDims } from './slices/uiSlice'; +// import reducers from './reducers'; +import App from './App'; + +import CrossfilterClient from './CrossfilterClient'; +import type { IDataClient } from './DataClient'; + +// function for populating a div with a trelliscope app +const trelliscopeApp = ( + element: string | HTMLElement, // either the id of the div or the div itself + config: string | ITrelliscopeAppSpec, + options: { logger?: boolean; mockData?: boolean } = {} as AppOptions, +) => { + // Sets up msw worker for mocking api calls + /* if (import.meta.env.MODE !== 'production' && options.mockData) { + const worker = await import('./test/__mockData__/worker'); + worker.default.start(import { IDataClient } from './DataClient'; +); + } */ + const crossFilterClient = new CrossfilterClient(); + + let el: HTMLElement; + let container: HTMLElement; + let id: string; + let divInput: boolean = false; + if (typeof element === 'string') { + id = element; + el = document.getElementById(element) as HTMLElement; + container = document.getElementById(element) as HTMLElement; + } else { + divInput = true; + el = element; + addClass(el, 'trelliscope-not-spa'); + container = element; + id = el.id; + if (id === '') { + id = `trelliscope-app-${Math.random().toString(36).substring(2, 15)}`; + el.id = id; + } + } + // const el = document.getElementById(id) as HTMLElement; + // const container = document.getElementById(id) as HTMLElement; + const root = ReactDOMClient.createRoot(container); + + addClass(el, 'trelliscope-app'); + addClass(el, 'trelliscope-app-container'); + if (el.style.position !== 'relative') { + el.style.position = 'relative'; + } + if (el.style.overflow !== 'hidden') { + el.style.overflow = 'hidden'; + } + el.style['font-family' as unknown as number] = '"Poppins", sans-serif'; + el.style['font-weight' as unknown as number] = '300'; + el.style['-webkit-tap-highlight-color' as unknown as number] = 'rgba(0,0,0,0)'; + + // if there is only one div in the whole document and it doesn't have dimensions + // then we treat this as a single-page application + const noHeight = el.style.height === undefined || el.style.height === '' || el.style.height === '100%'; + const noWidth = el.style.width === undefined || el.style.width === '' || el.style.width === '100%'; + + let singlePageApp = false; + + if (!el.classList.contains('trelliscope-not-spa') && (noHeight || noWidth)) { + singlePageApp = true; + // el.parentNode.nodeName === 'BODY' + el.style.width = '100%'; + el.style.height = '100%'; + } else if (noHeight) { + const nSiblings = + [].slice + .call(el?.parentNode?.childNodes) + .map((d: { nodeType: number }) => d.nodeType !== 3 && d.nodeType !== 8) + .filter(Boolean).length - 1; + if (nSiblings === 0) { + el.style.height = `${el?.parentNode?.firstElementChild?.clientHeight}px`; + el.style.width = `${el?.parentNode?.firstElementChild?.clientWidth}px`; + } + } + + // need to store original app dims (constant) if it isn't a SPA + // this will only be used in that case, but store it always anyway + const appDims = {} as { width: number; height: number }; + + // set size of app + if (singlePageApp) { + appDims.width = window.innerWidth; + appDims.height = window.innerHeight; + } else { + appDims.width = el.clientWidth; + appDims.height = el.clientHeight; + } + + root.render( + + + , + ); + + if (divInput) { + return el; + } + + return { + resize: (width: number, height: number) => { + el.style.height = `${height}px`; + el.style.width = `${width}px`; + store.dispatch(setAppDims({ width, height })); + store.dispatch(windowResize({ width, height })); + }, + setLayout: (nrow: number, ncol: number) => { + store.dispatch(setLayout({ nrow, ncol })); + }, + currentMeta: () => crossFilterClient.getData(), + }; +}; + +export default trelliscopeApp; diff --git a/src/types/configs.d.ts b/src/types/configs.d.ts index 61aeb8b3..c8ea0a43 100644 --- a/src/types/configs.d.ts +++ b/src/types/configs.d.ts @@ -516,4 +516,5 @@ interface ITrelliscopeAppSpec { aspect: number; func: PanelFunction; }): ITrelliscopeAppSpec; + view({ width: number, height: number }): HTMLElement; }