Repository: magland/sortingview (language: python)

Issue:
implement figurl GridContainer, GridItem



Discussion / hints:

Fix patch:
diff --git a/devel/figurl_average_waveforms.py b/devel/figurl_average_waveforms.py
index 59f4d244..e9e9d207 100644
--- a/devel/figurl_average_waveforms.py
+++ b/devel/figurl_average_waveforms.py
@@ -1,29 +1,23 @@
 from typing import List
 import kachery_client as kc
+import sortingview.figurl as fig
 
-def figurl_average_waveforms(*, channel: str, recording_id: str, sorting_id: str, workspace_uri: str, unit_ids: List[int]):
+def figurl_average_waveforms(*, recording_id: str, sorting_id: str, workspace_uri: str, unit_ids: List[int]):
     """
     Generate a sortingview url that shows the average waveforms page
     """
-    base_url = 'http://localhost:3000'
-    object_uri = kc.store_json({
-        'type': 'sortingview.average-waveforms.1',
-        'data': {
-            'workspaceUri': workspace_uri,
-            'recordingId': recording_id,
-            'sortingId': sorting_id,
-            'unitIds': [1, 2, 3, 4]
-        }
-    })
-    object_hash = object_uri.split('/')[2]
-    url = f'{base_url}/fig?channel={channel}&figureObject={object_hash}'
-    return url
+    data = {
+        'workspaceUri': workspace_uri,
+        'recordingId': recording_id,
+        'sortingId': sorting_id,
+        'unitIds': [1, 2, 3, 4]
+    }
+    return fig.Figure(type='sortingview.average-waveforms.1', data=data)
 
 url = figurl_average_waveforms(
-    channel='ccm',
     recording_id='R-fa746904d460',
     sorting_id='S-83c498c391ed',
     workspace_uri='workspace://acf9d87b54e5daefbf1a6797bdaf5e1faee4834372e6704bdfdd78ed34353ca3',
     unit_ids=[1, 2, 3, 4]
-)
+).url()
 print(url)
diff --git a/devel/figurl_average_waveforms_numpy.py b/devel/figurl_average_waveforms_numpy.py
index 618d66a6..ebf2ebe8 100644
--- a/devel/figurl_average_waveforms_numpy.py
+++ b/devel/figurl_average_waveforms_numpy.py
@@ -1,26 +1,16 @@
 import numpy as np
 from typing import Any, List
-import kachery_client as kc
-from sortingview.serialize_wrapper import _serialize
+import sortingview.figurl as fig
 
-def store_json(x: dict):
-    return kc.store_json(_serialize(x))
-
-def figurl_average_waveforms_numpy(*, channel: str, waveforms: List[Any], electrode_channels: List[Any]):
+def figurl_average_waveforms_numpy(*, waveforms: List[Any], electrode_channels: List[Any]):
     """
-    Generate a sortingview url that shows the average waveforms numpy page
+    Generate a sortingview figure that shows the average waveforms numpy page
     """
-    base_url = 'http://localhost:3000'
-    object_uri = store_json({
-        'type': 'sortingview.average-waveforms-numpy.1',
-        'data': {
-            'waveforms': waveforms,
-            'electrodeChannels': electrode_channels
-        }
-    })
-    object_hash = object_uri.split('/')[2]
-    url = f'{base_url}/fig?channel={channel}&figureObject={object_hash}'
-    return url
+    data = {
+        'waveforms': waveforms,
+        'electrodeChannels': electrode_channels
+    }
+    return fig.Figure(type='sortingview.average-waveforms-numpy.1', data=data)
 
 waveforms = [
     {
@@ -64,8 +54,7 @@ def figurl_average_waveforms_numpy(*, channel: str, waveforms: List[Any], electr
 ]
 
 url = figurl_average_waveforms_numpy(
-    channel='ccm',
     waveforms=waveforms,
     electrode_channels=electrode_channels
-)
+).url()
 print(url)
diff --git a/devel/figurl_box_layout.py b/devel/figurl_box_layout.py
new file mode 100644
index 00000000..1cbcc241
--- /dev/null
+++ b/devel/figurl_box_layout.py
@@ -0,0 +1,26 @@
+import sortingview.figurl as fig
+import altair as alt
+import pandas as pd
+
+source = pd.DataFrame({
+    'a': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
+    'b': [28, 55, 43, 91, 81, 53, 19, 87, 52]
+})
+chart = alt.Chart(source).mark_bar().encode(
+    x='a',
+    y='b'
+)
+
+F1 = fig.Altair(chart)
+F2 = fig.Altair(chart)
+F = fig.BoxLayout(
+    [
+        fig.BoxLayout([F1, F2], direction='row'),
+        F2,
+        F1
+    ],
+    direction="column"
+)
+
+url = F.url()
+print(url)
diff --git a/devel/figurl_mountainview.py b/devel/figurl_mountainview.py
new file mode 100644
index 00000000..3afec278
--- /dev/null
+++ b/devel/figurl_mountainview.py
@@ -0,0 +1,19 @@
+import sortingview.figurl as fig
+
+def figurl_mountainview(*, recording_id: str, sorting_id: str, workspace_uri: str):
+    """
+    Generate a sortingview url that shows the mountainview page
+    """
+    data = {
+        'workspaceUri': workspace_uri,
+        'recordingId': recording_id,
+        'sortingId': sorting_id
+    }
+    return fig.Figure(type='sortingview.mountainview.1', data=data)
+
+url = figurl_mountainview(
+    recording_id='R-fa746904d460',
+    sorting_id='S-83c498c391ed',
+    workspace_uri='workspace://acf9d87b54e5daefbf1a6797bdaf5e1faee4834372e6704bdfdd78ed34353ca3'
+).url()
+print(url)
diff --git a/devel/figurl_vegalite.py b/devel/figurl_vegalite.py
new file mode 100644
index 00000000..f2fc7265
--- /dev/null
+++ b/devel/figurl_vegalite.py
@@ -0,0 +1,15 @@
+import sortingview.figurl as fig
+import altair as alt
+import pandas as pd
+
+source = pd.DataFrame({
+    'a': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
+    'b': [28, 55, 43, 91, 81, 53, 19, 87, 52]
+})
+chart = alt.Chart(source).mark_bar().encode(
+    x='a',
+    y='b'
+)
+
+url = fig.Altair(chart).url()
+print(url)
diff --git a/devel/figurl_workspace.py b/devel/figurl_workspace.py
index eed9b4ab..98dd5c50 100644
--- a/devel/figurl_workspace.py
+++ b/devel/figurl_workspace.py
@@ -1,23 +1,6 @@
-from typing import List
-import kachery_client as kc
+import sortingview as sv
 
-def figurl_workspace(*, channel: str, workspace_uri: str):
-    """
-    Generate a sortingview url that shows the average waveforms page
-    """
-    base_url = 'http://localhost:3000'
-    object_uri = kc.store_json({
-        'type': 'sortingview.workspace.1',
-        'data': {
-            'workspaceUri': workspace_uri
-        }
-    })
-    object_hash = object_uri.split('/')[2]
-    url = f'{base_url}/fig?channel={channel}&figureObject={object_hash}'
-    return url
-
-url = figurl_workspace(
-    channel='ccm',
-    workspace_uri='workspace://acf9d87b54e5daefbf1a6797bdaf5e1faee4834372e6704bdfdd78ed34353ca3'
-)
+workspace_uri = 'workspace://acf9d87b54e5daefbf1a6797bdaf5e1faee4834372e6704bdfdd78ed34353ca3'
+workspace = sv.Workspace(workspace_uri=workspace_uri)
+url = workspace.figurl().url()
 print(url)
diff --git a/package.json b/package.json
index e118edb1..ae02e359 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
     "async-mutex": "^0.3.1",
     "axios": "^0.21.1",
     "bson": "^4.4.0",
+    "d3-array": "^3.0.1",
     "github-markdown-css": "^4.0.0",
     "mathjs": "^7.5.1",
     "object-hash": "^2.1.1",
@@ -32,10 +33,14 @@
     "react-scripts": "4.0.3",
     "react-sizeme": "^2.6.12",
     "react-syntax-highlighter": "^15.4.3",
+    "react-vega": "^7.4.3",
     "react-vis": "^1.11.7",
     "react-visibility-sensor": "^5.1.1",
     "tweetnacl": "^1.0.3",
     "typescript": "^4.1.2",
+    "vega": "^5.20.2",
+    "vega-lite": "^5.1.0",
+    "vega-util": "^1.16.1",
     "web-vitals": "^1.0.1"
   },
   "scripts": {
@@ -50,18 +55,11 @@
       "react-app/jest"
     ]
   },
