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"}]
--------------------------------------------------------------------------------