├── .nvmrc ├── .prettierignore ├── src ├── stylesheets │ ├── docs.css │ └── home.css ├── utils │ ├── stringify.ts │ ├── wrangle.ts │ ├── hooks.tsx │ ├── api.ts │ └── monaco.ts ├── languages │ ├── index.tsx │ ├── sample-language.tsx │ ├── unit-vis.tsx │ ├── data-table.tsx │ └── vega.tsx ├── templates │ ├── loading.ts │ ├── waffle-plot.ts │ ├── table.ts │ ├── atom-example.ts │ ├── vega-common.ts │ ├── simple-scatterplot.ts │ ├── gallery.ts │ ├── bee-swarm.ts │ └── scatterplot.ts ├── reducers │ ├── reducer-utils.ts │ ├── filter-actions.ts │ ├── gui-actions.ts │ ├── undo-actions.ts │ ├── view-actions.ts │ ├── apt-actions.ts │ └── data-actions.ts ├── components │ ├── split-pane.tsx │ ├── controlled-input.tsx │ ├── tooltips │ │ ├── reset-template-map-tooltip.tsx │ │ ├── publish-template-tooltip.tsx │ │ ├── unpublish-instance-tooltip.tsx │ │ ├── fork-state-tooltip.tsx │ │ ├── new-template-tooltip.tsx │ │ ├── unpublish-template-tooltip.tsx │ │ └── publish-instance-tooltip.tsx │ ├── error-boundary.tsx │ ├── widgets │ │ ├── section-widget.tsx │ │ ├── text-widget.tsx │ │ ├── free-text-widget.tsx │ │ ├── widget-common.tsx │ │ ├── switch-widget.tsx │ │ └── data-target-widget.tsx │ ├── tooltips.tsx │ ├── allowed-types-list.tsx │ ├── modals │ │ ├── user-modal.tsx │ │ └── modal.tsx │ ├── prong-wrapper.tsx │ ├── selector.tsx │ ├── filter-target.tsx │ ├── app-wrap.tsx │ ├── thumbnail.tsx │ ├── show-data.tsx │ ├── related-views.tsx │ ├── shelf.tsx │ ├── monaco-wrapper.tsx │ ├── header.tsx │ └── gallery.tsx ├── app.tsx ├── containers │ ├── docs.tsx │ └── hot-key-provider.tsx ├── constants │ └── index.ts ├── actions │ └── action-types.tsx └── types.ts ├── .gitattributes ├── public ├── logo.png ├── favicon.ico ├── assets │ ├── logo.png │ ├── atom-logo.png │ ├── example-chart.png │ ├── polestar-logo.png │ └── chart-gallery-logo.png └── index.html ├── .stylelintrc.json ├── .eslintignore ├── scripts ├── construct-build.sh ├── generate-types.sh ├── generate-schema-mod.ts ├── add-lang-ref.js ├── build-web-site-show-hider.js ├── collect-type-counts-for-small-examples.js ├── build-complexity-levels.js └── collect-type-counts-for-vega-datasets.js ├── netlify ├── functions │ ├── templates.ts │ ├── template-instances.ts │ ├── publish.ts │ ├── template.ts │ ├── template-instance.ts │ ├── remove-instance.ts │ ├── publish-instance.ts │ ├── remove.ts │ ├── user-log.ts │ └── thumbnail.ts └── utils.ts ├── .prettierrc ├── example-datasets ├── simple-bar-labels.json ├── color-encoding-names.json ├── gantt.json ├── radial-example-data-2.json ├── total-time.json ├── image-scatterplot.json ├── null-vals.json ├── pyramid-pie.json ├── radial-example-data.json ├── log-histogram.json ├── emoji-bar.json ├── bar-chart-example.json ├── discretizing-scales.json ├── hover-select-bar.json ├── bars-with-negative-vals.json ├── dashed-part.json ├── precomputed-penguin-summaries.json ├── voyager-2-study.json ├── from-binned-data.json ├── dot-plot-1.json ├── selectable-heatmap.json ├── waterfall-example.json ├── horizon-data.json ├── match-data.json ├── top-k.json ├── trellis-grid.json └── dot-plot-3.json ├── .editorconfig ├── tsconfig.node.json ├── babel.config.js ├── test ├── setup.ts ├── suggetions.test.ts ├── schema-mod.test.ts └── utils-tests.test.ts ├── .gitignore ├── .github └── workflows │ └── test.yml ├── .tslintrc.json ├── untyped └── index.d.ts ├── tsconfig.json ├── .eslintrc.json ├── index.html ├── vite.config.ts ├── LICENSE ├── README.md ├── TODO-delete └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.16.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.css -------------------------------------------------------------------------------- /src/stylesheets/docs.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-language=Javascript 2 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/ivy/HEAD/public/logo.png -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": {} 4 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/ivy/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | _maps/* 4 | bundle.js 5 | 0.bundle.worker.js 6 | -------------------------------------------------------------------------------- /public/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/ivy/HEAD/public/assets/logo.png -------------------------------------------------------------------------------- /public/assets/atom-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/ivy/HEAD/public/assets/atom-logo.png -------------------------------------------------------------------------------- /scripts/construct-build.sh: -------------------------------------------------------------------------------- 1 | # this is really just for netlify to run 2 | yarn 3 | yarn build 4 | mv dist/* ./ -------------------------------------------------------------------------------- /public/assets/example-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/ivy/HEAD/public/assets/example-chart.png -------------------------------------------------------------------------------- /public/assets/polestar-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/ivy/HEAD/public/assets/polestar-logo.png -------------------------------------------------------------------------------- /netlify/functions/templates.ts: -------------------------------------------------------------------------------- 1 | import {getMany} from '../utils'; 2 | 3 | export const handler = getMany('templates'); 4 | -------------------------------------------------------------------------------- /public/assets/chart-gallery-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcnuttandrew/ivy/HEAD/public/assets/chart-gallery-logo.png -------------------------------------------------------------------------------- /netlify/functions/template-instances.ts: -------------------------------------------------------------------------------- 1 | import {getMany} from '../utils'; 2 | 3 | export const handler = getMany('template-instances'); 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": false, 5 | "jsxBracketSameLine": false, 6 | "semi": true, 7 | "parser": "typescript", 8 | "printWidth": 110 9 | } -------------------------------------------------------------------------------- /example-datasets/simple-bar-labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "a": "A", 4 | "b": 28 5 | }, 6 | { 7 | "a": "B", 8 | "b": 55 9 | }, 10 | { 11 | "a": "C", 12 | "b": 43 13 | } 14 | ] -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /scripts/generate-types.sh: -------------------------------------------------------------------------------- 1 | # requires having https://github.com/YousefED/typescript-json-schema installed globally 2 | SCHEMA="./assets/ivy.json" 3 | typescript-json-schema "./src/lang-types.ts" "*" --ignoreErrors > $SCHEMA && ./scripts/add-lang-ref.js $SCHEMA 4 | -------------------------------------------------------------------------------- /example-datasets/color-encoding-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "color": "red", 4 | "b": 28 5 | }, 6 | { 7 | "color": "green", 8 | "b": 55 9 | }, 10 | { 11 | "color": "blue", 12 | "b": 43 13 | } 14 | ] -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/stringify.ts: -------------------------------------------------------------------------------- 1 | // some dumb foot work to try to make both build and test envs happy 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | // const stringify = require('json-stringify-pretty-compact'); 4 | import stringify from 'json-stringify-pretty-compact'; 5 | export default stringify; 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | 8 | // 9 | // "babel": { 10 | // "presets": [ 11 | // "es2015", 12 | // "stage-2", 13 | // "react" 14 | // ] 15 | // }, 16 | -------------------------------------------------------------------------------- /example-datasets/gantt.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "task": "A", 4 | "start": 1, 5 | "end": 3 6 | }, 7 | { 8 | "task": "B", 9 | "start": 3, 10 | "end": 8 11 | }, 12 | { 13 | "task": "C", 14 | "start": 8, 15 | "end": 10 16 | } 17 | ] -------------------------------------------------------------------------------- /example-datasets/radial-example-data-2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "value": 12 4 | }, 5 | { 6 | "value": 23 7 | }, 8 | { 9 | "value": 47 10 | }, 11 | { 12 | "value": 6 13 | }, 14 | { 15 | "value": 52 16 | }, 17 | { 18 | "value": 19 19 | } 20 | ] -------------------------------------------------------------------------------- /scripts/generate-schema-mod.ts: -------------------------------------------------------------------------------- 1 | import {modifyJSONSchema} from '../src/ivy-lang'; 2 | import {promises as fs} from 'fs'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-var-requires 5 | const json = require('vega-lite/build/vega-lite-schema.json'); 6 | fs.writeFile('./assets/schema-mod.json', JSON.stringify(modifyJSONSchema(json), null, 2)); 7 | -------------------------------------------------------------------------------- /scripts/add-lang-ref.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const args = process.argv.slice(2); 3 | console.log(args); 4 | 5 | import {promises as fs} from 'fs'; 6 | 7 | fs.getFile(args[0]) 8 | // getFile(args[0]) 9 | .then(d => JSON.parse(d)) 10 | .then(d => { 11 | d.$ref = '#/definitions/Template'; 12 | fs.writeFile(args[0], JSON.stringify(d, null, 2)); 13 | }); 14 | -------------------------------------------------------------------------------- /src/languages/index.tsx: -------------------------------------------------------------------------------- 1 | import Vega from './vega'; 2 | import VegaLite from './vega-lite'; 3 | import UnitVis from './unit-vis'; 4 | import DataTable from './data-table'; 5 | 6 | const DEFAULT_LANGUAGES = { 7 | [VegaLite.language]: VegaLite, 8 | [Vega.language]: Vega, 9 | [UnitVis.language]: UnitVis, 10 | [DataTable.language]: DataTable, 11 | }; 12 | 13 | export default DEFAULT_LANGUAGES; 14 | -------------------------------------------------------------------------------- /src/templates/loading.ts: -------------------------------------------------------------------------------- 1 | import {Template} from '../types'; 2 | import {AUTHORS} from '../constants/index'; 3 | 4 | const LOADING: Template = { 5 | templateName: '____loading____', 6 | templateDescription: 'LOADING', 7 | templateAuthor: AUTHORS, 8 | templateLanguage: 'Loading', 9 | disallowFanOut: true, 10 | widgets: [], 11 | code: '["LOADING"]', 12 | }; 13 | export default LOADING; 14 | -------------------------------------------------------------------------------- /example-datasets/total-time.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Activity": "Sleeping", 4 | "Time": 8 5 | }, 6 | { 7 | "Activity": "Eating", 8 | "Time": 2 9 | }, 10 | { 11 | "Activity": "TV", 12 | "Time": 4 13 | }, 14 | { 15 | "Activity": "Work", 16 | "Time": 8 17 | }, 18 | { 19 | "Activity": "Exercise", 20 | "Time": 2 21 | } 22 | ] -------------------------------------------------------------------------------- /example-datasets/image-scatterplot.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "x": 0.5, 4 | "y": 0.5, 5 | "img": "https://vega.github.io/editor/data/ffox.png" 6 | }, 7 | { 8 | "x": 1.5, 9 | "y": 1.5, 10 | "img": "https://vega.github.io/editor/data/gimp.png" 11 | }, 12 | { 13 | "x": 2.5, 14 | "y": 2.5, 15 | "img": "https://vega.github.io/editor/data/7zip.png" 16 | } 17 | ] -------------------------------------------------------------------------------- /example-datasets/null-vals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "x": 1, 4 | "y": 10 5 | }, 6 | { 7 | "x": 2, 8 | "y": 30 9 | }, 10 | { 11 | "x": 3, 12 | "y": null 13 | }, 14 | { 15 | "x": 4, 16 | "y": 15 17 | }, 18 | { 19 | "x": 5, 20 | "y": null 21 | }, 22 | { 23 | "x": 6, 24 | "y": 40 25 | }, 26 | { 27 | "x": 7, 28 | "y": 20 29 | } 30 | ] -------------------------------------------------------------------------------- /example-datasets/pyramid-pie.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category": "Sky", 4 | "value": 75, 5 | "order": 3, 6 | "color": "#416D9D" 7 | }, 8 | { 9 | "category": "Shady side of a pyramid", 10 | "value": 10, 11 | "order": 1, 12 | "color": "#674028" 13 | }, 14 | { 15 | "category": "Sunny side of a pyramid", 16 | "value": 15, 17 | "order": 2, 18 | "color": "#DEAC58" 19 | } 20 | ] -------------------------------------------------------------------------------- /example-datasets/radial-example-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "category": "a", 4 | "value": 4 5 | }, 6 | { 7 | "category": "b", 8 | "value": 6 9 | }, 10 | { 11 | "category": "c", 12 | "value": 10 13 | }, 14 | { 15 | "category": "d", 16 | "value": 3 17 | }, 18 | { 19 | "category": "e", 20 | "value": 7 21 | }, 22 | { 23 | "category": "f", 24 | "value": 8 25 | } 26 | ] -------------------------------------------------------------------------------- /example-datasets/log-histogram.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "x": 0.01 4 | }, 5 | { 6 | "x": 0.1 7 | }, 8 | { 9 | "x": 1 10 | }, 11 | { 12 | "x": 1 13 | }, 14 | { 15 | "x": 1 16 | }, 17 | { 18 | "x": 1 19 | }, 20 | { 21 | "x": 10 22 | }, 23 | { 24 | "x": 10 25 | }, 26 | { 27 | "x": 100 28 | }, 29 | { 30 | "x": 500 31 | }, 32 | { 33 | "x": 800 34 | } 35 | ] -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | class LocalStorageMock { 2 | store: Record; 3 | constructor() { 4 | this.store = {}; 5 | } 6 | 7 | clear() { 8 | this.store = {}; 9 | } 10 | 11 | getItem(key) { 12 | return this.store[key] || null; 13 | } 14 | 15 | setItem(key, value) { 16 | this.store[key] = String(value); 17 | } 18 | 19 | removeItem(key) { 20 | delete this.store[key]; 21 | } 22 | } 23 | 24 | // @ts-ignore 25 | global.localStorage = new LocalStorageMock(); 26 | // @ts-ignore 27 | global.window = {}; 28 | -------------------------------------------------------------------------------- /src/reducers/reducer-utils.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer'; 2 | import {AppState, ActionResponse} from '../types'; 3 | export const blindSet = 4 | (key: string): ActionResponse => 5 | (state, payload): AppState => 6 | produce(state, (draftState) => { 7 | // @ts-ignore 8 | draftState[key] = payload; 9 | }); 10 | export const toggle = 11 | (key: string): ActionResponse => 12 | (state): AppState => 13 | produce(state, (draftState) => { 14 | // @ts-ignore 15 | draftState[key] = !state[key]; 16 | }); 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bundle.js 3 | bundle.js.map 4 | dist.zip 5 | dist/bundle.js 6 | app-bundle.js 7 | app-bundle.js.map 8 | dist/app-bundle.js 9 | background-bundle.js 10 | background-bundle.js.map 11 | dist/background-bundle.js 12 | backups/ 13 | .DS_Store 14 | dist/ 15 | stats.json 16 | 17 | assets/schema-mod.json 18 | 19 | *.blg 20 | *.log 21 | *.aux 22 | *.fdb_latexmk 23 | *.bbl 24 | *.fls 25 | *.out 26 | *.synctex.gz 27 | .DS_Store 28 | *.synctex.gz(busy) 29 | *.lbl 30 | *.brf 31 | secrets.md 32 | coverage/* 33 | 34 | # Local Netlify folder 35 | .netlify 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and lint 2 | on: [push] 3 | jobs: 4 | build-and-deploy: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 🛎️ 8 | uses: actions/checkout@v2.3.1 9 | 10 | - name: install 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 11 | run: yarn 12 | - name: Test 13 | run: yarn test 14 | - name: lint 15 | run: yarn lint 16 | 17 | -------------------------------------------------------------------------------- /example-datasets/emoji-bar.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "🍊", 4 | "count": 21 5 | }, 6 | { 7 | "name": "🍇", 8 | "count": 13 9 | }, 10 | { 11 | "name": "🍏", 12 | "count": 8 13 | }, 14 | { 15 | "name": "🍌", 16 | "count": 5 17 | }, 18 | { 19 | "name": "🍐", 20 | "count": 3 21 | }, 22 | { 23 | "name": "🍋", 24 | "count": 2 25 | }, 26 | { 27 | "name": "🍎", 28 | "count": 1 29 | }, 30 | { 31 | "name": "🍉", 32 | "count": 1 33 | } 34 | ] -------------------------------------------------------------------------------- /scripts/build-web-site-show-hider.js: -------------------------------------------------------------------------------- 1 | import {promises as fs} from 'fs'; 2 | 3 | import {tsvParse} from 'd3-dsv'; 4 | fs.getFile('./backups/Ivy-Gallery-Rebuild.tsv') 5 | .then(x => tsvParse(x)) 6 | .then(instances => { 7 | const solvedInstances = instances.filter(row => row.Instance).map(x => x.Title); 8 | console.log( 9 | ` 10 | const done = new Set(${JSON.stringify(solvedInstances)}); 11 | [...document.querySelectorAll('.imagegroup')].forEach(node => { 12 | if (done.has(node.textContent.trim())) { 13 | node.setAttribute('style', 'opacity:0.2'); 14 | } 15 | }); 16 | `, 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /example-datasets/bar-chart-example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "a": "A", 4 | "b": 28 5 | }, 6 | { 7 | "a": "B", 8 | "b": 55 9 | }, 10 | { 11 | "a": "C", 12 | "b": 43 13 | }, 14 | { 15 | "a": "D", 16 | "b": 91 17 | }, 18 | { 19 | "a": "E", 20 | "b": 81 21 | }, 22 | { 23 | "a": "F", 24 | "b": 53 25 | }, 26 | { 27 | "a": "G", 28 | "b": 19 29 | }, 30 | { 31 | "a": "H", 32 | "b": 87 33 | }, 34 | { 35 | "a": "I", 36 | "b": 52 37 | } 38 | ] -------------------------------------------------------------------------------- /example-datasets/discretizing-scales.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "a": "A", 4 | "b": 28 5 | }, 6 | { 7 | "a": "B", 8 | "b": 55 9 | }, 10 | { 11 | "a": "C", 12 | "b": 43 13 | }, 14 | { 15 | "a": "D", 16 | "b": 91 17 | }, 18 | { 19 | "a": "E", 20 | "b": 81 21 | }, 22 | { 23 | "a": "F", 24 | "b": 53 25 | }, 26 | { 27 | "a": "G", 28 | "b": 19 29 | }, 30 | { 31 | "a": "H", 32 | "b": 87 33 | }, 34 | { 35 | "a": "I", 36 | "b": 52 37 | } 38 | ] -------------------------------------------------------------------------------- /example-datasets/hover-select-bar.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "a": "A", 4 | "b": 28 5 | }, 6 | { 7 | "a": "B", 8 | "b": 55 9 | }, 10 | { 11 | "a": "C", 12 | "b": 43 13 | }, 14 | { 15 | "a": "D", 16 | "b": 91 17 | }, 18 | { 19 | "a": "E", 20 | "b": 81 21 | }, 22 | { 23 | "a": "F", 24 | "b": 53 25 | }, 26 | { 27 | "a": "G", 28 | "b": 19 29 | }, 30 | { 31 | "a": "H", 32 | "b": 87 33 | }, 34 | { 35 | "a": "I", 36 | "b": 52 37 | } 38 | ] -------------------------------------------------------------------------------- /example-datasets/bars-with-negative-vals.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "a": "A", 4 | "b": -28 5 | }, 6 | { 7 | "a": "B", 8 | "b": 55 9 | }, 10 | { 11 | "a": "C", 12 | "b": -33 13 | }, 14 | { 15 | "a": "D", 16 | "b": 91 17 | }, 18 | { 19 | "a": "E", 20 | "b": 81 21 | }, 22 | { 23 | "a": "F", 24 | "b": 53 25 | }, 26 | { 27 | "a": "G", 28 | "b": -19 29 | }, 30 | { 31 | "a": "H", 32 | "b": 87 33 | }, 34 | { 35 | "a": "I", 36 | "b": 52 37 | } 38 | ] -------------------------------------------------------------------------------- /netlify/functions/publish.ts: -------------------------------------------------------------------------------- 1 | import {insertOne} from '../utils'; 2 | 3 | function prepareQuery(body: any) { 4 | const parsed = JSON.parse(body); 5 | const {template} = parsed; 6 | if (!template || typeof template !== 'object') { 7 | throw new Error('bad inputs'); 8 | } 9 | const {templateAuthor, templateName} = template; 10 | if (!templateAuthor || !templateName) { 11 | throw new Error('bad inputs'); 12 | } 13 | 14 | return { 15 | template: JSON.stringify(template), 16 | creator: templateAuthor, 17 | name: templateName, 18 | }; 19 | } 20 | 21 | export const handler = insertOne('templates', prepareQuery); 22 | -------------------------------------------------------------------------------- /example-datasets/dashed-part.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "a": "A", 4 | "b": 28, 5 | "predicted": false 6 | }, 7 | { 8 | "a": "B", 9 | "b": 55, 10 | "predicted": false 11 | }, 12 | { 13 | "a": "D", 14 | "b": 91, 15 | "predicted": false 16 | }, 17 | { 18 | "a": "E", 19 | "b": 81, 20 | "predicted": false 21 | }, 22 | { 23 | "a": "E", 24 | "b": 81, 25 | "predicted": true 26 | }, 27 | { 28 | "a": "G", 29 | "b": 19, 30 | "predicted": true 31 | }, 32 | { 33 | "a": "H", 34 | "b": 87, 35 | "predicted": true 36 | } 37 | ] -------------------------------------------------------------------------------- /.tslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended" 7 | ], 8 | "plugins": [ 9 | "react", 10 | "@typescript-eslint", 11 | "prettier" 12 | ], 13 | "env": { 14 | "browser": true, 15 | "jasmine": true, 16 | "jest": true, 17 | "es6": true 18 | }, 19 | "rules": { 20 | "prettier/prettier": [ 21 | "error", 22 | { 23 | "singleQuote": true 24 | } 25 | ] 26 | }, 27 | "settings": { 28 | "react": { 29 | "pragma": "React", 30 | "version": "detect" 31 | } 32 | }, 33 | "parser": "@typescript-eslint/parser" 34 | } -------------------------------------------------------------------------------- /untyped/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'type-analyzer'; 2 | declare module 'datalib'; 3 | 4 | declare module 'vega-projection-extended'; 5 | declare module '*.md'; 6 | declare module '*.json' { 7 | const value: any; 8 | export default value; 9 | } 10 | 11 | // TODO i guess 12 | // { 13 | // // export type Data = {[key: string]: string}[]; 14 | // export interface AnalyzerContainer { 15 | // computeColMeta(): (data: any, rules: any, options: any) => any; 16 | // } 17 | // // export interface Analyzer { 18 | // // computeColMeta: () 19 | // // } 20 | // // 21 | // // export interface DATA_TYPES { 22 | // // 23 | // // } 24 | // // 25 | // // export interface RegexList { 26 | // // 27 | // // } 28 | // } 29 | -------------------------------------------------------------------------------- /example-datasets/precomputed-penguin-summaries.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Species": "Adelie", 4 | "lower": 2850, 5 | "q1": 3350, 6 | "median": 3700, 7 | "q3": 4000, 8 | "upper": 4775, 9 | "outliers": [] 10 | }, 11 | { 12 | "Species": "Chinstrap", 13 | "lower": 2700, 14 | "q1": 3487.5, 15 | "median": 3700, 16 | "q3": 3950, 17 | "upper": 4800, 18 | "outliers": [ 19 | 2700, 20 | 4800 21 | ] 22 | }, 23 | { 24 | "Species": "Gentoo", 25 | "lower": 3950, 26 | "q1": 4700, 27 | "median": 5000, 28 | "q3": 5500, 29 | "upper": 6300, 30 | "outliers": [] 31 | } 32 | ] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["es2022", "ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react", 16 | 17 | /* Linting */ 18 | "strict": false, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": false 22 | }, 23 | "include": [ 24 | "src/**/*", 25 | "untyped/index.d.ts" 26 | ], 27 | "references": [{ "path": "./tsconfig.node.json" }] 28 | } 29 | -------------------------------------------------------------------------------- /src/components/split-pane.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SplitPane from 'react-split-pane'; 3 | 4 | import {getHeight, writeHeight} from '../utils/local-storage'; 5 | 6 | // wrap the split pane functionality into a HOC 7 | export default function SplitPaneWrapper(props: any): JSX.Element { 8 | if (props.showProgrammaticMode && props.showGUIView) { 9 | return ( 10 | // @ts-ignore 11 | 18 | {props.children} 19 | 20 | ); 21 | } 22 | 23 | return
{props.children}
; 24 | } 25 | -------------------------------------------------------------------------------- /example-datasets/voyager-2-study.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "measure": "Open Exploration", 4 | "mean": 1.813, 5 | "lo": 1.255, 6 | "hi": 2.37, 7 | "study": "PoleStar vs Voyager" 8 | }, 9 | { 10 | "measure": "Focused Question Answering", 11 | "mean": -1.688, 12 | "lo": -2.325, 13 | "hi": -1.05, 14 | "study": "PoleStar vs Voyager" 15 | }, 16 | { 17 | "measure": "Open Exploration", 18 | "mean": 2.1875, 19 | "lo": 1.665, 20 | "hi": 2.71, 21 | "study": "PoleStar vs Voyager 2" 22 | }, 23 | { 24 | "measure": "Focused Question Answering", 25 | "mean": -0.0625, 26 | "lo": -0.474, 27 | "hi": 0.349, 28 | "study": "PoleStar vs Voyager 2" 29 | } 30 | ] -------------------------------------------------------------------------------- /src/components/controlled-input.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | interface OnBlurInputProps { 3 | label: string; 4 | initialValue: string; 5 | update: (newVal: string) => any; 6 | } 7 | export default function OnBlurInput(props: OnBlurInputProps): JSX.Element { 8 | const {label, initialValue, update} = props; 9 | const [value, setValue] = useState(initialValue); 10 | useEffect(() => { 11 | setValue(initialValue); 12 | }, [initialValue]); 13 | return ( 14 |
15 | setValue(event.target.value)} 20 | onBlur={(): any => update(value)} 21 | /> 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /example-datasets/from-binned-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "bin_start": 8, 4 | "bin_end": 10, 5 | "count": 7 6 | }, 7 | { 8 | "bin_start": 10, 9 | "bin_end": 12, 10 | "count": 29 11 | }, 12 | { 13 | "bin_start": 12, 14 | "bin_end": 14, 15 | "count": 71 16 | }, 17 | { 18 | "bin_start": 14, 19 | "bin_end": 16, 20 | "count": 127 21 | }, 22 | { 23 | "bin_start": 16, 24 | "bin_end": 18, 25 | "count": 94 26 | }, 27 | { 28 | "bin_start": 18, 29 | "bin_end": 20, 30 | "count": 54 31 | }, 32 | { 33 | "bin_start": 20, 34 | "bin_end": 22, 35 | "count": 17 36 | }, 37 | { 38 | "bin_start": 22, 39 | "bin_end": 24, 40 | "count": 5 41 | } 42 | ] -------------------------------------------------------------------------------- /netlify/functions/template.ts: -------------------------------------------------------------------------------- 1 | import {MongoClient} from 'mongodb'; 2 | import type {Handler, HandlerEvent, HandlerContext} from '@netlify/functions'; 3 | import {errorResponse, getOne} from '../utils'; 4 | 5 | function getParametersFromPath(path) { 6 | const [_, __, ...parameters] = path.split('/').map((x) => x.replace(/%20/g, ' ')); 7 | const [_0, _1, author, ...rest] = parameters; 8 | return {author, name: rest.join('/')}; 9 | } 10 | 11 | function pathToQuery(path: string) { 12 | const params = getParametersFromPath(path); 13 | const query = {name: params.name, creator: params.author}; 14 | return query; 15 | } 16 | 17 | export const handler = getOne('templates', pathToQuery, (x) => { 18 | try { 19 | return {body: x.template, statusCode: 200}; 20 | } catch (e) { 21 | console.log('parse error', e); 22 | return false; 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/templates/waffle-plot.ts: -------------------------------------------------------------------------------- 1 | // { 2 | // "$schema": "https://vega.github.io/schema/vega-lite/v4.json", 3 | // "description": "A Rectangular Unit-Chart", 4 | // "width": 600, 5 | // "height": 400, 6 | // "mark": {"type": "rect"}, 7 | // "transform": [ 8 | // {"sort": [{"field": "[Dim1]"}], "window": [{"field": "[Dim1]", "op": "count", "as": "countX"}]}, 9 | // {"window": [{"field": "[Dim1]", "op": "count", "as": "count"}]}, 10 | // {"calculate": "ceil(datum.count/ 10)", "as": "X"}, 11 | // {"calculate": "datum.count - (datum.X - 1) *10", "as": "Y"} 12 | // ], 13 | // "encoding": { 14 | // "x": {"field": "X", "type": "ordinal", "axis": null}, 15 | // "y": {"field": "Y", "type": "ordinal", "axis": null}, 16 | // "color": {"field": "[Dim1]", "type": "nominal"}, 17 | // "tooltip": {"field": "[Dim1]", "type": "nominal"} 18 | // } 19 | // } 20 | -------------------------------------------------------------------------------- /src/utils/wrangle.ts: -------------------------------------------------------------------------------- 1 | import {DataTransform, DataRow} from '../types'; 2 | 3 | export function wrangle(data: DataRow[], transforms: DataTransform[]): any { 4 | const predicates = transforms.map((d) => (row: DataRow): boolean => { 5 | const fieldVal = row[d.filter.field]; 6 | switch (d.filter.type) { 7 | case 'DIMENSION': 8 | return !!d.filter.range.find((key: string) => `${key}` === `${fieldVal}`); 9 | case 'MEASURE': 10 | return Number(fieldVal) >= d.filter.range[0] && Number(fieldVal) <= d.filter.range[1]; 11 | case 'TIME': 12 | return ( 13 | new Date(fieldVal) >= new Date(d.filter.range[0]) && 14 | new Date(fieldVal) <= new Date(d.filter.range[1]) 15 | ); 16 | default: 17 | return true; 18 | } 19 | }); 20 | return data.filter((row) => predicates.every((pred) => pred(row))); 21 | } 22 | -------------------------------------------------------------------------------- /src/templates/table.ts: -------------------------------------------------------------------------------- 1 | import {Template} from '../types'; 2 | import {AUTHORS} from '../constants/index'; 3 | const TABLE_EXAMPLE: any = { 4 | $schema: 'data-table', 5 | columns: '[columns]', 6 | }; 7 | 8 | const DATATABLE: Template = { 9 | templateName: 'Data Table', 10 | templateDescription: 11 | 'A good old fashioned data table, show any type of data in a tabular format. A great way to double check your data.', 12 | templateLanguage: 'data-table', 13 | templateAuthor: AUTHORS, 14 | code: JSON.stringify(TABLE_EXAMPLE, null, 2), 15 | widgets: [ 16 | { 17 | name: 'columns', 18 | type: 'MultiDataTarget', 19 | config: { 20 | allowedTypes: ['MEASURE', 'DIMENSION', 'TIME'], 21 | required: true, 22 | minNumberOfTargets: 0, 23 | maxNumberOfTargets: 5, 24 | }, 25 | }, 26 | ], 27 | }; 28 | export default DATATABLE; 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ], 7 | "plugins": ["react", "@typescript-eslint", "prettier", "react-hooks"], 8 | "env": { 9 | "browser": true, 10 | "jasmine": true, 11 | "jest": true, 12 | "es6": true 13 | }, 14 | "rules": { 15 | "prettier/prettier": ["error", { "singleQuote": true }], 16 | "@typescript-eslint/no-explicit-any": 0 , 17 | "@typescript-eslint/ban-ts-ignore": 0, 18 | "no-case-declarations": 0, 19 | "react/prop-types": 0, 20 | "no-console": 0, 21 | "@typescript-eslint/no-empty-function": 0, 22 | "@typescript-eslint/ban-ts-comment": 0 23 | }, 24 | "settings": { 25 | "react": { 26 | "pragma": "React", 27 | "version": "detect" 28 | } 29 | }, 30 | "parser": "@typescript-eslint/parser" 31 | } 32 | -------------------------------------------------------------------------------- /netlify/functions/template-instance.ts: -------------------------------------------------------------------------------- 1 | import {getOne, getParametersFromPath} from '../utils'; 2 | 3 | function pathToQuery(path: string) { 4 | const params = getParametersFromPath(path); 5 | const query = { 6 | template_name: params.name, 7 | template_creator: params.author, 8 | name: params.instance, 9 | }; 10 | return query; 11 | } 12 | 13 | export const handler = getOne('template-instances', pathToQuery, (x) => { 14 | try { 15 | const output = Object.fromEntries( 16 | [ 17 | 'id', 18 | 'template_name', 19 | 'template_creator', 20 | 'name', 21 | 'instance_creator', 22 | 'template_instance', 23 | 'dataset', 24 | ].map((key) => [key, x[key]]), 25 | ); 26 | return {body: JSON.stringify(output), statusCode: 200}; 27 | } catch (e) { 28 | console.log('parse error', e); 29 | return false; 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /src/languages/sample-language.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {LanguageExtension, Template, RendererProps} from '../types'; 3 | export const BLANK_TEMPLATE: Template = { 4 | templateAuthor: '', 5 | templateLanguage: 'sample-lang', 6 | templateName: 'BLANK TEMPLATE', 7 | templateDescription: 'FILL IN DESCRIPTION', 8 | disallowFanOut: false, 9 | customCards: [], 10 | code: JSON.stringify({$schema: 'YOUR_LANG'}, null, 2), 11 | widgets: [], 12 | }; 13 | 14 | // eslint-disable-next-line 15 | function ExampleRenderer(_: RendererProps): JSX.Element { 16 | // const {spec, data, onError} = props; 17 | return
; 18 | } 19 | 20 | const SampleLang: LanguageExtension = { 21 | language: 'sample-lang', 22 | blankTemplate: BLANK_TEMPLATE, 23 | suggestion: () => [], 24 | renderer: ExampleRenderer, 25 | getDataViews: () => Promise.resolve([]), 26 | }; 27 | 28 | export default SampleLang; 29 | -------------------------------------------------------------------------------- /src/utils/hooks.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | 3 | type windowSize = {width: number; height: number}; 4 | export function useWindowSize(): windowSize { 5 | const isClient = typeof window === 'object'; 6 | 7 | function getSize(): windowSize { 8 | return { 9 | width: isClient ? window.innerWidth : undefined, 10 | height: isClient ? window.innerHeight : undefined, 11 | }; 12 | } 13 | 14 | const [windowSize, setWindowSize] = useState(getSize); 15 | 16 | useEffect(() => { 17 | if (!isClient) { 18 | return; 19 | } 20 | 21 | function handleResize(): void { 22 | setWindowSize(getSize()); 23 | } 24 | 25 | window.addEventListener('resize', handleResize); 26 | return (): any => window.removeEventListener('resize', handleResize); 27 | }, []); // Empty array ensures that effect is only run on mount and unmount 28 | 29 | return windowSize; 30 | } 31 | -------------------------------------------------------------------------------- /example-datasets/dot-plot-1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "val": 1 4 | }, 5 | { 6 | "val": 1 7 | }, 8 | { 9 | "val": 1 10 | }, 11 | { 12 | "val": 1 13 | }, 14 | { 15 | "val": 1 16 | }, 17 | { 18 | "val": 1 19 | }, 20 | { 21 | "val": 1 22 | }, 23 | { 24 | "val": 1 25 | }, 26 | { 27 | "val": 1 28 | }, 29 | { 30 | "val": 1 31 | }, 32 | { 33 | "val": 2 34 | }, 35 | { 36 | "val": 2 37 | }, 38 | { 39 | "val": 2 40 | }, 41 | { 42 | "val": 3 43 | }, 44 | { 45 | "val": 3 46 | }, 47 | { 48 | "val": 4 49 | }, 50 | { 51 | "val": 4 52 | }, 53 | { 54 | "val": 4 55 | }, 56 | { 57 | "val": 4 58 | }, 59 | { 60 | "val": 4 61 | }, 62 | { 63 | "val": 4 64 | } 65 | ] -------------------------------------------------------------------------------- /src/components/tooltips/reset-template-map-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {HoverTooltip} from '../tooltips'; 3 | import {TiArrowSync} from 'react-icons/ti'; 4 | import {GenericAction} from '../../actions'; 5 | interface Props { 6 | fillTemplateMapWithDefaults: GenericAction; 7 | } 8 | 9 | export default function NewTemplateTooltip(props: Props): JSX.Element { 10 | const {fillTemplateMapWithDefaults} = props; 11 | 12 | return ( 13 |
14 | 15 |
fillTemplateMapWithDefaults()}> 16 |
17 | 18 |
19 | Reset 20 |
21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {log} from '../utils'; 3 | interface Props { 4 | children: JSX.Element; 5 | } 6 | interface State { 7 | hasError: boolean; 8 | } 9 | export default class ErrorBoundary extends React.Component { 10 | constructor(props: Props) { 11 | super(props); 12 | this.state = {hasError: false}; 13 | } 14 | 15 | static getDerivedStateFromError(): any { 16 | // Update state so the next render will show the fallback UI. 17 | return {hasError: true}; 18 | } 19 | 20 | componentDidCatch(error: any, errorInfo: any): void { 21 | // You can also log the error to an error reporting service 22 | log(error, errorInfo); 23 | } 24 | 25 | render(): JSX.Element { 26 | if (this.state.hasError) { 27 | // You can render any custom fallback UI 28 | return

Something went wrong.

; 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example-datasets/selectable-heatmap.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "actual": "A", 4 | "predicted": "A", 5 | "count": 13 6 | }, 7 | { 8 | "actual": "A", 9 | "predicted": "B", 10 | "count": 0 11 | }, 12 | { 13 | "actual": "A", 14 | "predicted": "C", 15 | "count": 0 16 | }, 17 | { 18 | "actual": "B", 19 | "predicted": "A", 20 | "count": 0 21 | }, 22 | { 23 | "actual": "B", 24 | "predicted": "B", 25 | "count": 10 26 | }, 27 | { 28 | "actual": "B", 29 | "predicted": "C", 30 | "count": 6 31 | }, 32 | { 33 | "actual": "C", 34 | "predicted": "A", 35 | "count": 0 36 | }, 37 | { 38 | "actual": "C", 39 | "predicted": "B", 40 | "count": 0 41 | }, 42 | { 43 | "actual": "C", 44 | "predicted": "C", 45 | "count": 9 46 | } 47 | ] -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Ivy - Integrated Visualization Editor 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/widgets/section-widget.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {SectionWidget, Widget} from '../../types'; 3 | import {GeneralWidget, WidgetBuilder} from './general-widget'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | function SectionWidgetConfiguration(_: GeneralWidget): JSX.Element { 7 | return
; 8 | } 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 11 | function SectionWidgetComponent(_: GeneralWidget): JSX.Element { 12 | return
; 13 | } 14 | 15 | const SectionBuilder: WidgetBuilder = (widget, common) => { 16 | const widg = widget as Widget; 17 | return { 18 | controls: , 19 | uiElement: , 20 | materializationOptions: (): {name: string; group?: string}[] => [], 21 | }; 22 | }; 23 | 24 | export default SectionBuilder; 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // for production builds swap to using the published version of prong editor, rather than the local one 5 | function fixStringify() { 6 | return { 7 | name: 'fix-stringify', 8 | 9 | transform(src, id) { 10 | if (id.includes('utils/stringify.ts')) { 11 | const code = src.replace( 12 | 'const stringify = require("json-stringify-pretty-compact");', 13 | "import stringify from 'json-stringify-pretty-compact';", 14 | ); 15 | return {code, map: null}; 16 | } 17 | }, 18 | }; 19 | } 20 | 21 | export default defineConfig({ 22 | optimizeDeps: { 23 | include: ['react/jsx-runtime'], 24 | }, 25 | assetsInclude: ['**/*.md'], 26 | plugins: [react(), fixStringify()], 27 | build: { 28 | minify: false, 29 | }, 30 | define: { 31 | // By default, Vite doesn't include shims for NodeJS 32 | global: {}, 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import AppWrap from './components/app-wrap'; 4 | import setupMonaco from './utils/monaco'; 5 | import {PREVENT_ACCIDENTAL_LEAVE} from './constants/index'; 6 | import {randomSetUserNameIfUnset} from './utils/local-storage'; 7 | 8 | (window as any).global = window; 9 | 10 | import './stylesheets/main.css'; 11 | import './stylesheets/home.css'; 12 | import './stylesheets/docs.css'; 13 | import 'github-markdown-css'; 14 | import './stylesheets/rc-slider.css'; 15 | import 'rc-tooltip/assets/bootstrap_white.css'; 16 | import 'react-datepicker/dist/react-datepicker.css'; 17 | 18 | setupMonaco(); 19 | randomSetUserNameIfUnset(); 20 | 21 | ReactDOM.render(, document.querySelector('#root-container')); 22 | if (document.querySelector('.loading-msg')) { 23 | document.querySelector('.loading-msg').remove(); 24 | } 25 | 26 | if (PREVENT_ACCIDENTAL_LEAVE) { 27 | window.onbeforeunload = (): string => 'Are you sure you want to leave?'; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/tooltips.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tooltip from 'rc-tooltip'; 3 | import {TiInfoLarge} from 'react-icons/ti'; 4 | 5 | interface SimpleTooltipProps { 6 | message: string; 7 | } 8 | export function SimpleTooltip(props: SimpleTooltipProps): JSX.Element { 9 | const {message} = props; 10 | return ( 11 | {message}}> 12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | interface HoverTooltipProps { 20 | children: JSX.Element; 21 | message: string; 22 | delay?: number; 23 | } 24 | export function HoverTooltip(props: HoverTooltipProps): JSX.Element { 25 | const {children, message, delay} = props; 26 | return ( 27 | {message}} 32 | > 33 | {children} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/allowed-types-list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tooltip from 'rc-tooltip'; 3 | import {classnames} from '../utils'; 4 | 5 | interface AllowedTypesListProps { 6 | allowedTypes: string[]; 7 | } 8 | export default function AllowedTypesList(props: AllowedTypesListProps): JSX.Element { 9 | const {allowedTypes} = props; 10 | return ( 11 |
12 | {allowedTypes.map((type) => { 13 | return ( 14 | {`Indicates that this data target accepts columns of ${type} type`} 20 | } 21 | > 22 |
29 | 30 | ); 31 | })} 32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /netlify/functions/remove-instance.ts: -------------------------------------------------------------------------------- 1 | import {errorResponse} from '../utils'; 2 | import {Handler} from '@netlify/functions'; 3 | import {MongoClient} from 'mongodb'; 4 | 5 | const DB_URL = process.env.DB_URL || 'mongodb://localhost:27017'; 6 | const DB_NAME = 'ivy-be'; 7 | 8 | export const handler: Handler = (event, context, callback) => { 9 | let parsedArgs: Record; 10 | try { 11 | parsedArgs = JSON.parse(event.body!); 12 | } catch (e) { 13 | errorResponse(callback, 'Bad submit'); 14 | return; 15 | } 16 | 17 | MongoClient.connect(`${DB_URL}/${DB_NAME}`) 18 | .then(async (connection) => { 19 | const db = connection.db(DB_NAME); 20 | 21 | await db.collection('template-instances').deleteMany({ 22 | template_name: parsedArgs.templateName, 23 | template_creator: parsedArgs.templateAuthor, 24 | name: parsedArgs.instanceName, 25 | instance_creator: parsedArgs.userName, 26 | }); 27 | 28 | callback!(null, {statusCode: 200}); 29 | connection.close(); 30 | }) 31 | .catch((err) => errorResponse(callback, err)); 32 | }; 33 | -------------------------------------------------------------------------------- /example-datasets/waterfall-example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "Begin", 4 | "amount": 4000 5 | }, 6 | { 7 | "label": "Jan", 8 | "amount": 1707 9 | }, 10 | { 11 | "label": "Feb", 12 | "amount": -1425 13 | }, 14 | { 15 | "label": "Mar", 16 | "amount": -1030 17 | }, 18 | { 19 | "label": "Apr", 20 | "amount": 1812 21 | }, 22 | { 23 | "label": "May", 24 | "amount": -1067 25 | }, 26 | { 27 | "label": "Jun", 28 | "amount": -1481 29 | }, 30 | { 31 | "label": "Jul", 32 | "amount": 1228 33 | }, 34 | { 35 | "label": "Aug", 36 | "amount": 1176 37 | }, 38 | { 39 | "label": "Sep", 40 | "amount": 1146 41 | }, 42 | { 43 | "label": "Oct", 44 | "amount": 1205 45 | }, 46 | { 47 | "label": "Nov", 48 | "amount": -1388 49 | }, 50 | { 51 | "label": "Dec", 52 | "amount": 1492 53 | }, 54 | { 55 | "label": "End", 56 | "amount": 0 57 | } 58 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 - Andrew McNutt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /netlify/functions/publish-instance.ts: -------------------------------------------------------------------------------- 1 | import {insertOne} from '../utils'; 2 | function prepareQuery(string: any) { 3 | const parsed = JSON.parse(string); 4 | const expectedArgs = [ 5 | 'templateAuthor', 6 | 'templateName', 7 | 'templateMap', 8 | 'templateInstance', 9 | 'dataset', 10 | 'instanceCreator', 11 | 'thumbnail', 12 | ]; 13 | if (typeof parsed !== 'object') { 14 | throw new Error('bad inputs'); 15 | } 16 | const noUnexpectedKeys = Object.keys(parsed).every((key) => expectedArgs.includes(key)); 17 | const allExpectedKeys = expectedArgs.every((key) => Object.keys(parsed).includes(key)); 18 | if (!noUnexpectedKeys || !allExpectedKeys) { 19 | throw new Error('bad inputs'); 20 | } 21 | const query = { 22 | template_creator: parsed.templateAuthor, 23 | template_name: parsed.templateName, 24 | name: parsed.templateInstance, 25 | template_instance: parsed.templateMap, 26 | instance_creator: parsed.instanceCreator, 27 | dataset: parsed.dataset, 28 | thumbnail: parsed.thumbnail, 29 | }; 30 | return query; 31 | } 32 | 33 | export const handler = insertOne('template-instances', prepareQuery); 34 | -------------------------------------------------------------------------------- /netlify/functions/remove.ts: -------------------------------------------------------------------------------- 1 | import {errorResponse} from '../utils'; 2 | import {Handler} from '@netlify/functions'; 3 | import {MongoClient} from 'mongodb'; 4 | 5 | const DB_URL = process.env.DB_URL || 'mongodb://localhost:27017'; 6 | const DB_NAME = 'ivy-be'; 7 | 8 | export const handler: Handler = (event, context, callback) => { 9 | let parsedArgs: Record; 10 | try { 11 | parsedArgs = JSON.parse(event.body!); 12 | } catch (e) { 13 | errorResponse(callback, 'Bad submit'); 14 | return; 15 | } 16 | 17 | MongoClient.connect(`${DB_URL}/${DB_NAME}`) 18 | .then(async (connection) => { 19 | const db = connection.db(DB_NAME); 20 | 21 | await db.collection('templates').deleteMany({ 22 | name: parsedArgs.templateName, 23 | creator: parsedArgs.templateAuthor, 24 | }); 25 | 26 | await db.collection('template-instances').deleteMany({ 27 | template_name: parsedArgs.templateName, 28 | template_creator: parsedArgs.templateAuthor, 29 | }); 30 | 31 | callback!(null, {statusCode: 200}); 32 | connection.close(); 33 | }) 34 | .catch((err) => errorResponse(callback, err)); 35 | }; 36 | -------------------------------------------------------------------------------- /netlify/functions/user-log.ts: -------------------------------------------------------------------------------- 1 | import {errorResponse} from '../utils'; 2 | import {Handler} from '@netlify/functions'; 3 | import {MongoClient} from 'mongodb'; 4 | 5 | const DB_URL = process.env.DB_URL || 'mongodb://localhost:27017'; 6 | const DB_NAME = 'ivy-be'; 7 | 8 | function getParametersFromPath(path) { 9 | const [_, __, ...parameters] = path.split('/').map((x) => x.replace(/%20/g, ' ')); 10 | const [_0, _1, userName] = parameters; 11 | return userName; 12 | } 13 | 14 | export const handler: Handler = (event, context, callback) => { 15 | let userName: string; 16 | try { 17 | userName = getParametersFromPath(event.path); 18 | } catch (e) { 19 | errorResponse(callback, 'Bad submit'); 20 | return; 21 | } 22 | 23 | 24 | MongoClient.connect(`${DB_URL}/${DB_NAME}`) 25 | .then(async (connection) => { 26 | const db = connection.db(DB_NAME); 27 | 28 | const result = await db.collection('user-log').updateOne( 29 | {name: userName}, 30 | {$inc: {count: 1}}, 31 | {upsert: true}); 32 | callback!(null, {statusCode: 200}); 33 | connection.close(); 34 | }) 35 | .catch((err) => errorResponse(callback, err)); 36 | }; 37 | -------------------------------------------------------------------------------- /src/templates/atom-example.ts: -------------------------------------------------------------------------------- 1 | import stringify from '../utils/stringify'; 2 | import {Template} from '../types'; 3 | import {AUTHORS} from '../constants/index'; 4 | const UNIT_VIS_EXAMPLE: any = { 5 | $schema: 'https://unit-vis.netlify.com/assets/unit-vis-schema.json', 6 | mark: {color: {key: '[category]', type: 'categorical'}}, 7 | layouts: [ 8 | {subgroup: {type: 'groupby', key: '[Key1]'}, aspect_ratio: 'fillX'}, 9 | {subgroup: {type: 'bin', key: '[Key2]', numBin: 10}, aspect_ratio: 'fillY'}, 10 | {subgroup: {type: 'flatten'}, aspect_ratio: 'maxfill'}, 11 | ], 12 | }; 13 | 14 | const UNITVIS: Template = { 15 | templateName: 'UnitVis Test', 16 | templateDescription: 17 | 'A simple unit vis chart with a rudimentary preconfiguration, it makes use of the unit vis language.', 18 | templateLanguage: 'unit-vis', 19 | templateAuthor: AUTHORS, 20 | code: stringify(UNIT_VIS_EXAMPLE), 21 | widgets: [ 22 | {name: 'Key1', type: 'DataTarget', config: {allowedTypes: ['DIMENSION'], required: true}}, 23 | {name: 'Key2', type: 'DataTarget', config: {allowedTypes: ['MEASURE'], required: true}}, 24 | {name: 'category', type: 'DataTarget', config: {allowedTypes: ['DIMENSION'], required: true}}, 25 | ], 26 | }; 27 | export default UNITVIS; 28 | -------------------------------------------------------------------------------- /src/components/widgets/text-widget.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TextWidget, Widget} from '../../types'; 3 | import {GeneralWidget, WidgetBuilder} from './general-widget'; 4 | 5 | export function TextWidgetConfiguration(props: GeneralWidget): JSX.Element { 6 | const {widget, idx, setWidgetValue} = props; 7 | return ( 8 |
9 |