-  "browserslist": {
-    "production": [
-      ">0.2%",
-      "not dead",
-      "not op_mini all"
-    ],
-    "development": [
-      "last 1 chrome version",
-      "last 1 firefox version",
-      "last 1 safari version"
-    ]
-  },
+  "browserslist": [
+    ">0.2%",
+    "not dead",
+    "not op_mini all"
+  ],
   "devDependencies": {
     "@types/mathjs": "^6.0.6",
     "@types/object-hash": "^1.3.4",
diff --git a/src/figurl/FigurlApp.tsx b/src/figurl/FigurlApp.tsx
index afc770d3..d14d3ade 100644
--- a/src/figurl/FigurlApp.tsx
+++ b/src/figurl/FigurlApp.tsx
@@ -7,6 +7,7 @@ import { nodeLabel, TaskFunctionId } from 'kachery-js/types/kacheryTypes';
 import React, { FunctionComponent, useMemo, useReducer } from 'react';
 import { BrowserRouter } from 'react-router-dom';
 import './App.css';
+import FigurlSetup from './FigurlSetup';
 import './index.css';
 import { initialRecentFigures, recentFiguresReducer } from './RecentFigures';
 // import logo from './logo.svg';
@@ -39,14 +40,15 @@ const FigurlApp: FunctionComponent<Props> = ({
         <BrowserRouter>
           <GoogleSignInSetup>
             <KacheryNodeSetup nodeLabel={nodeLabel("sortingview")}>
-              <MainWindow
-                packageName={packageName}
-                plugins={plugins}
-                logo={logo}
-                homePageProps={homePageProps}
-                recentFigures={recentFigures}
-                recentFiguresDispatch={recentFiguresDispatch}
-              />
+              <FigurlSetup plugins={plugins}>
+                <MainWindow
+                  packageName={packageName}
+                  logo={logo}
+                  homePageProps={homePageProps}
+                  recentFigures={recentFigures}
+                  recentFiguresDispatch={recentFiguresDispatch}
+                />
+              </FigurlSetup>
             </KacheryNodeSetup>
           </GoogleSignInSetup>
         </BrowserRouter>
diff --git a/src/figurl/FigurlContext.ts b/src/figurl/FigurlContext.ts
new file mode 100644
index 00000000..0bcd8fac
--- /dev/null
+++ b/src/figurl/FigurlContext.ts
@@ -0,0 +1,6 @@
+import React from 'react'
+import { FigurlPlugin } from './types'
+
+const FigurlContext = React.createContext<{plugins: FigurlPlugin[]} | undefined>(undefined)
+
+export default FigurlContext
\ No newline at end of file
diff --git a/src/figurl/FigurlSetup.tsx b/src/figurl/FigurlSetup.tsx
new file mode 100644
index 00000000..bf423654
--- /dev/null
+++ b/src/figurl/FigurlSetup.tsx
@@ -0,0 +1,17 @@
+import React, { FunctionComponent } from 'react';
+import FigurlContext from './FigurlContext';
+import { FigurlPlugin } from './types';
+
+type Props = {
+    plugins: FigurlPlugin[]
+}
+
+const FigurlSetup: FunctionComponent<Props> = ({plugins, children}) => {
+    return (
+        <FigurlContext.Provider value={{plugins}}>
+            {children}
+        </FigurlContext.Provider>
+    )
+}
+
+export default FigurlSetup
\ No newline at end of file
diff --git a/src/figurl/labbox-react/MainWindow/Figure.tsx b/src/figurl/labbox-react/MainWindow/Figure.tsx
index c6cfe4fd..dce57bd2 100644
--- a/src/figurl/labbox-react/MainWindow/Figure.tsx
+++ b/src/figurl/labbox-react/MainWindow/Figure.tsx
@@ -1,23 +1,23 @@
-import { FigureObject, FigurlPlugin, isFigureObject } from 'figurl/types';
-import { isJSONObject, JSONObject, Sha1Hash } from 'kachery-js/types/kacheryTypes';
 import { useChannel, usePureCalculationTask } from 'figurl/kachery-react';
 import TaskStatusView from 'figurl/kachery-react/components/TaskMonitor/TaskStatusView';
-import React, { FunctionComponent } from 'react';
 import { RecentFiguresAction } from 'figurl/RecentFigures';
-import { useEffect } from 'react';
+import { FigureObject, isFigureObject } from 'figurl/types';
+import useFigurlPlugins from 'figurl/useFigurlPlugins';
+import { isJSONObject, JSONObject, Sha1Hash } from 'kachery-js/types/kacheryTypes';
+import React, { FunctionComponent, useEffect } from 'react';
 import { useLocation } from 'react-router-dom';
 
 type Props = {
-    plugins: FigurlPlugin[]
     width: number
     height: number
     packageName: string
     figureObjectOrHash?: JSONObject | Sha1Hash
-    recentFiguresDispatch: (a: RecentFiguresAction) => void
+    recentFiguresDispatch?: (a: RecentFiguresAction) => void
 }
 
-const useFigureObject = (packageName: string, plugins: FigurlPlugin[], figureObjectOrHash?: JSONObject | Sha1Hash) => {
+const useFigureObject = (packageName: string, figureObjectOrHash?: JSONObject | Sha1Hash) => {
     const {channelName} = useChannel()
+    const plugins = useFigurlPlugins()
     let {returnValue: object, task} = usePureCalculationTask<FigureObject>(
         figureObjectOrHash && (typeof(figureObjectOrHash) === 'string') ? `${packageName}.get_figure_object.1` : undefined,
         {figure_object_hash: figureObjectOrHash},
@@ -41,6 +41,7 @@ const useFigureObject = (packageName: string, plugins: FigurlPlugin[], figureObj
             return {error: `Figure plugin not found: ${o.type}`}
         }
         if (!p.validateData(o.data)) {
+            console.warn(o.data)
             return {error: `Problem validating figure data for figure of type: ${o.type}`}
         }
         return {
@@ -52,15 +53,15 @@ const useFigureObject = (packageName: string, plugins: FigurlPlugin[], figureObj
     return {task}
 }
 
-const Figure: FunctionComponent<Props> = ({plugins, width, height, figureObjectOrHash, packageName, recentFiguresDispatch}) => {
-    const {plugin, figureData, task, error} = useFigureObject(packageName, plugins, figureObjectOrHash)
+const Figure: FunctionComponent<Props> = ({width, height, figureObjectOrHash, packageName, recentFiguresDispatch}) => {
+    const {plugin, figureData, task, error} = useFigureObject(packageName, figureObjectOrHash)
     const {channelName} = useChannel()
     const location = useLocation()
 
     useEffect(() => {
         if (!plugin) return
         if (!figureData) return
-        recentFiguresDispatch({type: 'add', recentFigure: {
+        recentFiguresDispatch && recentFiguresDispatch({type: 'add', recentFigure: {
             channel: channelName,
             type: plugin.type,
             data: figureData,
diff --git a/src/figurl/labbox-react/MainWindow/MainWindow.tsx b/src/figurl/labbox-react/MainWindow/MainWindow.tsx
index 57605495..56df47ee 100644
--- a/src/figurl/labbox-react/MainWindow/MainWindow.tsx
+++ b/src/figurl/labbox-react/MainWindow/MainWindow.tsx
@@ -1,23 +1,21 @@
-import { FigurlPlugin } from 'figurl/types'
+import { RecentFigure, RecentFigures, RecentFiguresAction } from 'figurl/RecentFigures'
+import React, { useCallback } from 'react'
+import { useHistory, useLocation } from 'react-router-dom'
 import { useWindowDimensions } from '..'
 import { HomePageProps } from '../HomePage/HomePage'
-import React, { useCallback } from 'react'
 import ApplicationBar from './ApplicationBar/ApplicationBar'
 import Routes from './Routes'
 import useRoute from './useRoute'
-import { RecentFigure, RecentFigures, RecentFiguresAction } from 'figurl/RecentFigures'
-import { useHistory, useLocation } from 'react-router-dom'
 
 type Props = {
     packageName: string
     logo?: any
-    plugins: FigurlPlugin[]
     homePageProps: HomePageProps
     recentFigures: RecentFigures
     recentFiguresDispatch: (a: RecentFiguresAction) => void
 }
 
-const MainWindow: React.FunctionComponent<Props> = ({packageName, logo, plugins, homePageProps, recentFiguresDispatch}) => {
+const MainWindow: React.FunctionComponent<Props> = ({packageName, logo, homePageProps, recentFiguresDispatch}) => {
     const {setRoute} = useRoute()
     const {width, height} = useWindowDimensions()
     const location = useLocation()
@@ -43,7 +41,6 @@ const MainWindow: React.FunctionComponent<Props> = ({packageName, logo, plugins,
                     packageName={packageName}
                     width={width}
                     height={height - 50}
-                    plugins={plugins}
                     homePageProps={{...homePageProps, onOpenFigure: handleOpenFigure}}
                     recentFiguresDispatch={recentFiguresDispatch}
                 />
diff --git a/src/figurl/labbox-react/MainWindow/Routes.tsx b/src/figurl/labbox-react/MainWindow/Routes.tsx
index 1bbce765..d072e486 100644
--- a/src/figurl/labbox-react/MainWindow/Routes.tsx
+++ b/src/figurl/labbox-react/MainWindow/Routes.tsx
@@ -1,5 +1,5 @@
 import { RecentFiguresAction } from 'figurl/RecentFigures'
-import { FigureObject, FigurlPlugin } from 'figurl/types'
+import { FigureObject } from 'figurl/types'
 import { isFeedId } from 'kachery-js/types/kacheryTypes'
 import React, { FunctionComponent, useEffect } from 'react'
 import HomePage, { HomePageProps } from '../HomePage/HomePage'
@@ -9,7 +9,6 @@ import useRoute from './useRoute'
 
 type Props = {
     packageName: string
-    plugins: FigurlPlugin[]
     width: number
     height: number
     homePageProps: HomePageProps
@@ -17,7 +16,7 @@ type Props = {
 }
 
 const Routes: FunctionComponent<Props> = (props) => {
-    const {packageName, plugins, width, height, homePageProps, recentFiguresDispatch} = props
+    const {packageName, width, height, homePageProps, recentFiguresDispatch} = props
     const {routePath, figureObjectOrHash, query, setRoute} = useRoute()
 
     useEffect(() => {
@@ -83,7 +82,6 @@ const Routes: FunctionComponent<Props> = (props) => {
         return (
             <Figure
                 packageName={packageName}
-                plugins={plugins}
                 figureObjectOrHash={figureObjectOrHash}
                 width={width}
                 height={height}
diff --git a/src/figurl/labbox-react/MainWindow/SelectWorkspace.tsx b/src/figurl/labbox-react/MainWindow/SelectWorkspace.tsx
index c6f60e07..3528377e 100644
--- a/src/figurl/labbox-react/MainWindow/SelectWorkspace.tsx
+++ b/src/figurl/labbox-react/MainWindow/SelectWorkspace.tsx
@@ -1,4 +1,3 @@
-import { Button } from '@material-ui/core';
 import { FigureObject } from 'figurl/types';
 import React, { FunctionComponent, useCallback } from 'react';
 import { useVisible } from '..';
diff --git a/src/figurl/useFigurlPlugins.ts b/src/figurl/useFigurlPlugins.ts
new file mode 100644
index 00000000..8b0ec205
--- /dev/null
+++ b/src/figurl/useFigurlPlugins.ts
@@ -0,0 +1,10 @@
+import { useContext } from "react"
+import FigurlContext from "./FigurlContext"
+
+const useFigurlPlugins = () => {
+    const context = useContext(FigurlContext)
+    if (!context) throw Error('Figurl context is undefined. Use <FigurlSetup>.')
+    return context.plugins
+}
+
+export default useFigurlPlugins
\ No newline at end of file
diff --git a/src/figurlPlugins/BoxLayoutPlugin/BoxLayoutItem.tsx b/src/figurlPlugins/BoxLayoutPlugin/BoxLayoutItem.tsx
new file mode 100644
index 00000000..3b3a8fd8
--- /dev/null
+++ b/src/figurlPlugins/BoxLayoutPlugin/BoxLayoutItem.tsx
@@ -0,0 +1,23 @@
+import Figure from 'figurl/labbox-react/MainWindow/Figure';
+import { FigureObject } from 'figurl/types';
+import packageName from 'python/sortingview/gui/packageName';
+import React, { FunctionComponent } from 'react';
+
+type Props = {
+    figureObject: FigureObject
+    width: number
+    height: number
+}
+
+const BoxLayoutItem: FunctionComponent<Props> = ({figureObject, width, height}) => {
+    return (
+        <Figure
+            figureObjectOrHash={figureObject}
+            packageName={packageName}
+            width={width}
+            height={height}
+        />
+    )
+}
+
+export default BoxLayoutItem
\ No newline at end of file
diff --git a/src/figurlPlugins/BoxLayoutPlugin/BoxLayoutPlugin.tsx b/src/figurlPlugins/BoxLayoutPlugin/BoxLayoutPlugin.tsx
new file mode 100644
index 00000000..3a14ac1b
--- /dev/null
+++ b/src/figurlPlugins/BoxLayoutPlugin/BoxLayoutPlugin.tsx
@@ -0,0 +1,81 @@
+import { Grid } from "@material-ui/core";
+import { FigureObject, FigurlPlugin, isFigureObject } from "figurl/types";
+import { isArrayOf, _validateObject } from "kachery-js/types/kacheryTypes";
+import { FunctionComponent, useMemo } from "react";
+import BoxLayoutItem from "./BoxLayoutItem";
+
+type BoxLayoutData = {
+    children: FigureObject[]
+    direction: 'row' | 'column'
+}
+const isBoxLayoutData = (x: any): x is BoxLayoutData => {
+    return _validateObject(x, {
+        children: isArrayOf(isFigureObject),
+        direction: (x: any) => (['row', 'column'].includes(x))
+    })
+}
+
+type Props = {
+    data: BoxLayoutData
+    width: number
+    height: number
+}
+
+const BoxLayoutComponent: FunctionComponent<Props> = ({data, width, height}) => {
+    const {children, direction} = data
+    const spacing = 2
+    const margin = 10
+    const itemInfos: {figureObject: FigureObject, width: number, height: number, style: React.CSSProperties}[] = useMemo(() => {
+        const n = children.length
+        if (direction === 'row') {
+            const W = Math.floor((width - 2 * margin - spacing * (n - 1)) / n)
+            return children.map((c, i) => ({
+                figureObject: c,
+                width: W,
+                height: height - 2 * margin,
+                style: {paddingTop: margin, paddingLeft: i === 0 ? margin : 0, paddingRight: spacing}
+            }))
+        }
+        else {
+            const H = Math.floor((height - 2 * margin - spacing * (n - 1)) / n)
+            return children.map((c, i) => ({
+                figureObject: c,
+                width: width - 2 * margin,
+                height: H,
+                style: {paddingLeft: margin, paddingTop: i === 0 ? margin : 0, paddingBottom: spacing}
+            }))
+        }
+    }, [children, width, height, direction])
+    return (
+        <Grid
+            container
+            direction={direction}
+            spacing={0}
+        >
+            {
+                itemInfos.map((x) => (
+                    <div style={x.style}>
+                        <BoxLayoutItem
+                            figureObject={x.figureObject}
+                            width={x.width}
+                            height={x.height}
+                        />
+                    </div>
+                ))
+            }
+        </Grid>
+    )
+}
+
+const getLabel = (x: BoxLayoutData) => {
+    return `BoxLayout`
+}
+
+const BoxLayoutPlugin: FigurlPlugin = {
+    type: 'BoxLayout.1',
+    validateData: isBoxLayoutData,
+    component: BoxLayoutComponent,
+    getLabel
+}
+
+export default BoxLayoutPlugin
\ No newline at end of file
diff --git a/src/figurlPlugins/VegaLitePlugin/VegaLitePlugin.css b/src/figurlPlugins/VegaLitePlugin/VegaLitePlugin.css
new file mode 100644
index 00000000..0c292bfe
--- /dev/null
+++ b/src/figurlPlugins/VegaLitePlugin/VegaLitePlugin.css
@@ -0,0 +1,5 @@
+/* See https://github.com/vega/react-vega/issues/85#issuecomment-795138175 */
+.vega-embed {
+    width: 100%;
+    height: 100%;
+}
\ No newline at end of file
diff --git a/src/figurlPlugins/VegaLitePlugin/VegaLitePlugin.tsx b/src/figurlPlugins/VegaLitePlugin/VegaLitePlugin.tsx
new file mode 100644
index 00000000..a2d1bb75
--- /dev/null
+++ b/src/figurlPlugins/VegaLitePlugin/VegaLitePlugin.tsx
@@ -0,0 +1,49 @@
+import { FigurlPlugin } from "figurl/types";
+import { _validateObject } from "kachery-js/types/kacheryTypes";
+import { FunctionComponent, useMemo } from 'react';
+import { VegaLite } from 'react-vega'
+
+// See https://github.com/vega/react-vega/issues/85#issuecomment-795138175
+import './VegaLitePlugin.css'
+
+type VegaLiteData = {
+    spec: any
+}
+const isVegaLiteData = (x: any): x is VegaLiteData => {
+    return _validateObject(x, {
+        spec: () => (true)
+    })
+}
+
+type Props = {
+    data: VegaLiteData
+    width: number
+    height: number
+}
+
+const VegaLiteComponent: FunctionComponent<Props> = ({data, width, height}) => {
+    const {spec} = data
+    const spec2 = useMemo(() => {
+        return {...spec, width: "container", height: "container"}
+    }, [spec])
+    return (
+        <div style={{width, height}}>
+            <VegaLite
+                spec={spec2}
+            />
+        </div>
+    )
+}
+
+const getLabel = (x: VegaLiteData) => {
+    return `VegaLite`
+}
+
+const VegaLitePlugin: FigurlPlugin = {
+    type: 'VegaLite.1',
+    validateData: isVegaLiteData,
+    component: VegaLiteComponent,
+    getLabel
+}
+
+export default VegaLitePlugin
\ No newline at end of file
diff --git a/src/figurlPlugins/index.ts b/src/figurlPlugins/index.ts
index 8fb897be..7caaaa83 100644
--- a/src/figurlPlugins/index.ts
+++ b/src/figurlPlugins/index.ts
@@ -3,12 +3,16 @@ import AverageWaveformsPlugin from "./AverageWaveformsPlugin/AverageWaveformsPlu
 import AverageWaveformsNumpyPlugin from "./AverageWaveformsNumpyPlugin/AverageWaveformsNumpyPlugin"
 import MountainViewPlugin from "./MountainViewPlugin/MountainViewPlugin"
 import WorkspacePlugin from "./WorkspacePlugin/WorkspacePlugin"
+import VegaLitePlugin from "./VegaLitePlugin/VegaLitePlugin"
+import BoxLayoutPlugin from "./BoxLayoutPlugin/BoxLayoutPlugin"
 
 const plugins: FigurlPlugin[] = [
     MountainViewPlugin,
     AverageWaveformsPlugin,
     WorkspacePlugin,
-    AverageWaveformsNumpyPlugin
+    AverageWaveformsNumpyPlugin,
+    VegaLitePlugin,
+    BoxLayoutPlugin
 ]
 
 export default plugins
\ No newline at end of file
diff --git a/src/python/sortingview/figurl/Altair.py b/src/python/sortingview/figurl/Altair.py
new file mode 100644
index 00000000..6ffec7ab
--- /dev/null
+++ b/src/python/sortingview/figurl/Altair.py
@@ -0,0 +1,9 @@
+from .Figure import Figure
+import altair as alt
+
+class Altair(Figure):
+    def __init__(self, chart: alt.Chart):
+        data = {
+            'spec': chart.to_dict()
+        }
+        super().__init__(type='VegaLite.1', data=data)
\ No newline at end of file
diff --git a/src/python/sortingview/figurl/BoxLayout.py b/src/python/sortingview/figurl/BoxLayout.py
new file mode 100644
index 00000000..a76e2592
--- /dev/null
+++ b/src/python/sortingview/figurl/BoxLayout.py
@@ -0,0 +1,12 @@
+from typing import List
+from .Figure import Figure
+
+class BoxLayout(Figure):
+    def __init__(self, items: List[Figure], direction: str='row'):
+        data = {
+            'direction': direction,
+            'children': [
+                item.object for item in items
+            ]
+        }
+        super().__init__(type='BoxLayout.1', data=data)
\ No newline at end of file
diff --git a/src/python/sortingview/figurl/Figure.py b/src/python/sortingview/figurl/Figure.py
new file mode 100644
index 00000000..b105497b
--- /dev/null
+++ b/src/python/sortingview/figurl/Figure.py
@@ -0,0 +1,38 @@
+import os
+from typing import Any, Union
+import kachery_client as kc
+from ..serialize_wrapper import _serialize
+
+class Figure:
+    def __init__(self, type: str, data: Any):
+        self._type = type
+        self._data = data
+        self._object = {'type': type, 'data': data}
+        self._object_uri: Union[str, None] = None
+    @property
+    def object(self):
+        return self._object
+    @property
+    def type(self):
+        return self._type
+    @property
+    def data(self):
+        return self._data
+    def url(self, *, channel: Union[str, None]=None, base_url: Union[str, None]=None):
+        if base_url is None:
+            base_url = default_base_url
+        if channel is None:
+            if default_channel is None:
+                raise Exception('No channel specified and FIGURL_CHANNEL is not set.')
+            channel = default_channel
+        if self._object_uri is None:
+            self._object_uri = store_json(self._object)
+        object_hash = self._object_uri.split('/')[2]
+        url = f'{base_url}/fig?channel={channel}&figureObject={object_hash}'
+        return url
+
+def store_json(x: dict):
+    return kc.store_json(_serialize(x))
+
+default_base_url = os.getenv('FIGURL_BASE_URL', 'https://sortingview.vercel.app')
+default_channel = os.getenv('FIGURL_CHANNEL', None)
\ No newline at end of file
diff --git a/src/python/sortingview/figurl/__init__.py b/src/python/sortingview/figurl/__init__.py
new file mode 100644
index 00000000..4c68b543
--- /dev/null
+++ b/src/python/sortingview/figurl/__init__.py
@@ -0,0 +1,3 @@
+from .Figure import Figure
+from .BoxLayout import BoxLayout
+from .Altair import Altair
\ No newline at end of file
diff --git a/src/python/sortingview/workspace/workspace.py b/src/python/sortingview/workspace/workspace.py
index 4c80ccd2..16c1050d 100644
--- a/src/python/sortingview/workspace/workspace.py
+++ b/src/python/sortingview/workspace/workspace.py
@@ -2,8 +2,9 @@
 from typing import Any, Dict, List, Tuple, Union
 import uuid
 import kachery_client as kc
+from ..figurl.Figure import Figure
 import spikeextractors as se
-from sortingview.extractors import LabboxEphysRecordingExtractor, LabboxEphysSortingExtractor
+from ..extractors import LabboxEphysRecordingExtractor, LabboxEphysSortingExtractor
 
 def parse_workspace_uri(workspace_uri: str):
     if not workspace_uri.startswith('workspace://'):
@@ -133,13 +134,11 @@ def set_snippet_len(self, snippet_len: Tuple[int, int]):
         main_subfeed = self._feed.load_subfeed('main')
         _set_snippet_len_for_workspace(main_subfeed, snippet_len)
         self._snippet_len = snippet_len
-    def figurl(self, channel: str, base_url='https://sortingview.vercel.app'):
-        url = figurl_workspace(
-            channel=channel,
-            base_url=base_url,
-            workspace_uri=self.uri
-        )
-        return url
+    def figurl(self):
+        data = {
+            'workspaceUri': self.uri
+        }
+        return Figure(type='sortingview.workspace.1', data=data)
     @property
     def snippet_len(self):
         return self._snippet_len
@@ -179,20 +178,6 @@ def get_curated_sorting_extractor(self, sorting_id):
             }
         })
 
-def figurl_workspace(*, channel: str, workspace_uri: str, base_url: str):
-    """
-    Generate a sortingview url that shows the average waveforms page
-    """
-    object_uri = kc.store_json({
-        'type': 'sortingview.workspace.1',
-        'data': {
-            'workspaceUri': workspace_uri
-        }
-    })
-    object_hash = object_uri.split('/')[2]
-    url = f'{base_url}/fig?channel={channel}&figureObject={object_hash}'
-    return url
-
 def create_workspace(*, label: Union[str, None]=None):
     feed = kc.create_feed()
     feed_id = feed.feed_id
diff --git a/yarn.lock b/yarn.lock
index c56bc812..777e574c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1804,6 +1804,11 @@
   dependencies:
     "@babel/types" "^7.3.0"
 
+"@types/clone@~2.1.0":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@types/clone/-/clone-2.1.1.tgz#9b880d0ce9b1f209b5e0bd6d9caa38209db34024"
+  integrity sha512-BZIU34bSYye0j/BFcPraiDZ5ka6MJADjcDVELGf7glr9K+iE8NYVjFslJFVWzskSxkLLyCrSPScE82/UUoBSvg==
+
 "@types/eslint@^7.2.6":
   version "7.2.6"
   resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.6.tgz#5e9aff555a975596c03a98b59ecd103decc70c3c"
@@ -1822,6 +1827,13 @@
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
   integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
 
+"@types/fast-json-stable-stringify@^2.0.0":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@types/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#c4d9c003d546b7ca9496ea924e9e9faca72873b4"
+  integrity sha512-IyNhGHu71jH1jCXTHmafuoAAdsbBON3kDh7u/UUhLmjYgN5TYB54e1R8ckTCiIevl2UuZaCsi9XRxineY5yUjw==
+  dependencies:
+    fast-json-stable-stringify "*"
+
 "@types/glob@^7.1.1":
   version "7.1.3"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
@@ -1997,6 +2009,15 @@
     "@types/scheduler" "*"
     csstype "^3.0.2"
 
+"@types/react@^16.9.19":
+  version "16.14.12"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.12.tgz#1e38e2114e568f6541f88628a207f72630ee161f"
+  integrity sha512-7nOJgNsRbARhZhvwPm7cnzahtzEi5VJ9OvcQk8ExEEb1t+zaFklwLVkJz7G1kfxX4X/mDa/icTmzE0vTmqsqBg==
+  dependencies:
+    "@types/prop-types" "*"
+    "@types/scheduler" "*"
+    csstype "^3.0.2"
+
 "@types/resolve@0.0.8":
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
@@ -2592,6 +2613,11 @@ arr-union@^3.1.0:
   resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
   integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
 
+array-flat-polyfill@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz#1e3a4255be619dfbffbfd1d635c1cf357cd034e7"
+  integrity sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==
+
 array-flatten@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@@ -3633,6 +3659,15 @@ cliui@^6.0.0:
     strip-ansi "^6.0.0"
     wrap-ansi "^6.2.0"
 
+cliui@^7.0.2:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^7.0.0"
+
 clone-response@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
@@ -3640,6 +3675,11 @@ clone-response@^1.0.2:
   dependencies:
     mimic-response "^1.0.0"
 
+clone@~2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
+
 clsx@^1.0.4:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
@@ -3734,7 +3774,7 @@ comma-separated-tokens@^1.0.0:
   resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
   integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
 
-commander@^2.20.0:
+commander@2, commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -4280,6 +4320,20 @@ d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
   integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
 
+"d3-array@1 - 2", d3-array@2, d3-array@^2.3.0, d3-array@^2.5.0, d3-array@^2.7.1:
+  version "2.12.1"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
+  integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
+  dependencies:
+    internmap "^1.0.0"
+
+d3-array@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.0.1.tgz#ca45c263f5bb780ab5a34a6e1d3d5883fe4a8d14"
+  integrity sha512-l3Bh5o8RSoC3SBm5ix6ogaFW+J6rOUm42yOtZ2sQPCEvCqUMepeX7zgrlLLGIemxgOyo9s2CsWEidnLv5PwwRw==
+  dependencies:
+    internmap "1 - 2"
+
 d3-collection@1, d3-collection@^1.0.3:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
@@ -4290,6 +4344,11 @@ d3-color@1, d3-color@^1.0.3:
   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
   integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
 
+"d3-color@1 - 2", d3-color@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e"
+  integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==
+
 d3-contour@^1.1.0:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3"
@@ -4297,11 +4356,63 @@ d3-contour@^1.1.0:
   dependencies:
     d3-array "^1.1.1"
 
+d3-delaunay@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-5.3.0.tgz#b47f05c38f854a4e7b3cea80e0bb12e57398772d"
+  integrity sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==
+  dependencies:
+    delaunator "4"
+
+"d3-dispatch@1 - 2":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf"
+  integrity sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==
+
+d3-dsv@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-2.0.0.tgz#b37b194b6df42da513a120d913ad1be22b5fe7c5"
+  integrity sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==
+  dependencies:
+    commander "2"
+    iconv-lite "0.4"
+    rw "1"
+
+d3-force@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-2.1.1.tgz#f20ccbf1e6c9e80add1926f09b51f686a8bc0937"
+  integrity sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==
+  dependencies:
+    d3-dispatch "1 - 2"
+    d3-quadtree "1 - 2"
+    d3-timer "1 - 2"
+
 d3-format@1, d3-format@^1.2.0:
   version "1.4.5"
   resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
   integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
 
+"d3-format@1 - 2", d3-format@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767"
+  integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==
+
+d3-geo-projection@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-geo-projection/-/d3-geo-projection-3.0.0.tgz#45ad8ce756cdbfa8340b11b2988644d8e1fa42e4"
+  integrity sha512-1JE+filVbkEX2bT25dJdQ05iA4QHvUwev6o0nIQHOSrNlHCAKfVss/U10vEM3pA4j5v7uQoFdQ4KLbx9BlEbWA==
+  dependencies:
+    commander "2"
+    d3-array "1 - 2"
+    d3-geo "1.12.0 - 2"
+    resolve "^1.1.10"
+
+"d3-geo@1.12.0 - 2", d3-geo@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-2.0.2.tgz#c065c1b71fe8c5f1be657e5f43d9bdd010383c40"
+  integrity sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==
+  dependencies:
+    d3-array "^2.5.0"
+
 d3-geo@^1.6.4:
   version "1.12.1"
   resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f"
@@ -4319,6 +4430,11 @@ d3-hierarchy@^1.1.4:
   resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
   integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==
 
+d3-hierarchy@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz#dab88a58ca3e7a1bc6cab390e89667fcc6d20218"
+  integrity sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==
+
 d3-interpolate@1, d3-interpolate@^1.1.4:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
@@ -4326,11 +4442,28 @@ d3-interpolate@1, d3-interpolate@^1.1.4:
   dependencies:
     d3-color "1"
 
+"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163"
+  integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==
+  dependencies:
+    d3-color "1 - 2"
+
 d3-path@1:
   version "1.0.9"
   resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
   integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
 
+"d3-path@1 - 2", d3-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8"
+  integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==
+
+"d3-quadtree@1 - 2":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-2.0.0.tgz#edbad045cef88701f6fee3aee8e93fb332d30f9d"
+  integrity sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==
+
 d3-sankey@^0.7.1:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.7.1.tgz#d229832268fc69a7fec84803e96c2256a614c521"
@@ -4353,6 +4486,17 @@ d3-scale@^1.0.5:
     d3-time "1"
     d3-time-format "2"
 
+d3-scale@^3.2.2:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3"
+  integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==
+  dependencies:
+    d3-array "^2.3.0"
+    d3-format "1 - 2"
+    d3-interpolate "1.2.0 - 2"
+    d3-time "^2.1.1"
+    d3-time-format "2 - 3"
+
 d3-shape@^1.1.0, d3-shape@^1.2.0:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
@@ -4360,6 +4504,13 @@ d3-shape@^1.1.0, d3-shape@^1.2.0:
   dependencies:
     d3-path "1"
 
+d3-shape@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f"
+  integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==
+  dependencies:
+    d3-path "1 - 2"
+
 d3-time-format@2:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
@@ -4367,11 +4518,30 @@ d3-time-format@2:
   dependencies:
     d3-time "1"
 
+"d3-time-format@2 - 3", d3-time-format@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6"
+  integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==
+  dependencies:
+    d3-time "1 - 2"
+
 d3-time@1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
   integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
 
+"d3-time@1 - 2", d3-time@^2.0.0, d3-time@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682"
+  integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==
+  dependencies:
+    d3-array "2"
+
+"d3-timer@1 - 2", d3-timer@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6"
+  integrity sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==
+
 d3-voronoi@^1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297"
@@ -4536,6 +4706,11 @@ del@^4.1.1:
     pify "^4.0.1"
     rimraf "^2.6.3"
 
+delaunator@4:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-4.0.1.tgz#3d779687f57919a7a418f8ab947d3bddb6846957"
+  integrity sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==
+
 delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -5471,7 +5646,7 @@ extsprintf@^1.2.0:
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
   integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
 
-fast-deep-equal@^3.1.1:
+fast-deep-equal@^3.1.1, fast-deep-equal@~3.1.3:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -5488,7 +5663,12 @@ fast-glob@^3.1.1:
     micromatch "^4.0.2"
     picomatch "^2.2.1"
 
-fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
+fast-json-patch@^3.0.0-1:
+  version "3.0.0-1"
+  resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz#4c68f2e7acfbab6d29d1719c44be51899c93dabb"
+  integrity sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw==
+
+fast-json-stable-stringify@*, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@~2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
   integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
@@ -5807,7 +5987,7 @@ gensync@^1.0.0-beta.1:
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
   integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 
-get-caller-file@^2.0.1:
+get-caller-file@^2.0.1, get-caller-file@^2.0.5:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
   integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@@ -6373,7 +6553,7 @@ hyphenate-style-name@^1.0.3:
   resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
   integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
 
-iconv-lite@0.4.24:
+iconv-lite@0.4, iconv-lite@0.4.24:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -6547,6 +6727,16 @@ internal-slot@^1.0.3:
     has "^1.0.3"
     side-channel "^1.0.4"
 
+"internmap@1 - 2":
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.1.tgz#33d0fa016185397549fb1a14ea3dbe5a2949d1cd"
+  integrity sha512-Ujwccrj9FkGqjbY3iVoxD1VV+KdZZeENx0rphrtzmRXbFvkFO88L80BL/zeSIguX/7T+y8k04xqtgWgS5vxwxw==
+
+internmap@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95"
+  integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
+
 invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -7564,6 +7754,11 @@ json-stable-stringify-without-jsonify@^1.0.1:
   resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
   integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
 
+json-stringify-pretty-compact@^3.0.0, json-stringify-pretty-compact@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz#f71ef9d82ef16483a407869556588e91b681d9ab"
+  integrity sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==
+
 json-stringify-safe@~5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
@@ -8398,6 +8593,11 @@ no-case@^3.0.4:
     lower-case "^2.0.2"
     tslib "^2.0.3"
 
+node-fetch@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
+  integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
+
 node-forge@^0.10.0:
   version "0.10.0"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
@@ -10314,6 +10514,15 @@ react-transition-group@^4.4.0:
     loose-envify "^1.4.0"
     prop-types "^15.6.2"
 
+react-vega@^7.4.3:
+  version "7.4.3"
+  resolved "https://registry.yarnpkg.com/react-vega/-/react-vega-7.4.3.tgz#06b6acbc05eb97e628830f37c57b3a42c0cfb881"
+  integrity sha512-SjsXUh4xEzix0gaZ8/b+axF9Tvb6l2pG5DN3Ym7NlNGzqj1erga+GvSm0EwzxVygvzoIfhKI+3MszZ2Rdc0xaA==
+  dependencies:
+    "@types/react" "^16.9.19"
+    fast-deep-equal "^3.1.1"
+    vega-embed "^6.5.1"
+
 react-vis@^1.11.7:
   version "1.11.7"
   resolved "https://registry.yarnpkg.com/react-vis/-/react-vis-1.11.7.tgz#909902af00158895d14da1adfe1d0dc0045228ff"
@@ -10726,7 +10935,7 @@ resolve@1.18.1:
     is-core-module "^2.0.0"
     path-parse "^1.0.6"
 
-resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1:
+resolve@^1.1.10, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.8.1:
   version "1.20.0"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
   integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
@@ -10855,6 +11064,11 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
     aproba "^1.1.1"
 
+rw@1:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+  integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=
+
 safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -11011,6 +11225,13 @@ semver@^7.2.1, semver@^7.3.2:
   dependencies:
     lru-cache "^6.0.0"
 
+semver@^7.3.5:
+  version "7.3.5"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
 send@0.17.1:
   version "0.17.1"
   resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -11937,6 +12158,13 @@ toidentifier@1.0.0:
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
   integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
 
+topojson-client@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99"
+  integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==
+  dependencies:
+    commander "2"
+
 tough-cookie@^2.3.3, tough-cookie@~2.5.0:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
@@ -12017,11 +12245,16 @@ tslib@^2.0.3:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
   integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
 
-tslib@^2.1.0:
+tslib@^2.1.0, tslib@^2.2.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
   integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
 
+tslib@~2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
+  integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
+
 tsutils@^3.17.1:
   version "3.20.0"
   resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698"
@@ -12452,6 +12685,364 @@ vary@~1.1.2:
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
 
+vega-canvas@^1.2.5:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/vega-canvas/-/vega-canvas-1.2.6.tgz#55e032ce9a62acd17229f6bac66d99db3d6879cd"
+  integrity sha512-rgeYUpslYn/amIfnuv3Sw6n4BGns94OjjZNtUc9IDji6b+K8LGS/kW+Lvay8JX/oFqtulBp8RLcHN6QjqPLA9Q==
+
+vega-crossfilter@~4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/vega-crossfilter/-/vega-crossfilter-4.0.5.tgz#cf6a5fca60821928f976b32f22cf66cfd9cbeeae"
+  integrity sha512-yF+iyGP+ZxU7Tcj5yBsMfoUHTCebTALTXIkBNA99RKdaIHp1E690UaGVLZe6xde2n5WaYpho6I/I6wdAW3NXcg==
+  dependencies:
+    d3-array "^2.7.1"
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-dataflow@^5.7.3, vega-dataflow@^5.7.4, vega-dataflow@~5.7.4:
+  version "5.7.4"
+  resolved "https://registry.yarnpkg.com/vega-dataflow/-/vega-dataflow-5.7.4.tgz#7cafc0a41b9d0b11dd2e34a513f8b7ca345dfd74"
+  integrity sha512-JGHTpUo8XGETH3b1V892we6hdjzCWB977ybycIu8DPqRoyrZuj6t1fCVImazfMgQD1LAfJlQybWP+alwKDpKig==
+  dependencies:
+    vega-format "^1.0.4"
+    vega-loader "^4.3.2"
+    vega-util "^1.16.1"
+
+vega-embed@^6.5.1:
+  version "6.18.2"
+  resolved "https://registry.yarnpkg.com/vega-embed/-/vega-embed-6.18.2.tgz#1fcbf9c1eca1fc58db337efacbca7b9c74db43ca"
+  integrity sha512-wcDyQPE4J5aiCDc3/suH5RQDvrKkjuLkhzUcbOLwEkNF8/+pp17tS0JghzEvAPNRg+5aG1/N2ydixq8Lk3dOlg==
+  dependencies:
+    fast-json-patch "^3.0.0-1"
+    json-stringify-pretty-compact "^3.0.0"
+    semver "^7.3.5"
+    tslib "^2.2.0"
+    vega-schema-url-parser "^2.2.0"
+    vega-themes "^2.10.0"
+    vega-tooltip "^0.25.1"
+
+vega-encode@~4.8.3:
+  version "4.8.3"
+  resolved "https://registry.yarnpkg.com/vega-encode/-/vega-encode-4.8.3.tgz#b3048fb39845d72f18d8dc302ad697f826e0ff83"
+  integrity sha512-JoRYtaV2Hs8spWLzTu/IjR7J9jqRmuIOEicAaWj6T9NSZrNWQzu2zF3IVsX85WnrIDIRUDaehXaFZvy9uv9RQg==
+  dependencies:
+    d3-array "^2.7.1"
+    d3-interpolate "^2.0.1"
+    vega-dataflow "^5.7.3"
+    vega-scale "^7.0.3"
+    vega-util "^1.15.2"
+
+vega-event-selector@^2.0.6, vega-event-selector@~2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/vega-event-selector/-/vega-event-selector-2.0.6.tgz#6beb00e066b78371dde1a0f40cb5e0bbaecfd8bc"
+  integrity sha512-UwCu50Sqd8kNZ1X/XgiAY+QAyQUmGFAwyDu7y0T5fs6/TPQnDo/Bo346NgSgINBEhEKOAMY1Nd/rPOk4UEm/ew==
+
+vega-expression@^4.0.1, vega-expression@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/vega-expression/-/vega-expression-4.0.1.tgz#c03e4fc68a00acac49557faa4e4ed6ac8a59c5fd"
+  integrity sha512-ZrDj0hP8NmrCpdLFf7Rd/xMUHGoSYsAOTaYp7uXZ2dkEH5x0uPy5laECMc8TiQvL8W+8IrN2HAWCMRthTSRe2Q==
+  dependencies:
+    vega-util "^1.16.0"
+
+vega-force@~4.0.7:
+  version "4.0.7"
+  resolved "https://registry.yarnpkg.com/vega-force/-/vega-force-4.0.7.tgz#6dc39ecb7889d9102661244d62fbc8d8714162ee"
+  integrity sha512-pyLKdwXSZ9C1dVIqdJOobvBY29rLvZjvRRTla9BU/nMwAiAGlGi6WKUFdRGdneyGe3zo2nSZDTZlZM/Z5VaQNA==
+  dependencies:
+    d3-force "^2.1.1"
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-format@^1.0.4, vega-format@~1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/vega-format/-/vega-format-1.0.4.tgz#40c0c252d11128738b845ee73d8173f8064d6626"
+  integrity sha512-oTAeub3KWm6nKhXoYCx1q9G3K43R6/pDMXvqDlTSUtjoY7b/Gixm8iLcir5S9bPjvH40n4AcbZsPmNfL/Up77A==
+  dependencies:
+    d3-array "^2.7.1"
+    d3-format "^2.0.0"
+    d3-time-format "^3.0.0"
+    vega-time "^2.0.3"
+    vega-util "^1.15.2"
+
+vega-functions@^5.10.0, vega-functions@^5.12.0, vega-functions@~5.12.0:
+  version "5.12.0"
+  resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668"
+  integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ==
+  dependencies:
+    d3-array "^2.7.1"
+    d3-color "^2.0.0"
+    d3-geo "^2.0.1"
+    vega-dataflow "^5.7.3"
+    vega-expression "^4.0.1"
+    vega-scale "^7.1.1"
+    vega-scenegraph "^4.9.3"
+    vega-selections "^5.3.0"
+    vega-statistics "^1.7.9"
+    vega-time "^2.0.4"
+    vega-util "^1.16.0"
+
+vega-geo@~4.3.8:
+  version "4.3.8"
+  resolved "https://registry.yarnpkg.com/vega-geo/-/vega-geo-4.3.8.tgz#5629d18327bb4f3700cdf05db4aced0a43abbf4a"
+  integrity sha512-fsGxV96Q/QRgPqOPtMBZdI+DneIiROKTG3YDZvGn0EdV16OG5LzFhbNgLT5GPzI+kTwgLpAsucBHklexlB4kfg==
+  dependencies:
+    d3-array "^2.7.1"
+    d3-color "^2.0.0"
+    d3-geo "^2.0.1"
+    vega-canvas "^1.2.5"
+    vega-dataflow "^5.7.3"
+    vega-projection "^1.4.5"
+    vega-statistics "^1.7.9"
+    vega-util "^1.15.2"
+
+vega-hierarchy@~4.0.9:
+  version "4.0.9"
+  resolved "https://registry.yarnpkg.com/vega-hierarchy/-/vega-hierarchy-4.0.9.tgz#4b4bafbc181a14a280ecdbee8874c0db7e369f47"
+  integrity sha512-4XaWK6V38/QOZ+vllKKTafiwL25m8Kd+ebHmDV+Q236ONHmqc/gv82wwn9nBeXPEfPv4FyJw2SRoqa2Jol6fug==
+  dependencies:
+    d3-hierarchy "^2.0.0"
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-label@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/vega-label/-/vega-label-1.0.0.tgz#c3bea3a608a62217ca554ecc0f7fe0395d81bd1b"
+  integrity sha512-hCdm2pcHgkKgxnzW9GvX5JmYNiUMlOXOibtMmBzvFBQHX3NiV9giQ5nsPiQiFbV08VxEPtM+VYXr2HyrIcq5zQ==
+  dependencies:
+    vega-canvas "^1.2.5"
+    vega-dataflow "^5.7.3"
+    vega-scenegraph "^4.9.2"
+    vega-util "^1.15.2"
+
+vega-lite@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/vega-lite/-/vega-lite-5.1.0.tgz#14cbfd3e1158db06ae0bf923505d3b5954a8df3f"
+  integrity sha512-HEyf0iHnCNmWkWFIbEmMphcJwZpcBnfnU8v+Ojrndr7ihDueojHMOSikoyz/GNpdkai+QFxLboA6DDCTtFv9iQ==
+  dependencies:
+    "@types/clone" "~2.1.0"
+    "@types/fast-json-stable-stringify" "^2.0.0"
+    array-flat-polyfill "^1.0.1"
+    clone "~2.1.2"
+    fast-deep-equal "~3.1.3"
+    fast-json-stable-stringify "~2.1.0"
+    json-stringify-pretty-compact "~3.0.0"
+    tslib "~2.2.0"
+    vega-event-selector "~2.0.6"
+    vega-expression "~4.0.1"
+    vega-util "~1.16.1"
+    yargs "~16.2.0"
+
+vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/vega-loader/-/vega-loader-4.4.0.tgz#fc515b7368c46b2be8df1fcf3c35c696c13c453d"
+  integrity sha512-e5enQECdau7rJob0NFB5pGumh3RaaSWWm90+boxMy3ay2b4Ki/3XIvo+C4F1Lx04qSxvQF7tO2LJcklRm6nqRA==
+  dependencies:
+    d3-dsv "^2.0.0"
+    node-fetch "^2.6.1"
+    topojson-client "^3.1.0"
+    vega-format "^1.0.4"
+    vega-util "^1.16.0"
+
+vega-parser@~6.1.3:
+  version "6.1.3"
+  resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.3.tgz#df72785e4b086eceb90ee6219a399210933b507b"
+  integrity sha512-8oiVhhW26GQ4GZBvolId8FVFvhn3s1KGgPlD7Z+4P2wkV+xe5Nqu0TEJ20F/cn3b88fd0Vj48X3BH3dlSeKNFg==
+  dependencies:
+    vega-dataflow "^5.7.3"
+    vega-event-selector "^2.0.6"
+    vega-functions "^5.12.0"
+    vega-scale "^7.1.1"
+    vega-util "^1.16.0"
+
+vega-projection@^1.4.5, vega-projection@~1.4.5:
+  version "1.4.5"
+  resolved "https://registry.yarnpkg.com/vega-projection/-/vega-projection-1.4.5.tgz#020cb646b4eaae535359da25f4f48eef8d324af2"
+  integrity sha512-85kWcPv0zrrNfxescqHtSYpRknilrS0K3CVRZc7IYQxnLtL1oma9WEbrSr1LCmDoCP5hl2Z1kKbomPXkrQX5Ag==
+  dependencies:
+    d3-geo "^2.0.1"
+    d3-geo-projection "^3.0.0"
+
+vega-regression@~1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/vega-regression/-/vega-regression-1.0.9.tgz#f33da47fe457e03ad134782c11414bcef7b1da82"
+  integrity sha512-KSr3QbCF0vJEAWFVY2MA9X786oiJncTTr3gqRMPoaLr/Yo3f7OPKXRoUcw36RiWa0WCOEMgTYtM28iK6ZuSgaA==
+  dependencies:
+    d3-array "^2.7.1"
+    vega-dataflow "^5.7.3"
+    vega-statistics "^1.7.9"
+    vega-util "^1.15.2"
+
+vega-runtime@^6.1.3, vega-runtime@~6.1.3:
+  version "6.1.3"
+  resolved "https://registry.yarnpkg.com/vega-runtime/-/vega-runtime-6.1.3.tgz#01e18246f7a80cee034a96017ac30113b92c4034"
+  integrity sha512-gE+sO2IfxMUpV0RkFeQVnHdmPy3K7LjHakISZgUGsDI/ZFs9y+HhBf8KTGSL5pcZPtQsZh3GBQ0UonqL1mp9PA==
+  dependencies:
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/vega-scale/-/vega-scale-7.1.1.tgz#b69a38d1980f6fc1093390f796e556be63fdc808"
+  integrity sha512-yE0to0prA9E5PBJ/XP77TO0BMkzyUVyt7TH5PAwj+CZT7PMsMO6ozihelRhoIiVcP0Ae/ByCEQBUQkzN5zJ0ZA==
+  dependencies:
+    d3-array "^2.7.1"
+    d3-interpolate "^2.0.1"
+    d3-scale "^3.2.2"
+    vega-time "^2.0.4"
+    vega-util "^1.15.2"
+
+vega-scenegraph@^4.9.2, vega-scenegraph@^4.9.3, vega-scenegraph@^4.9.4, vega-scenegraph@~4.9.4:
+  version "4.9.4"
+  resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.4.tgz#468408c1e89703fa9d3450445daabff623de2757"
+  integrity sha512-QaegQzbFE2yhYLNWAmHwAuguW3yTtQrmwvfxYT8tk0g+KKodrQ5WSmNrphWXhqwtsgVSvtdZkfp2IPeumcOQJg==
+  dependencies:
+    d3-path "^2.0.0"
+    d3-shape "^2.0.0"
+    vega-canvas "^1.2.5"
+    vega-loader "^4.3.3"
+    vega-scale "^7.1.1"
+    vega-util "^1.15.2"
+
+vega-schema-url-parser@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.2.0.tgz#a0d1e02915adfbfcb1fd517c8c2ebe2419985c1e"
+  integrity sha512-yAtdBnfYOhECv9YC70H2gEiqfIbVkq09aaE4y/9V/ovEFmH9gPKaEgzIZqgT7PSPQjKhsNkb6jk6XvSoboxOBw==
+
+vega-selections@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7"
+  integrity sha512-vC4NPsuN+IffruFXfH0L3i2A51RgG4PqpLv85TvrEAIYnSkyKDE4bf+wVraR3aPdnLLkc3+tYuMi6le5FmThIA==
+  dependencies:
+    vega-expression "^4.0.1"
+    vega-util "^1.16.0"
+
+vega-statistics@^1.7.9, vega-statistics@~1.7.9:
+  version "1.7.9"
+  resolved "https://registry.yarnpkg.com/vega-statistics/-/vega-statistics-1.7.9.tgz#feec01d463e1b50593d890d20631f72138fcb65d"
+  integrity sha512-T0sd2Z08k/mHxr1Vb4ajLWytPluLFYnsYqyk4SIS5czzUs4errpP2gUu63QJ0B7CKNu33vnS9WdOMOo/Eprr/Q==
+  dependencies:
+    d3-array "^2.7.1"
+
+vega-themes@^2.10.0:
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/vega-themes/-/vega-themes-2.10.0.tgz#82768b14686e3fbfbdab0e77cb63e12c62b4911e"
+  integrity sha512-prePRUKFUFGWniuZsJOfkdb+27Gwrrm82yAlVuU+912kcknsx1DVmMSg2yF79f4jdtqnAFIGycZgxoj13SEIuQ==
+
+vega-time@^2.0.3, vega-time@^2.0.4, vega-time@~2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/vega-time/-/vega-time-2.0.4.tgz#ff308358a831de927caa44e281cdc96f0863ba08"
+  integrity sha512-U314UDR9+ZlWrD3KBaeH+j/c2WSMdvcZq5yJfFT0yTg1jsBKAQBYFGvl+orackD8Zx3FveHOxx3XAObaQeDX+Q==
+  dependencies:
+    d3-array "^2.7.1"
+    d3-time "^2.0.0"
+    vega-util "^1.15.2"
+
+vega-tooltip@^0.25.1:
+  version "0.25.1"
+  resolved "https://registry.yarnpkg.com/vega-tooltip/-/vega-tooltip-0.25.1.tgz#cb7e438438649eb46896e7bee6f54e25d25b3c09"
+  integrity sha512-ugGwGi2/p3OpB8N15xieuzP8DyV5DreqMWcmJ9zpWT8GlkyKtef4dGRXnvHeHQ+iJFmWrq4oZJ+kLTrdiECjAg==
+  dependencies:
+    vega-util "^1.16.0"
+
+vega-transforms@~4.9.4:
+  version "4.9.4"
+  resolved "https://registry.yarnpkg.com/vega-transforms/-/vega-transforms-4.9.4.tgz#5cf6b91bda9f184bbbaba63838be8e5e6a571235"
+  integrity sha512-JGBhm5Bf6fiGTUSB5Qr5ckw/KU9FJcSV5xIe/y4IobM/i/KNwI1i1fP45LzP4F4yZc0DMTwJod2UvFHGk9plKA==
+  dependencies:
+    d3-array "^2.7.1"
+    vega-dataflow "^5.7.4"
+    vega-statistics "^1.7.9"
+    vega-time "^2.0.4"
+    vega-util "^1.16.1"
+
+vega-typings@~0.21.0:
+  version "0.21.0"
+  resolved "https://registry.yarnpkg.com/vega-typings/-/vega-typings-0.21.0.tgz#2e860f0d27d3db67ae06fa22a157b9b924224af8"
+  integrity sha512-dG0RtnJUn3+BQMO4NjjTdcp5UTBR56yQsLXPPCAFUHeLuycEVKlyhBa/kbvAZv2r+QxdeEYwKUNYy9CQotF5KA==
+  dependencies:
+    vega-util "^1.15.2"
+
+vega-util@^1.15.2, vega-util@^1.16.0, vega-util@^1.16.1, vega-util@~1.16.1:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/vega-util/-/vega-util-1.16.1.tgz#992bf3c3b6e145797214d99862841baea417ba39"
+  integrity sha512-FdgD72fmZMPJE99FxvFXth0IL4BbLA93WmBg/lvcJmfkK4Uf90WIlvGwaIUdSePIsdpkZjBPyQcHMQ8OcS8Smg==
+
+vega-view-transforms@~4.5.8:
+  version "4.5.8"
+  resolved "https://registry.yarnpkg.com/vega-view-transforms/-/vega-view-transforms-4.5.8.tgz#c8dc42c3c7d4aa725d40b8775180c9f23bc98f4e"
+  integrity sha512-966m7zbzvItBL8rwmF2nKG14rBp7q+3sLCKWeMSUrxoG+M15Smg5gWEGgwTG3A/RwzrZ7rDX5M1sRaAngRH25g==
+  dependencies:
+    vega-dataflow "^5.7.3"
+    vega-scenegraph "^4.9.2"
+    vega-util "^1.15.2"
+
+vega-view@~5.10.1:
+  version "5.10.1"
+  resolved "https://registry.yarnpkg.com/vega-view/-/vega-view-5.10.1.tgz#b69348bb32a9845a1bd341fdd946df98684fadc3"
+  integrity sha512-4xvQ5KZcgKdZx1Z7jjenCUumvlyr/j4XcHLRf9gyeFrFvvS596dVpL92V8twhV6O++DmS2+fj+rHagO8Di4nMg==
+  dependencies:
+    d3-array "^2.7.1"
+    d3-timer "^2.0.0"
+    vega-dataflow "^5.7.3"
+    vega-format "^1.0.4"
+    vega-functions "^5.10.0"
+    vega-runtime "^6.1.3"
+    vega-scenegraph "^4.9.4"
+    vega-util "^1.16.1"
+
+vega-voronoi@~4.1.5:
+  version "4.1.5"
+  resolved "https://registry.yarnpkg.com/vega-voronoi/-/vega-voronoi-4.1.5.tgz#e7af574d4c27fd9cb12d70082f12c6f59b80b445"
+  integrity sha512-950IkgCFLj0zG33EWLAm1hZcp+FMqWcNQliMYt+MJzOD5S4MSpZpZ7K4wp2M1Jktjw/CLKFL9n38JCI0i3UonA==
+  dependencies:
+    d3-delaunay "^5.3.0"
+    vega-dataflow "^5.7.3"
+    vega-util "^1.15.2"
+
+vega-wordcloud@~4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/vega-wordcloud/-/vega-wordcloud-4.1.3.tgz#ce90900333f4e0d3ee706ba4f36bb0905f8b4a9f"
+  integrity sha512-is4zYn9FMAyp9T4SAcz2P/U/wqc0Lx3P5YtpWKCbOH02a05vHjUQrQ2TTPOuvmMfAEDCSKvbMSQIJMOE018lJA==
+  dependencies:
+    vega-canvas "^1.2.5"
+    vega-dataflow "^5.7.3"
+    vega-scale "^7.1.1"
+    vega-statistics "^1.7.9"
+    vega-util "^1.15.2"
+
+vega@^5.20.2:
+  version "5.20.2"
+  resolved "https://registry.yarnpkg.com/vega/-/vega-5.20.2.tgz#41178edc6417aa8f743e981f8ccbc7588b13af90"
+  integrity sha512-qmH7aD9GGPpssVdxL1xgcdxTbQzyRUeRR16Os385ymvQhiwCYQNA6+eXUPAZDTVLfk0RXu6Jzj6kUE5jQ80EVw==
+  dependencies:
+    vega-crossfilter "~4.0.5"
+    vega-dataflow "~5.7.4"
+    vega-encode "~4.8.3"
+    vega-event-selector "~2.0.6"
+    vega-expression "~4.0.1"
+    vega-force "~4.0.7"
+    vega-format "~1.0.4"
+    vega-functions "~5.12.0"
+    vega-geo "~4.3.8"
+    vega-hierarchy "~4.0.9"
+    vega-label "~1.0.0"
+    vega-loader "~4.4.0"
+    vega-parser "~6.1.3"
+    vega-projection "~1.4.5"
+    vega-regression "~1.0.9"
+    vega-runtime "~6.1.3"
+    vega-scale "~7.1.1"
+    vega-scenegraph "~4.9.4"
+    vega-statistics "~1.7.9"
+    vega-time "~2.0.4"
+    vega-transforms "~4.9.4"
+    vega-typings "~0.21.0"
+    vega-util "~1.16.1"
+    vega-view "~5.10.1"
+    vega-view-transforms "~4.5.8"
+    vega-voronoi "~4.1.5"
+    vega-wordcloud "~4.1.3"
+
 vendors@^1.0.0:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
@@ -12927,6 +13518,15 @@ wrap-ansi@^6.2.0:
     string-width "^4.1.0"
     strip-ansi "^6.0.0"
 
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@@ -12991,6 +13591,11 @@ y18n@^4.0.0:
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
   integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
 
+y18n@^5.0.5:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
 yallist@^3.0.2:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
@@ -13022,6 +13627,11 @@ yargs-parser@^18.1.2:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
+yargs-parser@^20.2.2:
+  version "20.2.9"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
 yargs@^13.3.2:
   version "13.3.2"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
@@ -13055,6 +13665,19 @@ yargs@^15.4.1:
     y18n "^4.0.0"
     yargs-parser "^18.1.2"
 
+yargs@~16.2.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
 yn@3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"


Test patch:
diff --git a/devel/figurl_test.py b/devel/figurl_test.py
deleted file mode 100644
index 347c833c..00000000
--- a/devel/figurl_test.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import kachery_client as kc
-
-def figurl_mountainview(*, channel: str, recording_id: str, sorting_id: str, workspace_uri: str):
-    """
-    Generate a sortingview url that shows the mountainview page
-    """
-    base_url = 'http://localhost:3000'
-    object_uri = kc.store_json({
-        'type': 'sortingview.mountainview.1',
-        'data': {
-            'workspaceUri': workspace_uri,
-            'recordingId': recording_id,
-            'sortingId': sorting_id
-        }
-    })
-    object_hash = object_uri.split('/')[2]
-    url = f'{base_url}/fig?channel={channel}&figureObject={object_hash}'
-    return url
-
-url = figurl_mountainview(
-    channel='ccm',
-    recording_id='R-fa746904d460',
-    sorting_id='S-83c498c391ed',
-    workspace_uri='workspace://acf9d87b54e5daefbf1a6797bdaf5e1faee4834372e6704bdfdd78ed34353ca3'
-)
-print(url)