├── .gitignore ├── src ├── shared │ ├── data │ ├── scss │ │ ├── base.css │ │ ├── base.scss │ │ ├── Visualization.css │ │ └── Visualization.scss │ ├── .DS_Store │ └── js │ │ ├── utilities │ │ └── util.js │ │ └── components │ │ └── Visualization.js ├── index.js ├── .eslintrc.json ├── index │ ├── scss │ │ ├── App.css │ │ ├── App.scss │ │ ├── ToolTile.css │ │ └── ToolTile.scss │ └── js │ │ ├── components │ │ ├── ToolTile.js │ │ └── App.js │ │ └── utilities │ │ └── registerServiceWorker.js ├── datasetviewer │ ├── scss │ │ ├── DatasetViewer.css │ │ ├── DatasetViewer.scss │ │ ├── DatasetChooser.scss │ │ └── DatasetChooser.css │ └── js │ │ └── components │ │ ├── DatasetChooser.js │ │ └── DatasetViewer.js ├── specviewer │ ├── scss │ │ ├── SpecViewer.css │ │ └── SpecViewer.scss │ └── js │ │ └── components │ │ └── SpecViewer.js └── labeler │ ├── scss │ ├── Labeler.scss │ └── Labeler.css │ └── js │ └── components │ └── Labeler.js ├── public ├── spec_pairs ├── generated_visualizations │ ├── specs │ ├── cars_mod.json │ ├── interactions.json │ └── random_data.json ├── favicon.ico ├── manifest.json └── index.html ├── README.md ├── package.json └── server ├── db_utils.py ├── example.json └── labeler.py /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /src/shared/data: -------------------------------------------------------------------------------- 1 | ../../../data/ -------------------------------------------------------------------------------- /public/spec_pairs: -------------------------------------------------------------------------------- 1 | ../../data/spec_pairs/ -------------------------------------------------------------------------------- /src/shared/scss/base.css: -------------------------------------------------------------------------------- 1 | /** Borders */ 2 | -------------------------------------------------------------------------------- /public/generated_visualizations/specs: -------------------------------------------------------------------------------- 1 | ../../../data/to_label/ -------------------------------------------------------------------------------- /public/generated_visualizations/cars_mod.json: -------------------------------------------------------------------------------- 1 | ../../../data/cars_mod.json -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwdata/draco-tools/master/public/favicon.ico -------------------------------------------------------------------------------- /src/shared/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwdata/draco-tools/master/src/shared/.DS_Store -------------------------------------------------------------------------------- /src/shared/scss/base.scss: -------------------------------------------------------------------------------- 1 | $extra-light-grey: #f3f3f3; 2 | $light-grey: #e2e2e2; 3 | $med-grey: #606060; 4 | $dark-grey: #383838; 5 | 6 | $light-blue: #eff6ff; 7 | $med-blue: #7492c1; 8 | 9 | /** Borders */ 10 | $border-radius: 4px; 11 | -------------------------------------------------------------------------------- /src/shared/scss/Visualization.css: -------------------------------------------------------------------------------- 1 | /** Borders */ 2 | .Visualization { 3 | flex-direction: column; 4 | pointer-events: none; } 5 | .Visualization .vega-actions a { 6 | pointer-events: all; 7 | font-size: .9em; 8 | color: #e2e2e2; } 9 | -------------------------------------------------------------------------------- /src/shared/js/utilities/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true iff a and be are equal (deep equality). 3 | * 4 | * @param {Object} a 5 | * @param {Object} b 6 | */ 7 | export function equals(a, b) { 8 | return JSON.stringify(a) === JSON.stringify(b); 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/scss/Visualization.scss: -------------------------------------------------------------------------------- 1 | @import 'shared/scss/base.scss'; 2 | 3 | .Visualization { 4 | flex-direction: column; 5 | pointer-events: none; 6 | 7 | .vega-actions a { 8 | pointer-events: all; 9 | font-size: .9em; 10 | color: $light-grey; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './index/js/components/App'; 4 | import registerServiceWorker from './index/js/utilities/registerServiceWorker'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | registerServiceWorker(); 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Draco Tools 2 | 3 | Tools to generate labeled data or view examples from [Draco](https://github.com/uwdata/draco). 4 | 5 | To start the dev server 6 | > yarn start 7 | 8 | ## Directory Structure 9 | 10 | Under `src` each child folder represents a different tool. 11 | 12 | TODO: documentation for adding new tools. 13 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true, 7 | "experimentalObjectRestSpread": true 8 | } 9 | }, 10 | "rules": { 11 | "semi": 2, 12 | "quotes": ["error", "single"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/index/scss/App.css: -------------------------------------------------------------------------------- 1 | /** Borders */ 2 | @import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,400i,700,700i"); 3 | body { 4 | margin: 0; 5 | font-family: 'Noto Sans', sans-serif; } 6 | 7 | .App .title { 8 | width: 100%; 9 | font-size: 24px; 10 | color: #383838; 11 | font-weight: 700; 12 | padding: 16px 24px 16px 24px; 13 | background-color: #f3f3f3; } 14 | 15 | .App .tools { 16 | display: flex; 17 | flex-direction: row; 18 | margin: 24px; } 19 | -------------------------------------------------------------------------------- /public/generated_visualizations/interactions.json: -------------------------------------------------------------------------------- 1 | [ 2 | "aggregate.json", 3 | "bin-channel.json", 4 | "bin.json", 5 | "channel-aggregate.json", 6 | "channel-channel.json", 7 | "channel-scale.json", 8 | "channel.json", 9 | "interesting-channel.json", 10 | "mark-aggregate.json", 11 | "mark-channel.json", 12 | "mark-scale.json", 13 | "mark-type.json", 14 | "mark.json", 15 | "scale.json", 16 | "sort.json", 17 | "stack.json", 18 | "type-channel.json" 19 | ] 20 | -------------------------------------------------------------------------------- /src/index/scss/App.scss: -------------------------------------------------------------------------------- 1 | @import 'shared/scss/base.scss'; 2 | 3 | @import url('https://fonts.googleapis.com/css?family=Noto+Sans:400,400i,700,700i'); 4 | 5 | body { 6 | margin: 0; 7 | font-family: 'Noto Sans', sans-serif; 8 | } 9 | 10 | .App { 11 | .title { 12 | width: 100%; 13 | font-size: 24px; 14 | color: $dark-grey; 15 | font-weight: 700; 16 | padding: 16px 24px 16px 24px; 17 | background-color: $extra-light-grey; 18 | } 19 | 20 | .tools { 21 | display: flex; 22 | flex-direction: row; 23 | margin: 24px; 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/index/scss/ToolTile.css: -------------------------------------------------------------------------------- 1 | /** Borders */ 2 | .ToolTile { 3 | width: 240px; 4 | height: 120px; 5 | border: 1px solid #e2e2e2; 6 | border-radius: 8px; 7 | display: flex; 8 | flex-direction: column; 9 | padding: 16px; 10 | margin-right: 16px; 11 | margin-bottom: 16px; 12 | cursor: pointer; } 13 | .ToolTile .name { 14 | font-size: 21px; 15 | font-weight: 700; 16 | color: #383838; } 17 | .ToolTile .description { 18 | font-size: 14px; 19 | color: #606060; 20 | font-weight: 400; 21 | margin-top: 8px; } 22 | 23 | .ToolTile:hover { 24 | background-color: #f3f3f3; } 25 | -------------------------------------------------------------------------------- /src/index/scss/ToolTile.scss: -------------------------------------------------------------------------------- 1 | @import 'shared/scss/base.scss'; 2 | 3 | .ToolTile { 4 | width: 240px; 5 | height: 120px; 6 | border: 1px solid $light-grey; 7 | border-radius: 8px; 8 | display: flex; 9 | flex-direction: column; 10 | padding: 16px; 11 | margin-right: 16px; 12 | margin-bottom: 16px; 13 | cursor: pointer; 14 | 15 | .name { 16 | font-size: 21px; 17 | font-weight: 700; 18 | color: $dark-grey; 19 | } 20 | 21 | .description { 22 | font-size: 14px; 23 | color: $med-grey; 24 | font-weight: 400; 25 | margin-top: 8px; 26 | } 27 | } 28 | 29 | .ToolTile:hover { 30 | background-color: $extra-light-grey; 31 | } -------------------------------------------------------------------------------- /src/index/js/components/ToolTile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Redirect } from 'react-router' 3 | 4 | import '../../scss/ToolTile.css'; 5 | 6 | class ToolTile extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | redirect: false, 11 | }; 12 | } 13 | 14 | render() { 15 | if (this.state.redirect) { 16 | return 17 | } 18 | return ( 19 |
20 |
21 | {this.props.name} 22 |
23 |
24 | {this.props.description} 25 |
26 |
27 | ); 28 | } 29 | 30 | redirect() { 31 | this.setState({ 32 | redirect: true 33 | }); 34 | } 35 | } 36 | 37 | export default ToolTile; 38 | -------------------------------------------------------------------------------- /src/datasetviewer/scss/DatasetViewer.css: -------------------------------------------------------------------------------- 1 | /** Borders */ 2 | .DatasetViewer { 3 | display: flex; 4 | flex-direction: column; 5 | padding: 24px; } 6 | .DatasetViewer .header { 7 | display: flex; 8 | flex-direction: row; 9 | align-items: baseline; } 10 | .DatasetViewer .header .summary { 11 | margin-left: 48px; } 12 | .DatasetViewer .group { 13 | display: flex; 14 | flex-direction: row; 15 | flex-wrap: wrap; 16 | border: 1px solid #e2e2e2; 17 | border-radius: 4px; 18 | margin-bottom: 16px; } 19 | .DatasetViewer .group .visualization { 20 | padding: 20px; 21 | display: flex; 22 | flex-direction: column; 23 | cursor: pointer; } 24 | .DatasetViewer .spec-view { 25 | z-index: 1; 26 | position: absolute; 27 | padding: 8px; 28 | background-color: #fff; 29 | font-size: 10pt; 30 | border: 1px solid #e2e2e2; 31 | border-radius: 4px; } 32 | -------------------------------------------------------------------------------- /src/datasetviewer/scss/DatasetViewer.scss: -------------------------------------------------------------------------------- 1 | @import 'shared/scss/base.scss'; 2 | 3 | .DatasetViewer { 4 | display: flex; 5 | flex-direction: column; 6 | padding: 24px; 7 | 8 | .header { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: baseline; 12 | 13 | .summary { 14 | margin-left: 48px; 15 | } 16 | } 17 | 18 | .group { 19 | display: flex; 20 | flex-direction: row; 21 | flex-wrap: wrap; 22 | border: 1px solid $light-grey; 23 | border-radius: 4px; 24 | margin-bottom: 16px; 25 | 26 | .visualization { 27 | padding: 20px; 28 | display: flex; 29 | flex-direction: column; 30 | cursor: pointer; 31 | } 32 | } 33 | 34 | .spec-view { 35 | z-index: 1; 36 | position: absolute; 37 | padding: 8px; 38 | background-color: #fff; 39 | font-size: 10pt; 40 | border: 1px solid $light-grey; 41 | border-radius: 4px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "draco-tools", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "classnames": "^2.2.6", 7 | "json-stable-stringify": "^1.0.1", 8 | "node-sass-chokidar": "^1.3.0", 9 | "npm-run-all": "^4.1.3", 10 | "react": "^16.4.1", 11 | "diff": "^3.5.0", 12 | "react-dom": "^16.4.1", 13 | "react-router-dom": "^4.3.1", 14 | "react-scripts": "1.1.4", 15 | "vega-embed": "^3.15.0" 16 | }, 17 | "scripts": { 18 | "build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", 19 | "watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", 20 | "start-js": "react-scripts start", 21 | "start": "npm-run-all -p watch-css start-js", 22 | "build-js": "react-scripts build", 23 | "build": "npm-run-all build-css build-js", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/specviewer/scss/SpecViewer.css: -------------------------------------------------------------------------------- 1 | /** Borders */ 2 | .SpecViewer { 3 | padding: 20px; 4 | max-width: 1240px; 5 | margin: 0px auto; } 6 | .SpecViewer > p { 7 | font-size: 0.9em; 8 | color: #606060; } 9 | .SpecViewer .main { 10 | display: grid; 11 | grid-template-columns: 50% 50%; } 12 | .SpecViewer .main .label { 13 | font-size: 1.1em; 14 | font-weight: 600; 15 | margin-bottom: .6em; 16 | text-align: center; } 17 | .SpecViewer .main .label small { 18 | font-weight: normal; } 19 | .SpecViewer .main .spec { 20 | grid-column: 1 / span 2; 21 | border: 1px solid #e2e2e2; 22 | border-radius: 4px; 23 | margin-top: 16px; 24 | color: #606060; } 25 | .SpecViewer .main .spec .visualizations { 26 | display: grid; 27 | grid-template-columns: 50% 50%; 28 | border-bottom: 1px solid #f3f3f3; } 29 | .SpecViewer .main .spec .visualizations > * { 30 | padding: 20px; } 31 | .SpecViewer .main .spec .visualizations .Visualization { 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; } 35 | .SpecViewer .main .spec .visualizations svg { 36 | max-height: 600px; 37 | max-width: 400px; } 38 | .SpecViewer .main .spec p { 39 | margin-left: 20px; 40 | margin-right: 20px; } 41 | -------------------------------------------------------------------------------- /src/specviewer/scss/SpecViewer.scss: -------------------------------------------------------------------------------- 1 | @import 'shared/scss/base.scss'; 2 | 3 | .SpecViewer { 4 | padding: 20px; 5 | max-width: 1240px; 6 | margin: 0px auto; 7 | 8 | &> p { 9 | font-size: 0.9em; 10 | color: $med-grey; 11 | } 12 | 13 | .main { 14 | display: grid; 15 | grid-template-columns: 50% 50%; 16 | 17 | .label { 18 | font-size: 1.1em; 19 | font-weight: 600; 20 | margin-bottom: .6em; 21 | 22 | text-align: center; 23 | 24 | small { 25 | font-weight: normal; 26 | } 27 | } 28 | 29 | .spec { 30 | grid-column: 1 / span 2; 31 | 32 | .visualizations { 33 | display: grid; 34 | grid-template-columns: 50% 50%; 35 | 36 | border-bottom: 1px solid $extra-light-grey; 37 | } 38 | 39 | border: 1px solid $light-grey; 40 | border-radius: 4px; 41 | margin-top: 16px; 42 | color: $med-grey; 43 | 44 | .visualizations { 45 | &> * { 46 | padding: 20px; 47 | } 48 | 49 | .Visualization { 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | } 54 | 55 | svg { 56 | max-height: 600px; 57 | max-width: 400px; 58 | } 59 | } 60 | 61 | p { 62 | margin-left: 20px; 63 | margin-right: 20px; 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Draco Tools 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/shared/js/components/Visualization.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { equals } from '../../../shared/js/utilities/util'; 3 | import vegaEmbed, { vega } from 'vega-embed'; 4 | 5 | import '../../scss/Visualization.css'; 6 | 7 | export const datasets = { 8 | 'data/cars.json': require('../../data/cars.json'), 9 | 'data/cars_mod.json': require('../../data/cars_mod.json'), 10 | 'data/movies.json': require('../../data/movies.json'), 11 | 'data/weather.json': require('../../data/weather.json') 12 | }; 13 | 14 | /** 15 | * A Visualization component accepts a `vlSpec` as a prop 16 | * and renders the resulting svg. 17 | */ 18 | class Visualization extends Component { 19 | componentDidMount() { 20 | this.updateView(this.props.vlSpec); 21 | } 22 | 23 | componentWillReceiveProps(nextProps) { 24 | if (!equals(this.props, nextProps)) { 25 | this.updateView(nextProps.vlSpec); 26 | } 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 |
33 | ); 34 | } 35 | 36 | /** 37 | * Updates this to use the given vlSpec. 38 | * 39 | * @param {Object} vlSpec The Vega-Lite spec to use. 40 | */ 41 | updateView(vlSpec) { 42 | if (!vlSpec) { 43 | console.warn('no spec passed to viz view'); 44 | return; 45 | } 46 | 47 | const loader = vega.loader(); 48 | 49 | const original_http = loader.http; 50 | loader.http = (url, options) => { 51 | console.debug(url); 52 | 53 | if (url in datasets) { 54 | return datasets[url]; 55 | } 56 | return original_http(url, options); 57 | }; 58 | 59 | vegaEmbed(this.refs.vis, vlSpec, { renderer: this.props.renderer, loader: loader, mode: 'vega-lite', actions: { editor: false, export: false } }); 60 | } 61 | } 62 | 63 | export default Visualization; 64 | -------------------------------------------------------------------------------- /src/index/js/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 3 | 4 | import '../../scss/App.css'; 5 | 6 | import ToolTile from './ToolTile'; 7 | import SpecViewer from '../../../specviewer/js/components/SpecViewer'; 8 | import Labeler from '../../../labeler/js/components/Labeler'; 9 | import DatasetViewer from '../../../datasetviewer/js/components/DatasetViewer'; 10 | 11 | const TOOLS = [ 12 | { 13 | name: 'Spec Viewer', 14 | description: 'View many specs side by side', 15 | route: '/specviewer', 16 | }, 17 | { 18 | name: 'Labeler', 19 | description: 'Label pairs of visualizations', 20 | route: '/labeler', 21 | }, 22 | { 23 | name: 'Dataset Viewer', 24 | description: 'View specs generated for labeling (to be pooled)', 25 | route: '/datasetviewer' 26 | } 27 | ]; 28 | 29 | class App extends Component { 30 | render() { 31 | const Home = () => { 32 | const tools = []; 33 | 34 | for (const tool of TOOLS) { 35 | tools.push( 36 | 37 | ); 38 | } 39 | 40 | return ( 41 |
42 |
43 | Tools for Draco 44 |
45 |
46 | {tools} 47 |
48 |
49 | ); 50 | }; 51 | 52 | return ( 53 | 54 |
55 | 56 | 57 | 58 | 59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /src/datasetviewer/scss/DatasetChooser.scss: -------------------------------------------------------------------------------- 1 | @import 'shared/scss/base.scss'; 2 | 3 | .DatasetChooser { 4 | height: 35px; 5 | line-height: 35px; 6 | vertical-align: middle; 7 | margin-top: 1px; 8 | display: flex; 9 | flex-direction: row; 10 | margin-bottom: 16px; 11 | width: 600px; 12 | 13 | .search-title { 14 | flex: 0 0 auto; 15 | padding-left: 8px; 16 | padding-right: 8px; 17 | border-radius: 4px 0 0 4px; 18 | background-color: #e8e8e8; 19 | font-size: 16px; 20 | } 21 | 22 | .search-bar { 23 | flex: 1 1 auto; 24 | display: flex; 25 | position: relative; 26 | flex-direction: column; 27 | border: 1px solid #e8e8e8; 28 | border-radius: 0; 29 | } 30 | 31 | .dimensions { 32 | display: flex; 33 | flex-direction: row; 34 | background-color: $light-grey; 35 | border-radius: 0 4px 4px 0; 36 | 37 | .dim-option { 38 | padding-left: 8px; 39 | padding-right: 8px; 40 | cursor: pointer; 41 | } 42 | 43 | .dim-option.selected { 44 | background-color: $med-blue; 45 | border-radius: 4px; 46 | } 47 | } 48 | 49 | .search-input { 50 | flex: 1 1 auto; 51 | border: none; 52 | font-size: 12px; 53 | color: #b5b5b5; 54 | padding: 0 4px 0 8px; 55 | } 56 | 57 | .search-input::placeholder { 58 | color: #c9c8c8; 59 | } 60 | 61 | .search-input:focus { 62 | outline: none; 63 | } 64 | 65 | .dropdown-content { 66 | display: none; 67 | position: absolute; 68 | list-style-type: none; 69 | width: 100%; 70 | top: 35px; 71 | background-color: #fff; 72 | max-height: 240px; 73 | overflow-y: scroll; 74 | overflow-x: hidden; 75 | z-index: 1; 76 | margin: 0; 77 | padding: 0; 78 | font-size: 12px; 79 | border: 1px solid #e8e8e8; 80 | border-top-style: none; 81 | } 82 | 83 | .dropdown-item { 84 | margin: 0; 85 | padding: 0 8px 0 8px; 86 | cursor: pointer; 87 | } 88 | 89 | .dropdown-item:hover { 90 | background-color: #e8e8e8; 91 | } 92 | 93 | .search-bar:focus-within .dropdown-content { 94 | display: block; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/specviewer/js/components/SpecViewer.js: -------------------------------------------------------------------------------- 1 | import '../../scss/SpecViewer.css'; 2 | 3 | import * as stringify from 'json-stable-stringify'; 4 | import React, { Component } from 'react'; 5 | import Visualization from '../../../shared/js/components/Visualization'; 6 | 7 | class SpecViewer extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | data: undefined, 13 | specs: (new URL(window.location.href)).searchParams.get('data') || '/spec_pairs/data.json' 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | fetch(this.state.specs) 19 | .then(response => response.json()) 20 | .then(data => this.setState({ data: data })); 21 | } 22 | 23 | render() { 24 | const data = this.state.data; 25 | 26 | if (!data) { 27 | return
loading...
; 28 | } 29 | 30 | const headers = data.headers; 31 | 32 | const pairs = []; 33 | for (let i = 0; i < data.specs.length; i++) { 34 | const pair = data.specs[i]; 35 | 36 | const properties = Object.keys(pair.properties || {}).map((p, i) =>

{p}: {stringify(pair.properties[p])}

); 37 | 38 | pairs.push( 39 |
40 |
41 | 42 | 43 |
44 | {properties} 45 |
46 | ); 47 | } 48 | 49 | return ( 50 |
51 |

52 | You are viewing {this.state.specs}. Append ?data=spec_pairs/FILE.json to the URL to change the source. 53 |

54 |
55 |
56 | {headers.first.title}
{headers.first.subtitle} 57 |
58 |
59 | {headers.second.title}
{headers.second.subtitle} 60 |
61 | {pairs} 62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | export default SpecViewer; 69 | -------------------------------------------------------------------------------- /src/datasetviewer/scss/DatasetChooser.css: -------------------------------------------------------------------------------- 1 | /** Borders */ 2 | .DatasetChooser { 3 | height: 35px; 4 | line-height: 35px; 5 | vertical-align: middle; 6 | margin-top: 1px; 7 | display: flex; 8 | flex-direction: row; 9 | margin-bottom: 16px; 10 | width: 600px; } 11 | .DatasetChooser .search-title { 12 | flex: 0 0 auto; 13 | padding-left: 8px; 14 | padding-right: 8px; 15 | border-radius: 4px 0 0 4px; 16 | background-color: #e8e8e8; 17 | font-size: 16px; } 18 | .DatasetChooser .search-bar { 19 | flex: 1 1 auto; 20 | display: flex; 21 | position: relative; 22 | flex-direction: column; 23 | border: 1px solid #e8e8e8; 24 | border-radius: 0; } 25 | .DatasetChooser .dimensions { 26 | display: flex; 27 | flex-direction: row; 28 | background-color: #e2e2e2; 29 | border-radius: 0 4px 4px 0; } 30 | .DatasetChooser .dimensions .dim-option { 31 | padding-left: 8px; 32 | padding-right: 8px; 33 | cursor: pointer; } 34 | .DatasetChooser .dimensions .dim-option.selected { 35 | background-color: #7492c1; 36 | border-radius: 4px; } 37 | .DatasetChooser .search-input { 38 | flex: 1 1 auto; 39 | border: none; 40 | font-size: 12px; 41 | color: #b5b5b5; 42 | padding: 0 4px 0 8px; } 43 | .DatasetChooser .search-input::placeholder { 44 | color: #c9c8c8; } 45 | .DatasetChooser .search-input:focus { 46 | outline: none; } 47 | .DatasetChooser .dropdown-content { 48 | display: none; 49 | position: absolute; 50 | list-style-type: none; 51 | width: 100%; 52 | top: 35px; 53 | background-color: #fff; 54 | max-height: 240px; 55 | overflow-y: scroll; 56 | overflow-x: hidden; 57 | z-index: 1; 58 | margin: 0; 59 | padding: 0; 60 | font-size: 12px; 61 | border: 1px solid #e8e8e8; 62 | border-top-style: none; } 63 | .DatasetChooser .dropdown-item { 64 | margin: 0; 65 | padding: 0 8px 0 8px; 66 | cursor: pointer; } 67 | .DatasetChooser .dropdown-item:hover { 68 | background-color: #e8e8e8; } 69 | .DatasetChooser .search-bar:focus-within .dropdown-content { 70 | display: block; } 71 | -------------------------------------------------------------------------------- /src/datasetviewer/js/components/DatasetChooser.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import '../../scss/DatasetChooser.css'; 3 | 4 | const classnames = require('classnames'); 5 | 6 | class DatasetChooser extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | dropdown: false 11 | }; 12 | } 13 | 14 | render() { 15 | const datasetOptions = []; 16 | for (const dataset of this.props.datasets) { 17 | if (dataset.includes(this.state.inputText) || !this.state.inputText) { 18 | datasetOptions.push( 19 |
  • { 20 | this.props.setDataset(e.target.textContent); 21 | }}>{dataset}
  • 22 | ); 23 | } 24 | } 25 | 26 | const dropdown = ( 27 |
      28 | {datasetOptions} 29 |
    30 | ); 31 | 32 | 33 | const dimensions = []; 34 | if (this.props.availableDimensions) { 35 | for (let i = 0; i < this.props.availableDimensions.length; i++) { 36 | const d = this.props.availableDimensions[i]; 37 | 38 | const dimClasses = classnames({ 39 | 'dim-option': true, 40 | 'selected': d === this.props.selectedDimension 41 | }); 42 | 43 | dimensions.push( 44 |
    this.props.setDimension(d) }>{d}
    45 | ); 46 | } 47 | } 48 | 49 | return ( 50 |
    51 |
    52 | {this.props.dataset} 53 |
    54 |
    55 | { this.setInputText(e); }}> 57 | {dropdown} 58 |
    59 |
    60 | dimensions: 61 | {dimensions} 62 |
    63 |
    64 | ); 65 | } 66 | 67 | setInputText(e) { 68 | this.setState({ 69 | inputText: e.target.value 70 | }); 71 | } 72 | } 73 | 74 | export default DatasetChooser; 75 | -------------------------------------------------------------------------------- /server/db_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pathlib 4 | import sqlite3 5 | import sys 6 | from typing import Dict 7 | 8 | import numpy as np 9 | 10 | from draco.learn import data_util 11 | from draco.spec import Query, Task 12 | 13 | 14 | def create_database(db_file: str): 15 | ''' initialize the databsae and insert default entries into it. ''' 16 | conn = sqlite3.connect(db_file) 17 | c = conn.cursor() 18 | 19 | # Create table 20 | c.execute('CREATE TABLE pairs (id text primary key, source text, task text, left text, right text)') 21 | c.execute('CREATE TABLE labels (id text, label text, user text)') 22 | 23 | conn.close() 24 | 25 | 26 | def insert_unlabeled_data(db_file: str): 27 | # generate feature vector and store in database 28 | 29 | conn = sqlite3.connect(db_file) 30 | c = conn.cursor() 31 | 32 | specs, features = data_util.get_unlabeled_data() 33 | 34 | for key in specs: 35 | 36 | entry = specs[key] 37 | feature = features.loc[key] 38 | 39 | pair_id = entry.pair_id 40 | source = entry.source 41 | task = entry.task 42 | left_spec = entry.left 43 | right_spec = entry.right 44 | #vec1 = feature.negative 45 | #vec2 = feature.positive 46 | 47 | print(pair_id + (task or 'No Task')) 48 | 49 | stmt = 'INSERT INTO pairs VALUES (?, ?, ?, ?, ?)' 50 | 51 | c.execute(stmt, (pair_id, source, task, json.dumps(left_spec), json.dumps(right_spec))) 52 | 53 | conn.commit() 54 | 55 | conn.close() 56 | 57 | 58 | def load_labeled_specs(db_file: str): 59 | """ load all pairs have been labeled 60 | Args: the database file containing corresponding entries 61 | Returns: 62 | A list of object files containing pairs and their labels, 63 | in the form of { 64 | "id": xx, 65 | "label": xx, 66 | "left_spec": xx, //dict obj represented spec 67 | "right_spec": xx, // dict obj represented spec 68 | "left_feature": xx, 69 | "right_feature": xx 70 | } 71 | """ 72 | 73 | # todo: complete this function 74 | 75 | conn = sqlite3.connect(db_file) 76 | c = conn.cursor() 77 | 78 | c.execute('''SELECT pairs.id, 79 | pairs.task, 80 | pairs.source, 81 | pairs.left, 82 | pairs.right, 83 | labels.label, 84 | labels.user 85 | FROM labels JOIN pairs 86 | WHERE labels.id = pairs.id''') 87 | 88 | label_and_features = c.fetchall() 89 | 90 | return [{ 91 | "id": r[0], 92 | "task": r[1], 93 | "source": r[2], 94 | "left": json.loads(r[3]), 95 | "right": json.loads(r[4]), 96 | "label": r[5], 97 | "labeler": r[6] 98 | } for r in label_and_features] 99 | 100 | 101 | def build_database(db_file): 102 | """ init and insert data """ 103 | if pathlib.Path(db_file).exists(): 104 | print('[Err] The database {} exists, won\'t create one.'.format(db_file)) 105 | sys.exit(-1) 106 | 107 | create_database(db_file) 108 | insert_unlabeled_data(db_file) 109 | 110 | 111 | if __name__ == '__main__': 112 | db_file = os.path.join(os.path.dirname(__file__), 'label_data.db') 113 | build_database(db_file) 114 | labeled = load_labeled_specs(db_file) 115 | print(labeled) 116 | -------------------------------------------------------------------------------- /server/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 0, 3 | "left": { 4 | "mark": "point", 5 | "encoding": { 6 | "x": { 7 | "field": "q1", 8 | "type": "quantitative" 9 | }, 10 | "y": { 11 | "field": "q2", 12 | "type": "quantitative" 13 | }, 14 | "row": { 15 | "field": "n", 16 | "type": "nominal" 17 | } 18 | }, 19 | "data": { 20 | "values": [ 21 | { 22 | "q1": 5.3471937513273735, 23 | "q2": 1.3626291214835566, 24 | "n": 6 25 | }, 26 | { 27 | "q1": 1.7876704150460694, 28 | "q2": 5.104594393888288, 29 | "n": 5 30 | }, 31 | { 32 | "q1": 1.747225077547867, 33 | "q2": 7.158966994989114, 34 | "n": 5 35 | }, 36 | { 37 | "q1": -0.9840805161743793, 38 | "q2": 2.2600355638857526, 39 | "n": 4 40 | }, 41 | { 42 | "q1": -0.7807890435536153, 43 | "q2": 1.0209364919885022, 44 | "n": 0 45 | }, 46 | { 47 | "q1": -0.5886547634341959, 48 | "q2": 3.647426625837295, 49 | "n": 2 50 | }, 51 | { 52 | "q1": 0.28514952049893694, 53 | "q2": 3.571696048867717, 54 | "n": 0 55 | }, 56 | { 57 | "q1": 1.6436672918960933, 58 | "q2": 3.7089171201718876, 59 | "n": 4 60 | }, 61 | { 62 | "q1": 1.9604503307919952, 63 | "q2": 0.877126733603903, 64 | "n": 4 65 | }, 66 | { 67 | "q1": 1.2699951793991064, 68 | "q2": 2.403889731783689, 69 | "n": 1 70 | } 71 | ] 72 | } 73 | }, 74 | "right": { 75 | "mark": "point", 76 | "encoding": { 77 | "y": { 78 | "field": "q1", 79 | "type": "quantitative" 80 | }, 81 | "x": { 82 | "field": "q2", 83 | "type": "quantitative" 84 | }, 85 | "color": { 86 | "field": "n", 87 | "type": "nominal" 88 | } 89 | }, 90 | "data": { 91 | "values": [ 92 | { 93 | "q1": 5.3471937513273735, 94 | "q2": 1.3626291214835566, 95 | "n": 6 96 | }, 97 | { 98 | "q1": 1.7876704150460694, 99 | "q2": 5.104594393888288, 100 | "n": 5 101 | }, 102 | { 103 | "q1": 1.747225077547867, 104 | "q2": 7.158966994989114, 105 | "n": 5 106 | }, 107 | { 108 | "q1": -0.9840805161743793, 109 | "q2": 2.2600355638857526, 110 | "n": 4 111 | }, 112 | { 113 | "q1": -0.7807890435536153, 114 | "q2": 1.0209364919885022, 115 | "n": 0 116 | }, 117 | { 118 | "q1": -0.5886547634341959, 119 | "q2": 3.647426625837295, 120 | "n": 2 121 | }, 122 | { 123 | "q1": 0.28514952049893694, 124 | "q2": 3.571696048867717, 125 | "n": 0 126 | }, 127 | { 128 | "q1": 1.6436672918960933, 129 | "q2": 3.7089171201718876, 130 | "n": 4 131 | }, 132 | { 133 | "q1": 1.9604503307919952, 134 | "q2": 0.877126733603903, 135 | "n": 4 136 | }, 137 | { 138 | "q1": 1.2699951793991064, 139 | "q2": 2.403889731783689, 140 | "n": 1 141 | } 142 | ] 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/datasetviewer/js/components/DatasetViewer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import '../../scss/DatasetViewer.css'; 3 | 4 | import Visualization from '../../../shared/js/components/Visualization'; 5 | import DatasetChooser from './DatasetChooser'; 6 | 7 | const BASE_DIR = '/generated_visualizations/'; 8 | const SPEC_DIR = BASE_DIR + 'specs/'; 9 | const INTERACTIONS = BASE_DIR + 'interactions.json'; 10 | const DEFAULT_DATASET = 'mark.json'; 11 | 12 | class DatasetViewer extends Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | data: undefined, 18 | dataset: DEFAULT_DATASET, 19 | datasets: undefined, 20 | specs: undefined, 21 | currentDimension: undefined 22 | }; 23 | 24 | } 25 | 26 | componentDidMount() { 27 | fetch(INTERACTIONS) 28 | .then(response => response.json()) 29 | .then(data => this.setState({ 'datasets': data })); 30 | 31 | this.fetchDataset(DEFAULT_DATASET); 32 | } 33 | 34 | render() { 35 | if (!this.state.datasets) { 36 | return
    loading...
    ; 37 | } 38 | 39 | let vizGroups; 40 | let info; 41 | let specView; 42 | if (!this.state.specs) { 43 | vizGroups =
    loading...
    ; 44 | } else { 45 | let pairs = 0; 46 | let count = 0; 47 | 48 | 49 | const groups = this.state.specs[this.state.currentDimension]; 50 | 51 | vizGroups = []; 52 | for (let i = 0; i < groups.length; i++) { 53 | const group = groups[i]; 54 | 55 | const visualizations = []; 56 | for (let j = 0; j < group.length; j++) { 57 | const spec = group[j]; 58 | 59 | const specNoData = {}; 60 | Object.assign(specNoData, spec); 61 | delete specNoData['data']; 62 | 63 | 64 | visualizations.push( 65 |
    66 | 67 |
    68 | ); 69 | } 70 | 71 | vizGroups.push( 72 |
    73 | {visualizations} 74 |
    75 | ); 76 | 77 | count += group.length; 78 | 79 | if (group.length > 0) { 80 | pairs += factorial(group.length) / (2 * factorial(group.length - 2)); 81 | } 82 | } 83 | 84 | info =
    {count} visualizations and {pairs} pairs
    ; 85 | } 86 | 87 | return ( 88 |
    89 |
    90 | 95 | {info} 96 |
    97 | {vizGroups} 98 | {specView} 99 |
    100 | ); 101 | } 102 | 103 | setDataset(name) { 104 | this.setState({ 105 | dataset: name, 106 | specs: undefined 107 | }); 108 | 109 | fetch(SPEC_DIR + name) 110 | .then(response => response.json()) 111 | .then(data => this.setState({ dataset: name, specs: data })); 112 | } 113 | 114 | fetchDataset(name) { 115 | fetch(SPEC_DIR + name) 116 | .then(response => response.json()) 117 | .then(data => { 118 | const dimensions = []; 119 | for (let d in data) { 120 | dimensions.push(d); 121 | } 122 | dimensions.sort(); 123 | 124 | this.setState({ 125 | dataset: name, 126 | specs: data, 127 | availableDimensions: dimensions, 128 | currentDimension: dimensions[0] }); 129 | }); 130 | } 131 | 132 | setCurrentDimension(d) { 133 | this.setState({ 134 | currentDimension: d 135 | }); 136 | } 137 | } 138 | 139 | function factorial(n) { 140 | if (n === 0) { 141 | return 1; 142 | } 143 | 144 | let result = 1; 145 | for (let i = 2; i <= n; i++) { 146 | result *= i; 147 | } 148 | 149 | return result; 150 | } 151 | 152 | export default DatasetViewer; 153 | -------------------------------------------------------------------------------- /src/labeler/scss/Labeler.scss: -------------------------------------------------------------------------------- 1 | @import 'shared/scss/base.scss'; 2 | 3 | .Labeler { 4 | display: flex; 5 | flex-direction: column; 6 | 7 | .chooser { 8 | display: flex; 9 | flex-direction: column; 10 | 11 | border-bottom: 3px solid $light-grey; 12 | 13 | .display { 14 | display: flex; 15 | flex-direction: row; 16 | align-items: stretch; 17 | 18 | .hover { 19 | background-color: $extra-light-grey; 20 | } 21 | 22 | .chosen { 23 | background-color: $light-blue; 24 | } 25 | 26 | .visualization { 27 | flex-grow: 1; 28 | flex-basis: 0; 29 | display: flex; 30 | justify-content: center; 31 | cursor: pointer; 32 | overflow: hidden; 33 | 34 | .Visualization { 35 | height: 900px; 36 | max-height: 85vh; 37 | 38 | display: flex; 39 | align-items: center; 40 | flex-direction: row; 41 | 42 | .vega-actions { 43 | display: none; 44 | } 45 | 46 | svg { 47 | max-height: 100%; 48 | max-width: 100%; 49 | } 50 | } 51 | } 52 | 53 | .same { 54 | display: flex; 55 | flex-direction: column; 56 | flex-shrink: 0; 57 | flex-grow: 0; 58 | width: 128px; 59 | 60 | .equals { 61 | flex: 1; 62 | width: 128px; 63 | cursor: pointer; 64 | 65 | display: flex; 66 | align-items: center; 67 | justify-content: center; 68 | 69 | .indicator { 70 | font-size: 64px; 71 | color: $med-grey; 72 | pointer-events: none; 73 | } 74 | } 75 | 76 | .terrible { 77 | cursor: pointer; 78 | display: flex; 79 | align-items: center; 80 | justify-content: center; 81 | width: 128px; 82 | height: 128px; 83 | font-weight: bold; 84 | color: $med-grey; 85 | } 86 | } 87 | } 88 | 89 | .display.block { 90 | .visualization { 91 | pointer-events: none; 92 | } 93 | 94 | .equals { 95 | pointer-events: none; 96 | display: flex; 97 | } 98 | } 99 | } 100 | 101 | .specs { 102 | margin: 30px 0; 103 | 104 | display: flex; 105 | flex-direction: row; 106 | justify-content: space-around; 107 | 108 | pre { 109 | margin: 0 20px 0 26px; 110 | white-space: pre-wrap; 111 | 112 | &.diff { 113 | color: #999; 114 | } 115 | 116 | .added { 117 | color: green; 118 | &:before { 119 | content: "+"; 120 | margin-left: -1em; 121 | } 122 | } 123 | .removed { 124 | color: #b90000; 125 | &:before { 126 | content: "-"; 127 | margin-left: -1em; 128 | } 129 | } 130 | } 131 | } 132 | 133 | .table { 134 | display: flex; 135 | align-items: left; 136 | flex-direction: column; 137 | max-width: 1600px; 138 | margin: 0 auto; 139 | 140 | table { 141 | border-collapse: collapse; 142 | font-size: 0.8em; 143 | min-width: 800px; 144 | margin-top: 1em; 145 | 146 | tr { 147 | background-color: #fff; 148 | } 149 | tr:nth-child(2n), thead tr { 150 | background-color: $extra-light-grey; 151 | } 152 | 153 | thead { 154 | border-bottom: 1px solid med-grey; 155 | } 156 | 157 | td, th { 158 | text-align: left; 159 | padding: 8px 10px; 160 | border: 1px solid $light-grey; 161 | } 162 | } 163 | 164 | .remaining { 165 | font-size: 1.1em; 166 | margin: .9em 0; 167 | color: $med-grey; 168 | } 169 | } 170 | 171 | .task { 172 | font-size: 1.2em; 173 | color: $light-grey; 174 | font-weight: bold; 175 | position: absolute; 176 | top: 10px; 177 | left: 16px; 178 | 179 | &.active { 180 | color: $dark-grey; 181 | } 182 | } 183 | 184 | .anonymous { 185 | font-size: 2em; 186 | color: firebrick; 187 | font-weight: bold; 188 | position: absolute; 189 | top: 10px; 190 | width: 100%; 191 | text-align: center; 192 | 193 | span { 194 | font-size: 12px; 195 | color: $dark-grey; 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/labeler/scss/Labeler.css: -------------------------------------------------------------------------------- 1 | /** Borders */ 2 | .Labeler { 3 | display: flex; 4 | flex-direction: column; } 5 | .Labeler .chooser { 6 | display: flex; 7 | flex-direction: column; 8 | border-bottom: 3px solid #e2e2e2; } 9 | .Labeler .chooser .display { 10 | display: flex; 11 | flex-direction: row; 12 | align-items: stretch; } 13 | .Labeler .chooser .display .hover { 14 | background-color: #f3f3f3; } 15 | .Labeler .chooser .display .chosen { 16 | background-color: #eff6ff; } 17 | .Labeler .chooser .display .visualization { 18 | flex-grow: 1; 19 | flex-basis: 0; 20 | display: flex; 21 | justify-content: center; 22 | cursor: pointer; 23 | overflow: hidden; } 24 | .Labeler .chooser .display .visualization .Visualization { 25 | height: 900px; 26 | max-height: 85vh; 27 | display: flex; 28 | align-items: center; 29 | flex-direction: row; } 30 | .Labeler .chooser .display .visualization .Visualization .vega-actions { 31 | display: none; } 32 | .Labeler .chooser .display .visualization .Visualization svg { 33 | max-height: 100%; 34 | max-width: 100%; } 35 | .Labeler .chooser .display .same { 36 | display: flex; 37 | flex-direction: column; 38 | flex-shrink: 0; 39 | flex-grow: 0; 40 | width: 128px; } 41 | .Labeler .chooser .display .same .equals { 42 | flex: 1; 43 | width: 128px; 44 | cursor: pointer; 45 | display: flex; 46 | align-items: center; 47 | justify-content: center; } 48 | .Labeler .chooser .display .same .equals .indicator { 49 | font-size: 64px; 50 | color: #606060; 51 | pointer-events: none; } 52 | .Labeler .chooser .display .same .terrible { 53 | cursor: pointer; 54 | display: flex; 55 | align-items: center; 56 | justify-content: center; 57 | width: 128px; 58 | height: 128px; 59 | font-weight: bold; 60 | color: #606060; } 61 | .Labeler .chooser .display.block .visualization { 62 | pointer-events: none; } 63 | .Labeler .chooser .display.block .equals { 64 | pointer-events: none; 65 | display: flex; } 66 | .Labeler .specs { 67 | margin: 30px 0; 68 | display: flex; 69 | flex-direction: row; 70 | justify-content: space-around; } 71 | .Labeler .specs pre { 72 | margin: 0 20px 0 26px; 73 | white-space: pre-wrap; } 74 | .Labeler .specs pre.diff { 75 | color: #999; } 76 | .Labeler .specs pre .added { 77 | color: green; } 78 | .Labeler .specs pre .added:before { 79 | content: "+"; 80 | margin-left: -1em; } 81 | .Labeler .specs pre .removed { 82 | color: #b90000; } 83 | .Labeler .specs pre .removed:before { 84 | content: "-"; 85 | margin-left: -1em; } 86 | .Labeler .table { 87 | display: flex; 88 | align-items: left; 89 | flex-direction: column; 90 | max-width: 1600px; 91 | margin: 0 auto; } 92 | .Labeler .table table { 93 | border-collapse: collapse; 94 | font-size: 0.8em; 95 | min-width: 800px; 96 | margin-top: 1em; } 97 | .Labeler .table table tr { 98 | background-color: #fff; } 99 | .Labeler .table table tr:nth-child(2n), .Labeler .table table thead tr { 100 | background-color: #f3f3f3; } 101 | .Labeler .table table thead { 102 | border-bottom: 1px solid med-grey; } 103 | .Labeler .table table td, .Labeler .table table th { 104 | text-align: left; 105 | padding: 8px 10px; 106 | border: 1px solid #e2e2e2; } 107 | .Labeler .table .remaining { 108 | font-size: 1.1em; 109 | margin: .9em 0; 110 | color: #606060; } 111 | .Labeler .task { 112 | font-size: 1.2em; 113 | color: #e2e2e2; 114 | font-weight: bold; 115 | position: absolute; 116 | top: 10px; 117 | left: 16px; } 118 | .Labeler .task.active { 119 | color: #383838; } 120 | .Labeler .anonymous { 121 | font-size: 2em; 122 | color: firebrick; 123 | font-weight: bold; 124 | position: absolute; 125 | top: 10px; 126 | width: 100%; 127 | text-align: center; } 128 | .Labeler .anonymous span { 129 | font-size: 12px; 130 | color: #383838; } 131 | -------------------------------------------------------------------------------- /src/index/js/utilities/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /server/labeler.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request, g 2 | from flask_cors import CORS 3 | 4 | import json 5 | import numpy as np 6 | import os 7 | from draco.learn import data_util 8 | 9 | import sqlite3 10 | 11 | app = Flask(__name__) 12 | CORS(app) 13 | 14 | DATABASE = os.path.join(os.path.dirname(__file__), 'label_data.db') 15 | 16 | # not thread safe, not process safe 17 | global_state = { 18 | "db": None, 19 | "lev_scores": None, 20 | "unlabeled": None, 21 | "features": None 22 | } 23 | 24 | 25 | def get_db(): 26 | db = global_state["db"] 27 | if db is None: 28 | print("connect to db") 29 | global_state["db"] = sqlite3.connect(DATABASE) 30 | db = global_state["db"] 31 | return db 32 | 33 | 34 | def get_features(): 35 | features = global_state["features"] 36 | if features is None: 37 | _, features = data_util.get_unlabeled_data() 38 | global_state["features"] = features 39 | return features 40 | 41 | 42 | def get_leverage_score(): 43 | """ get leverage score """ 44 | 45 | lev_scores = global_state["lev_scores"] 46 | 47 | features = get_features() 48 | 49 | if lev_scores is None: 50 | 51 | print("calculating lev scores") 52 | 53 | X = features.negative - features.positive 54 | 55 | u, s, vh = np.linalg.svd(X, full_matrices=False) 56 | 57 | raw_lev_scores = list((np.sum(u*u, 1) * 1000)) 58 | 59 | lev_scores = {} 60 | 61 | for i, key in enumerate(list(X.index)): 62 | lev_scores[key] = raw_lev_scores[i] 63 | 64 | global_state["lev_scores"] = lev_scores 65 | 66 | return lev_scores 67 | 68 | 69 | def get_unlabeled_data(): 70 | """ load unlabeled data into memory """ 71 | 72 | # todo: optimize this process is necessary 73 | 74 | unlabeled_pairs = global_state["unlabeled"] 75 | 76 | if unlabeled_pairs is None: 77 | 78 | print("fetching unlabeled data...") 79 | 80 | db = get_db() 81 | c = db.cursor() 82 | 83 | c.execute('''SELECT pairs.id, pairs.source, pairs.task, pairs.left, pairs.right 84 | FROM pairs 85 | WHERE NOT EXISTS (SELECT id FROM labels WHERE labels.id = pairs.id)''') 86 | 87 | content = c.fetchall() 88 | 89 | result = {} 90 | for row in content: 91 | pair_id = row[0] 92 | data = { 93 | "id": row[0], 94 | "source": row[1], 95 | "task": row[2], 96 | "left": json.loads(row[3]), 97 | "right": json.loads(row[4]) 98 | } 99 | 100 | result[row[0]] = data 101 | 102 | unlabeled_pairs = global_state["unlabeled"] = result 103 | 104 | return unlabeled_pairs 105 | 106 | 107 | @app.route('/backdoor', methods=['GET']) 108 | def backdoor(): 109 | """ something will happen... """ 110 | lev_score = get_leverage_score() 111 | return jsonify(lev_score) 112 | 113 | 114 | @app.teardown_appcontext 115 | def close_connection(exception): 116 | db = getattr(g, '_database', None) 117 | if db is not None: 118 | db.close() 119 | 120 | 121 | @app.route('/fetch_pair', methods=['GET']) 122 | def fetch_pair(): 123 | """ fetch an unlabeled pair from the server """ 124 | 125 | num_pairs = request.args.get('num_pairs', default=1, type=int) 126 | unlabeled_data = get_unlabeled_data() 127 | 128 | mode = np.random.choice([0, 1], p=[0.9, 0.1]) 129 | id_list = list(unlabeled_data.keys()) 130 | 131 | if mode == 0: 132 | # sampling randomly 133 | rand_indices = np.random.choice(id_list, size=num_pairs, replace=False) 134 | elif mode == 1: 135 | # sampling base on leverage scores 136 | lev_scores = get_leverage_score() 137 | probs = np.array([lev_scores[key] for key in id_list]) 138 | probs = probs / np.sum(probs) 139 | rand_indices = np.random.choice(id_list, size=num_pairs, replace=False, p=probs) 140 | 141 | return jsonify([unlabeled_data[i] for i in rand_indices]) 142 | 143 | 144 | @app.route('/upload_label', methods=['POST']) 145 | def upload_label(): 146 | """ upload a label to the server """ 147 | if not request or not 'id' in request.json or not 'label' in request.json: 148 | abort(400) 149 | 150 | # get user / a string 151 | if not 'user' in request.json: 152 | user = 'anonymous' 153 | else: 154 | user = request.json['user'] 155 | 156 | db = get_db() 157 | c = db.cursor() 158 | 159 | pair_id = request.json['id'] 160 | label = request.json['label'] 161 | 162 | stmt = "INSERT INTO labels VALUES (?, ?, ?)" 163 | c.execute(stmt, (pair_id, label, user)) 164 | 165 | db.commit() 166 | 167 | # update the in memory copy 168 | get_unlabeled_data().pop(pair_id, None) 169 | 170 | print(f"[OK] Insert pair {pair_id} with label {label} by user {user}.") 171 | 172 | return 'success' 173 | 174 | 175 | if __name__ == '__main__': 176 | app.run(debug=True, host='0.0.0.0', threaded=False, processes=1) 177 | -------------------------------------------------------------------------------- /src/labeler/js/components/Labeler.js: -------------------------------------------------------------------------------- 1 | import '../../scss/Labeler.css'; 2 | 3 | import { diffJson } from 'diff'; 4 | import * as stringify from 'json-stable-stringify'; 5 | import React, { Component } from 'react'; 6 | import Visualization, {datasets} from '../../../shared/js/components/Visualization'; 7 | import { duplicate, unique } from 'vega-lite/build/src/util'; 8 | 9 | const classnames = require('classnames'); 10 | 11 | const UNK = '?'; 12 | const LEFT = '>'; 13 | const EQUALS = '='; 14 | const RIGHT = '<'; 15 | const TERRIBLE = 'bad'; 16 | 17 | const KEYS = { 18 | 37: LEFT, // left arrow 19 | 39: RIGHT, // right arrow 20 | 38: EQUALS, // up arrow 21 | 40: TERRIBLE // down arrow 22 | }; 23 | 24 | const url = new URL(document.location.href); 25 | const REQUEST_PATH = `${url.protocol}//${url.hostname}:5000/`; 26 | 27 | function cleanUpSpec(spec) { 28 | if (!spec) { 29 | return spec; 30 | } 31 | 32 | spec = duplicate(spec); 33 | delete spec.$schema; 34 | delete spec.data.format; 35 | return spec; 36 | } 37 | 38 | class Labeler extends Component { 39 | constructor(props) { 40 | super(props); 41 | this.state = { 42 | id: null, 43 | left: null, 44 | right: null, 45 | task: null, 46 | chosen: null, 47 | hover: UNK, 48 | requesting: false, 49 | next: [], 50 | user: (new URL(window.location.href)).searchParams.get('user') || 'anonymous' 51 | }; 52 | } 53 | 54 | componentDidMount() { 55 | this.fetchPairIfNecessary(); 56 | document.body.addEventListener('keydown', this.handleKeyDown.bind(this)); 57 | } 58 | 59 | componentWillUnMount() { 60 | document.body.removeEventListener('keyup', this.handleKeyDown.bind(this)); 61 | } 62 | 63 | render() { 64 | let leftViz; 65 | if (this.state.left) { 66 | leftViz = ; 67 | } 68 | 69 | let rightViz; 70 | if (this.state.right) { 71 | rightViz = ; 72 | } 73 | 74 | const displayClasses = classnames({ 75 | 'display': true, 76 | 'block': this.state.chosen !== null, // block events during confirmation 77 | }); 78 | 79 | const leftClasses = classnames({ 80 | 'visualization': true, 81 | 'chosen': this.state.chosen === LEFT, 82 | 'hover': this.state.hover === LEFT && !(this.state.chosen === LEFT) 83 | }); 84 | 85 | const equalsClasses = classnames({ 86 | 'equals': true, 87 | 'chosen': this.state.chosen === EQUALS, 88 | 'hover': this.state.hover === EQUALS && !(this.state.chosen === EQUALS) 89 | }); 90 | 91 | const terribleClasses = classnames({ 92 | 'terrible': true, 93 | 'chosen': this.state.chosen === TERRIBLE, 94 | 'hover': this.state.hover === TERRIBLE && !(this.state.chosen === TERRIBLE) 95 | }); 96 | 97 | const rightClasses = classnames({ 98 | 'visualization': true, 99 | 'chosen': this.state.chosen === RIGHT, 100 | 'hover': this.state.hover === RIGHT && !(this.state.chosen === RIGHT) 101 | }); 102 | 103 | const taskClasses = classnames({ 104 | 'task': true, 105 | 'active': !!this.state.task 106 | }); 107 | 108 | const leftSpec = cleanUpSpec(this.state.left); 109 | const rightSpec = cleanUpSpec(this.state.right); 110 | 111 | let data; 112 | 113 | if (this.state.left) { 114 | const d = this.state.left.data; 115 | if (d.values) { 116 | data = d.values; 117 | } else { 118 | data = datasets[d.url]; 119 | } 120 | } 121 | 122 | let table = ''; 123 | 124 | if (data) { 125 | const l = Object.values(this.state.left.encoding).map(e => e.field).filter(d => d); 126 | const r = Object.values(this.state.right.encoding).map(e => e.field).filter(d => d); 127 | const fields = unique(l.concat(r), f => f); // Object.keys(data[0]); 128 | 129 | const header = fields.map(t => {t}); 130 | const tableBody = data.slice(0, 20).map((r, i) => 131 | {fields.map(f => {r[f]})} 132 | ); 133 | const remaining = data.length - tableBody.length; 134 | 135 | table =
    136 | 137 | 138 | 139 | {header} 140 | 141 | 142 | 143 | {tableBody} 144 | 145 |
    146 | {remaining > 0 ? ...{remaining} more rows : ''} 147 |
    ; 148 | } 149 | 150 | const specDiff = diffJson(leftSpec, rightSpec).map((part, idx) => { 151 | const className = classnames({ 152 | added: part.added, 153 | removed: part.removed 154 | }); 155 | return {part.value}; 156 | }); 157 | 158 | return ( 159 |
    {this.hover(UNK);}}> 160 | {this.state.user === 'anonymous' ?
    Labeling as Anonymous!
    Please add ?user=NAME to the URL!
    : ''} 161 |
    Task: {this.state.task || 'NO TASK'}
    162 |
    163 |
    164 |
    {this.choose(this.state.id, 'left');}} 166 | onMouseEnter={() => {this.hover(LEFT);}}> 167 | {leftViz} 168 |
    169 |
    170 |
    {this.choose(this.state.id, 'same');}} 172 | onMouseEnter={() => {this.hover(EQUALS);}}> 173 |
    174 | {this.state.hover} 175 |
    176 |
    177 |
    {this.choose(this.state.id, 'terrible');}} 179 | onMouseEnter={() => {this.hover(TERRIBLE);}}> 180 | Both are
    181 | really bad 182 |
    183 |
    184 |
    {this.choose(this.state.id, 'right');}} 186 | onMouseEnter={() => {this.hover(RIGHT);}}> 187 | {rightViz} 188 |
    189 |
    190 |
    191 |
    192 |
    {stringify(leftSpec, {space: 2})}
    193 |
    {specDiff}
    194 |
    {stringify(rightSpec, {space: 2})}
    195 |
    196 | { table } 197 |
    198 | ); 199 | } 200 | 201 | hover(label) { 202 | this.setState({ 203 | hover: label 204 | }); 205 | } 206 | 207 | choose(id, label) { 208 | console.info(`Current cache size: ${this.state.next.length}`); 209 | 210 | this.setState({ 211 | chosen: label 212 | }); 213 | 214 | const message = { 215 | id: id, 216 | label: label, 217 | user: this.state.user 218 | }; 219 | 220 | // apply next state 221 | let next_state, next; 222 | if (this.state.next.length) { 223 | next_state = this.state.next[0]; 224 | next = this.state.next.slice(1); 225 | } else { 226 | next_state = {}; 227 | next = []; 228 | } 229 | 230 | this.setState({ 231 | id: null, 232 | left: null, 233 | right: null, 234 | task: null, 235 | chosen: null, 236 | ...next_state, 237 | next 238 | }); 239 | 240 | this.fetchPairIfNecessary(); 241 | 242 | fetch(REQUEST_PATH + 'upload_label', { 243 | body: JSON.stringify(message), 244 | method: 'post', 245 | headers: { 246 | 'Accept': 'application/json, text/plain, */*', 247 | 'Content-Type': 'application/json' 248 | }, 249 | }).then((response) => { 250 | if (response.ok) { 251 | this.fetchPairIfNecessary(); 252 | } else { 253 | alert('failed POST'); 254 | } 255 | }); 256 | } 257 | 258 | fetchPairIfNecessary() { 259 | if (this.state.next.length > 7 || this.state.requesting) { 260 | // still have a cache or are requesting 261 | return; 262 | } 263 | 264 | this.setState({requesting: true}); 265 | fetch(REQUEST_PATH + 'fetch_pair?num_pairs=5', { 266 | method: 'get' 267 | }).then((response) => { 268 | if (response.ok) { 269 | response.json().then((data) => { 270 | if (this.state.id === null) { 271 | if (this.state.next.length) { alert('bad state'); } 272 | 273 | this.setState({ 274 | ...data[0], 275 | requesting: false, 276 | next: data.slice(1) 277 | }); 278 | } else { 279 | this.setState({ 280 | requesting: false, 281 | next: unique(this.state.next.concat(data), stringify) 282 | }); 283 | } 284 | // we may not have fetched anything new 285 | this.fetchPairIfNecessary(); 286 | }); 287 | } else { 288 | console.error('failed GET'); 289 | this.setState({requesting: false}); 290 | } 291 | }).catch( e => { 292 | console.error(e); 293 | this.setState({requesting: false}); 294 | }); 295 | } 296 | 297 | handleKeyDown(event) { 298 | // block events during confirmation 299 | if (this.state.chosen === null) { 300 | const comparison = KEYS[event.keyCode]; 301 | if (comparison) { 302 | if (comparison === this.state.hover) { 303 | this.setState({ 304 | hover: UNK 305 | }); 306 | this.choose(this.state.id, comparison); 307 | } else { 308 | this.hover(comparison); 309 | } 310 | 311 | event.preventDefault(); 312 | } 313 | } 314 | } 315 | } 316 | 317 | export default Labeler; 318 | -------------------------------------------------------------------------------- /public/generated_visualizations/random_data.json: -------------------------------------------------------------------------------- 1 | [{"q1":3.02,"q2":1,"q3":-1,"q4":73,"n1":"blue","n2":"dog","n3":"Seattle","n4":"home","o1":6,"o2":"sad","o3":"XS","o4":"light","t1":"03/04/2018","t2":"10/22/2017","t3":"01/28/2018","t4":"01/02/2018"}, 2 | {"q1":0.98,"q2":3,"q3":-1,"q4":78,"n1":"red","n2":"fish","n3":"San Francisco","n4":"home","o1":8,"o2":"neutral","o3":"XL","o4":"light","t1":"03/05/2018","t2":"04/27/2017","t3":"01/21/2018","t4":"01/04/2018"}, 3 | {"q1":0.23,"q2":6,"q3":-1,"q4":8,"n1":"green","n2":"dog","n3":"San Francisco","n4":"away","o1":6,"o2":"neutral","o3":"S","o4":"medium","t1":"03/03/2018","t2":"12/18/2017","t3":"01/14/2018","t4":"01/02/2018"}, 4 | {"q1":-1.15,"q2":1,"q3":-1,"q4":29,"n1":"green","n2":"dog","n3":"San Francisco","n4":"away","o1":8,"o2":"sad","o3":"M","o4":"light","t1":"03/03/2018","t2":"01/21/2018","t3":"01/08/2018","t4":"01/04/2018"}, 5 | {"q1":1.35,"q2":1,"q3":-1,"q4":36,"n1":"red","n2":"hamster","n3":"Seattle","n4":"away","o1":4,"o2":"neutral","o3":"L","o4":"dark","t1":"03/06/2018","t2":"04/21/2017","t3":"01/16/2018","t4":"01/01/2018"}, 6 | {"q1":-0.17,"q2":2,"q3":-1,"q4":67,"n1":"blue","n2":"hamster","n3":"New York","n4":"home","o1":4,"o2":"happy","o3":"S","o4":"dark","t1":"03/06/2018","t2":"08/20/2017","t3":"01/12/2018","t4":"01/04/2018"}, 7 | {"q1":0.98,"q2":1,"q3":-1,"q4":8,"n1":"red","n2":"cat","n3":"Seattle","n4":"away","o1":4,"o2":"happy","o3":"M","o4":"medium","t1":"03/07/2018","t2":"07/30/2017","t3":"01/23/2018","t4":"12/31/2017"}, 8 | {"q1":-1.57,"q2":5,"q3":-1,"q4":84,"n1":"red","n2":"fish","n3":"New York","n4":"away","o1":4,"o2":"happy","o3":"M","o4":"light","t1":"03/03/2018","t2":"08/24/2017","t3":"01/27/2018","t4":"12/31/2017"}, 9 | {"q1":0.34,"q2":1,"q3":-1,"q4":78,"n1":"green","n2":"dog","n3":"Seattle","n4":"home","o1":4,"o2":"happy","o3":"S","o4":"dark","t1":"03/09/2018","t2":"02/26/2018","t3":"01/21/2018","t4":"01/05/2018"}, 10 | {"q1":1.78,"q2":1,"q3":-1,"q4":71,"n1":"red","n2":"cat","n3":"New York","n4":"away","o1":8,"o2":"neutral","o3":"L","o4":"dark","t1":"03/03/2018","t2":"10/16/2017","t3":"01/02/2018","t4":"01/04/2018"}, 11 | {"q1":-1.27,"q2":3,"q3":-1,"q4":99,"n1":"red","n2":"hamster","n3":"San Francisco","n4":"away","o1":2,"o2":"happy","o3":"L","o4":"medium","t1":"03/07/2018","t2":"04/07/2017","t3":"01/24/2018","t4":"01/05/2018"}, 12 | {"q1":-0.87,"q2":2,"q3":-1,"q4":63,"n1":"green","n2":"dog","n3":"New York","n4":"away","o1":2,"o2":"sad","o3":"XL","o4":"medium","t1":"03/07/2018","t2":"04/12/2017","t3":"01/26/2018","t4":"01/03/2018"}, 13 | {"q1":-0.64,"q2":2,"q3":-1,"q4":45,"n1":"blue","n2":"hamster","n3":"New York","n4":"away","o1":4,"o2":"sad","o3":"XS","o4":"medium","t1":"03/09/2018","t2":"11/23/2017","t3":"01/30/2018","t4":"01/02/2018"}, 14 | {"q1":0.79,"q2":4,"q3":-1,"q4":76,"n1":"green","n2":"hamster","n3":"Seattle","n4":"away","o1":2,"o2":"sad","o3":"M","o4":"light","t1":"03/07/2018","t2":"06/29/2017","t3":"01/14/2018","t4":"01/01/2018"}, 15 | {"q1":0.95,"q2":1,"q3":-1,"q4":75,"n1":"green","n2":"cat","n3":"New York","n4":"away","o1":2,"o2":"neutral","o3":"L","o4":"light","t1":"03/07/2018","t2":"12/05/2017","t3":"01/04/2018","t4":"01/02/2018"}, 16 | {"q1":-0.65,"q2":1,"q3":-1,"q4":87,"n1":"red","n2":"dog","n3":"Seattle","n4":"home","o1":2,"o2":"neutral","o3":"S","o4":"medium","t1":"03/04/2018","t2":"06/15/2017","t3":"01/18/2018","t4":"01/04/2018"}, 17 | {"q1":0.9,"q2":2,"q3":-1,"q4":19,"n1":"green","n2":"hamster","n3":"San Francisco","n4":"home","o1":8,"o2":"sad","o3":"XL","o4":"light","t1":"03/06/2018","t2":"02/22/2018","t3":"01/15/2018","t4":"01/01/2018"}, 18 | {"q1":-0.51,"q2":3,"q3":-1,"q4":96,"n1":"green","n2":"cat","n3":"San Francisco","n4":"away","o1":6,"o2":"happy","o3":"M","o4":"light","t1":"03/09/2018","t2":"05/12/2017","t3":"01/06/2018","t4":"01/04/2018"}, 19 | {"q1":0.52,"q2":2,"q3":-1,"q4":59,"n1":"blue","n2":"hamster","n3":"Seattle","n4":"away","o1":2,"o2":"happy","o3":"L","o4":"light","t1":"03/04/2018","t2":"03/28/2017","t3":"01/08/2018","t4":"01/04/2018"}, 20 | {"q1":0.51,"q2":1,"q3":-1,"q4":85,"n1":"blue","n2":"fish","n3":"New York","n4":"home","o1":2,"o2":"neutral","o3":"XS","o4":"light","t1":"03/05/2018","t2":"05/16/2017","t3":"01/11/2018","t4":"01/05/2018"}, 21 | {"q1":-0.2,"q2":1,"q3":-1,"q4":7,"n1":"blue","n2":"hamster","n3":"San Francisco","n4":"home","o1":4,"o2":"neutral","o3":"XL","o4":"light","t1":"03/09/2018","t2":"12/14/2017","t3":"01/25/2018","t4":"01/05/2018"}, 22 | {"q1":-1.4,"q2":1,"q3":-1,"q4":73,"n1":"green","n2":"fish","n3":"New York","n4":"home","o1":2,"o2":"sad","o3":"XS","o4":"light","t1":"03/06/2018","t2":"11/16/2017","t3":"01/08/2018","t4":"01/02/2018"}, 23 | {"q1":1.18,"q2":3,"q3":-1,"q4":98,"n1":"red","n2":"dog","n3":"New York","n4":"home","o1":6,"o2":"sad","o3":"M","o4":"light","t1":"03/06/2018","t2":"09/08/2017","t3":"01/17/2018","t4":"01/01/2018"}, 24 | {"q1":-0.64,"q2":1,"q3":-1,"q4":92,"n1":"green","n2":"cat","n3":"Seattle","n4":"home","o1":8,"o2":"sad","o3":"XS","o4":"medium","t1":"03/05/2018","t2":"10/26/2017","t3":"01/24/2018","t4":"01/05/2018"}, 25 | {"q1":-1.27,"q2":3,"q3":-1,"q4":3,"n1":"blue","n2":"fish","n3":"San Francisco","n4":"away","o1":4,"o2":"neutral","o3":"L","o4":"medium","t1":"03/06/2018","t2":"11/20/2017","t3":"01/28/2018","t4":"01/05/2018"}, 26 | {"q1":-0.49,"q2":2,"q3":-1,"q4":76,"n1":"green","n2":"dog","n3":"Seattle","n4":"home","o1":8,"o2":"happy","o3":"L","o4":"light","t1":"03/05/2018","t2":"09/30/2017","t3":"01/19/2018","t4":"01/03/2018"}, 27 | {"q1":-0.87,"q2":4,"q3":-1,"q4":29,"n1":"blue","n2":"hamster","n3":"New York","n4":"home","o1":8,"o2":"sad","o3":"XS","o4":"dark","t1":"03/07/2018","t2":"02/04/2018","t3":"01/09/2018","t4":"12/31/2017"}, 28 | {"q1":-1.28,"q2":1,"q3":-1,"q4":58,"n1":"red","n2":"fish","n3":"San Francisco","n4":"home","o1":2,"o2":"sad","o3":"L","o4":"light","t1":"03/03/2018","t2":"04/22/2017","t3":"01/12/2018","t4":"01/02/2018"}, 29 | {"q1":0.98,"q2":2,"q3":-1,"q4":66,"n1":"green","n2":"cat","n3":"San Francisco","n4":"home","o1":6,"o2":"sad","o3":"M","o4":"dark","t1":"03/04/2018","t2":"02/03/2018","t3":"01/07/2018","t4":"01/02/2018"}, 30 | {"q1":-0.96,"q2":1,"q3":-1,"q4":28,"n1":"green","n2":"dog","n3":"Seattle","n4":"home","o1":6,"o2":"happy","o3":"M","o4":"medium","t1":"03/06/2018","t2":"11/24/2017","t3":"01/22/2018","t4":"01/03/2018"}, 31 | {"q1":-0.5,"q2":1,"q3":-1,"q4":36,"n1":"green","n2":"dog","n3":"Seattle","n4":"home","o1":4,"o2":"neutral","o3":"XL","o4":"medium","t1":"03/07/2018","t2":"06/15/2017","t3":"01/09/2018","t4":"01/01/2018"}, 32 | {"q1":1.2,"q2":1,"q3":-1,"q4":58,"n1":"green","n2":"cat","n3":"San Francisco","n4":"home","o1":8,"o2":"neutral","o3":"S","o4":"dark","t1":"03/04/2018","t2":"03/29/2017","t3":"01/13/2018","t4":"01/02/2018"}, 33 | {"q1":0.2,"q2":2,"q3":-1,"q4":77,"n1":"red","n2":"dog","n3":"New York","n4":"away","o1":2,"o2":"sad","o3":"L","o4":"dark","t1":"03/05/2018","t2":"03/14/2017","t3":"01/18/2018","t4":"01/03/2018"}, 34 | {"q1":0.25,"q2":2,"q3":-1,"q4":11,"n1":"green","n2":"dog","n3":"San Francisco","n4":"home","o1":2,"o2":"sad","o3":"XL","o4":"medium","t1":"03/07/2018","t2":"01/20/2018","t3":"01/23/2018","t4":"01/02/2018"}, 35 | {"q1":1.09,"q2":1,"q3":-1,"q4":56,"n1":"blue","n2":"dog","n3":"San Francisco","n4":"home","o1":6,"o2":"neutral","o3":"S","o4":"dark","t1":"03/04/2018","t2":"12/30/2017","t3":"01/19/2018","t4":"01/02/2018"}, 36 | {"q1":0.44,"q2":2,"q3":-1,"q4":64,"n1":"green","n2":"dog","n3":"Seattle","n4":"away","o1":4,"o2":"neutral","o3":"M","o4":"dark","t1":"03/05/2018","t2":"12/10/2017","t3":"01/17/2018","t4":"01/01/2018"}, 37 | {"q1":0.02,"q2":3,"q3":-1,"q4":96,"n1":"red","n2":"fish","n3":"New York","n4":"away","o1":4,"o2":"sad","o3":"M","o4":"light","t1":"03/07/2018","t2":"03/07/2017","t3":"01/27/2018","t4":"01/05/2018"}, 38 | {"q1":-0.24,"q2":4,"q3":-1,"q4":75,"n1":"blue","n2":"dog","n3":"San Francisco","n4":"away","o1":2,"o2":"sad","o3":"M","o4":"dark","t1":"03/06/2018","t2":"02/24/2018","t3":"01/01/2018","t4":"01/04/2018"}, 39 | {"q1":0.4,"q2":6,"q3":-1,"q4":78,"n1":"red","n2":"fish","n3":"New York","n4":"home","o1":6,"o2":"sad","o3":"XS","o4":"medium","t1":"03/09/2018","t2":"09/27/2017","t3":"01/25/2018","t4":"01/03/2018"}, 40 | {"q1":-1.14,"q2":4,"q3":-1,"q4":75,"n1":"blue","n2":"dog","n3":"San Francisco","n4":"home","o1":6,"o2":"happy","o3":"M","o4":"medium","t1":"03/09/2018","t2":"06/18/2017","t3":"01/23/2018","t4":"01/02/2018"}, 41 | {"q1":0.07,"q2":1,"q3":-1,"q4":76,"n1":"red","n2":"hamster","n3":"New York","n4":"away","o1":2,"o2":"neutral","o3":"XS","o4":"light","t1":"03/08/2018","t2":"09/15/2017","t3":"01/12/2018","t4":"01/04/2018"}, 42 | {"q1":-1.06,"q2":3,"q3":-1,"q4":87,"n1":"red","n2":"hamster","n3":"New York","n4":"home","o1":2,"o2":"neutral","o3":"S","o4":"light","t1":"03/03/2018","t2":"07/04/2017","t3":"01/26/2018","t4":"01/01/2018"}, 43 | {"q1":-0.37,"q2":1,"q3":-1,"q4":99,"n1":"red","n2":"dog","n3":"Seattle","n4":"away","o1":6,"o2":"happy","o3":"XL","o4":"medium","t1":"03/05/2018","t2":"10/17/2017","t3":"01/30/2018","t4":"01/01/2018"}, 44 | {"q1":0.06,"q2":2,"q3":-1,"q4":99,"n1":"red","n2":"cat","n3":"San Francisco","n4":"home","o1":4,"o2":"happy","o3":"L","o4":"medium","t1":"03/07/2018","t2":"11/07/2017","t3":"01/16/2018","t4":"01/04/2018"}, 45 | {"q1":-0.59,"q2":1,"q3":-1,"q4":15,"n1":"green","n2":"dog","n3":"New York","n4":"home","o1":6,"o2":"happy","o3":"S","o4":"medium","t1":"03/04/2018","t2":"07/08/2017","t3":"01/15/2018","t4":"01/01/2018"}, 46 | {"q1":-0.77,"q2":1,"q3":-1,"q4":5,"n1":"blue","n2":"fish","n3":"Seattle","n4":"away","o1":8,"o2":"sad","o3":"S","o4":"medium","t1":"03/04/2018","t2":"05/28/2017","t3":"01/21/2018","t4":"01/03/2018"}, 47 | {"q1":-1.19,"q2":4,"q3":-1,"q4":56,"n1":"blue","n2":"fish","n3":"Seattle","n4":"home","o1":8,"o2":"sad","o3":"S","o4":"medium","t1":"03/04/2018","t2":"07/26/2017","t3":"01/05/2018","t4":"01/05/2018"}, 48 | {"q1":-0.38,"q2":1,"q3":-1,"q4":74,"n1":"green","n2":"dog","n3":"Seattle","n4":"away","o1":4,"o2":"neutral","o3":"M","o4":"dark","t1":"03/06/2018","t2":"08/07/2017","t3":"01/03/2018","t4":"01/03/2018"}, 49 | {"q1":1.33,"q2":1,"q3":-1,"q4":67,"n1":"green","n2":"dog","n3":"San Francisco","n4":"home","o1":6,"o2":"happy","o3":"S","o4":"light","t1":"03/07/2018","t2":"10/07/2017","t3":"01/12/2018","t4":"01/01/2018"}, 50 | {"q1":-0.2,"q2":2,"q3":-1,"q4":21,"n1":"green","n2":"cat","n3":"New York","n4":"home","o1":8,"o2":"sad","o3":"M","o4":"dark","t1":"03/03/2018","t2":"02/08/2018","t3":"01/24/2018","t4":"01/04/2018"}, 51 | {"q1":0.45,"q2":2,"q3":-1,"q4":60,"n1":"red","n2":"hamster","n3":"San Francisco","n4":"home","o1":4,"o2":"sad","o3":"M","o4":"medium","t1":"03/03/2018","t2":"08/11/2017","t3":"01/12/2018","t4":"12/31/2017"}, 52 | {"q1":1.93,"q2":6,"q3":-1,"q4":6,"n1":"blue","n2":"hamster","n3":"Seattle","n4":"away","o1":4,"o2":"happy","o3":"XS","o4":"dark","t1":"03/06/2018","t2":"05/30/2017","t3":"01/01/2018","t4":"01/03/2018"}, 53 | {"q1":0.64,"q2":1,"q3":-1,"q4":74,"n1":"green","n2":"dog","n3":"Seattle","n4":"home","o1":4,"o2":"sad","o3":"XL","o4":"dark","t1":"03/07/2018","t2":"02/08/2018","t3":"01/09/2018","t4":"01/05/2018"}, 54 | {"q1":-0.99,"q2":3,"q3":-1,"q4":8,"n1":"green","n2":"cat","n3":"New York","n4":"away","o1":8,"o2":"happy","o3":"M","o4":"medium","t1":"03/06/2018","t2":"04/21/2017","t3":"01/27/2018","t4":"01/01/2018"}, 55 | {"q1":1.76,"q2":2,"q3":-1,"q4":59,"n1":"green","n2":"fish","n3":"Seattle","n4":"away","o1":6,"o2":"neutral","o3":"XL","o4":"medium","t1":"03/04/2018","t2":"10/19/2017","t3":"01/07/2018","t4":"01/02/2018"}, 56 | {"q1":0.55,"q2":1,"q3":-1,"q4":21,"n1":"green","n2":"hamster","n3":"New York","n4":"home","o1":4,"o2":"neutral","o3":"S","o4":"light","t1":"03/03/2018","t2":"01/25/2018","t3":"01/18/2018","t4":"01/03/2018"}, 57 | {"q1":0.09,"q2":1,"q3":-1,"q4":95,"n1":"red","n2":"hamster","n3":"New York","n4":"home","o1":2,"o2":"sad","o3":"XL","o4":"light","t1":"03/04/2018","t2":"08/20/2017","t3":"01/10/2018","t4":"01/02/2018"}, 58 | {"q1":0.45,"q2":3,"q3":-1,"q4":75,"n1":"blue","n2":"cat","n3":"New York","n4":"away","o1":8,"o2":"neutral","o3":"XL","o4":"dark","t1":"03/04/2018","t2":"03/20/2017","t3":"01/30/2018","t4":"01/02/2018"}, 59 | {"q1":1.37,"q2":1,"q3":-1,"q4":63,"n1":"blue","n2":"dog","n3":"Seattle","n4":"home","o1":2,"o2":"happy","o3":"S","o4":"light","t1":"03/07/2018","t2":"05/22/2017","t3":"01/10/2018","t4":"01/02/2018"}, 60 | {"q1":0.07,"q2":2,"q3":-1,"q4":72,"n1":"blue","n2":"dog","n3":"Seattle","n4":"home","o1":4,"o2":"neutral","o3":"L","o4":"dark","t1":"03/07/2018","t2":"02/16/2018","t3":"01/08/2018","t4":"01/02/2018"}, 61 | {"q1":0.59,"q2":1,"q3":-1,"q4":25,"n1":"blue","n2":"fish","n3":"Seattle","n4":"away","o1":2,"o2":"sad","o3":"S","o4":"dark","t1":"03/07/2018","t2":"09/20/2017","t3":"01/25/2018","t4":"01/04/2018"}, 62 | {"q1":2.11,"q2":2,"q3":-1,"q4":6,"n1":"green","n2":"cat","n3":"San Francisco","n4":"away","o1":6,"o2":"sad","o3":"M","o4":"medium","t1":"03/06/2018","t2":"10/11/2017","t3":"01/08/2018","t4":"01/02/2018"}, 63 | {"q1":0.21,"q2":3,"q3":-1,"q4":75,"n1":"red","n2":"fish","n3":"New York","n4":"away","o1":8,"o2":"sad","o3":"XS","o4":"dark","t1":"03/06/2018","t2":"03/05/2017","t3":"01/25/2018","t4":"01/01/2018"}, 64 | {"q1":-1.27,"q2":1,"q3":-1,"q4":3,"n1":"blue","n2":"dog","n3":"New York","n4":"home","o1":8,"o2":"sad","o3":"M","o4":"medium","t1":"03/09/2018","t2":"04/11/2017","t3":"01/19/2018","t4":"01/01/2018"}, 65 | {"q1":-1.3,"q2":3,"q3":-1,"q4":30,"n1":"blue","n2":"cat","n3":"Seattle","n4":"home","o1":2,"o2":"sad","o3":"XL","o4":"light","t1":"03/08/2018","t2":"05/17/2017","t3":"01/19/2018","t4":"01/04/2018"}, 66 | {"q1":0.09,"q2":1,"q3":-1,"q4":29,"n1":"blue","n2":"fish","n3":"New York","n4":"away","o1":4,"o2":"neutral","o3":"XL","o4":"light","t1":"03/06/2018","t2":"11/09/2017","t3":"01/27/2018","t4":"01/04/2018"}, 67 | {"q1":0.35,"q2":2,"q3":-1,"q4":95,"n1":"blue","n2":"hamster","n3":"San Francisco","n4":"home","o1":4,"o2":"neutral","o3":"XL","o4":"medium","t1":"03/09/2018","t2":"11/01/2017","t3":"01/15/2018","t4":"01/05/2018"}, 68 | {"q1":0.65,"q2":1,"q3":-1,"q4":4,"n1":"blue","n2":"dog","n3":"San Francisco","n4":"away","o1":8,"o2":"neutral","o3":"XS","o4":"medium","t1":"03/06/2018","t2":"10/21/2017","t3":"01/20/2018","t4":"12/31/2017"}, 69 | {"q1":-0.84,"q2":2,"q3":-1,"q4":23,"n1":"green","n2":"hamster","n3":"New York","n4":"away","o1":8,"o2":"neutral","o3":"S","o4":"medium","t1":"03/03/2018","t2":"05/10/2017","t3":"01/11/2018","t4":"12/31/2017"}, 70 | {"q1":-1.26,"q2":4,"q3":-1,"q4":92,"n1":"blue","n2":"dog","n3":"Seattle","n4":"away","o1":2,"o2":"happy","o3":"M","o4":"light","t1":"03/08/2018","t2":"08/12/2017","t3":"01/02/2018","t4":"01/02/2018"}, 71 | {"q1":0.17,"q2":1,"q3":-1,"q4":92,"n1":"blue","n2":"dog","n3":"San Francisco","n4":"home","o1":4,"o2":"sad","o3":"XL","o4":"dark","t1":"03/09/2018","t2":"12/23/2017","t3":"01/21/2018","t4":"01/02/2018"}, 72 | {"q1":0.75,"q2":1,"q3":-1,"q4":44,"n1":"red","n2":"hamster","n3":"San Francisco","n4":"home","o1":2,"o2":"happy","o3":"L","o4":"dark","t1":"03/03/2018","t2":"12/16/2017","t3":"01/17/2018","t4":"01/04/2018"}, 73 | {"q1":2.08,"q2":1,"q3":-1,"q4":33,"n1":"green","n2":"hamster","n3":"Seattle","n4":"away","o1":4,"o2":"neutral","o3":"L","o4":"light","t1":"03/03/2018","t2":"01/07/2018","t3":"01/19/2018","t4":"01/04/2018"}, 74 | {"q1":0.62,"q2":1,"q3":-1,"q4":88,"n1":"red","n2":"dog","n3":"New York","n4":"home","o1":2,"o2":"neutral","o3":"S","o4":"light","t1":"03/08/2018","t2":"11/24/2017","t3":"01/29/2018","t4":"12/31/2017"}, 75 | {"q1":-1.54,"q2":1,"q3":-1,"q4":19,"n1":"blue","n2":"cat","n3":"Seattle","n4":"home","o1":2,"o2":"neutral","o3":"XL","o4":"medium","t1":"03/04/2018","t2":"04/13/2017","t3":"01/30/2018","t4":"01/02/2018"}, 76 | {"q1":-0.4,"q2":2,"q3":-1,"q4":46,"n1":"green","n2":"hamster","n3":"San Francisco","n4":"away","o1":4,"o2":"sad","o3":"XS","o4":"medium","t1":"03/07/2018","t2":"09/23/2017","t3":"01/03/2018","t4":"01/02/2018"}, 77 | {"q1":0.21,"q2":6,"q3":-1,"q4":75,"n1":"red","n2":"hamster","n3":"Seattle","n4":"away","o1":2,"o2":"neutral","o3":"XL","o4":"light","t1":"03/03/2018","t2":"03/17/2017","t3":"01/05/2018","t4":"01/04/2018"}, 78 | {"q1":0.04,"q2":1,"q3":-1,"q4":53,"n1":"green","n2":"cat","n3":"New York","n4":"away","o1":6,"o2":"happy","o3":"S","o4":"light","t1":"03/09/2018","t2":"04/29/2017","t3":"01/03/2018","t4":"01/01/2018"}, 79 | {"q1":-0.36,"q2":4,"q3":-1,"q4":72,"n1":"green","n2":"fish","n3":"San Francisco","n4":"away","o1":2,"o2":"sad","o3":"S","o4":"light","t1":"03/04/2018","t2":"01/09/2018","t3":"01/21/2018","t4":"01/05/2018"}, 80 | {"q1":-1.6,"q2":1,"q3":-1,"q4":14,"n1":"blue","n2":"hamster","n3":"New York","n4":"away","o1":8,"o2":"happy","o3":"L","o4":"light","t1":"03/03/2018","t2":"08/27/2017","t3":"01/09/2018","t4":"01/04/2018"}, 81 | {"q1":-0.67,"q2":1,"q3":-1,"q4":13,"n1":"red","n2":"dog","n3":"New York","n4":"home","o1":6,"o2":"neutral","o3":"L","o4":"dark","t1":"03/03/2018","t2":"09/10/2017","t3":"01/05/2018","t4":"01/05/2018"}, 82 | {"q1":-0.51,"q2":2,"q3":-1,"q4":11,"n1":"green","n2":"fish","n3":"San Francisco","n4":"away","o1":2,"o2":"neutral","o3":"XS","o4":"medium","t1":"03/07/2018","t2":"02/08/2018","t3":"01/09/2018","t4":"01/02/2018"}, 83 | {"q1":0.36,"q2":2,"q3":-1,"q4":85,"n1":"red","n2":"dog","n3":"Seattle","n4":"away","o1":6,"o2":"happy","o3":"S","o4":"light","t1":"03/07/2018","t2":"05/18/2017","t3":"01/06/2018","t4":"01/05/2018"}, 84 | {"q1":-0.22,"q2":6,"q3":-1,"q4":16,"n1":"blue","n2":"fish","n3":"New York","n4":"away","o1":4,"o2":"neutral","o3":"XL","o4":"light","t1":"03/08/2018","t2":"10/28/2017","t3":"01/16/2018","t4":"01/05/2018"}, 85 | {"q1":0.18,"q2":2,"q3":-1,"q4":34,"n1":"blue","n2":"dog","n3":"San Francisco","n4":"away","o1":4,"o2":"happy","o3":"M","o4":"medium","t1":"03/05/2018","t2":"07/09/2017","t3":"01/23/2018","t4":"01/03/2018"}, 86 | {"q1":-1.71,"q2":2,"q3":-1,"q4":95,"n1":"green","n2":"hamster","n3":"Seattle","n4":"home","o1":6,"o2":"neutral","o3":"S","o4":"medium","t1":"03/09/2018","t2":"07/17/2017","t3":"01/19/2018","t4":"12/31/2017"}, 87 | {"q1":1.48,"q2":2,"q3":-1,"q4":78,"n1":"green","n2":"fish","n3":"San Francisco","n4":"home","o1":8,"o2":"sad","o3":"L","o4":"dark","t1":"03/08/2018","t2":"01/26/2018","t3":"01/07/2018","t4":"01/05/2018"}, 88 | {"q1":-0.53,"q2":1,"q3":-1,"q4":83,"n1":"blue","n2":"cat","n3":"New York","n4":"home","o1":8,"o2":"happy","o3":"S","o4":"dark","t1":"03/04/2018","t2":"08/29/2017","t3":"01/08/2018","t4":"01/04/2018"}, 89 | {"q1":-0.1,"q2":2,"q3":-1,"q4":61,"n1":"green","n2":"dog","n3":"New York","n4":"home","o1":2,"o2":"sad","o3":"XL","o4":"medium","t1":"03/03/2018","t2":"12/24/2017","t3":"01/21/2018","t4":"01/02/2018"}, 90 | {"q1":-0.58,"q2":3,"q3":-1,"q4":33,"n1":"blue","n2":"cat","n3":"New York","n4":"away","o1":6,"o2":"happy","o3":"L","o4":"light","t1":"03/09/2018","t2":"08/01/2017","t3":"01/25/2018","t4":"01/02/2018"}, 91 | {"q1":1.24,"q2":2,"q3":-1,"q4":38,"n1":"blue","n2":"cat","n3":"San Francisco","n4":"home","o1":4,"o2":"neutral","o3":"M","o4":"dark","t1":"03/05/2018","t2":"07/02/2017","t3":"01/03/2018","t4":"01/03/2018"}, 92 | {"q1":0.38,"q2":2,"q3":-1,"q4":36,"n1":"red","n2":"dog","n3":"Seattle","n4":"home","o1":6,"o2":"sad","o3":"XS","o4":"light","t1":"03/03/2018","t2":"12/16/2017","t3":"01/04/2018","t4":"01/02/2018"}, 93 | {"q1":1.08,"q2":1,"q3":-1,"q4":16,"n1":"green","n2":"cat","n3":"San Francisco","n4":"home","o1":2,"o2":"happy","o3":"M","o4":"light","t1":"03/09/2018","t2":"11/28/2017","t3":"01/24/2018","t4":"01/05/2018"}, 94 | {"q1":-0.8,"q2":4,"q3":-1,"q4":98,"n1":"green","n2":"cat","n3":"San Francisco","n4":"home","o1":4,"o2":"happy","o3":"XL","o4":"medium","t1":"03/08/2018","t2":"03/04/2017","t3":"01/21/2018","t4":"01/02/2018"}, 95 | {"q1":-0.55,"q2":1,"q3":-1,"q4":91,"n1":"red","n2":"fish","n3":"Seattle","n4":"home","o1":6,"o2":"sad","o3":"L","o4":"medium","t1":"03/06/2018","t2":"05/04/2017","t3":"01/21/2018","t4":"01/02/2018"}, 96 | {"q1":0.12,"q2":4,"q3":-1,"q4":59,"n1":"blue","n2":"cat","n3":"Seattle","n4":"home","o1":4,"o2":"neutral","o3":"XL","o4":"dark","t1":"03/09/2018","t2":"11/02/2017","t3":"01/16/2018","t4":"01/01/2018"}, 97 | {"q1":2.16,"q2":1,"q3":-1,"q4":23,"n1":"blue","n2":"fish","n3":"San Francisco","n4":"home","o1":4,"o2":"happy","o3":"L","o4":"medium","t1":"03/09/2018","t2":"08/24/2017","t3":"01/12/2018","t4":"01/01/2018"}, 98 | {"q1":-0.85,"q2":1,"q3":-1,"q4":1,"n1":"blue","n2":"cat","n3":"San Francisco","n4":"home","o1":4,"o2":"happy","o3":"S","o4":"light","t1":"03/04/2018","t2":"07/03/2017","t3":"01/18/2018","t4":"01/04/2018"}, 99 | {"q1":0.61,"q2":3,"q3":-1,"q4":95,"n1":"green","n2":"dog","n3":"New York","n4":"away","o1":4,"o2":"happy","o3":"S","o4":"dark","t1":"03/03/2018","t2":"07/17/2017","t3":"01/02/2018","t4":"12/31/2017"}, 100 | {"q1":-2.41,"q2":2,"q3":-1,"q4":21,"n1":"blue","n2":"hamster","n3":"New York","n4":"away","o1":6,"o2":"neutral","o3":"XL","o4":"light","t1":"03/03/2018","t2":"08/24/2017","t3":"01/27/2018","t4":"01/03/2018"}] --------------------------------------------------------------------------------