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;
}