├── README ├── public ├── favicon.ico ├── images │ ├── sandbox.png │ └── shot-chart-harden-screen-shot.png ├── apple-touch-icon-precomposed.png ├── css │ ├── styles.scss │ ├── demo.scss │ └── syntax.css └── js │ ├── react-in-jekyll.min.js │ └── react-histogram.min.js ├── Gemfile ├── _layouts ├── page.html ├── demo_default.html ├── demo.haml ├── default.html └── post.html ├── Procfile.dev ├── demos.md ├── src ├── components │ ├── rect.js │ ├── d3_scale.js │ ├── arc.js │ ├── mark.js │ ├── hello.js │ ├── axis.js │ ├── Dropdown.js │ └── basketball │ │ ├── court.js │ │ ├── interactions.js │ │ └── aggregates.js ├── react-in-jekyll.js ├── explorer │ ├── reducers │ │ └── index.js │ ├── stores │ │ └── index.js │ ├── middleware │ │ └── index.js │ ├── components │ │ ├── FieldIcon.js │ │ ├── vis │ │ │ ├── Pane.js │ │ │ ├── marks │ │ │ │ ├── layout.js │ │ │ │ ├── Line.js │ │ │ │ ├── Point.js │ │ │ │ └── Area.js │ │ │ └── Axis.js │ │ ├── FieldDragLayer.js │ │ ├── DataSource.js │ │ ├── TableContainer.js │ │ └── TableLayout.js │ ├── css │ │ ├── components │ │ │ ├── graphic.scss │ │ │ ├── table.scss │ │ │ ├── datasource.scss │ │ │ ├── querybuilder.scss │ │ │ ├── field.scss │ │ │ └── shelf.scss │ │ └── main.scss │ ├── data │ │ ├── local.js │ │ ├── nest.js │ │ ├── scale.js │ │ ├── axis.js │ │ ├── domain.js │ │ └── pane.js │ ├── images │ │ └── color │ │ │ ├── set1.svg │ │ │ ├── set2.svg │ │ │ ├── pastel2.svg │ │ │ ├── pastel1.svg │ │ │ ├── category10.svg │ │ │ ├── set3.svg │ │ │ ├── category20c.svg │ │ │ ├── category20b.svg │ │ │ └── category20.svg │ ├── helpers │ │ ├── color.js │ │ └── table.js │ ├── ducks │ │ ├── chartspec.js │ │ ├── visualspec.js │ │ ├── datasources.js │ │ └── result.js │ └── index.js ├── css │ └── utilities.scss ├── react-histogram.js ├── react-scatterplot.js ├── react-histogram-transition.js ├── nba-shot-chart.js ├── explore-sql.js ├── react-scatterplot-2.js └── nba-shot-chart-vega.js ├── posts.md ├── Procfile ├── .gitignore ├── _demos ├── explorer.md ├── react-histogram.md ├── react-scatterplot-animation.md ├── react-histogram-transition.md ├── react-scatterplot-animation-2.md ├── nba-shot-chart.md └── nba-shot-chart-vega.md ├── _includes ├── built_javascript.html ├── javascript.html ├── comments.html ├── sidebar.html └── head.html ├── _plugins └── environment_variables.rb ├── _sass ├── _layout.scss ├── _message.scss ├── _masthead.scss ├── _posts.scss ├── _utilities.scss ├── _pagination.scss ├── _base.scss ├── _code.scss ├── _type.scss └── _syntax.scss ├── _posts ├── 2015-08-26-setting-up.md └── 2015-08-27-react-in-jekyll.md ├── 404.html ├── about.md ├── atom.xml ├── index.html ├── _config.yml ├── gulpfile.js ├── webpack.config.js ├── package.json ├── webpack.hot.config.js └── Gemfile.lock /README: -------------------------------------------------------------------------------- 1 | Sandbox for John Le 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandbox/sandbox.github.com/master/public/favicon.ico -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | source "https://rubygems.org" 3 | ruby "2.2.3" 4 | 5 | gem 'github-pages' 6 | -------------------------------------------------------------------------------- /public/images/sandbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandbox/sandbox.github.com/master/public/images/sandbox.png -------------------------------------------------------------------------------- /public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandbox/sandbox.github.com/master/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/css/styles.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # comment to ensure Jekyll properly reads file. 3 | --- 4 | 5 | @import "base"; 6 | @import "type"; 7 | @import "layout"; -------------------------------------------------------------------------------- /public/images/shot-chart-harden-screen-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandbox/sandbox.github.com/master/public/images/shot-chart-harden-screen-shot.png -------------------------------------------------------------------------------- /_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 |

{{ page.title }}

7 | {{ content }} 8 |
9 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | webpack_watch: webpack --progress --colors --watch --devtool hidden-source-map 2 | webpack_server: webpack-dev-server --config webpack.hot.config.js 3 | gulp: gulp -------------------------------------------------------------------------------- /demos.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Demos 4 | --- 5 | 6 | {% for post in site.demos %} 7 | * {{ post.title }} 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /src/components/rect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | class Rect extends React.Component { 4 | render() { 5 | return 6 | } 7 | } 8 | 9 | export default Rect 10 | -------------------------------------------------------------------------------- /src/react-in-jekyll.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import HelloWorld from './components/hello' 4 | 5 | ReactDOM.render(, document.getElementById("react-body")); 6 | -------------------------------------------------------------------------------- /_layouts/demo_default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 | {{ content }} 6 | {% include comments.html %} 7 | 8 | 9 | -------------------------------------------------------------------------------- /posts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Posts 4 | --- 5 | 6 | ### Archive 7 | 8 | {% for post in site.posts %} 9 | * {{ post.date | date_to_string }} » [ {{ post.title }} ]({{ post.url }}) 10 | {% endfor %} 11 | -------------------------------------------------------------------------------- /_layouts/demo.haml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: demo_default 3 | --- 4 | 5 |
6 |

{{ page.title }}

7 | Blog 8 | About 9 |
10 | {{ content }} 11 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | webpack_watch: webpack --progress --colors --watch --devtool hidden-source-map 2 | webpack_server: webpack-dev-server --config webpack.hot.config.js 3 | gulp: gulp 4 | jekyll: env JEKYLL_SERVING=true bundle exec jekyll build --watch -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gems 2 | .bundle 3 | .venv 4 | .nodeenv 5 | 6 | _site 7 | _gh_pages 8 | .ruby-version 9 | .sass-cache 10 | node_modules 11 | npm-debug.log 12 | 13 | *.js.map 14 | *.hot.js 15 | 16 | .DS_Store 17 | .emacs.desktop 18 | .emacs.desktop* -------------------------------------------------------------------------------- /_demos/explorer.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: demo 3 | title: Explore | Table Builder 4 | bodyclass: demo explorer 5 | custom_js: 6 | - datalib 7 | --- 8 | 9 |
10 |
11 | 12 | {% include javascript.html js_file="explore-sql" %} 13 | -------------------------------------------------------------------------------- /_includes/built_javascript.html: -------------------------------------------------------------------------------- 1 | {% if site.serving %} 2 | 3 | {% else %} 4 | 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /_includes/javascript.html: -------------------------------------------------------------------------------- 1 | {% if site.serving %} 2 | 3 | {% else %} 4 | 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /_plugins/environment_variables.rb: -------------------------------------------------------------------------------- 1 | module Jekyll 2 | 3 | class EnvironmentVariablesGenerator < Generator 4 | 5 | def generate(site) 6 | site.config['serving'] = ENV['JEKYLL_SERVING'] 7 | # Add other environment variables to `site.config` here... 8 | end 9 | 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include sidebar.html %} 9 | 10 |
11 | {{ content }} 12 | {% include comments.html %} 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /_sass/_layout.scss: -------------------------------------------------------------------------------- 1 | // Layout 2 | // 3 | // Styles for managing the structural hierarchy of the site. 4 | 5 | .container { 6 | max-width: 38rem; 7 | padding-left: 1.5rem; 8 | padding-right: 1.5rem; 9 | margin-left: auto; 10 | margin-right: auto; 11 | } 12 | 13 | footer { 14 | margin-bottom: 2rem; 15 | } 16 | -------------------------------------------------------------------------------- /_sass/_message.scss: -------------------------------------------------------------------------------- 1 | // Messages 2 | // 3 | // Show alert messages to users. You may add it to single elements like a `

`, 4 | // or to a parent if there are multiple elements to show. 5 | 6 | .message { 7 | margin-bottom: 1rem; 8 | padding: 1rem; 9 | color: #717171; 10 | background-color: #f9f9f9; 11 | } 12 | -------------------------------------------------------------------------------- /_posts/2015-08-26-setting-up.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Setting up this blog 4 | --- 5 | 6 | I used [poole](http://getpoole.com) to set up this blog. I'm planning 7 | to use this blog as a sandbox for small experiments. Particularly with 8 | code, data, visualization, statistics, or whatever else I find 9 | interesting. Like the word sandbox. 10 | -------------------------------------------------------------------------------- /src/components/d3_scale.js: -------------------------------------------------------------------------------- 1 | function d3_scaleExtent(domain) { 2 | var start = domain[0], stop = domain[domain.length - 1] 3 | return start < stop ? [start, stop] : [stop, start] 4 | } 5 | 6 | function d3_scaleRange(scale) { 7 | return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range()) 8 | } 9 | 10 | export { d3_scaleExtent, d3_scaleRange } 11 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "404: Page not found" 4 | permalink: 404.html 5 | --- 6 | 7 |

8 |

404: Page not found

9 |

Sorry, we've misplaced that URL or it's pointing to something that doesn't exist. Head back home to try finding it again.

10 |
11 | -------------------------------------------------------------------------------- /about.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: About 4 | --- 5 | 6 |

7 | I'm John Le, a co-founder at Statwing, part of a company summer camp batch in 2012 (YC). 8 | I'm using this blog as a sandbox for small experiments. Particularly with 9 | code, data, visualization, statistics, or whatever else I find 10 | interesting. 11 |

12 | -------------------------------------------------------------------------------- /src/explorer/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import datasources from '../ducks/datasources' 3 | import queryspec from '../ducks/queryspec' 4 | import visualspec from '../ducks/visualspec' 5 | import result from '../ducks/result' 6 | import chartspec from '../ducks/chartspec' 7 | 8 | const rootReducer = combineReducers({datasources, queryspec, visualspec, result, chartspec}) 9 | export default rootReducer 10 | -------------------------------------------------------------------------------- /_sass/_masthead.scss: -------------------------------------------------------------------------------- 1 | // Masthead 2 | // 3 | // Super small header above the content for site name and short description. 4 | 5 | .masthead { 6 | padding-top: 1rem; 7 | padding-bottom: 1rem; 8 | margin-bottom: 3rem; 9 | } 10 | 11 | .masthead-title { 12 | margin-top: 0; 13 | margin-bottom: 0; 14 | color: #505050; 15 | 16 | a { 17 | color: #505050; 18 | } 19 | 20 | small { 21 | font-size: 75%; 22 | font-weight: 400; 23 | color: #c0c0c0; 24 | letter-spacing: 0; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 |

{{ page.title }}

7 | 8 | {{ content }} 9 |
10 | 11 | 26 | -------------------------------------------------------------------------------- /src/explorer/stores/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunkMiddleware from 'redux-thunk' 3 | import rootReducer from '../reducers' 4 | import queryRunner from '../middleware' 5 | 6 | const createWithMiddleware = applyMiddleware(queryRunner, thunkMiddleware)(createStore) 7 | 8 | export default function configureStore(initialState) { 9 | const store = createWithMiddleware(rootReducer, initialState) 10 | 11 | if (module.hot) { 12 | module.hot.accept('../reducers', () => { 13 | const nextReducer = require('../reducers') 14 | store.replaceReducer(nextReducer) 15 | }) 16 | } 17 | 18 | return store 19 | } 20 | -------------------------------------------------------------------------------- /src/components/arc.js: -------------------------------------------------------------------------------- 1 | function polarToCartesian(centerX, centerY, radiusX, radiusY, angleInDegrees) { 2 | var angleInRadians = angleInDegrees * Math.PI / 180.0 3 | 4 | return { 5 | x: centerX + (radiusX * Math.cos(angleInRadians)), 6 | y: centerY + (radiusY * Math.sin(angleInRadians)) 7 | } 8 | } 9 | 10 | function describeArc(x, y, radiusX, radiusY, startAngle, endAngle){ 11 | var start = polarToCartesian(x, y, radiusX, radiusY, endAngle) 12 | var end = polarToCartesian(x, y, radiusX, radiusY, startAngle) 13 | var arcSweep = endAngle - startAngle <= 180 ? "0" : "1" 14 | return [ 15 | "M", start.x, start.y, 16 | "A", radiusX, radiusY, 0, arcSweep, 0, end.x, end.y 17 | ].join(" ") 18 | } 19 | 20 | export { describeArc } 21 | -------------------------------------------------------------------------------- /_includes/comments.html: -------------------------------------------------------------------------------- 1 | {% unless site.serving %} 2 | {% if page.comments %} 3 |
4 | 15 | 16 | {% endif %} 17 | {% endunless %} 18 | -------------------------------------------------------------------------------- /public/css/demo.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # scss file jekkyl 3 | --- 4 | 5 | .demo { 6 | margin: 1em auto 4em auto; 7 | position: relative; 8 | tab-size: 2; 9 | } 10 | 11 | .demo-title { 12 | overflow: auto; 13 | } 14 | 15 | @media (min-width: 48em) { 16 | .demo-title h1 { 17 | float: left; 18 | } 19 | } 20 | 21 | .demo-title a { 22 | margin-top: 1rem; 23 | padding-left: 1rem; 24 | float: right; 25 | } 26 | 27 | @media (max-width: 48em) { 28 | .demo-title { 29 | margin-bottom: 1rem; 30 | } 31 | } 32 | 33 | @media (max-width: 960px) { 34 | .demo { 35 | padding-left: 4rem; 36 | padding-right: 4rem; 37 | } 38 | } 39 | 40 | @media (min-width: 960px) { 41 | .demo { 42 | width: 960px; 43 | padding-left: 2rem; 44 | padding-right: 2rem; 45 | } 46 | } 47 | 48 | .demo h1 { 49 | font-weight: 400; 50 | } 51 | -------------------------------------------------------------------------------- /atom.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: null 3 | --- 4 | 5 | 6 | 7 | 8 | {{ site.title }} 9 | 10 | 11 | {{ site.time | date_to_xmlschema }} 12 | {{ site.url }} 13 | 14 | {{ site.author.name }} 15 | {{ site.author.email }} 16 | 17 | 18 | {% for post in site.posts %} 19 | 20 | {{ post.title }} 21 | 22 | {{ post.date | date_to_xmlschema }} 23 | {{ site.url }}{{ post.id }} 24 | {{ post.content | xml_escape }} 25 | 26 | {% endfor %} 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/explorer/middleware/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import * as queryspec from '../ducks/queryspec' 3 | import * as visualspec from '../ducks/visualspec' 4 | import { getField } from '../ducks/datasources' 5 | import { runCurrentQueryIfNecessary } from '../ducks/result' 6 | 7 | const QUERYSPEC_ACTIONS = _.values(queryspec) 8 | 9 | const queryRunner = ({dispatch, getState}) => next => action => { 10 | console.group(action.type) 11 | let result = next(action) 12 | let isQueryChange = ( 13 | _.contains(QUERYSPEC_ACTIONS, action.type) 14 | || visualspec.SET_TABLE_ENCODING == action.type 15 | || visualspec.SET_PROPERTY_SETTING == action.type 16 | ) 17 | if (isQueryChange) { 18 | result = next(runCurrentQueryIfNecessary()) 19 | } 20 | console.log('next state', getState()) 21 | console.groupEnd(action.type) 22 | return result 23 | } 24 | 25 | export default queryRunner 26 | -------------------------------------------------------------------------------- /_demos/react-histogram.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: demo 3 | title: Histogram rendered with React 4 | bodyclass: demo 5 | custom_css: 6 | - demo 7 | --- 8 | 9 | 30 |
31 | {% include javascript.html js_file="react-histogram" %} 32 |

33 | 34 | A basic histogram rendered with React using scale and data functions 35 | from d3. The purpose of this example is to get a feel for using React to 36 | manipulate the DOM instead of d3. 37 | [Here](http://bl.ocks.org/mbostock/3048450) is the blocks example from 38 | which this was inspired. 39 | -------------------------------------------------------------------------------- /_demos/react-scatterplot-animation.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: demo 3 | title: React Scatterplot Animation 4 | bodyclass: demo 5 | custom_css: 6 | - demo 7 | --- 8 | 9 | 26 |
27 |

28 | 29 | A simple scatterplot with react and d3 using 30 | [React Tween State](https://github.com/chenglou/react-tween-state) for 31 | animation. 32 | 33 | The points should all start from the origin and then transition to the 34 | final positions. 35 | 36 | There are 500 points rendered, at which it starts to feel a little 37 | choppy. At 1,000 points choppiness is very noticeable and at 10,000 38 | points it might as well be static. 39 | 40 | {% include javascript.html js_file="react-scatterplot" %} 41 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | --- 5 | 6 |
7 | {% for post in paginator.posts %} 8 | 19 | {% endfor %} 20 |
21 | 22 | 34 | -------------------------------------------------------------------------------- /_demos/react-histogram-transition.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: demo 3 | title: Histogram animation with React 4 | bodyclass: demo 5 | custom_css: 6 | - demo 7 | --- 8 | 9 | 38 |
39 |

40 | 41 | Using 42 | [React Tween State](https://github.com/chenglou/react-tween-state) to 43 | get a feel for animating elements with react. This animates the 44 | histogram bars height starting from 0 height and animates the width 45 | starting from 0 width. 46 | 47 | {% include javascript.html js_file="react-histogram-transition" %} 48 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # Permalinks 2 | # 3 | # Use of `relative_permalinks` ensures post links from the index work properly. 4 | permalink: pretty 5 | 6 | gems: [jekyll-paginate] 7 | 8 | # Setup 9 | title: sandbox 10 | tagline: sandbox's sandbox 11 | description: sandbox's sandbox 12 | url: https://sandbox.github.io 13 | paginate: 2 14 | disqusid: "moonpiesandbox" 15 | 16 | # Assets 17 | # 18 | # We specify the directory for Jekyll so we can use @imports. 19 | sass: 20 | sass_dir: _sass 21 | style: :compressed 22 | 23 | # About/contact 24 | author: 25 | name: John Le 26 | url: https://twitter.com/moonpiesandbox 27 | email: moonpiesandbox@gmail.com 28 | 29 | github: 30 | url: http://github.com/sandbox 31 | 32 | exclude: [ README, Gemfile, Gemfile.lock, Procfile, gulpfile.js, Gruntfile.js, package.json, webpack.config.js, webpack.hot.config.js, node_modules, src, .git, .*, .**/*] 33 | 34 | collections: 35 | demos: 36 | output: true 37 | permalink: /demos/:path/ -------------------------------------------------------------------------------- /src/components/mark.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Mixin as tweenMixin} from 'react-tween-state' 3 | 4 | function animateMark(Component, transitionAttributes) { 5 | const VisualMark = React.createClass({ 6 | mixins: [tweenMixin], 7 | getInitialState() { 8 | let state = {} 9 | transitionAttributes.forEach( 10 | transition => 11 | state[transition.prop] = transition.start == null ? 0 : transition.start) 12 | return state 13 | }, 14 | componentDidMount() { 15 | transitionAttributes.forEach( 16 | transition => 17 | this.tweenState(transition.prop, { 18 | easing: transition.ease, 19 | duration: transition.duration, 20 | endValue: this.props[transition.prop] 21 | })) 22 | }, 23 | render() { 24 | let props = {} 25 | transitionAttributes.forEach( 26 | transition => props[transition.prop] = this.getTweeningValue(transition.prop)) 27 | return 28 | } 29 | }) 30 | 31 | return VisualMark 32 | } 33 | 34 | export default animateMark 35 | -------------------------------------------------------------------------------- /src/explorer/components/FieldIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import className from 'classnames' 3 | import { getExternalType } from '../helpers/field' 4 | const {div, i: icon, span} = React.DOM 5 | 6 | export class FieldIcon extends React.Component { 7 | render() { 8 | const { type, typecast } = this.props 9 | if (typecast != null && getExternalType(typecast) != getExternalType(type)) { 10 | return div({}, 11 | , 12 | icon({className: "fa fa-long-arrow-right"}), 13 | ) 14 | } 15 | 16 | let iconClass 17 | if (type == 'aggregate') { 18 | iconClass = "bar-chart" 19 | } else if (['date', 'timestamp', 'time'].indexOf(type) >= 0) { 20 | iconClass = "clock-o" 21 | } else if (["string", "text"].indexOf(type) >= 0) { 22 | iconClass = "font" 23 | } else if (['integer', 'number'].indexOf(type) >= 0) { 24 | return span({}, "#") 25 | } else { 26 | return span({}, type) 27 | } 28 | return icon({className: className("fa", `fa-${iconClass}`)}) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /_sass/_posts.scss: -------------------------------------------------------------------------------- 1 | // Posts and pages 2 | // 3 | // Each post is wrapped in `.post` and is used on default and post layouts. Each 4 | // page is wrapped in `.page` and is only used on the page layout. 5 | 6 | .page, 7 | .post { 8 | margin-bottom: 4em; 9 | } 10 | 11 | // Blog post or page title 12 | .page-title, 13 | .post-title, 14 | .post-title a { 15 | color: #303030; 16 | } 17 | .page-title, 18 | .post-title { 19 | margin-top: 0; 20 | } 21 | 22 | // Meta data line below post title 23 | .post-date { 24 | display: block; 25 | margin-top: -.5rem; 26 | margin-bottom: 1rem; 27 | color: #9a9a9a; 28 | } 29 | 30 | 31 | // Related posts 32 | .related { 33 | padding-top: 2rem; 34 | padding-bottom: 2rem; 35 | border-top: 1px solid #eee; 36 | } 37 | 38 | .related-posts { 39 | padding-left: 0; 40 | list-style: none; 41 | 42 | h3 { 43 | margin-top: 0; 44 | } 45 | 46 | li { 47 | small { 48 | font-size: 75%; 49 | color: #999; 50 | } 51 | 52 | a:hover { 53 | color: #268bd2; 54 | text-decoration: none; 55 | 56 | small { 57 | color: inherit; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/explorer/components/vis/Pane.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { getFieldQueryType, getAccessorName } from '../../helpers/field' 3 | 4 | const MARKS = { 5 | 'bar': React.createFactory(require('./marks/Bar')), 6 | 'line': React.createFactory(require('./marks/Line')), 7 | 'area': React.createFactory(require('./marks/Area')), 8 | 'point': React.createFactory(require('./marks/Point')) 9 | } 10 | 11 | export class Pane extends React.Component { 12 | render() { 13 | const { fieldScales, paneData, rowAxis, colAxis } = this.props 14 | const { markData } = paneData 15 | let transformFields = _.filter( 16 | fieldScales, 17 | (fs) => { 18 | return fs.scale && ( 19 | (fs.shelf != 'col' && fs.shelf != 'row') 20 | || ((fs.shelf == 'row' && rowAxis.hasField(fs.field)) 21 | || 22 | (fs.shelf == 'col' && colAxis.hasField(fs.field)))) 23 | }) 24 | 25 | let markComponent = MARKS[this.props.markType] 26 | if (markComponent) { 27 | return markComponent(_.extend({markData, transformFields}, this.props)) 28 | } 29 | else { 30 | return null 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /_posts/2015-08-27-react-in-jekyll.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Using React on this blog 4 | --- 5 | 6 |
7 | {% include javascript.html js_file="react-in-jekyll" %} 8 | 9 | The changes in the `webpack.hot.config.js` are shown in full below: 10 | 11 | {% highlight javascript %} 12 | { 13 | //... webpack config 14 | module: { 15 | loaders: [ 16 | { 17 | test: /\.js$/, 18 | loaders: ['react-hot', 'babel-loader'], 19 | include: path.join(__dirname, 'src') 20 | } 21 | ] 22 | }, 23 | output: { 24 | path: path.join(__dirname, "public", "js"), 25 | filename: '[name].hot.js', 26 | publicPath: 'http://localhost:8080/' 27 | }, 28 | devServer: { 29 | publicPath: 'http://localhost:8080/', 30 | contentBase: "./src", 31 | hot: true, 32 | inline: true, 33 | headers: { 'Access-Control-Allow-Origin': '*' } 34 | }, 35 | plugins: [ 36 | new webpack.HotModuleReplacementPlugin(), 37 | new webpack.NoErrorsPlugin() 38 | ] 39 | } 40 | {% endhighlight %} 41 | 42 | Finally, this last paragraph's text was generated by jekyll and not 43 | react, to demonstrate they can mix seamlessly. 44 | -------------------------------------------------------------------------------- /_sass/_utilities.scss: -------------------------------------------------------------------------------- 1 | @mixin overflow-ellipsis { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | white-space: nowrap; 5 | } 6 | 7 | @mixin keyframes($animation-name) { 8 | @-webkit-keyframes #{$animation-name} { @content; } 9 | @-moz-keyframes #{$animation-name} { @content; } 10 | @-o-keyframes #{$animation-name} { @content; } 11 | @keyframes #{$animation-name} { @content; } 12 | } 13 | 14 | @mixin transition($transition) { 15 | -webkit-transition: $transition; 16 | -moz-transition: $transition; 17 | -o-transition: $transition; 18 | transition: $transition; 19 | } 20 | 21 | @mixin animation($animation) { 22 | -webkit-animation: $animation; 23 | -moz-animation: $animation; 24 | -o-animation: $animation; 25 | animation: $animation; 26 | } 27 | 28 | @mixin transform($transform) { 29 | -webkit-transform: $transform; 30 | -moz-transform: $transform; 31 | -ms-transform: $transform; 32 | -o-transform: $transform; 33 | transform: $transform; 34 | } 35 | 36 | .remove-link { 37 | color: #999; 38 | 39 | &:hover { 40 | color: #f18794; 41 | text-decoration: none; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/css/utilities.scss: -------------------------------------------------------------------------------- 1 | @mixin overflow-ellipsis { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | white-space: nowrap; 5 | } 6 | 7 | @mixin keyframes($animation-name) { 8 | @-webkit-keyframes #{$animation-name} { @content; } 9 | @-moz-keyframes #{$animation-name} { @content; } 10 | @-o-keyframes #{$animation-name} { @content; } 11 | @keyframes #{$animation-name} { @content; } 12 | } 13 | 14 | @mixin transition($transition) { 15 | -webkit-transition: $transition; 16 | -moz-transition: $transition; 17 | -o-transition: $transition; 18 | transition: $transition; 19 | } 20 | 21 | @mixin animation($animation) { 22 | -webkit-animation: $animation; 23 | -moz-animation: $animation; 24 | -o-animation: $animation; 25 | animation: $animation; 26 | } 27 | 28 | @mixin transform($transform) { 29 | -webkit-transform: $transform; 30 | -moz-transform: $transform; 31 | -ms-transform: $transform; 32 | -o-transform: $transform; 33 | transform: $transform; 34 | } 35 | 36 | .remove-link { 37 | color: #999; 38 | 39 | &:hover { 40 | color: #f18794; 41 | text-decoration: none; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /_demos/react-scatterplot-animation-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: demo 3 | title: React Scatterplot Animation II 4 | bodyclass: demo 5 | custom_css: 6 | - demo 7 | --- 8 | 9 | 26 |
27 |

28 | 29 | A simple scatterplot with react and d3 using 30 | [React Tween State](https://github.com/chenglou/react-tween-state) for 31 | animation. 32 | 33 | The points should all start from the origin and then transition to the 34 | final positions. 35 | 36 | This is a second version of [this](/demos/react-scatterplot-animation), to test a if 37 | grouping tween transitions into a single component is more performant 38 | than using tween state on each individual point. It is not and appears 39 | to take too much time updating all state at once to have any animation 40 | at all (no requestAnimationFrame calls get made). 41 | 42 | {% include javascript.html js_file="react-scatterplot-2" %} 43 | -------------------------------------------------------------------------------- /src/explorer/css/components/graphic.scss: -------------------------------------------------------------------------------- 1 | @import '../../../css/utilities.scss'; 2 | .graphic-container { 3 | background-color: white; 4 | border-width: 1px; 5 | border-style: solid; 6 | border-color: #dfdfdf; 7 | border-radius: 1px; 8 | } 9 | 10 | .loading-overlay { 11 | position: absolute; 12 | top: 0; bottom: 0; left: 0; right: 0; 13 | z-index: 2; 14 | 15 | .loading-overlay-background { 16 | position: absolute; 17 | top: 0; bottom: 0; left: 0; right: 0; 18 | background-color: #fff; 19 | opacity: 0.5; 20 | } 21 | 22 | i.fa.fa-spinner.fa-pulse { 23 | position: absolute; 24 | font-size: 4rem; 25 | top: 30%; left: 50%; 26 | margin-left: -2rem; 27 | } 28 | } 29 | 30 | .axis text { 31 | font-size: 10px; 32 | fill: #666; 33 | } 34 | 35 | .axis path, 36 | .axis line { 37 | fill: none; 38 | stroke: #dfdfdf; 39 | shape-rendering: crispEdges; 40 | } 41 | 42 | .axis-label { 43 | position: absolute; 44 | @include overflow-ellipsis; 45 | 46 | &.left { 47 | top: 0; 48 | left: 0; 49 | right: 40px; 50 | padding: 0 4px; 51 | } 52 | 53 | &.bottom { 54 | bottom: 10px; 55 | left: 0; 56 | right: 0; 57 | text-align: center; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserSync = require('browser-sync').create(); 3 | var cp = require('child_process'); 4 | var env = require('gulp-env'); 5 | 6 | gulp.task('jekyll-build', [], function (done) { 7 | var jekyll = cp.spawn('bundle', ['exec', 'jekyll build'], {stdio: 'inherit'}).on('close', done); 8 | }); 9 | 10 | gulp.task('browser-stream', [], function () { 11 | gulp.src(['_site/public/css/*.css', 12 | '!_site/public/css/hyde.css', 13 | '!_site/public/css/poole.css', 14 | '!_site/public/css/styles.css', 15 | '!_site/public/css/syntax.css']).pipe(browserSync.stream()); 16 | }); 17 | 18 | gulp.task('set-env', function () { 19 | env({ 20 | vars: { 21 | JEKYLL_SERVING: true 22 | } 23 | }); 24 | }); 25 | 26 | gulp.task('browser-sync', [], function (gulpCallback) { 27 | browserSync.init({ 28 | server: { 29 | baseDir: '_site' 30 | } 31 | }, function () { 32 | gulp.watch(['_sass/*', '_sass/**', 'public/css/*'], ['jekyll-build']); 33 | gulp.watch(['_site/public/css/*.css'], ['browser-stream']); 34 | gulpCallback(); 35 | }); 36 | }); 37 | 38 | gulp.task('default', ['set-env', 'jekyll-build', 'browser-sync']); 39 | -------------------------------------------------------------------------------- /_includes/sidebar.html: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /_sass/_pagination.scss: -------------------------------------------------------------------------------- 1 | // Pagination 2 | // 3 | // Super lightweight (HTML-wise) blog pagination. `span`s are provide for when 4 | // there are no more previous or next posts to show. 5 | 6 | .pagination { 7 | overflow: hidden; // clearfix 8 | margin: 0 -1.5rem 1rem; 9 | font-family: "PT Sans", Helvetica, Arial, sans-serif; 10 | color: #ccc; 11 | text-align: center; 12 | } 13 | 14 | // Pagination items can be `span`s or `a`s 15 | .pagination-item { 16 | display: block; 17 | padding: 1rem; 18 | border: solid #eee; 19 | border-width: 1px 0; 20 | 21 | &:first-child { 22 | margin-bottom: -1px; 23 | } 24 | } 25 | 26 | // Only provide a hover state for linked pagination items 27 | a.pagination-item:hover { 28 | background-color: #f5f5f5; 29 | } 30 | 31 | @media (min-width: 30em) { 32 | .pagination { 33 | margin: 3rem 0; 34 | } 35 | 36 | .pagination-item { 37 | float: left; 38 | width: 50%; 39 | border-width: 1px; 40 | 41 | &:first-child { 42 | margin-bottom: 0; 43 | border-top-left-radius: 4px; 44 | border-bottom-left-radius: 4px; 45 | } 46 | &:last-child { 47 | margin-left: -1px; 48 | border-top-right-radius: 4px; 49 | border-bottom-right-radius: 4px; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /_demos/nba-shot-chart.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: demo 3 | title: NBA Shot Chart (React) - James Harden 4 | bodyclass: demo 5 | custom_css: 6 | - demo 7 | --- 8 | 9 | 21 | 22 |
23 |

24 | 25 | This was inspired by [@savvas_tj](https://twitter.com/savvas_tj)'s 26 | [post](http://savvastjortjoglou.com/nba-shot-sharts.html#Plotting-the-Shot-Chart-Data) 27 | on creating NBA shot charts in python. This is built with a 28 | combination of d3 and React for fun, using data from 29 | [stats.nba.com](http://stats.nba.com), specifically from this [link](http://stats.nba.com/stats/shotchartdetail?CFID=33&CFPARAMS=2014-15&ContextFilter=&ContextMeasure=FGA&DateFrom=&DateTo=&GameID=&GameSegment=&LastNGames=0&LeagueID=00&Location=&MeasureType=Base&Month=0&OpponentTeamID=0&Outcome=&PaceAdjust=N&PerMode=PerGame&Period=0&PlayerID=201935&PlusMinus=N&Position=&Rank=N&RookieYear=&Season=2014-15&SeasonSegment=&SeasonType=Regular+Season&TeamID=0&VsConference=&VsDivision=&mode=Advanced&showDetails=0&showShots=1&showZones=0). 30 | 31 | {% include javascript.html js_file="nba-shot-chart" %} 32 | -------------------------------------------------------------------------------- /src/explorer/data/local.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import dl from 'datalib' 3 | import { getFieldQueryType, getFieldGroupByName, isGroupByField } from '../helpers/field' 4 | 5 | export function translateTableQuery(queryspec, data) { 6 | if (_.isEmpty(queryspec)) return null 7 | const {groupby, operator, aggregate, value} = _(queryspec).values().flatten().groupBy(getFieldQueryType).value() 8 | 9 | let summarize = translateSummary(operator, aggregate, value) 10 | return { 11 | groupby: _.map(groupby, _.curry(getFieldGroupByName)(data)), 12 | summarize, 13 | where: [], 14 | having: [], 15 | order: [] 16 | } 17 | } 18 | 19 | export function translateSummary(operator, aggregate, value) { 20 | return _.merge( 21 | _(aggregate).groupBy('name').mapValues(fields => _.map(fields, 'func')).value(), 22 | operator ? { '*': _.map(operator, 'op') } : {}, 23 | { '*': ['values'] }, 24 | (a, b) => { if (_.isArray(a)) { return a.concat(b) } }) 25 | } 26 | export function performQuery(query, data) { 27 | if (query == null) return null 28 | return dl.groupby(query.groupby).summarize(query.summarize).execute(data) 29 | } 30 | 31 | export function requestQuery(queryspec, datasource) { 32 | let query = translateTableQuery(queryspec, datasource) 33 | let result = performQuery(query, datasource) 34 | return { query, queryspec, result } 35 | } 36 | -------------------------------------------------------------------------------- /src/react-histogram.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import d3 from 'd3' 4 | import Axis from './components/axis' 5 | 6 | var element = document.getElementById("react-histogram") 7 | var margin = {top: 10, right: 30, bottom: 30, left: 30} 8 | var width = element.offsetWidth - margin.left - margin.right 9 | var height = 550 - margin.top - margin.bottom 10 | var values = d3.range(1000).map(d3.random.bates(10)) 11 | var formatCount = d3.format(",.0f") 12 | var xscale = d3.scale.linear().domain([0, 1]).range([0, width]) 13 | var data = d3.layout.histogram().bins(xscale.ticks(20))(values) 14 | var yscale = d3.scale.linear().domain([0, d3.max(data, (d) => d.y)]).range([height, 0]) 15 | 16 | var bars = data.map( 17 | (d, i) => 18 | 19 | 20 | {formatCount(d.y)} 21 | ) 22 | 23 | ReactDOM.render( 24 | 25 | 26 | {bars} 27 | 28 | 29 | 30 | , element) 31 | -------------------------------------------------------------------------------- /src/explorer/images/color/set1.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/explorer/images/color/set2.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/explorer/images/color/pastel2.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /_sass/_base.scss: -------------------------------------------------------------------------------- 1 | // Body resets 2 | // 3 | // Update the foundational and global aspects of the page. 4 | 5 | * { 6 | -webkit-box-sizing: border-box; 7 | -moz-box-sizing: border-box; 8 | box-sizing: border-box; 9 | } 10 | 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | html { 18 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | font-size: 16px; 20 | line-height: 1.5; 21 | 22 | @media (min-width: 38em) { 23 | font-size: 18px; 24 | } 25 | } 26 | 27 | body { 28 | color: #515151; 29 | background-color: #fff; 30 | -webkit-text-size-adjust: 100%; 31 | -ms-text-size-adjust: 100%; 32 | } 33 | 34 | // No `:visited` state is required by default (browsers will use `a`) 35 | a { 36 | color: #268bd2; 37 | text-decoration: none; 38 | 39 | // `:focus` is linked to `:hover` for basic accessibility 40 | &:hover, 41 | &:focus { 42 | text-decoration: underline; 43 | } 44 | 45 | strong { 46 | color: inherit; 47 | } 48 | } 49 | 50 | img { 51 | display: block; 52 | max-width: 100%; 53 | margin: 0 0 1rem; 54 | border-radius: 5px; 55 | } 56 | 57 | table { 58 | margin-bottom: 1rem; 59 | width: 100%; 60 | font-size: 85%; 61 | border: 1px solid #e5e5e5; 62 | border-collapse: collapse; 63 | } 64 | 65 | td, 66 | th { 67 | padding: .25rem .5rem; 68 | border: 1px solid #e5e5e5; 69 | } 70 | 71 | th { 72 | text-align: left; 73 | } 74 | 75 | tbody tr:nth-child(odd) td, 76 | tbody tr:nth-child(odd) th { 77 | background-color: #f9f9f9; 78 | } 79 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var grunt = require('grunt'); 3 | var webpack = require('webpack'); 4 | 5 | var entries = grunt.file.expand({cwd: path.resolve('src')}, "*").reduce( 6 | function(map, page) { 7 | if (page.match(/.js$/)) { 8 | map[page.slice(0, page.length - 3)] = "./" + page; 9 | } 10 | return map; 11 | }, {}); 12 | 13 | module.exports = { 14 | context: __dirname + "/src", 15 | entry: entries, 16 | output: { 17 | path: path.join(__dirname, "public", "js"), 18 | filename: '[name].min.js' 19 | }, 20 | module: { 21 | loaders: [ 22 | { 23 | test: /\.coffee$/, 24 | loaders: [ 'coffee-loader', 'cjsx-loader' ], 25 | include: path.join(__dirname, 'src') 26 | }, 27 | { 28 | test: /\.js$/, 29 | loaders: ['babel-loader?stage=1'], 30 | include: path.join(__dirname, 'src'), 31 | exclude: /node_modules/ 32 | }, 33 | { test: /\.css$/, loader: "style-loader!css-loader" }, 34 | { test: /\.scss$/, loader: "style!css!sass" }, 35 | { test: /\.svg$/, loader: "raw-loader" } 36 | ] 37 | }, 38 | externals: { 39 | "vega": "vg", 40 | "d3": "d3", 41 | "datalib": "dl", 42 | "react": "React", 43 | "react-dom": "ReactDOM", 44 | "lodash": "_" 45 | }, 46 | plugins: [ 47 | new webpack.optimize.UglifyJsPlugin({minimize: true}) 48 | ], 49 | resolve: { 50 | extensions: ['', '.js', '.json', '.coffee'] 51 | }, 52 | resolveLoader: { 53 | modulesDirectories: [ 54 | 'node_modules' 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /_demos/nba-shot-chart-vega.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: demo 3 | title: Interactive NBA Shot Chart (Vega) 4 | bodyclass: demo 5 | custom_css: 6 | - demo 7 | not_responsive: true 8 | comments: true 9 | --- 10 | 11 | Click and drag to outline a rectangular box over the shots or any of 12 | the histograms. The FG% and Points per Attempt numbers update 13 | automatically. Select from available shot charts in the upper left corner. 14 | 15 |
16 |
17 |

18 | 19 | This was inspired by [@savvas_tj](https://twitter.com/savvas_tj)'s 20 | [post](http://savvastjortjoglou.com/nba-shot-sharts.html#Plotting-the-Shot-Chart-Data) 21 | on creating NBA shot charts in python, as well as [Kirk Goldsberry's articles on Grantland](https://grantland.com/the-triangle/golden-state-warriors-illustrated/). 22 | This is an interactive version built with a 23 | [vega](http://vega.github.io/vega/), using data from 24 | [stats.nba.com](http://stats.nba.com), specifically from this 25 | [link](http://stats.nba.com/stats/shotchartdetail?CFID=33&CFPARAMS=2014-15&ContextFilter=&ContextMeasure=FGA&DateFrom=&DateTo=&GameID=&GameSegment=&LastNGames=0&LeagueID=00&Location=&MeasureType=Base&Month=0&OpponentTeamID=0&Outcome=&PaceAdjust=N&PerMode=PerGame&Period=0&PlayerID=201935&PlusMinus=N&Position=&Rank=N&RookieYear=&Season=2014-15&SeasonSegment=&SeasonType=Regular+Season&TeamID=0&VsConference=&VsDivision=&mode=Advanced&showDetails=0&showShots=1&showZones=0) 26 | for James Harden. Points from free throws do not appear included. 27 | 28 | Steph's numbers are ridiculous. 29 | 30 | {% include javascript.html js_file="nba-shot-chart-vega" %} 31 | -------------------------------------------------------------------------------- /src/explorer/images/color/pastel1.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/hello.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class HelloWorld extends React.Component { 4 | render() { 5 | return
6 |

This is a post mostly generated using react and ES6.

7 |

Webpack builds the source automatically on file changes and then jekyll serves it. 8 | I start these processes using: foreman start --formation="webpack_watch=1,webpack_server=1,jekyll=1". 9 | For convenience, I bound this to npm start in the package.json. 10 |

11 |

To speed up testing while updating react classes, I set up react-hot-reload.

12 |

This allows real-time updates to the react classes, without a reload!

13 |

Things I had to do to get this work:

14 |
    15 |
  • Added react-hot to the js loaders:
  • 16 |
  • Change output.publicPath to my webpack-dev-server host and port:
  • 17 |
  • Add plugins: webpack.HotModuleReplacementPlugin and webpack.NoErrorsPlugin
  • 18 |
  • Set up webpack config devServer for serving, particularly adding headers for 'Access-Control-Allow-Origin': '*'
  • 19 |
  • Remove react from webpack externals and load it as a node module
  • 20 |
  • Add flag to javascript include to switch between serving from webpack-dev-server during development to the final build location
  • 21 |
  • Serve the javascript files from the webpack-dev-server publicPath
  • 22 |
  • Then run webpack-dev-server --config webpack.hot.config.js
  • 23 |
24 |
25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/explorer/helpers/color.js: -------------------------------------------------------------------------------- 1 | const COLOR_PALETTES = { 2 | 'category10': [ 3 | '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf' 4 | ], 5 | 'category20': [ 6 | '#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728', '#ff9896', '#9467bd', '#c5b0d5', 7 | '#8c564b', '#c49c94', '#e377c2', '#f7b6d2', '#7f7f7f', '#c7c7c7', '#bcbd22', '#dbdb8d', '#17becf', '#9edae5' 8 | ], 9 | 'category20b': [ 10 | '#393b79', '#5254a3', '#6b6ecf', '#9c9ede', '#637939', '#8ca252', '#b5cf6b', '#cedb9c', '#8c6d31', '#bd9e39', 11 | '#e7ba52', '#e7cb94', '#843c39', '#ad494a', '#d6616b', '#e7969c', '#7b4173', '#a55194', '#ce6dbd', '#de9ed6' 12 | ], 13 | 'category20c': [ 14 | '#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#e6550d', '#fd8d3c', '#fdae6b', '#fdd0a2', '#31a354', '#74c476', 15 | '#a1d99b', '#c7e9c0', '#756bb1', '#9e9ac8', '#bcbddc', '#dadaeb', '#636363', '#969696', '#bdbdbd', '#d9d9d9' 16 | ], 17 | 'pastel1': [ 18 | "#fbb4ae", "#b3cde3", "#ccebc5", "#decbe4", "#fed9a6", "#ffffcc", "#e5d8bd", "#fddaec", "#f2f2f2" 19 | ], 20 | 'pastel2': [ 21 | "#b3e2cd", "#fdcdac", "#cbd5e8", "#f4cae4", "#e6f5c9", "#fff2ae", "#f1e2cc", "#cccccc" 22 | ], 23 | 'set1': [ 24 | "#377eb8", "#4daf4a", "#984ea3", "#ff7f0", "#ffff33", "#a65628", "#f781bf", "#999999" 25 | ], 26 | 'set2': [ 27 | "#66c2a5", "#fc8d62", "#8da0cb", "#e78ac3", "#a6d854", "#ffd92f", "#e5c494", "#b3b3b3" 28 | ], 29 | 'set3': [ 30 | "#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5", "#ffed6f" 31 | ] 32 | } 33 | 34 | export { COLOR_PALETTES } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sandbox.github.com", 3 | "version": "1.0.0", 4 | "description": "Sandbox blog node setup", 5 | "main": "webpack.config.js", 6 | "dependencies": { 7 | "d3": "^3.5.12", 8 | "datalib": "^1.5.8", 9 | "es6-promise": "^3.0.2", 10 | "fixed-data-table": "^0.6.0", 11 | "immutable": "^3.7.5", 12 | "isomorphic-fetch": "^2.1.1", 13 | "lodash": "^3.10.1", 14 | "react": "^0.14.3", 15 | "react-dnd": "^2.0.2", 16 | "react-dnd-html5-backend": "^2.0.0", 17 | "react-dom": "^0.14.0", 18 | "react-redux": "^2.1.2", 19 | "react-tween-state": "^0.1.3", 20 | "redux": "^3.0.3", 21 | "redux-thunk": "^1.0.0", 22 | "updeep": "^0.10.1" 23 | }, 24 | "devDependencies": { 25 | "babel-core": "^5.8.34", 26 | "babel-loader": "^5.4.0", 27 | "browser-sync": "^2.9.3", 28 | "cjsx-loader": "^2.0.1", 29 | "coffee-loader": "^0.7.2", 30 | "coffee-react-transform": "^3.2.0", 31 | "coffee-script": "^1.10.0", 32 | "css-loader": "^0.19.0", 33 | "gulp": "^3.9.0", 34 | "gulp-env": "^0.2.0", 35 | "raw-loader": "^0.5.1", 36 | "react-hot-loader": "^1.2.9", 37 | "redux-devtools": "^2.1.2", 38 | "style-loader": "^0.12.4", 39 | "webpack": "^1.12.2", 40 | "webpack-dev-server": "^1.12.1" 41 | }, 42 | "scripts": { 43 | "start": "foreman start -f Procfile.dev", 44 | "test": "echo \"Error: no test specified\" && exit 1" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/sandbox/sandbox.github.com.git" 49 | }, 50 | "author": "John Le", 51 | "license": "ISC", 52 | "bugs": { 53 | "url": "https://github.com/sandbox/sandbox.github.com/issues" 54 | }, 55 | "homepage": "https://github.com/sandbox/sandbox.github.com#readme" 56 | } 57 | -------------------------------------------------------------------------------- /src/explorer/ducks/chartspec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import u from 'updeep' 3 | import { prepareAxes } from '../data/axis' 4 | import { partitionPaneData, aggregatePanes } from '../data/pane' 5 | import { calculateDomains } from '../data/domain' 6 | import { calculateScales } from '../data/scale' 7 | 8 | export const UPDATE_CHART_DATA = 'explorer/chartspec/UPDATE_CHART_DATA' 9 | 10 | export function updateChartData(key, visualspec, queryResponse) { 11 | return { 12 | type: UPDATE_CHART_DATA, 13 | key, 14 | queryResponse, 15 | visualspec 16 | } 17 | } 18 | 19 | const chartState = { 20 | } 21 | 22 | export default function reducer(state = chartState, action) { 23 | switch(action.type) { 24 | case UPDATE_CHART_DATA: 25 | const { visualspec } = action 26 | const tableType = visualspec.table.type 27 | if (_.get([action.key, tableType], state)) return state 28 | 29 | const { query, queryspec, result } = action.queryResponse 30 | const { axes, nest } = query ? prepareAxes(queryspec, result) : {} 31 | const panes = query ? partitionPaneData(axes, nest, result) : {} 32 | aggregatePanes( 33 | panes, tableType, 34 | _(queryspec).omit('row', 'col').omit(_.isEmpty).values().flatten().value(), 35 | _(queryspec).pick('row', 'col').omit(_.isEmpty).mapValues( 36 | fields => _(fields).where({algebraType: 'Q'}).size() 37 | ).values().flatten().max() > 1) 38 | const domains = calculateDomains(result, _(queryspec).values().flatten().value(), axes) 39 | 40 | return u.updateIn( 41 | [action.key, tableType], 42 | { 43 | axes, 44 | panes, 45 | domains, 46 | scales: calculateScales(domains, queryspec, visualspec) 47 | }, 48 | state) 49 | default: 50 | return state 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/react-scatterplot.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import d3 from 'd3' 4 | import animateMark from './components/mark' 5 | import Axis from './components/axis' 6 | import {easingTypes} from 'react-tween-state' 7 | 8 | var element = document.getElementById("scatterplot") 9 | var margin = {top: 30, right: 100, bottom: 30, left: 100} 10 | var width = element.offsetWidth - margin.left - margin.right 11 | var height = 550 - margin.top - margin.bottom 12 | 13 | let normalDistribution = d3.random.normal(0, 1) 14 | var values = d3.range(1000).map(() => [normalDistribution(), normalDistribution()]) 15 | var xscale = d3.scale.linear().domain([-3, 3]).range([0, width]) 16 | var yscale = d3.scale.linear().domain([-3, 3]).range([height, 0]) 17 | 18 | class Circle extends React.Component { 19 | render() { 20 | return 21 | } 22 | } 23 | 24 | let TransitionCircle = animateMark(Circle, [ 25 | { prop: 'cx', duration: 2000, easing: easingTypes.linear, start: xscale(0)}, 26 | { prop: 'cy', duration: 2000, easing: easingTypes.linear, start: yscale(0)} 27 | ]) 28 | 29 | var points = values.map(function (d, i) { 30 | var [x, y] = d 31 | return 32 | }) 33 | 34 | ReactDOM.render( 35 | 36 | 37 | {points} 38 | 39 | 40 | 41 | , element) 42 | -------------------------------------------------------------------------------- /webpack.hot.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var grunt = require('grunt'); 3 | var webpack = require('webpack'); 4 | 5 | var entries = grunt.file.expand({cwd: path.resolve('src')}, "*").reduce( 6 | function(map, page) { 7 | if (page.match(/.js$/)) { 8 | map[page.slice(0, page.length - 3)] = "./" + page; 9 | } 10 | return map; 11 | }, {}); 12 | 13 | module.exports = { 14 | context: __dirname + "/src", 15 | entry: entries, 16 | output: { 17 | path: path.join(__dirname, "public", "js"), 18 | filename: '[name].hot.js', 19 | publicPath: 'http://localhost:8080/' 20 | }, 21 | module: { 22 | loaders: [ 23 | { test: /\.coffee$/, 24 | loaders: ['react-hot', 'coffee-loader', 'cjsx-loader'], 25 | include: path.join(__dirname, 'src') 26 | }, 27 | { 28 | test: /\.js$/, 29 | loaders: ['react-hot', 'babel-loader?stage=1'], 30 | include: path.join(__dirname, 'src') 31 | }, 32 | { test: /\.css$/, loader: "style-loader!css-loader" }, 33 | { test: /\.scss$/, loader: "style!css!sass" }, 34 | { test: /\.svg$/, loader: "raw-loader" } 35 | ] 36 | }, 37 | externals: { 38 | "vega": "vg", 39 | "d3": "d3", 40 | "datalib": "dl", 41 | "lodash": "_" 42 | }, 43 | devServer: { 44 | publicPath: 'http://localhost:8080/', 45 | contentBase: "./src", 46 | hot: true, 47 | inline: true, 48 | devtool: 'eval', 49 | headers: { 'Access-Control-Allow-Origin': '*' } 50 | }, 51 | plugins: [ 52 | new webpack.HotModuleReplacementPlugin(), 53 | new webpack.NoErrorsPlugin() 54 | ], 55 | resolve: { 56 | extensions: ['', '.js', '.json', '.coffee'] 57 | }, 58 | resolveLoader: { 59 | modulesDirectories: [ 60 | 'node_modules' 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /_sass/_code.scss: -------------------------------------------------------------------------------- 1 | // Code 2 | // 3 | // Inline and block-level code snippets. Includes tweaks to syntax highlighted 4 | // snippets from Pygments/Rouge and Gist embeds. 5 | 6 | code, 7 | pre { 8 | font-family: Menlo, Monaco, "Courier New", monospace; 9 | } 10 | 11 | code { 12 | padding: .25em .5em; 13 | font-size: 85%; 14 | color: #bf616a; 15 | background-color: #f9f9f9; 16 | border-radius: 3px; 17 | } 18 | 19 | pre { 20 | margin-top: 0; 21 | margin-bottom: 1rem; 22 | } 23 | 24 | pre code { 25 | padding: 0; 26 | font-size: 100%; 27 | color: inherit; 28 | background-color: transparent; 29 | } 30 | 31 | // Pygments via Jekyll 32 | .highlight { 33 | padding: 1rem; 34 | margin-bottom: 1rem; 35 | font-size: .8rem; 36 | line-height: 1.4; 37 | background-color: #f9f9f9; 38 | border-radius: .25rem; 39 | 40 | pre { 41 | margin-bottom: 0; 42 | overflow-x: auto; 43 | } 44 | 45 | .lineno { 46 | display: inline-block; // Ensures the null space also isn't selectable 47 | padding-right: .75rem; 48 | padding-left: .25rem; 49 | color: #999; 50 | // Make sure numbers aren't selectable 51 | -webkit-user-select: none; 52 | -moz-user-select: none; 53 | user-select: none; 54 | } 55 | } 56 | 57 | 58 | // Gist via GitHub Pages 59 | // .gist .gist-file { 60 | // font-family: Menlo, Monaco, "Courier New", monospace !important; 61 | // } 62 | // .gist .markdown-body { 63 | // padding: 15px; 64 | // } 65 | // .gist pre { 66 | // padding: 0; 67 | // background-color: transparent; 68 | // } 69 | // .gist .gist-file .gist-data { 70 | // font-size: .8rem !important; 71 | // line-height: 1.4; 72 | // } 73 | // .gist code { 74 | // padding: 0; 75 | // color: inherit; 76 | // background-color: transparent; 77 | // border-radius: 0; 78 | // } 79 | -------------------------------------------------------------------------------- /src/explorer/images/color/category10.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/explorer/helpers/table.js: -------------------------------------------------------------------------------- 1 | export const TABLE_ENCODINGS = { 2 | bar: { 3 | name: "Bar", 4 | icon: [{ 5 | className: "fa fa-bar-chart"}], 6 | properties: ['color', 'opacity'] }, 7 | point: { 8 | name: "Symbol", 9 | icon: [{ 10 | className: "material-icons", 11 | style: { 12 | position: 'relative', 13 | top: 4, 14 | fontSize: 22}}, 15 | "grain"], 16 | properties: ['size', 17 | 'color', 18 | 'opacity', 19 | 'shape'] }, 20 | line: { 21 | name: "Line", 22 | icon: [{ 23 | className: "fa fa-line-chart"}], 24 | properties: ['color'] }, 25 | area: { 26 | name: "Area", 27 | icon: [{ 28 | className: "fa fa-area-chart"}], 29 | properties: ['color'] }, 30 | rect: { 31 | name: "Gantt Bar", 32 | icon: [{ 33 | className: "material-icons", 34 | style: { 35 | position: 'relative', 36 | top: 4, 37 | fontSize: 22}}, 38 | "clear_all"], 39 | properties: ['x', 40 | 'x2', 41 | 'y', 42 | 'y2', 43 | 'size', 44 | 'color'] }, 45 | box: { 46 | name: "Box Plot", 47 | icon: [{ 48 | className: "material-icons", 49 | style: { 50 | position: 'relative', 51 | top: 4, 52 | fontSize: 18}}, 53 | "tune"], 54 | properties: ['color'] }, 55 | pie: { 56 | name: "Pie", 57 | icon: [{ 58 | className: "fa fa-pie-chart"}], 59 | properties: ['color'] }, 60 | donut: { 61 | name: "Donut", 62 | icon: [ 63 | { 64 | className: "material-icons", 65 | style: { 66 | position: 'relative', 67 | top: 4, 68 | fontSize: 18 69 | }}, 70 | "data_usage"], 71 | properties: ['color'] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/explorer/components/vis/marks/layout.js: -------------------------------------------------------------------------------- 1 | import { getAccessorName } from '../../../helpers/field' 2 | import _ from 'lodash' 3 | 4 | function cumsum(array, sum_fn = _.identity) { 5 | let sum = 0 6 | let current_sum 7 | let result = _.map(array, a => { 8 | current_sum = sum 9 | sum += sum_fn(a) 10 | return current_sum 11 | }) 12 | result.push(sum) 13 | return result 14 | } 15 | 16 | export function stackLayout(markData, name, binField) { 17 | let accessor = _.property(name) 18 | if (!binField) { 19 | let stacked = cumsum(markData, accessor) 20 | return (d, i) => stacked[i] 21 | } 22 | else { 23 | let sums = {} 24 | let binName = getAccessorName(binField.field) 25 | let stacked = _.map( 26 | markData, 27 | a => { 28 | if(null == sums[a[binName]]) sums[a[binName]] = 0 29 | let current_sum = sums[a[binName]] 30 | sums[a[binName]] += a[name] 31 | return current_sum 32 | }) 33 | return (d, i) => stacked[i] 34 | } 35 | } 36 | 37 | export function stackGroupedLayout(groupMarkData, name, binField) { 38 | let stacked 39 | if (!binField) { 40 | let sum = 0 41 | stacked = _.map( 42 | groupMarkData, 43 | a => { 44 | return _.map( 45 | a.values, 46 | b => { 47 | let current_sum = sum 48 | sum += b[name] 49 | return current_sum 50 | })}) 51 | } 52 | else { 53 | let sums = {} 54 | let binName = getAccessorName(binField.field) 55 | stacked = _.map( 56 | groupMarkData, 57 | a => { 58 | return _.map( 59 | a.values, 60 | b => { 61 | if(null == sums[b[binName]]) sums[b[binName]] = 0 62 | let current_sum = sums[b[binName]] 63 | sums[b[binName]] += b[name] 64 | return current_sum 65 | })}) 66 | } 67 | 68 | return (d, level, i) => stacked[level][i] 69 | } 70 | -------------------------------------------------------------------------------- /src/explorer/images/color/set3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/react-histogram-transition.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import d3 from 'd3' 4 | import animateMark from './components/mark' 5 | import Rect from './components/rect' 6 | import Axis from './components/axis' 7 | import {easingTypes} from 'react-tween-state' 8 | 9 | var element = document.getElementById("react-transition") 10 | var margin = {top: 10, right: 30, bottom: 30, left: 30} 11 | var width = element.offsetWidth - margin.left - margin.right 12 | var height = 550 - margin.top - margin.bottom 13 | var values = d3.range(1000).map(d3.random.bates(10)) 14 | var formatCount = d3.format(",.0f") 15 | var xscale = d3.scale.linear().domain([0, 1]).range([0, width]) 16 | var data = d3.layout.histogram().bins(xscale.ticks(20))(values) 17 | var yscale = d3.scale.linear().domain([0, d3.max(data, (d) => d.y)]).range([height, 0]) 18 | 19 | let TransitionRect = animateMark(Rect, [ 20 | {prop: 'width', duration: 300, easing: easingTypes.easeInOutQuad}, 21 | {prop: 'height', duration: 600, easing: easingTypes.linear} 22 | ]) 23 | 24 | class RectGroup extends React.Component { 25 | render() { 26 | return 27 | } 28 | } 29 | let TransitionGroup = animateMark(RectGroup, [ 30 | { prop: 'y', duration: 900, easing: easingTypes.linear, start: height } 31 | ]) 32 | 33 | var bars = data.map( 34 | (d, i) => 35 | 36 | 37 | {formatCount(d.y)} 38 | ) 39 | 40 | ReactDOM.render( 41 | 42 | 43 | {bars} 44 | 45 | 46 | 47 | , element) 48 | -------------------------------------------------------------------------------- /src/nba-shot-chart.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import d3 from 'd3' 4 | import classNames from 'classnames' 5 | import animateMark from './components/mark' 6 | import Axis from './components/axis' 7 | import {easingTypes} from 'react-tween-state' 8 | import {CourtBounds, BasketBall} from './components/basketball' 9 | 10 | function logData(d) { 11 | console.log('yes', d) 12 | } 13 | 14 | function renderShotChart(rows, header) { 15 | var element = document.getElementById("shot-chart") 16 | var margin = {top: 30, right: 179, bottom: 30, left: 179} 17 | 18 | var width = 600 19 | var height = 660 20 | 21 | var values = rows.map((row) => [row[17], row[18]]) 22 | var xscale = d3.scale.linear().domain([250, -250]).range([0, width]) 23 | var yscale = d3.scale.linear().domain([-47.5, 500]).range([height, 0]) 24 | 25 | var xballr = Math.abs(xscale(3.85) - xscale(0)) 26 | var yballr = Math.abs(yscale(0) - yscale(3.85)) 27 | 28 | let TransitionBall = animateMark(BasketBall, [ 29 | { prop: 'cx', duration: 1000, easing: easingTypes.linear, start: xscale(0)}, 30 | { prop: 'cy', duration: 1000, easing: easingTypes.linear, start: yscale(0)} 31 | ]) 32 | 33 | var points = rows.map(function (d, i) { 34 | var [x, y] = [d[17], d[18]] 35 | return 40 | }) 41 | 42 | ReactDOM.render( 43 | 44 | 45 | {points} 46 | 47 | 48 | , element) 49 | } 50 | 51 | d3.json( 52 | "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/5c74a5dcd7b257faa985f28c932a684ed4cea065/james-harden-shotchartdetail.json", 53 | function(error, json) { 54 | if (error) return console.warn(error) 55 | renderShotChart(json.resultSets[0].rowSet, json.resultSets[0].headers) 56 | }) 57 | -------------------------------------------------------------------------------- /_sass/_type.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | // 3 | // Headings, body text, lists, and other misc typographic elements. 4 | 5 | h1, h2, h3, h4, h5, h6 { 6 | margin-bottom: .5rem; 7 | font-weight: bold; 8 | line-height: 1.25; 9 | color: #313131; 10 | text-rendering: optimizeLegibility; 11 | } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | } 16 | 17 | h2 { 18 | margin-top: 1rem; 19 | font-size: 1.5rem; 20 | } 21 | 22 | h3 { 23 | margin-top: 1.5rem; 24 | font-size: 1.25rem; 25 | } 26 | 27 | h4, h5, h6 { 28 | margin-top: 1rem; 29 | font-size: 1rem; 30 | } 31 | 32 | p { 33 | margin-top: 0; 34 | margin-bottom: 1rem; 35 | } 36 | 37 | strong { 38 | color: #303030; 39 | } 40 | 41 | ul, ol, dl { 42 | margin-top: 0; 43 | margin-bottom: 1rem; 44 | } 45 | 46 | dt { 47 | font-weight: bold; 48 | } 49 | 50 | dd { 51 | margin-bottom: .5rem; 52 | } 53 | 54 | hr { 55 | position: relative; 56 | margin: 1.5rem 0; 57 | border: 0; 58 | border-top: 1px solid #eee; 59 | border-bottom: 1px solid #fff; 60 | } 61 | 62 | abbr { 63 | font-size: 85%; 64 | font-weight: bold; 65 | color: #555; 66 | text-transform: uppercase; 67 | 68 | &[title] { 69 | cursor: help; 70 | border-bottom: 1px dotted #e5e5e5; 71 | } 72 | } 73 | 74 | blockquote { 75 | padding: .5rem 1rem; 76 | margin: .8rem 0; 77 | color: #7a7a7a; 78 | border-left: .25rem solid #e5e5e5; 79 | 80 | p:last-child { 81 | margin-bottom: 0; 82 | } 83 | 84 | @media (min-width: 30em) { 85 | padding-right: 5rem; 86 | padding-left: 1.25rem; 87 | } 88 | } 89 | 90 | 91 | // Markdown footnotes 92 | // 93 | // See the example content post for an example. 94 | 95 | // Footnote number within body text 96 | a[href^="#fn:"], 97 | // Back to footnote link 98 | a[href^="#fnref:"] { 99 | display: inline-block; 100 | margin-left: .1rem; 101 | font-weight: bold; 102 | } 103 | 104 | // List of footnotes 105 | .footnotes { 106 | margin-top: 2rem; 107 | font-size: 85%; 108 | } 109 | 110 | // Custom type 111 | // 112 | // Extend paragraphs with `.lead` for larger introductory text. 113 | 114 | .lead { 115 | font-size: 1.25rem; 116 | font-weight: 300; 117 | } 118 | -------------------------------------------------------------------------------- /src/explorer/data/nest.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | function setProperty(obj, key, value) { 4 | if (key.length == 0) return 5 | let i = 0 6 | let len = key.length - 1 7 | for(; i < len; i++) { 8 | if (null == obj[key[i]]) { 9 | obj[key[i]] = {} 10 | } 11 | obj = obj[key[i]] 12 | } 13 | obj[key[i]] = value 14 | } 15 | 16 | function pushProperty(obj, key, value) { 17 | if (key.length == 0) return 18 | let i = 0 19 | let len = key.length - 1 20 | for(; i < len; i++) { 21 | if (null == obj[key[i]]) { 22 | obj[key[i]] = {} 23 | } 24 | obj = obj[key[i]] 25 | } 26 | if(null == obj[key[i]]) obj[key[i]] = [] 27 | obj[key[i]].push(value) 28 | } 29 | 30 | function nestHasSeen(seen, sofar) { 31 | for(let i = 0; i < sofar.length; i++) { 32 | if (!seen[sofar[i].key]) 33 | return false 34 | seen = seen[sofar[i].key] 35 | } 36 | return seen 37 | } 38 | 39 | function traverseNestTree(nest, rowLevels, colLevels, level, rowSoFar, colSoFar, seen, result) { 40 | if (level == rowLevels.length + colLevels.length) { 41 | if (!nestHasSeen(seen.row, rowSoFar)) { 42 | setProperty(seen.row, _.map(rowSoFar, 'key'), true) 43 | result.row.push(rowSoFar) 44 | } 45 | if (!nestHasSeen(seen.col, colSoFar)) { 46 | setProperty(seen.col, _.map(colSoFar, 'key'), true) 47 | result.col.push(colSoFar) 48 | } 49 | } 50 | else { 51 | for (let i = 0, keys = Object.keys(nest), l = keys.length; i < l; i++) { 52 | let k = keys[i] 53 | traverseNestTree( 54 | nest[k], rowLevels, colLevels, level + 1, 55 | level < rowLevels.length ? rowSoFar.concat([{ key: k, field: rowLevels[level] }]) : rowSoFar, 56 | level >= rowLevels.length ? colSoFar.concat([{ key: k, field: colLevels[level - rowLevels.length] }]) : colSoFar, 57 | seen, result) 58 | } 59 | } 60 | } 61 | 62 | export function partitionNestKey(nest, rowLevels, colLevels) { 63 | let result = { row: [], col: [] } 64 | let seen = { row: {}, col: {} } 65 | traverseNestTree(nest, rowLevels, colLevels, 0, [], [], seen, result) 66 | return result 67 | } 68 | 69 | export function calculateNest(data, key, f = pushProperty) { 70 | let nest = {} 71 | for (let i = 0, len = data.length; i < len; i++) { 72 | f(nest, key(data[i]), i) 73 | } 74 | return nest 75 | } 76 | -------------------------------------------------------------------------------- /src/explorer/ducks/visualspec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import u from 'updeep' 3 | import { combineReducers } from 'redux' 4 | 5 | /* ACTION TYPES */ 6 | export const SET_TABLE_ENCODING = 'explorer/visualspec/SET_TABLE_ENCODING' 7 | export const SET_PROPERTY_SETTING = 'explorer/visualspec/SET_PROPERTY_SETTING' 8 | 9 | /* ACTIONS */ 10 | export function setTableEncoding(encoding) { 11 | return { 12 | type: SET_TABLE_ENCODING, 13 | encoding 14 | } 15 | } 16 | 17 | export function setPropertySetting(property, setting, value) { 18 | return { 19 | type: SET_PROPERTY_SETTING, 20 | property, 21 | setting, 22 | value 23 | } 24 | } 25 | 26 | /* REDUCER */ 27 | const tableState = { 28 | type: 'bar' 29 | } 30 | 31 | function table(state = tableState, action) { 32 | switch(action.type) { 33 | case SET_TABLE_ENCODING: 34 | return u({type: action.encoding}, state) 35 | default: 36 | return state 37 | } 38 | } 39 | 40 | const propertiesState = { 41 | size: { 42 | ordinalRange: [30, 50, 80, 120, 170, 230, 300], 43 | 'default': 30, 44 | scaleRangeMin: 25, 45 | scaleRangeMax: 625 46 | }, 47 | shape: { 48 | ordinalRange: ["circle", "cross", "diamond", "square", "triangle-down", "triangle-up"], 49 | 'default': 'circle' 50 | }, 51 | color: { 52 | palette: "category10", 53 | 'default': "#356CA7", 54 | scale: "linear", 55 | scaleZero: true, 56 | scaleManualDomain: false, 57 | scaleManualRange: false, 58 | scaleRangeMin: "#d6ead1", 59 | scaleRangeMax: "#0c541f" 60 | }, 61 | background: { 62 | palette: "category10", 63 | 'default': "#356CA7", 64 | scale: "linear", 65 | scaleZero: true, 66 | scaleManualDomain: false, 67 | scaleManualRange: false, 68 | scaleRangeMin: "#d6ead1", 69 | scaleRangeMax: "#0c541f" 70 | }, 71 | opacity: { 72 | 'default': 1, 73 | ordinalRange: [0.2, 0.4, 0.6, 0.8, 1], 74 | scaleRangeMin: 0.1, 75 | scaleRangeMax: 1 76 | }, 77 | orientation: {}, 78 | x: {}, 79 | x2: {}, 80 | y: {}, 81 | y2: {}, 82 | text: {} 83 | } 84 | 85 | function properties(state = propertiesState, action) { 86 | switch(action.type) { 87 | case SET_PROPERTY_SETTING: 88 | return u({[action.property]: { [action.setting]: action.value }}, state) 89 | default: 90 | return state 91 | } 92 | } 93 | 94 | const reducer = combineReducers({ table, properties }) 95 | export default reducer 96 | -------------------------------------------------------------------------------- /src/explorer/css/main.scss: -------------------------------------------------------------------------------- 1 | @import '../../css/flexbox.scss'; 2 | 3 | $link-blue: #649AF3; 4 | 5 | html, 6 | body { 7 | font-size: 16px; 8 | background-color: #F4F8FB; 9 | 10 | height: 100%; 11 | min-width: 1024px; 12 | margin: 0; 13 | 14 | @include flexbox; 15 | @include flex-flow(column); 16 | } 17 | 18 | label { 19 | font-weight: 400; 20 | font-size: 0.85rem; 21 | } 22 | 23 | .demo.explorer .demo-title { 24 | padding: 0.5rem; 25 | border-bottom: 1px solid #dfdfdf; 26 | background-color: white; 27 | overflow: auto; 28 | @include flex(0 0 auto); 29 | 30 | h1.post-title { 31 | font-weight: 400; 32 | font-size: 1.25rem; 33 | margin: 0; 34 | float: left; 35 | } 36 | 37 | a { 38 | float: right; 39 | font-size: 1rem; 40 | margin: 0; 41 | padding-left: 1rem; 42 | } 43 | } 44 | 45 | #demo { 46 | font-size: 14px; 47 | @include flexbox; 48 | @include flex(1 1 auto); 49 | } 50 | 51 | .pane-container { 52 | @include flex(1 0 auto); 53 | @include flexbox; 54 | @include flex-flow(row); 55 | } 56 | 57 | .container-flex-fill-wrap { 58 | position: relative; 59 | @include flex(1 1 auto); 60 | 61 | .container-flex-fill { 62 | position: absolute; 63 | top: 0; bottom: 0; left: 0; right: 0; 64 | @include flexbox; 65 | 66 | pre { 67 | overflow: auto; 68 | white-space: pre; 69 | word-wrap: normal; 70 | margin-right: 10px; 71 | } 72 | } 73 | } 74 | 75 | .pane-container .pane { 76 | background-color: white; 77 | border-width: 1px; 78 | border-style: solid; 79 | border-color: #dfdfdf; 80 | border-radius: 1px; 81 | margin: 10px 10px; 82 | margin-left: 0; 83 | @include flexbox; 84 | @include flex(1 1 auto); 85 | @include flex-just(space-between); 86 | 87 | &.data-pane { 88 | @include flex-flow(column); 89 | @include flex(0 1 253px); 90 | border-left: 0; 91 | } 92 | 93 | &.graphic-pane { 94 | background-color: transparent; 95 | border: none; 96 | 97 | @include flex(1 1 auto); 98 | @include flex-flow(column); 99 | } 100 | 101 | &.shelf-pane { 102 | @include flex(0 1 243px); 103 | @include flex-flow(column); 104 | background-color: transparent; 105 | border: none; 106 | position: relative; 107 | } 108 | } 109 | 110 | @import './components/field'; 111 | @import './components/shelf'; 112 | @import './components/datasource'; 113 | @import './components/querybuilder'; 114 | @import './components/graphic'; 115 | -------------------------------------------------------------------------------- /_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% unless page.not_responsive %} 8 | 9 | {% endunless %} 10 | 11 | 12 | {% if page.title == "Home" %} 13 | {{ site.title }} · {{ site.tagline }} 14 | {% else %} 15 | {{ page.title }} · {{ site.title }} 16 | {% endif %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% if page.custom_css %} 28 | {% for css in page.custom_css %} 29 | 30 | {% endfor %} 31 | {% endif %} 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% if site.serving %} 39 | 40 | 41 | 42 | {% else %} 43 | 44 | 45 | 46 | 47 | 48 | {% endif %} 49 | 50 | {% if page.custom_js %} 51 | {% for js in page.custom_js %} 52 | {% include built_javascript.html js_file=js %} 53 | {% endfor %} 54 | {% endif %} 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/components/axis.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | import { d3_scaleRange } from './d3_scale' 4 | 5 | class Axis extends React.Component { 6 | render() { 7 | let orient = this.props.orient, scale = this.props.scale, innerTickSize = this.props.innerTickSize, outerTickSize = this.props.outerTickSize, 8 | tickArguments = this.props.tickArguments, 9 | tickValues = this.props.tickValues != null ? this.props.tickValues : (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()), 10 | tickFormat = this.props.tickFormat != null ? this.props.tickFormat : (scale.tickFormat ? scale.tickFormat.apply(scale, tickArguments) : (x => x)), 11 | tickSpacing = Math.max(this.props.innerTickSize, 0) + this.props.tickPadding, 12 | range = d3_scaleRange(scale), 13 | sign = orient === "top" || orient === "left" ? -1 : 1 14 | 15 | let tickDirection = orient === 'bottom' || orient === 'top' ? { 16 | x: 0, x2: 0, y: sign * tickSpacing, y2: sign * innerTickSize 17 | } : { 18 | x: sign * tickSpacing, x2: sign * innerTickSize, y: 0, y: 0 19 | } 20 | 21 | let tickTextProps = orient === 'bottom' || orient === 'top' ? { 22 | x: 0, 23 | y: sign * tickSpacing, 24 | dy: sign < 0 ? "0em" : ".71em", 25 | textAnchor: "middle" 26 | } : { 27 | x: sign * tickSpacing, 28 | y: 0, 29 | dy: ".32em", 30 | textAnchor: sign < 0 ? "end" : "start" 31 | } 32 | 33 | let axisClass = { 34 | axis: true, 35 | x: orient === 'top' || orient === 'bottom', 36 | y: orient === 'left' || orient === 'right' 37 | } 38 | 39 | let guide = orient === 'bottom' || orient === 'top' ? 40 | () : 41 | () 42 | 43 | let tickMarks = tickValues.map( 44 | (tick, i) => 45 | 46 | 47 | {tick} 48 | ) 49 | 50 | return {tickMarks}{guide} 51 | } 52 | } 53 | 54 | Axis.defaultProps = { 55 | orient: "bottom", 56 | innerTickSize: 6, 57 | outerTickSize: 6, 58 | tickPadding: 3, 59 | tickArguments: [10], 60 | tickValues: null, 61 | tickFormat: null 62 | } 63 | 64 | export default Axis 65 | -------------------------------------------------------------------------------- /src/explore-sql.js: -------------------------------------------------------------------------------- 1 | require('es6-promise').polyfill() 2 | import './explorer/css/main.scss' 3 | 4 | import React from 'react' 5 | import ReactDOM from 'react-dom' 6 | import d3 from 'd3' 7 | import dl from 'datalib' 8 | 9 | import _ from 'lodash' 10 | import { Provider } from 'react-redux' 11 | 12 | import Explorer from './explorer' 13 | import configureStore from './explorer/stores' 14 | import { selectTable, connectTableIfNecessary } from './explorer/ducks/datasources' 15 | import * as queryspec from './explorer/ducks/queryspec' 16 | 17 | let MockDataSources = { 18 | datasources: { 19 | IDS: [0, 1, 7, 2], 20 | BY_ID: { 21 | 7: { id: 7, name: "Iris", 22 | type: "dataframe", 23 | url: "http://vega.github.io/polestar/data/iris.json", 24 | settings: { format: "json" } 25 | }, 26 | 0: { id: 0, name: "Birdstrikes", 27 | type: "dataframe", 28 | url: "http://vega.github.io/polestar/data/birdstrikes.json", 29 | settings: { format: "json" } 30 | }, 31 | 1: { id: 1, name: "Cars", 32 | type: "dataframe", 33 | url: "http://vega.github.io/polestar/data/cars.json", 34 | settings: { format: "json" } 35 | }, 36 | 2: { id: 2, name: "NBA League Totals per year", 37 | type: "dataframe", 38 | url: "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/f6d6496474af6dfba0b73f36dbd0e00ce0fc2f42/leagues_NBA_player_totals.csv", 39 | settings: { format: "csv", delimiter: "," } 40 | } 41 | }}} 42 | 43 | _.each(MockDataSources.datasources.BY_ID, 44 | datasource => { 45 | _(datasource.tables).each( 46 | (table) => 47 | _(table.schema).each((field, i) => field.__id__ = i).value()).value() 48 | _(datasource.schema).each((field, i) => field.__id__ = i).value() 49 | }) 50 | 51 | let store = configureStore(MockDataSources) 52 | 53 | ReactDOM.render(, document.getElementById("demo")) 54 | 55 | store.dispatch(connectTableIfNecessary({datasource_id: 0})).then( 56 | () => { 57 | store.dispatch(selectTable({datasource_id: 0})) 58 | 59 | store.dispatch(queryspec.addField('row', { 60 | id: "agg_count", name: "COUNT" , type: "aggregate" , op: "count" 61 | })) 62 | 63 | store.dispatch(queryspec.addField('col', { 64 | "tableId": { 65 | id: 0, name: "Birdstrikes" 66 | }, 67 | "fieldId": 3, 68 | "func": "year" 69 | })) 70 | 71 | store.dispatch(queryspec.addField('color', { 72 | "tableId": { 73 | id: 0, name: "Birdstrikes" 74 | }, 75 | "fieldId": 7 76 | })) 77 | }) 78 | -------------------------------------------------------------------------------- /src/explorer/data/scale.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3' 2 | import _ from 'lodash' 3 | import { getFieldType, getAccessorName, isAggregateType } from '../helpers/field' 4 | import { TABLE_ENCODINGS } from '../helpers/table' 5 | import { COLOR_PALETTES } from '../helpers/color' 6 | 7 | function getOrdinalVisualRange(shelf, spec) { 8 | switch (shelf) { 9 | case 'color': 10 | return COLOR_PALETTES[spec.palette] 11 | case 'shape': 12 | case 'size': 13 | case 'opacity': 14 | return spec.ordinalRange 15 | default: 16 | return [] 17 | } 18 | } 19 | 20 | function getQuantitativeVisualRange(shelf, spec) { 21 | return [spec.scaleRangeMin, spec.scaleRangeMax] 22 | } 23 | 24 | function getQuantitativeScale(domain, zero) { 25 | let min = zero ? Math.min(0, domain.min) : domain.min 26 | let max = zero ? Math.max(0, domain.max) : domain.max 27 | let space = (max - min) / 25 28 | if (!zero) min = +min - space 29 | max = +max + space 30 | return { 31 | type: 'linear', 32 | domain: [min, max] 33 | } 34 | } 35 | 36 | function getVisualScale(algebraType, shelf, domain, spec) { 37 | let scaleType = 'O' == algebraType ? 'ordinal' : (spec.scale ? spec.scale : 'linear') 38 | let rangeFn = 'O' == algebraType ? getOrdinalVisualRange : getQuantitativeVisualRange 39 | let domainFn = 'O' == algebraType ? _.identity : (x => [x.min, x.max]) 40 | return { 41 | type: scaleType, 42 | domain: domainFn(domain), 43 | range: rangeFn(shelf, spec) 44 | } 45 | } 46 | 47 | export function calculateScales(domains, queryspec, visualspec) { 48 | let validProperties = TABLE_ENCODINGS[visualspec.table.type].properties 49 | 50 | let scales = _(queryspec).pick(['row', 'col']).mapValues( 51 | (fields, shelf) => { 52 | return _.reduce(fields, (acc, field) => { 53 | if ('Q' == field.algebraType) { 54 | let name = getAccessorName(field) 55 | let zero = isAggregateType(field) 56 | acc[name] = getQuantitativeScale(domains[name], zero) 57 | if ('time' == getFieldType(field) && _.contains(field.func, 'bin')) acc[name].type = 'time' 58 | } 59 | return acc 60 | }, {}) 61 | }).value() 62 | 63 | _.extend(scales, _(queryspec).pick(validProperties).mapValues( 64 | (fields, shelf) => { 65 | return _.reduce(fields, (acc, field) => { 66 | let name = getAccessorName(field) 67 | acc[name] = getVisualScale(field.algebraType, shelf, domains[name], visualspec.properties[shelf]) 68 | return acc 69 | }, {}) 70 | }).value()) 71 | 72 | return _.extend( 73 | {}, 74 | _.mapValues(_.pick(visualspec.properties, validProperties), (v) => { 75 | return { '__default__' : v } 76 | }), scales) 77 | } 78 | -------------------------------------------------------------------------------- /src/explorer/images/color/category20c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/explorer/images/color/category20b.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/explorer/css/components/table.scss: -------------------------------------------------------------------------------- 1 | @import '../../../css/utilities.scss'; 2 | 3 | .public_fixedDataTable_header, .public_fixedDataTable_header .public_fixedDataTableCell_main { 4 | background-color: white; 5 | background-image: none; 6 | } 7 | 8 | .public_fixedDataTable_footer .public_fixedDataTableCell_main, 9 | .public_fixedDataTableRow_highlighted, .public_fixedDataTableRow_highlighted .public_fixedDataTableCell_main { 10 | background-color: white; 11 | } 12 | 13 | .public_fixedDataTable_footer .public_fixedDataTableCell_main, 14 | .public_fixedDataTableRow_fixedColumnsDivider, 15 | .public_fixedDataTableCell_main { 16 | border-color: #dfdfdf; 17 | } 18 | 19 | .public_fixedDataTable_header .public_fixedDataTableCell_main { 20 | font-weight: initial; 21 | } 22 | 23 | .public_fixedDataTableRow_main.public_fixedDataTable_hasBottomBorder [data-reactid$=fixed_cells] .public_fixedDataTableCell_main, 24 | .public_fixedDataTableRow_main .public_fixedDataTableCell_axis.public_fixedDataTableCell_main, 25 | .table-no-footer .public_fixedDataTableRow_main .public_fixedDataTableCell_main, 26 | .table-row-bottom-border .public_fixedDataTableRow_main .public_fixedDataTableCell_main 27 | { 28 | border-bottom-style: solid; 29 | border-bottom-width: 1px; 30 | } 31 | 32 | .public_fixedDataTable_header .fixedDataTableCellLayout_wrap3 { 33 | vertical-align: bottom; 34 | } 35 | 36 | .public_fixedDataTableCell_axis .fixedDataTableCellLayout_wrap3, 37 | .public_fixedDataTable_footer .fixedDataTableCellLayout_wrap3 { 38 | vertical-align: top; 39 | } 40 | 41 | .public_fixedDataTableCell_cellContent { 42 | padding: 0px 4px; 43 | } 44 | 45 | .public_fixedDataTable_main, 46 | .table-no-header .public_fixedDataTable_header, 47 | .public_fixedDataTable_hasBottomBorder, 48 | .public_fixedDataTableRow_fixedColumnsDivider { 49 | border: none; 50 | } 51 | 52 | .public_fixedDataTable_header .public_fixedDataTableCell_main, 53 | .public_fixedDataTable_header .public_fixedDataTableRow_fixedColumnsDivider, 54 | .public_fixedDataTable_footer .public_fixedDataTableRow_fixedColumnsDivider, 55 | .public_fixedDataTable_footer .fixedDataTableCellLayout_main.public_fixedDataTableCell_main { 56 | border-left: none; 57 | border-right: none; 58 | border-top: none; 59 | } 60 | 61 | .public_fixedDataTable_footer .public_fixedDataTableRow_fixedColumnsDivider, 62 | .public_fixedDataTable_footer .fixedDataTableCellLayout_main.public_fixedDataTableCell_main { 63 | border-bottom: none; 64 | } 65 | 66 | .public_fixedDataTable_header .public_fixedDataTableCell_main { 67 | text-align: left; 68 | @include overflow-ellipsis; 69 | } 70 | 71 | .public_fixedDataTable_header [data-reactid$=fixed_cells] .public_fixedDataTableCell_main { 72 | text-align: left; 73 | } 74 | 75 | .public_fixedDataTableCell_main:not(.public_fixedDataTableCell_axis) .table-row-label.public_fixedDataTableCell_cellContent { 76 | text-align: right; 77 | @include overflow-ellipsis; 78 | } -------------------------------------------------------------------------------- /src/explorer/components/vis/marks/Line.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import dl from 'datalib' 3 | import d3 from 'd3' 4 | import React from 'react' 5 | import { getAccessorName, isBinField } from '../../../helpers/field' 6 | const { div, svg } = React.DOM 7 | 8 | export default class Line extends React.Component { 9 | getDefaultScales() { 10 | const { scales } = this.props 11 | return { 12 | stroke: scales.color.__default__, 13 | "stroke-width": 2, 14 | x: 0, 15 | y: 0 16 | } 17 | } 18 | 19 | getLineScales(props) { 20 | const { markData, width, height } = this.props 21 | const { field, scale, shelf } = props 22 | const name = getAccessorName(field) 23 | switch(shelf) { 24 | case 'row': 25 | return { 26 | y: (d) => scale(d[name]) 27 | } 28 | case 'col': 29 | return { 30 | x: (d) => scale(d[name]) 31 | } 32 | case 'color': 33 | return { 34 | stroke: (d) => scale(d[name]) 35 | } 36 | case 'size': 37 | return { 38 | // 'stroke-width': 39 | } 40 | default: 41 | return { 42 | } 43 | } 44 | } 45 | 46 | getAttributeTransforms() { 47 | const { transformFields } = this.props 48 | let transforms = _.merge( 49 | this.getDefaultScales(), 50 | _.reduce(_.map(transformFields, (fs) => this.getLineScales(fs)), _.merge, {})) 51 | return transforms 52 | } 53 | 54 | _d3Render() { 55 | d3.select(this.refs.d3container).selectAll("*").remove() 56 | const transforms = this.getAttributeTransforms() 57 | const line = d3.svg.line() 58 | .x(transforms.x) 59 | .y(transforms.y) 60 | .interpolate('linear') 61 | 62 | const lineGroups = dl.groupby( 63 | _.map(_.filter(this.props.transformFields, fs => !_.contains(['row', 'col'], fs.shelf)), 'field.accessor') 64 | ).execute(this.props.markData) 65 | 66 | const sortAccessors = _.filter([ 67 | isBinField(this.props.rowAxis.field) ? this.props.rowAxis.field.accessor : null, 68 | isBinField(this.props.colAxis.field) ? this.props.colAxis.field.accessor : null]) 69 | const markSort = values => 70 | _.isEmpty(sortAccessors) ? values : _.sortByAll(values, sortAccessors) 71 | 72 | let lines = d3.select(this.refs.d3container).selectAll("g.line") 73 | .data(lineGroups) // each line group 74 | 75 | lines.enter().append("g").attr("class", "line").append("path") 76 | lines.selectAll("g.line path") 77 | .attr('d', lineGroup => line(markSort(lineGroup.values))) 78 | .attr('stroke', transforms.stroke) 79 | .attr('stroke-width', transforms['stroke-width']) 80 | .attr('fill', 'none') 81 | lines.exit().remove() 82 | } 83 | 84 | componentDidMount() { 85 | this._d3Render() 86 | } 87 | 88 | componentDidUpdate() { 89 | this._d3Render() 90 | } 91 | 92 | render() { 93 | const { width, height } = this.props 94 | return svg({ref: 'd3container', width, height}) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/Dropdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | const { findDOMNode } = ReactDOM 4 | 5 | function contains(parent, child) { 6 | var node = child.parentNode 7 | while (node != null) { 8 | if (node == parent) { 9 | return true 10 | } 11 | node = node.parentNode 12 | } 13 | return false 14 | } 15 | 16 | export function handleOuterClick(Component) { 17 | class ClickHandler extends React.Component { 18 | constructor(props) { 19 | super(props) 20 | this.bindOuterClickHandler = this.bindOuterClickHandler.bind(this) 21 | this.unbindOuterClickHandler = this.unbindOuterClickHandler.bind(this) 22 | this.handleDocumentClick = this.handleDocumentClick.bind(this) 23 | } 24 | 25 | handleDocumentClick(onOuterClick) { 26 | return (evt) => { 27 | let target = evt.target || evt.srcElement 28 | if (target.parentNode != null && !contains(findDOMNode(this), target)) { 29 | onOuterClick() 30 | } 31 | } 32 | } 33 | 34 | bindOuterClickHandler(onOuterClick) { 35 | window.addEventListener('click', onOuterClick) 36 | } 37 | 38 | unbindOuterClickHandler(onOuterClick) { 39 | window.removeEventListener('click', onOuterClick) 40 | } 41 | 42 | render() { 43 | return 47 | } 48 | } 49 | 50 | return ClickHandler 51 | } 52 | 53 | export function createDropdownComponent(Component) { 54 | class Dropdown extends React.Component { 55 | constructor(props) { 56 | super(props) 57 | this.state = { open: false } 58 | this.openDropdown = this.openDropdown.bind(this) 59 | this.closeDropdown = this.closeDropdown.bind(this) 60 | this.toggleDropdown = this.toggleDropdown.bind(this) 61 | this.onOuterClick = this.props.handleDocumentClick(this.closeDropdown) 62 | } 63 | 64 | componentDidUpdate(prevProps, prevState) { 65 | if (this.state.open) 66 | this.props.bindOuterClickHandler(this.onOuterClick) 67 | else 68 | this.props.unbindOuterClickHandler(this.onOuterClick) 69 | } 70 | 71 | componentWillUnmount() { 72 | this.props.unbindOuterClickHandler(this.onOuterClick) 73 | } 74 | 75 | openDropdown() { 76 | this.setState({open: true}) 77 | } 78 | 79 | closeDropdown() { 80 | this.setState({open: false}) 81 | } 82 | 83 | toggleDropdown() { 84 | this.setState({open: !this.state.open}) 85 | } 86 | 87 | render() { 88 | return 94 | } 95 | } 96 | 97 | return handleOuterClick(Dropdown) 98 | } 99 | -------------------------------------------------------------------------------- /src/explorer/css/components/datasource.scss: -------------------------------------------------------------------------------- 1 | @import '../../../css/flexbox.scss'; 2 | @import '../../../css/utilities.scss'; 3 | 4 | @include keyframes(field-fade-in) { 5 | 0% { 6 | top: -20px; 7 | } 8 | 9 | 100% { 10 | top: 0px; 11 | } 12 | } 13 | 14 | .datasource-select { 15 | position: relative; 16 | border-bottom: 1px solid #dfdfdf; 17 | cursor: pointer; 18 | 19 | .datasource-title { 20 | position: relative; 21 | padding: 5px 5px 3px; 22 | padding-right: 15px; 23 | @include overflow-ellipsis; 24 | 25 | i.fa { 26 | padding-right: 5px; 27 | color: $link-blue; 28 | } 29 | 30 | i.fa.fa-caret-down, 31 | i.fa.fa-caret-up { 32 | position: absolute; 33 | top: 8px; 34 | right: 0px; 35 | color: #bbb; 36 | } 37 | } 38 | 39 | .datasource-dropdown { 40 | position: absolute; 41 | top: 100%; 42 | left: 0; 43 | right: -1px; 44 | z-index: 10; 45 | 46 | cursor: default; 47 | 48 | background-color: #fff; 49 | border-top: 1px solid #dfdfdf; 50 | border-right: 1px solid #dfdfdf; 51 | box-shadow: 0 3px 5px #c3c3c3; 52 | 53 | .datasource-list { 54 | max-height: 310px; 55 | overflow: auto; 56 | } 57 | 58 | .datasource-add { 59 | position: relative; 60 | padding: 5px; 61 | text-align: center; 62 | border-top: 1px solid #dfdfdf; 63 | color: $link-blue; 64 | cursor: pointer; 65 | 66 | &:hover, 67 | &:focus { 68 | background-color: $link-blue; 69 | color: white; 70 | } 71 | 72 | i.fa.fa-plus { 73 | position: absolute; 74 | top: 8px; 75 | left: 30px; 76 | } 77 | } 78 | 79 | .datasource-db-name, .datasource-name { 80 | @include overflow-ellipsis; 81 | } 82 | 83 | .datasource-db-name, .datasource-name, .datasource-db .datasource-db-tables { 84 | padding: 5px; 85 | 86 | i.fa { 87 | padding-right: 5px; 88 | } 89 | } 90 | 91 | .datasource-name { 92 | cursor: pointer; 93 | 94 | &:hover, 95 | &:focus { 96 | background-color: #3875d7; 97 | color: white; 98 | } 99 | } 100 | 101 | .datasource-db .datasource-db-tables { 102 | padding-left: 15px; 103 | } 104 | } 105 | } 106 | 107 | .datasource-table-container { 108 | @include flex(1 0 auto); 109 | position: relative; 110 | } 111 | 112 | .datasource-table-fields { 113 | position: absolute; 114 | top: 0; bottom: 0; left: 0; right: 0; 115 | padding: 5px 8px; 116 | overflow: auto; 117 | 118 | .datasource-table-field.field-wrap { 119 | margin-bottom: 5px; 120 | 121 | @include animation(field-fade-in .3s ease); 122 | 123 | .icon-wrap { 124 | width: 23px; 125 | } 126 | 127 | .name-wrap { 128 | @include flex(1 1 auto); 129 | } 130 | 131 | span { 132 | padding: 3px 0px; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/explorer/data/axis.js: -------------------------------------------------------------------------------- 1 | import dl from 'datalib' 2 | import _ from 'lodash' 3 | import { calculateNest, partitionNestKey } from './nest' 4 | import { QuantitativeAggregator } from './domain' 5 | import { getAccessorName } from '../helpers/field' 6 | 7 | class Axis { 8 | constructor(ordinals = [], field = null) { 9 | this.ordinals = ordinals 10 | this.key = _.map(ordinals, 'key') 11 | this.acceptsValues = _.isEmpty(this.key) 12 | this.field = field 13 | } 14 | cross(field) { 15 | return _.extend(new Axis(), this, {field}) 16 | } 17 | hasQuantitativeField() { 18 | return null != this.field 19 | } 20 | hasField(field) { 21 | let name = getAccessorName(field) 22 | return _.contains(this.key, name) || this.fieldAccessor() == name 23 | } 24 | map(f) { 25 | let result = [] 26 | for(let i=0, len=this.ordinals.length; i < len; i++) { 27 | result.push(f(this.ordinals[i].field, i)) 28 | } 29 | if (null != this.field) result.push(f(this.field, 'Q')) 30 | return result 31 | } 32 | label() { 33 | return `${this.key.join(' ')}${this.field ? ` ${this.field.name}` : ''}` 34 | } 35 | fieldAccessor() { 36 | return this.field ? getAccessorName(this.field) : null 37 | } 38 | addDomainValue(value) { 39 | if (this.field && null == this.domain) { 40 | this.domain = new QuantitativeAggregator() 41 | } 42 | if (null != this.domain) { 43 | this.domain.add(value) 44 | } 45 | return this 46 | } 47 | getDomain() { 48 | if (null == this.domain) return {} 49 | return this.domain.result() 50 | } 51 | } 52 | 53 | function setAxisIndex(axis) { 54 | for(let i = 0, len = axis.length; i < len; i++) { 55 | axis[i].index = i 56 | } 57 | } 58 | 59 | // nest ordinal fields then cross with concat-quantitative fields to build the axis 60 | export function prepareAxes(queryspec, data) { 61 | let shelves = _(queryspec).mapValues(fields => _.groupBy(fields, 'algebraType')).value() 62 | let accessors = _(shelves).pick('row', 'col').map((shelf) => _.map(shelf.O, 'name')).flatten().map(dl.$).value() 63 | let nest = _.isEmpty(accessors) ? {} : calculateNest(data, (datum) => _.map(accessors, f => f(datum))) 64 | let rowLevels = shelves.row && shelves.row.O ? shelves.row.O : [] 65 | let colLevels = shelves.col && shelves.col.O ? shelves.col.O : [] 66 | let ordinalAxesKeys = _.mapValues( 67 | partitionNestKey(nest, rowLevels, colLevels), 68 | (axis, shelf) => { 69 | axis = _.sortByAll(axis, _.times('row' == shelf ? rowLevels.length : colLevels.length, i => `${i}.key`)) 70 | return _.isEmpty(axis) ? [ new Axis() ] : _.map(axis, ordinals => new Axis(ordinals)) 71 | }) 72 | 73 | let axes = _.mapValues(ordinalAxesKeys, (axis, shelf) => { 74 | let qfields = shelves[shelf] && shelves[shelf].Q 75 | if (_.isEmpty(qfields)) return axis 76 | return _(axis).map(ordinalAxis => _.map(qfields, field => ordinalAxis.cross(field))).flatten().value() 77 | }) 78 | 79 | setAxisIndex(axes.row) 80 | setAxisIndex(axes.col) 81 | return { axes, nest } 82 | } 83 | -------------------------------------------------------------------------------- /src/react-scatterplot-2.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import d3 from 'd3' 4 | import Axis from './components/axis' 5 | import {Mixin as tweenMixin, easingTypes} from 'react-tween-state' 6 | 7 | var element = document.getElementById("scatterplot") 8 | var margin = {top: 30, right: 100, bottom: 30, left: 100} 9 | var width = element.offsetWidth - margin.left - margin.right 10 | var height = 550 - margin.top - margin.bottom 11 | 12 | let normalDistribution = d3.random.normal(0, 1) 13 | var values = d3.range(1000).map(() => [normalDistribution(), normalDistribution()]) 14 | var xscale = d3.scale.linear().domain([-3, 3]).range([0, width]) 15 | var yscale = d3.scale.linear().domain([-3, 3]).range([height, 0]) 16 | 17 | function animateGroup(Component, transitionAttributes) { 18 | const VisualGroup = React.createClass({ 19 | mixins: [tweenMixin], 20 | getInitialState() { 21 | let state = this.props.data.reduce( 22 | function (memo, d, i) { 23 | transitionAttributes.forEach( 24 | transition => 25 | memo[`__data:${i}:${transition.key}`] = (typeof transition.start === "function") ? transition.start(d) : (transition.start == null ? 0 : transition.start)) 26 | return memo 27 | }, {}) 28 | return state 29 | }, 30 | componentDidMount() { 31 | this.props.data.forEach( 32 | (d, i) => 33 | transitionAttributes.forEach( 34 | transition => 35 | this.tweenState(`__data:${i}:${transition.key}`, { 36 | easing: transition.ease, 37 | duration: transition.duration, 38 | endValue: d[transition.key] 39 | }))) 40 | }, 41 | render() { 42 | let marks = this.props.data.map( 43 | (d, i) => { 44 | var props = {} 45 | transitionAttributes.forEach( 46 | transition => props[transition.prop] = transition.scale(this.getTweeningValue(`__data:${i}:${transition.key}`))) 47 | return 48 | }) 49 | return {marks} 50 | } 51 | }) 52 | 53 | return VisualGroup 54 | } 55 | 56 | class Circle extends React.Component { 57 | render() { 58 | return 59 | } 60 | } 61 | 62 | let TransitionGroup = animateGroup(Circle, [ 63 | { key: 0, prop: 'cx', scale: xscale, duration: 2000, easing: easingTypes.linear, start: 0}, 64 | { key: 1, prop: 'cy', scale: yscale, duration: 2000, easing: easingTypes.linear, start: 0} 65 | ]) 66 | 67 | ReactDOM.render( 68 | 69 | 70 | 71 | 72 | 73 | 74 | , element) 75 | -------------------------------------------------------------------------------- /src/explorer/images/color/category20.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/explorer/components/vis/marks/Point.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import dl from 'datalib' 3 | import d3 from 'd3' 4 | import React from 'react' 5 | import { getAccessorName, isBinField } from '../../../helpers/field' 6 | const { svg } = React.DOM 7 | const XDEFAULT = d => 30 8 | const YDEFAULT = d => 15 9 | const FILL_OPACITY_DEFAULT = d => 0.4 10 | 11 | export default class Point extends React.Component { 12 | getDefaultScales() { 13 | const { scales } = this.props 14 | let symbol = d3.svg.symbol() 15 | return { 16 | opacity: FILL_OPACITY_DEFAULT, 17 | size: scales.size.__default__, 18 | symbol: symbol, 19 | shape: scales.shape.__default__, 20 | color: scales.color.__default__, 21 | x: XDEFAULT, 22 | y: YDEFAULT 23 | } 24 | } 25 | 26 | getSymbolScales(props) { 27 | const { markData, width, height } = this.props 28 | const { field, scale, shelf } = props 29 | const name = getAccessorName(field) 30 | switch(shelf) { 31 | case 'row': 32 | return { 33 | y: (d) => scale(d[name]) 34 | } 35 | case 'col': 36 | return { 37 | x: (d) => scale(d[name]) 38 | } 39 | case 'color': 40 | let color = (d) => scale(d[name]) 41 | return { 42 | color 43 | } 44 | case 'size': 45 | return { 46 | size: d => scale(d[name]) 47 | } 48 | case 'shape': 49 | return { 50 | shape: d => scale(d[name]) 51 | } 52 | case 'opacity': 53 | return { 54 | opacity: d => scale(d[name]) 55 | } 56 | default: 57 | return { 58 | } 59 | } 60 | } 61 | 62 | getAttributeTransforms() { 63 | const { transformFields } = this.props 64 | let transforms = _.merge( 65 | this.getDefaultScales(), 66 | _.reduce(_.map(transformFields, (fs) => this.getSymbolScales(fs)), _.merge, {})) 67 | transforms.transform = (d, i) => `translate(${transforms.x(d, i)}, ${transforms.y(d, i)})` 68 | transforms.d = d => { 69 | return transforms.symbol.size(transforms.size(d)).type(transforms.shape(d))() 70 | } 71 | return transforms 72 | } 73 | 74 | _d3Render() { 75 | const transforms = this.getAttributeTransforms() 76 | let symbols = d3.select(this.refs.d3container).selectAll("g.symbol") 77 | .data(this.props.markData) 78 | symbols.enter().append("g").attr("class", "symbol").append('path') 79 | symbols.selectAll('g.symbol path') 80 | .attr('stroke-width', 1) 81 | .attr('d', transforms.d) 82 | .attr('fill', transforms.color) 83 | .attr('fill-opacity', transforms.opacity) 84 | .attr('stroke-opacity', transforms.opacity) 85 | .attr('stroke', transforms.color) 86 | .attr('transform', transforms.transform) 87 | symbols.exit().remove() 88 | } 89 | 90 | componentDidMount() { 91 | this._d3Render() 92 | } 93 | 94 | componentDidUpdate() { 95 | this._d3Render() 96 | } 97 | 98 | render() { 99 | const { width, height } = this.props 100 | return svg({ref: 'd3container', width, height}) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/explorer/components/FieldDragLayer.js: -------------------------------------------------------------------------------- 1 | import className from 'classnames' 2 | import React, { PropTypes } from 'react' 3 | import { DragLayer } from 'react-dnd' 4 | const { div, i: icon } = React.DOM 5 | import { getFieldFunctionDisplayName } from '../helpers/field' 6 | import { FieldIcon } from './FieldIcon' 7 | 8 | function getItemStyles(props) { 9 | const { initialOffset, currentOffset } = props 10 | if (!initialOffset || !currentOffset) { 11 | return { 12 | display: 'none' 13 | } 14 | } 15 | 16 | let { x, y } = currentOffset 17 | 18 | const transform = `translate(${x}px, ${y}px)` 19 | return { 20 | transform: transform, 21 | WebkitTransform: transform, 22 | MozTransform: transform, 23 | msTransform: transform 24 | } 25 | } 26 | 27 | class ShelfFieldDragPreview extends React.Component { 28 | render() { 29 | const { name, type } = this.props 30 | const { typecast, func } = this.props.field 31 | return div({className: className("field-wrap", {"remove": this.props.showTrashCan})}, 32 | div({className: "icon-wrap"}, ), 33 | div({className: "func-wrap"}, getFieldFunctionDisplayName(func)), 34 | div({className: className("name-wrap", {"has-func": func != null})}, name), 35 | this.props.showTrashCan ? div({className: "option-wrap"} , icon({className: "fa fa-trash-o"})) : null) 36 | } 37 | } 38 | 39 | class TableFieldDragPreview extends React.Component { 40 | render() { 41 | return div({className: "field-wrap"}, 42 | div({className: "icon-wrap"}, ), 43 | div({className: "name-wrap", style: {width: 210}}, this.props.name)) 44 | } 45 | } 46 | 47 | class FieldDragLayer extends React.Component { 48 | renderItem(type, item) { 49 | switch (type) { 50 | case 'ShelfField': 51 | return 52 | case 'TableField': 53 | return 54 | default: 55 | return null 56 | } 57 | } 58 | 59 | render() { 60 | const { item, itemType, isDragging, showTrashCan } = this.props 61 | // if (!isDragging) return null 62 | return div({className: className("field-drag-layer")}, 63 | div({className: "field-drag-wrap", style: getItemStyles(this.props)}, 64 | this.renderItem(itemType, _.extend({showTrashCan}, item)))) 65 | } 66 | } 67 | 68 | FieldDragLayer.propTypes = { 69 | item: PropTypes.object, 70 | itemType: PropTypes.string, 71 | initialOffset: PropTypes.shape({ 72 | x: PropTypes.number.isRequired, 73 | y: PropTypes.number.isRequired 74 | }), 75 | currentOffset: PropTypes.shape({ 76 | x: PropTypes.number.isRequired, 77 | y: PropTypes.number.isRequired 78 | }), 79 | isDragging: PropTypes.bool.isRequired, 80 | showTrashCan: PropTypes.bool.isRequired 81 | } 82 | 83 | export default DragLayer((monitor) => ({ 84 | item: monitor.getItem(), 85 | itemType: monitor.getItemType(), 86 | initialOffset: monitor.getInitialSourceClientOffset(), 87 | currentOffset: monitor.getSourceClientOffset(), 88 | isDragging: monitor.isDragging() 89 | }))(FieldDragLayer) 90 | -------------------------------------------------------------------------------- /src/explorer/components/DataSource.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import className from 'classnames' 3 | import _ from 'lodash' 4 | import { createDropdownComponent } from '../../components/Dropdown' 5 | import { AGGREGATES } from '../helpers/field' 6 | import { getTable } from '../ducks/datasources' 7 | import { TableField } from './Field' 8 | 9 | const {div, i: icon, span} = React.DOM 10 | 11 | class DataSource extends React.Component { 12 | render() { 13 | switch(this.props.type) { 14 | case 'table': 15 | case 'dataframe': 16 | return div({className: "datasource-name", onClick: (() => this.props.onSelect(_.pick(this.props, ['id', 'datasource_id', 'name'])))}, 17 | icon({className: "fa fa-table"}), this.props.name) 18 | case 'db': 19 | return div({className: "datasource-db"}, 20 | div({className: "datasource-db-name"}, 21 | icon({className: "fa fa-database"}), this.props.name), 22 | div({className: "datasource-db-tables"}, 23 | this.props.tables.map( 24 | (table, i) => ))) 25 | } 26 | } 27 | } 28 | 29 | class DataSourceSelect extends React.Component { 30 | onSelect(table) { 31 | this.props.onSelectTable(table) 32 | this.props.closeDropdown() 33 | } 34 | 35 | render() { 36 | let is_open = this.props.isDropdownOpen 37 | let dropdown = !is_open ? null : div( 38 | {className: "datasource-dropdown"}, 39 | div({className: "datasource-list"}, 40 | this.props.sourceIds.map( 41 | datasource_id => 42 | )), 43 | div({className: "datasource-add"}, icon({className: "fa fa-plus"}), "Add data source")) 44 | 45 | let table = getTable(this.props.sources, this.props.tableId) 46 | 47 | return div({className: "datasource-select"}, 48 | div({className: "datasource-title", onClick: this.props.toggleDropdown}, 49 | icon({className: "fa fa-table"}), 50 | table ? table.name : "Connect data source...", 51 | icon({className: className("fa", {"fa-caret-down": !is_open, "fa-caret-up": is_open})})), 52 | dropdown) 53 | } 54 | } 55 | DataSourceSelect = createDropdownComponent(DataSourceSelect) 56 | 57 | class TableSchema extends React.Component { 58 | render() { 59 | const { tableId, onSelectField } = this.props 60 | 61 | if (tableId == null) 62 | return null 63 | 64 | const table = getTable(this.props.sources, tableId) 65 | 66 | if (table.isLoading) 67 | return div({className: "datasource-table-fields"}, "Loading...") 68 | 69 | return div({className: "datasource-table-fields"}, 70 | AGGREGATES.map( 71 | (agg) => 72 | onSelectField(agg)} />), 73 | table.schema.map( 74 | (field, i) => 75 | onSelectField({tableId, fieldId: field.__id__})} />)) 76 | } 77 | } 78 | 79 | export { DataSourceSelect, TableSchema } 80 | -------------------------------------------------------------------------------- /src/explorer/data/domain.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { getFieldType, getAccessorName, isAggregateType, isBinField, isGroupByField } from '../helpers/field' 3 | 4 | export class QuantitativeAggregator { 5 | constructor() { 6 | this._result = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY} 7 | } 8 | add(value) { 9 | if(value < this._result.min) { 10 | this._result.min = value 11 | } 12 | if(value > this._result.max) { 13 | this._result.max = value 14 | } 15 | return this 16 | } 17 | result() { 18 | return this._result 19 | } 20 | flush() { 21 | return this 22 | } 23 | } 24 | 25 | export class OrdinalAggregator { 26 | constructor() { 27 | this._result = {} 28 | } 29 | add(value) { 30 | this._result[value] = true 31 | return this 32 | } 33 | result() { 34 | let result = [] 35 | for (let i = 0, keys = Object.keys(this._result), len = keys.length; i < len; i++) { 36 | result.push(keys[i]) 37 | } 38 | return result.sort() 39 | } 40 | flush() { 41 | return this 42 | } 43 | } 44 | 45 | function aggregateDatum(aggregator, datum, key, binWidth) { 46 | if (null != datum[key]) { 47 | aggregator.add(datum[key]) 48 | if(binWidth) aggregator.add(binWidth(datum[key])) 49 | } 50 | else if (null != datum.values) { 51 | for(let i = 0, len = datum.values.length; i < len; i++) { 52 | aggregator.add(datum.values[i][key]) 53 | if(binWidth) aggregator.add(binWidth(datum.values[i][key])) 54 | } 55 | } 56 | else { 57 | throw Error(`Domain construction: Not supposed to get here: Missing key ${key} and no values`) 58 | } 59 | } 60 | 61 | function aggregateAxes(domains, axes) { 62 | for (let i = 0, len = axes.length; i < len; i++) { 63 | let axis = axes[i] 64 | if (null == axis.domain) continue 65 | let domain = axis.getDomain() 66 | domains[axis.field.accessor].add(domain.min).add(domain.max) 67 | } 68 | } 69 | 70 | const AGGREGATOR = { 71 | 'Q' : QuantitativeAggregator, 72 | 'O' : OrdinalAggregator 73 | } 74 | 75 | function nextStep(step) { 76 | return (d) => d + step 77 | } 78 | 79 | function nextTime(timeUnit, step) { 80 | return (d) => d3.time[timeUnit].offset(d, step) 81 | } 82 | 83 | export function calculateDomains(data, fields, axes) { 84 | let domains = {} 85 | if (data == null) return domains 86 | 87 | let isBin = {} 88 | for (let i = 0, len = fields.length; i < len; i++) { 89 | let field = fields[i] 90 | if (domains[field.accessor]) continue 91 | domains[field.accessor] = new AGGREGATOR[field.algebraType]() 92 | isBin[field.accessor] = isBinField(field) ? 93 | ('time' == getFieldType(field) && !_.contains(field.func, 'bin') 94 | ? nextStep(1) 95 | : 'time' == getFieldType(field) 96 | ? nextTime(field.binSettings.unit.type, field.binSettings.step) 97 | : nextStep(field.binSettings.step)) : null 98 | } 99 | 100 | aggregateAxes(domains, axes.row) 101 | aggregateAxes(domains, axes.col) 102 | for (let i = 0, keys = Object.keys(domains), len = data.length, klen = keys.length; i < len; i++) { 103 | let datum = data[i] 104 | for (let k = 0; k < klen; k++) { 105 | let key = keys[k] 106 | aggregateDatum(domains[key], datum, key, isBin[key]) 107 | } 108 | } 109 | 110 | return _.mapValues(domains, (agg) => agg.result()) 111 | } 112 | -------------------------------------------------------------------------------- /src/explorer/ducks/datasources.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import u from 'updeep' 3 | import fetch from 'isomorphic-fetch' 4 | import dl from 'datalib' 5 | 6 | export function getDatasource(sources, tableId) { 7 | if (tableId == null) return null 8 | return sources[tableId.id] || sources[tableId.datasource_id] 9 | } 10 | 11 | export function getTable(sources, tableId) { 12 | const datasource = getDatasource(sources, tableId) 13 | if (datasource == null) return null 14 | if (datasource.tables != null) { 15 | return _.find(datasource.tables, (datasource_table) => datasource_table.name === tableId.name) 16 | } else { 17 | return datasource 18 | } 19 | } 20 | 21 | export function getField(sources, tableId, fieldId) { 22 | if (fieldId == null) return null 23 | const table = getTable(sources, tableId) 24 | if (table == null) return null 25 | return table.schema[fieldId] 26 | } 27 | 28 | /* ACTION TYPES */ 29 | 30 | export const REQUEST_TABLE_DATA = 'explorer/datasources/REQUEST_TABLE_DATA' 31 | export const RECEIVE_TABLE_DATA = 'explorer/datasources/RECEIVE_TABLE_DATA' 32 | export const SELECT_TABLE = 'explorer/datasources/SELECT_TABLE' 33 | 34 | /* ACTIONS */ 35 | 36 | export function selectTable(tableId) { 37 | return { 38 | type: SELECT_TABLE, 39 | tableId 40 | } 41 | } 42 | 43 | export function requestTableData(tableId) { 44 | return { 45 | type: REQUEST_TABLE_DATA, 46 | tableId 47 | } 48 | } 49 | 50 | export function receiveTableData(tableId, data) { 51 | return { 52 | type: RECEIVE_TABLE_DATA, 53 | tableId, 54 | data 55 | } 56 | } 57 | 58 | export function connectTable(tableId) { 59 | return (dispatch, getState) => { 60 | const table = getTable(getState().datasources.BY_ID, tableId) 61 | dispatch(requestTableData(tableId)) 62 | 63 | return fetch(table.url).then( 64 | response => { 65 | switch(table.settings.format) { 66 | case 'csv': 67 | return response.text() 68 | default: 69 | return response.json() 70 | } 71 | } 72 | ).then( 73 | data => { 74 | data = dl.read(data, {type: table.settings.format, parse: 'auto'}) 75 | dispatch(receiveTableData(tableId, data)) 76 | } 77 | ) 78 | } 79 | } 80 | 81 | export function connectTableIfNecessary(tableId) { 82 | return (dispatch, getState) => { 83 | const table = getTable(getState().datasources.BY_ID, tableId) 84 | if (table.url != null && !table.isLoading && table.schema == null) { 85 | return dispatch(connectTable(tableId)) 86 | } 87 | } 88 | } 89 | 90 | /* REDUCER */ 91 | 92 | const initialState = { 93 | IDS: [], 94 | BY_ID: {} 95 | } 96 | 97 | export default function reducer(state = initialState, action) { 98 | let datasource, schema 99 | 100 | switch (action.type) { 101 | case REQUEST_TABLE_DATA: 102 | datasource = getTable(state.BY_ID, action.tableId) 103 | return u({BY_ID: { [datasource.id]: {isLoading: true}}}, state) 104 | case RECEIVE_TABLE_DATA: 105 | datasource = getTable(state.BY_ID, action.tableId) 106 | schema = _(action.data.__types__) 107 | .map((v, k) => { return {name: k, type: v} }) 108 | .each((field, i) => field.__id__ = i).value() 109 | return u({BY_ID: {[datasource.id]: {isLoading: false, data: () => action.data, schema: () => schema}}}, state) 110 | case SELECT_TABLE: 111 | return u({selectedTable: () => action.tableId}, state) 112 | default: 113 | return state 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /_sass/_syntax.scss: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffc; } 2 | .highlight .c { color: #999; } /* Comment */ 3 | .highlight .err { color: #a00; background-color: #faa } /* Error */ 4 | .highlight .k { color: #069; } /* Keyword */ 5 | .highlight .o { color: #555 } /* Operator */ 6 | .highlight .cm { color: #09f; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #099 } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999; } /* Comment.Single */ 9 | .highlight .cs { color: #999; } /* Comment.Special */ 10 | .highlight .gd { background-color: #fcc; border: 1px solid #c00 } /* Generic.Deleted */ 11 | .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #f00 } /* Generic.Error */ 13 | .highlight .gh { color: #030; } /* Generic.Heading */ 14 | .highlight .gi { background-color: #cfc; border: 1px solid #0c0 } /* Generic.Inserted */ 15 | .highlight .go { color: #aaa } /* Generic.Output */ 16 | .highlight .gp { color: #009; } /* Generic.Prompt */ 17 | .highlight .gs { } /* Generic.Strong */ 18 | .highlight .gu { color: #030; } /* Generic.Subheading */ 19 | .highlight .gt { color: #9c6 } /* Generic.Traceback */ 20 | .highlight .kc { color: #069; } /* Keyword.Constant */ 21 | .highlight .kd { color: #069; } /* Keyword.Declaration */ 22 | .highlight .kn { color: #069; } /* Keyword.Namespace */ 23 | .highlight .kp { color: #069 } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #069; } /* Keyword.Reserved */ 25 | .highlight .kt { color: #078; } /* Keyword.Type */ 26 | .highlight .m { color: #f60 } /* Literal.Number */ 27 | .highlight .s { color: #d44950 } /* Literal.String */ 28 | .highlight .na { color: #4f9fcf } /* Name.Attribute */ 29 | .highlight .nb { color: #366 } /* Name.Builtin */ 30 | .highlight .nc { color: #0a8; } /* Name.Class */ 31 | .highlight .no { color: #360 } /* Name.Constant */ 32 | .highlight .nd { color: #99f } /* Name.Decorator */ 33 | .highlight .ni { color: #999; } /* Name.Entity */ 34 | .highlight .ne { color: #c00; } /* Name.Exception */ 35 | .highlight .nf { color: #c0f } /* Name.Function */ 36 | .highlight .nl { color: #99f } /* Name.Label */ 37 | .highlight .nn { color: #0cf; } /* Name.Namespace */ 38 | .highlight .nt { color: #2f6f9f; } /* Name.Tag */ 39 | .highlight .nv { color: #033 } /* Name.Variable */ 40 | .highlight .ow { color: #000; } /* Operator.Word */ 41 | .highlight .w { color: #bbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #f60 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #f60 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #f60 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #f60 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #c30 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #c30 } /* Literal.String.Char */ 48 | .highlight .sd { color: #c30; font-style: italic } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #c30 } /* Literal.String.Double */ 50 | .highlight .se { color: #c30; } /* Literal.String.Escape */ 51 | .highlight .sh { color: #c30 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #a00 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #c30 } /* Literal.String.Other */ 54 | .highlight .sr { color: #3aa } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #c30 } /* Literal.String.Single */ 56 | .highlight .ss { color: #fc3 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #366 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #033 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #033 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #033 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #f60 } /* Literal.Number.Integer.Long */ 62 | 63 | .css .o, 64 | .css .o + .nt, 65 | .css .nt + .nt { color: #999; } 66 | -------------------------------------------------------------------------------- /public/css/syntax.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffc; } 2 | .highlight .c { color: #999; } /* Comment */ 3 | .highlight .err { color: #a00; background-color: #faa } /* Error */ 4 | .highlight .k { color: #069; } /* Keyword */ 5 | .highlight .o { color: #555 } /* Operator */ 6 | .highlight .cm { color: #09f; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #099 } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999; } /* Comment.Single */ 9 | .highlight .cs { color: #999; } /* Comment.Special */ 10 | .highlight .gd { background-color: #fcc; border: 1px solid #c00 } /* Generic.Deleted */ 11 | .highlight .ge { font-style: italic } /* Generic.Emph */ 12 | .highlight .gr { color: #f00 } /* Generic.Error */ 13 | .highlight .gh { color: #030; } /* Generic.Heading */ 14 | .highlight .gi { background-color: #cfc; border: 1px solid #0c0 } /* Generic.Inserted */ 15 | .highlight .go { color: #aaa } /* Generic.Output */ 16 | .highlight .gp { color: #009; } /* Generic.Prompt */ 17 | .highlight .gs { } /* Generic.Strong */ 18 | .highlight .gu { color: #030; } /* Generic.Subheading */ 19 | .highlight .gt { color: #9c6 } /* Generic.Traceback */ 20 | .highlight .kc { color: #069; } /* Keyword.Constant */ 21 | .highlight .kd { color: #069; } /* Keyword.Declaration */ 22 | .highlight .kn { color: #069; } /* Keyword.Namespace */ 23 | .highlight .kp { color: #069 } /* Keyword.Pseudo */ 24 | .highlight .kr { color: #069; } /* Keyword.Reserved */ 25 | .highlight .kt { color: #078; } /* Keyword.Type */ 26 | .highlight .m { color: #f60 } /* Literal.Number */ 27 | .highlight .s { color: #d44950 } /* Literal.String */ 28 | .highlight .na { color: #4f9fcf } /* Name.Attribute */ 29 | .highlight .nb { color: #366 } /* Name.Builtin */ 30 | .highlight .nc { color: #0a8; } /* Name.Class */ 31 | .highlight .no { color: #360 } /* Name.Constant */ 32 | .highlight .nd { color: #99f } /* Name.Decorator */ 33 | .highlight .ni { color: #999; } /* Name.Entity */ 34 | .highlight .ne { color: #c00; } /* Name.Exception */ 35 | .highlight .nf { color: #c0f } /* Name.Function */ 36 | .highlight .nl { color: #99f } /* Name.Label */ 37 | .highlight .nn { color: #0cf; } /* Name.Namespace */ 38 | .highlight .nt { color: #2f6f9f; } /* Name.Tag */ 39 | .highlight .nv { color: #033 } /* Name.Variable */ 40 | .highlight .ow { color: #000; } /* Operator.Word */ 41 | .highlight .w { color: #bbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #f60 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #f60 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #f60 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #f60 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #c30 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #c30 } /* Literal.String.Char */ 48 | .highlight .sd { color: #c30; font-style: italic } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #c30 } /* Literal.String.Double */ 50 | .highlight .se { color: #c30; } /* Literal.String.Escape */ 51 | .highlight .sh { color: #c30 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #a00 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #c30 } /* Literal.String.Other */ 54 | .highlight .sr { color: #3aa } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #c30 } /* Literal.String.Single */ 56 | .highlight .ss { color: #fc3 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #366 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #033 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #033 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #033 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #f60 } /* Literal.Number.Integer.Long */ 62 | 63 | .css .o, 64 | .css .o + .nt, 65 | .css .nt + .nt { color: #999; } 66 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | RedCloth (4.2.9) 5 | activesupport (4.2.5.1) 6 | i18n (~> 0.7) 7 | json (~> 1.7, >= 1.7.7) 8 | minitest (~> 5.1) 9 | thread_safe (~> 0.3, >= 0.3.4) 10 | tzinfo (~> 1.1) 11 | addressable (2.3.8) 12 | coffee-script (2.4.1) 13 | coffee-script-source 14 | execjs 15 | coffee-script-source (1.10.0) 16 | colorator (0.1) 17 | ethon (0.8.1) 18 | ffi (>= 1.3.0) 19 | execjs (2.6.0) 20 | faraday (0.9.2) 21 | multipart-post (>= 1.2, < 3) 22 | ffi (1.9.10) 23 | gemoji (2.1.0) 24 | github-pages (48) 25 | RedCloth (= 4.2.9) 26 | github-pages-health-check (= 0.6.1) 27 | jekyll (= 3.0.3) 28 | jekyll-coffeescript (= 1.0.1) 29 | jekyll-feed (= 0.3.1) 30 | jekyll-gist (= 1.4.0) 31 | jekyll-mentions (= 1.0.0) 32 | jekyll-paginate (= 1.1.0) 33 | jekyll-redirect-from (= 0.9.1) 34 | jekyll-sass-converter (= 1.3.0) 35 | jekyll-seo-tag (= 1.0.0) 36 | jekyll-sitemap (= 0.10.0) 37 | jekyll-textile-converter (= 0.1.0) 38 | jemoji (= 0.5.1) 39 | kramdown (= 1.9.0) 40 | liquid (= 3.0.6) 41 | mercenary (~> 0.3) 42 | rdiscount (= 2.1.8) 43 | redcarpet (= 3.3.3) 44 | rouge (= 1.10.1) 45 | terminal-table (~> 1.4) 46 | github-pages-health-check (0.6.1) 47 | addressable (~> 2.3) 48 | net-dns (~> 0.8) 49 | public_suffix (~> 1.4) 50 | typhoeus (~> 0.7) 51 | html-pipeline (2.3.0) 52 | activesupport (>= 2, < 5) 53 | nokogiri (>= 1.4) 54 | i18n (0.7.0) 55 | jekyll (3.0.3) 56 | colorator (~> 0.1) 57 | jekyll-sass-converter (~> 1.0) 58 | jekyll-watch (~> 1.1) 59 | kramdown (~> 1.3) 60 | liquid (~> 3.0) 61 | mercenary (~> 0.3.3) 62 | rouge (~> 1.7) 63 | safe_yaml (~> 1.0) 64 | jekyll-coffeescript (1.0.1) 65 | coffee-script (~> 2.2) 66 | jekyll-feed (0.3.1) 67 | jekyll-gist (1.4.0) 68 | octokit (~> 4.2) 69 | jekyll-mentions (1.0.0) 70 | html-pipeline (~> 2.2) 71 | jekyll (~> 3.0) 72 | jekyll-paginate (1.1.0) 73 | jekyll-redirect-from (0.9.1) 74 | jekyll (>= 2.0) 75 | jekyll-sass-converter (1.3.0) 76 | sass (~> 3.2) 77 | jekyll-seo-tag (1.0.0) 78 | jekyll (>= 2.0) 79 | jekyll-sitemap (0.10.0) 80 | jekyll-textile-converter (0.1.0) 81 | RedCloth (~> 4.0) 82 | jekyll-watch (1.3.1) 83 | listen (~> 3.0) 84 | jemoji (0.5.1) 85 | gemoji (~> 2.0) 86 | html-pipeline (~> 2.2) 87 | jekyll (>= 2.0) 88 | json (1.8.3) 89 | kramdown (1.9.0) 90 | liquid (3.0.6) 91 | listen (3.0.5) 92 | rb-fsevent (>= 0.9.3) 93 | rb-inotify (>= 0.9) 94 | mercenary (0.3.5) 95 | mini_portile2 (2.0.0) 96 | minitest (5.8.4) 97 | multipart-post (2.0.0) 98 | net-dns (0.8.0) 99 | nokogiri (1.6.7.2) 100 | mini_portile2 (~> 2.0.0.rc2) 101 | octokit (4.2.0) 102 | sawyer (~> 0.6.0, >= 0.5.3) 103 | public_suffix (1.5.3) 104 | rb-fsevent (0.9.7) 105 | rb-inotify (0.9.7) 106 | ffi (>= 0.5.0) 107 | rdiscount (2.1.8) 108 | redcarpet (3.3.3) 109 | rouge (1.10.1) 110 | safe_yaml (1.0.4) 111 | sass (3.4.21) 112 | sawyer (0.6.0) 113 | addressable (~> 2.3.5) 114 | faraday (~> 0.8, < 0.10) 115 | terminal-table (1.5.2) 116 | thread_safe (0.3.5) 117 | typhoeus (0.8.0) 118 | ethon (>= 0.8.0) 119 | tzinfo (1.2.2) 120 | thread_safe (~> 0.1) 121 | 122 | PLATFORMS 123 | ruby 124 | 125 | DEPENDENCIES 126 | github-pages 127 | -------------------------------------------------------------------------------- /public/js/react-in-jekyll.min.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(n){if(l[n])return l[n].exports;var r=l[n]={exports:{},id:n,loaded:!1};return e[n].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var l={};return t.m=e,t.c=l,t.p="",t(0)}({0:function(e,t,l){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}var r=l(5),a=n(r),o=l(6),u=n(o),c=l(328),d=n(c);u["default"].render(a["default"].createElement(d["default"],null),document.getElementById("react-body"))},5:function(e,t){e.exports=React},6:function(e,t){e.exports=ReactDOM},328:function(e,t,l){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var l=0;l tick + 0.5).value() 12 | if (_.last(tickValues) > _.last(axisScale.domain())) tickValues.pop() 13 | axis 14 | .tickValues(tickValues) 15 | .tickSubdivide(tickValues[1] - tickValues[0] - 1) 16 | .tickFormat((s) => format(Math.floor(s))) 17 | } 18 | 19 | function axisTickHorizontalLabelShift(d) { 20 | let bounds = this.getBBox() 21 | const baseVal = this.parentElement.transform.baseVal 22 | let left = (baseVal[0] || baseVal.getItem(0)).matrix.e + bounds.x 23 | if (left < 0) { 24 | return 1 - left 25 | } 26 | else { 27 | let parentWidth = this.parentElement.parentElement.parentElement.width.baseVal.value 28 | let right = left + bounds.width 29 | if (right > parentWidth) { 30 | return parentWidth - right - 2 31 | } 32 | else { 33 | let current = d3.select(this).attr('x') 34 | return current ? current : 0 35 | } 36 | } 37 | } 38 | 39 | function axisTickVerticalLabelShift(d) { 40 | let bounds = this.getBBox() 41 | const baseVal = this.parentElement.transform.baseVal 42 | let top = (baseVal[0] || baseVal.getItem(0)).matrix.f + bounds.y 43 | if (top < 0) { 44 | return 1 - top 45 | } 46 | else { 47 | let parentHeight = this.parentElement.parentElement.parentElement.height.baseVal.value 48 | let bottom = top + bounds.height 49 | if (bottom > parentHeight) { 50 | return parentHeight - bottom - 1 51 | } 52 | else { 53 | let current = d3.select(this).attr('y') 54 | return current ? current : 0 55 | } 56 | } 57 | } 58 | 59 | export class Axis extends React.Component { 60 | _d3Axis() { 61 | let { orient, scale, field, markType } = this.props 62 | let axis = d3.svg.axis().scale(scale).orient(orient).ticks(5) 63 | if ('bar' == markType) { 64 | if ('integer' == field.type && 'bin' == field.func && field.binSettings.step == 1) { 65 | axisDiscretizeTicks(axis, d3.format(",d")) 66 | } 67 | else if ('time' == getFieldType(field) && !_.contains(field.func, 'bin')) { 68 | axisDiscretizeTicks(axis, d3.format("d")) 69 | } 70 | } 71 | else if ('time' == getFieldType(field) && !_.contains(field.func, 'bin')) { 72 | axis.tickFormat(d3.format("d")) 73 | } 74 | return axis 75 | } 76 | 77 | _d3Render() { 78 | let tickText = d3.select(this.refs.axisContainer) 79 | .call(this._d3Axis()) 80 | .selectAll("text") 81 | if (this.isHorizontalAxis()) { 82 | tickText.attr("x", axisTickHorizontalLabelShift) 83 | } 84 | else { 85 | tickText.attr("y", axisTickVerticalLabelShift) 86 | } 87 | } 88 | 89 | _orientTransform() { 90 | let { orient, width, height } = this.props 91 | let x = 'left' == orient ? width - 1: 0 92 | let y = 'top' == orient ? height : 0 93 | return `translate(${x}, ${y})` 94 | } 95 | 96 | isHorizontalAxis() { 97 | return 'top' == this.props.orient || 'bottom' == this.props.orient 98 | } 99 | 100 | componentDidMount() { 101 | this._d3Render() 102 | } 103 | 104 | componentDidUpdate() { 105 | this._d3Render() 106 | } 107 | 108 | render() { 109 | let { domain, name, orient, width, height } = this.props 110 | return div({}, 111 | svg({className: 'axis', width, height}, ), 112 | div({className: className('axis-label', {[orient]: true})}, name)) 113 | } 114 | } 115 | Axis.defaultProps = { 116 | orient: 'bottom' 117 | } 118 | -------------------------------------------------------------------------------- /src/explorer/css/components/querybuilder.scss: -------------------------------------------------------------------------------- 1 | @import '../../../css/flexbox.scss'; 2 | 3 | .querybuilder { 4 | @include flexbox; 5 | @include flex(0 0 60px); 6 | @include flex-flow(row); 7 | 8 | border: 1px solid #dfdfdf; 9 | background-color: white; 10 | margin-bottom: 10px; 11 | 12 | .querybuilder-type-spec { 13 | @include flexbox; 14 | @include flex(1 1 auto); 15 | position: relative; 16 | 17 | &.row { 18 | @include flex-flow(row); 19 | } 20 | 21 | &.col { 22 | @include flex-flow(column); 23 | } 24 | } 25 | 26 | .querybuilder-type-spec.row .querybuilder-shelf { 27 | @include flex-flow(column); 28 | 29 | &:not(:last-of-type) { 30 | border-right: 1px solid #dfdfdf; 31 | } 32 | 33 | label { 34 | display: block; 35 | padding: 1px 3px 0px; 36 | } 37 | } 38 | 39 | .querybuilder-type-spec.row.key .querybuilder-shelf { 40 | &:first-of-type { 41 | @include flex(0 0 auto); 42 | } 43 | 44 | label { 45 | width: 150px; 46 | } 47 | } 48 | 49 | .querybuilder-type-spec.col .querybuilder-shelf { 50 | @include flex-flow(row); 51 | &:not(:last-of-type) { 52 | border-bottom: 1px solid #dfdfdf; 53 | } 54 | 55 | label { 56 | padding: 1px 3px 0px; 57 | border-right: 1px solid #dfdfdf; 58 | background-color: #efefef; 59 | 60 | @include flexbox; 61 | @include flex(0 0 80px); 62 | @include align-items(center); 63 | } 64 | } 65 | } 66 | 67 | .querybuilder-field-settings { 68 | position: absolute; 69 | top: 25px; left: 88px; 70 | top: 55px; 71 | background-color: white; 72 | border: 1px solid #dfdfdf; 73 | border-radius: 2px; 74 | border-top-left-radius: 0px; 75 | box-shadow: 0 3px 5px #ccc; 76 | padding: 5px; 77 | z-index: 10; 78 | width: 200px; 79 | 80 | &.querybuilder-property-settings { 81 | top: 20px; 82 | left: 0px; 83 | right: 0px; 84 | width: auto; 85 | border-top-left-radius: 2px; 86 | } 87 | 88 | .querybuilder-field-options > label { 89 | font-weight: 600; 90 | 91 | a { 92 | padding-left: 8px; 93 | font-weight: 400; 94 | cursor: pointer; 95 | } 96 | } 97 | 98 | i.fa.fa-times.remove-link { 99 | position: absolute; 100 | right: 5px; 101 | top: 5px; 102 | z-index: 1; 103 | } 104 | 105 | .querybuilder-field-remove { 106 | color: #f18794; 107 | border-top: 1px solid #dfdfdf; 108 | margin: 0 -5px -5px; 109 | padding: 5px; 110 | cursor: pointer; 111 | 112 | &:hover, 113 | &:focus { 114 | background-color: #f18794; 115 | color: white; 116 | } 117 | } 118 | 119 | .querybuilder-func .querybuilder-field-settings-option-value { 120 | width: 50%; 121 | } 122 | 123 | .querybuilder-field-settings-options { 124 | @include flexbox; 125 | @include flex-flow(row wrap); 126 | margin-bottom: 0.5rem; 127 | 128 | .querybuilder-field-settings-option-value { 129 | @include flex(1 0 auto); 130 | } 131 | } 132 | } 133 | 134 | .querybuilder-property-settings { 135 | .querybuilder-color-static span { 136 | padding-right: 10px; 137 | } 138 | 139 | .querybuilder-color-settings, .querybuilder-color-sub-settings { 140 | @include flexbox; 141 | @include flex-flow(row wrap); 142 | @include flex-just(flex-start); 143 | 144 | input[type=checkbox] { 145 | margin-bottom: 6px; 146 | margin-right: 6px; 147 | } 148 | } 149 | 150 | .querybuilder-color-sub-settings { 151 | padding-left: 20px; 152 | 153 | &.querybuilder-color-sub-settings-input span { 154 | @include flex(0 0 35px); 155 | } 156 | } 157 | 158 | .querybuilder-color-settings label { 159 | @include flex(1 0 60px); 160 | } 161 | 162 | .querybuilder-color-settings label div { 163 | margin: 5px 0; 164 | height: 24px; 165 | 166 | svg { 167 | width: 40px; 168 | height: 24px; 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /src/explorer/ducks/result.js: -------------------------------------------------------------------------------- 1 | import u from 'updeep' 2 | import _ from 'lodash' 3 | import { combineReducers } from 'redux' 4 | import { getField, getTable, getDatasource } from './datasources' 5 | import { getFullQueryspec } from './queryspec' 6 | import { updateChartData } from './chartspec' 7 | import * as local from '../data/local' 8 | 9 | export const CHANGE_REQUEST_DATA = 'explorer/result/CHANGE_REQUEST_DATA' 10 | export const REQUEST_RESULT_DATA = 'explorer/result/REQUEST_RESULT_DATA' 11 | export const RECEIVE_RESULT_DATA = 'explorer/result/RECEIVE_RESULT_DATA' 12 | 13 | function makeQueryKey(query) { 14 | return JSON.stringify(query) 15 | } 16 | 17 | export function changeRequestData(key, tableType) { 18 | return { 19 | type: CHANGE_REQUEST_DATA, 20 | key, 21 | tableType 22 | } 23 | } 24 | 25 | export function requestResultData(key) { 26 | return { 27 | type: REQUEST_RESULT_DATA, 28 | key 29 | } 30 | } 31 | 32 | export function receiveResultData(key, response, error=false) { 33 | return _.extend({ 34 | type: RECEIVE_RESULT_DATA, 35 | key, 36 | error 37 | }, response) 38 | } 39 | 40 | export function fetchQueryData(datasources, queryspec) { 41 | return new Promise((resolve, reject) => { 42 | let datasource = getDatasource(datasources.BY_ID, datasources.selectedTable) 43 | if (datasource.data) { 44 | setTimeout(() => resolve(local.requestQuery(queryspec, datasource.data)), 0) 45 | } 46 | else { 47 | reject(Error(`Querying adapter not defined for protocol: ${datasource.protocol}`)) 48 | } 49 | }) 50 | } 51 | 52 | export function runQuery(datasources, key, queryspec, visualspec) { 53 | return (dispatch, getState) => { 54 | dispatch(requestResultData(key)) 55 | return fetchQueryData(datasources, queryspec).then( 56 | response => { 57 | dispatch(updateChartData(key, visualspec, response)) 58 | dispatch(receiveResultData(key, response)) 59 | }).catch(error => { 60 | console.error('Error: query error', error) 61 | dispatch(receiveResultData(key, null, true)) 62 | })} 63 | } 64 | 65 | export function runCurrentQueryIfNecessary() { 66 | return (dispatch, getState) => { 67 | let { datasources, queryspec, visualspec, result } = getState() 68 | let getTableField = _.curry(getField)(datasources.BY_ID, datasources.selectedTable) 69 | let usableQueryspec = getFullQueryspec(getTableField, queryspec, visualspec.table.type) 70 | let key = makeQueryKey(usableQueryspec) 71 | let queryResponse = result.cache[key] 72 | if (queryResponse == null || (!queryResponse.isLoading && !queryResponse.error && queryResponse.result == null)) { 73 | dispatch(runQuery(datasources, key, usableQueryspec, visualspec)) 74 | } 75 | else if (queryResponse && !queryResponse.isLoading && queryResponse.result) { 76 | dispatch(updateChartData(key, visualspec, queryResponse)) 77 | } 78 | dispatch(changeRequestData(key, visualspec.table.type)) 79 | } 80 | } 81 | 82 | const cacheState = { 83 | } 84 | 85 | function cache(state = cacheState, action) { 86 | switch(action.type) { 87 | case REQUEST_RESULT_DATA: 88 | return _.extend({}, state, { [action.key] : { isLoading: true } }) 89 | case RECEIVE_RESULT_DATA: 90 | return _.extend({}, state, { 91 | [action.key]: { 92 | isLoading: false, 93 | error: action.error, 94 | key: action.key, 95 | queryspec: action.queryspec, 96 | query: action.query, 97 | result: action.result 98 | }}) 99 | default: 100 | return state 101 | } 102 | } 103 | 104 | const initialState = { 105 | last: null, 106 | current: null 107 | } 108 | 109 | function render(state=initialState, action) { 110 | switch(action.type) { 111 | case CHANGE_REQUEST_DATA: 112 | return { last: state.current, current: [action.key, action.tableType] } 113 | default: 114 | return state 115 | } 116 | } 117 | 118 | const reducer = combineReducers({render, cache}) 119 | export default reducer 120 | -------------------------------------------------------------------------------- /src/components/basketball/court.js: -------------------------------------------------------------------------------- 1 | import { ShotChartInteractionSignals, ShotChartInteractionPredicates, filterExclude } from './interactions' 2 | 3 | var ShotChart = { 4 | "name": "shotChart", 5 | "type": "group", 6 | "properties": { 7 | "update": { 8 | "x": { "value": 0 }, 9 | "y": { "value": 150 }, 10 | "width": {"value": 450 }, 11 | "height": {"value": 1.1 * 450 } 12 | } 13 | }, 14 | "scales": [ 15 | { 16 | "name": "width", 17 | "type": "linear", 18 | "range": "width", 19 | "domain": [0, 500] 20 | }, 21 | { 22 | "name": "height", 23 | "type": "linear", 24 | "range": "height", 25 | "domain": [0, 550] 26 | }, 27 | { 28 | "name": "x", 29 | "type": "linear", 30 | "range": "width", 31 | // "reverse" : true, // hoop on bottom view 32 | "domain": [-250, 250] 33 | }, 34 | { 35 | "name": "y", 36 | "type": "linear", 37 | "range": "height", 38 | "reverse": true, // hoop on top view 39 | "domain": [-50, 500] 40 | }, 41 | ], 42 | "marks": [ 43 | { 44 | "type": "symbol", 45 | "from": { 46 | "data": "table", 47 | "transform": [ 48 | { 49 | "type": "filter", 50 | "test": `${filterExclude()}` 51 | } 52 | ] 53 | }, 54 | "key": "shot_id", 55 | "properties": { 56 | "enter": { 57 | "shape": { "scale": "playerSymbol", "field": "PLAYER_NAME" }, 58 | "x": {"scale": "x", "value": 0}, 59 | "y": {"scale": "y", "value": 0}, 60 | "fill": { "scale": "makeColor", "field": "EVENT_TYPE" }, 61 | "size": { "scale": "width", "value": 70 } 62 | }, 63 | "update": { 64 | "x": {"scale": "x", "field": "LOC_X"}, 65 | "y": {"scale": "y", "field": "LOC_Y"}, 66 | "fillOpacity" : { 67 | "rule": [ 68 | { 69 | "predicate": {"name": "chartBrush", "x": {"field": "LOC_X"}, "y": {"field": "LOC_Y"}}, 70 | "value": 0.8 71 | }, 72 | { "value": 0.2 } 73 | ] 74 | } 75 | }, 76 | "exit": { 77 | "x": {"scale": "x", "value": 0}, 78 | "y": {"scale": "y", "value": 0} 79 | } 80 | } 81 | }, 82 | { 83 | "type": "arc", 84 | "from": {"data": "arcs"}, 85 | "properties": { 86 | "enter": { 87 | "stroke": {"value": "#000000"}, 88 | "strokeDash": {"field": "strokeDash"}, 89 | "x": {"scale": "x", "field": "x"}, 90 | "y": {"scale": "y", "field": "y"}, 91 | "outerRadius": {"scale": "width", "field": "radius"}, 92 | "innerRadius": {"scale": "width", "field": "radius"}, 93 | "startAngle": {"scale": "degreeRadians", "field": "startAngle"}, 94 | "endAngle": {"scale": "degreeRadians", "field": "endAngle"} 95 | } 96 | } 97 | }, 98 | { 99 | "type": "rect", 100 | "from": {"data": "courtLines"}, 101 | "properties": { 102 | "enter": { 103 | "fill": {"value": null}, 104 | "stroke": {"value": "#000000"}, 105 | "strokeWidth": {"value": 1}, 106 | "x": {"scale": "x", "field": "x"}, 107 | "y": {"scale": "y", "field": "y"}, 108 | "x2": {"scale": "x", "field": "x2"}, 109 | "y2": {"scale": "y", "field": "y2"} 110 | } 111 | } 112 | }, 113 | { 114 | "type": "rect", 115 | "properties": { 116 | "enter": { 117 | "fill": {"value": "grey"}, 118 | "fillOpacity": {"value": 0.2} 119 | }, 120 | "update": { 121 | "x": {"scale": "x", "signal": "minchartX"}, 122 | "x2": {"scale": "x", "signal": "maxchartX"}, 123 | "y": {"scale": "y", "signal": "minchartY"}, 124 | "y2": {"scale": "y", "signal": "maxchartY"} 125 | } 126 | } 127 | } 128 | ] 129 | } 130 | 131 | export { ShotChart } 132 | -------------------------------------------------------------------------------- /src/nba-shot-chart-vega.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import d3 from 'd3' 4 | import vg from 'vega' 5 | import { ShotChartSpec } from './components/basketball' 6 | 7 | // harden 201935 8 | // curry 201939 9 | // kobe 977 10 | // lebron 2544 11 | // korver 2594 12 | // klay 202691 13 | // westbrook 201566 14 | // durant 201142 15 | // dirk 1717 16 | // rose 201565 17 | // anthony davis 203076 18 | 19 | // http://stats.nba.com/stats/playerdashptshotlog?DateFrom=&DateTo=&GameSegment=&LastNGames=0&LeagueID=00&Location=&Month=0&OpponentTeamID=0&Outcome=&Period=0&PlayerID=&Season=2014-15&SeasonSegment=&SeasonType=Regular+Season&TeamID=0&VsConference=&VsDivision= 20 | 21 | var ShotChartView 22 | var Cache = {} 23 | var shotChartUrls = [ 24 | {"name": "James Harden (2014-2015)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/5c74a5dcd7b257faa985f28c932a684ed4cea065/james-harden-shotchartdetail.json", "dash_url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/f6d6496474af6dfba0b73f36dbd0e00ce0fc2f42/james-harden-2014-2015-player-dash.json"}, 25 | {"name": "Stephen Curry (2014-2015)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/d159840109c00928f515bf0ed496f4f487b326ba/stephen-curry-shotchartdetail.json" }, 26 | {"name": "Kobe Bryant (2007-2008)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/0fbd65f9f795a5fba8c8ccefce060fd3082264fb/kobe-2007-2008-shot-chart.json" }, 27 | {"name": "Kobe Bryant (2009-2010)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/a19ec840d7d67c388fc3f2eea3d51c9b7cdcf4b0/kobe-2009-2010-shot-chart.json" }, 28 | {"name": "Lebron James (2009-2010)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/0fbd65f9f795a5fba8c8ccefce060fd3082264fb/lebron-james-2009-2010-shot-chart.json" }, 29 | {"name": "Lebron James (2010-2011)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/0fbd65f9f795a5fba8c8ccefce060fd3082264fb/lebron-james-2010-2011-shot-chart.json" }, 30 | {"name": "Lebron James (2011-2012)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/d53dbb96502622b9509880fb671cf50846130636/lebron-james-2011-2012-shot-chart.json" }, 31 | {"name": "Lebron James (2012-2013)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/d53dbb96502622b9509880fb671cf50846130636/lebron-james-2012-2013-shot-chart.json" }, 32 | {"name": "Kevin Durant (2013-2014)", "url": "https://gist.githubusercontent.com/sandbox/7f6065c867a5f355207e/raw/d53dbb96502622b9509880fb671cf50846130636/kevin-durant-2013-2014-shot-chart.json" } 33 | ] 34 | 35 | vg.parse.spec(ShotChartSpec, function(chart) { 36 | ShotChartView = chart({el: "#shot-chart"}) 37 | ShotChartView.onSignal('minchartX', (signal, value) => console.log(signal, value)) 38 | ShotChartView.onSignal('maxchartX', (signal, value) => console.log(signal, value)) 39 | renderShotChart({ target: { value: shotChartUrls[0].url }}) 40 | }) 41 | 42 | function setChartData(data) { 43 | ShotChartView.data('table').remove(() => true).insert(data) 44 | ShotChartView.update({duration: 300, ease: "quad-in-out"}) 45 | console.log(ShotChartView.toImageURL('png')) 46 | } 47 | 48 | function renderShotChart(evt) { 49 | let url = evt.target.value 50 | if (Cache[url]) { 51 | setChartData(Cache[url]) 52 | } else { 53 | d3.json( 54 | url, 55 | function(error, json) { 56 | if (error) return console.warn(error) 57 | let headers = json.resultSets[0].headers 58 | let data = json.resultSets[0].rowSet.map(function(d) { 59 | let row = headers.reduce(function(memo, header, i) { 60 | memo[header] = d[i] 61 | return memo 62 | }, {}) 63 | row.shot_id = `${row.GAME_ID}_${row.GAME_EVENT_ID}` 64 | return row}) 65 | Cache[url] = data 66 | setChartData(Cache[url]) 67 | }) 68 | } 69 | } 70 | 71 | ReactDOM.render(, document.getElementById("shot-chart-player-select")) 76 | -------------------------------------------------------------------------------- /src/components/basketball/interactions.js: -------------------------------------------------------------------------------- 1 | function brushSignal(group, name, event, direction, scale, min, max) { 2 | return [ 3 | { 4 | "name": `${name}Start`, 5 | "init": -1, 6 | "streams": [{ 7 | "type": `@${group}:mousedown, @${group}:touchstart`, 8 | "expr": `${event}(scope)`, 9 | "scale": {"scope": "scope", "name": scale, "invert": true} 10 | }] 11 | }, 12 | { 13 | "name": `${name}End`, 14 | "init": -1, 15 | "streams": [{ 16 | "type": `@${group}:mousedown, @${group}:touchstart, [@${group}:mousedown, window:mouseup] > window:mousemove, [@${group}:touchstart, window:touchend] > window:touchmove`, 17 | "expr": `clamp(${event}(scope), 0, scope.${direction})`, 18 | "scale": {"scope": "scope", "name": scale, "invert": true} 19 | }] 20 | }, 21 | {"name": `min${name}`, "expr": `max(min(${name}Start, ${name}End), ${min})`}, 22 | {"name": `max${name}`, "expr": `min(max(${name}Start, ${name}End), ${max})`} 23 | ] 24 | } 25 | 26 | function brushPredicate(name, arg) { 27 | return [ 28 | { 29 | "name": `${name}Equal`, 30 | "type": "==", 31 | "operands": [{"signal": `${name}Start`}, {"signal": `${name}End`}] 32 | }, 33 | { 34 | "name": `${name}Range`, 35 | "type": "in", 36 | "item": {"arg": arg}, 37 | "range": [{"signal": `${name}Start`}, {"signal": `${name}End`}] 38 | }, 39 | { 40 | "name": `${name}Brush`, 41 | "type": "or", 42 | "operands": [{"predicate": `${name}Equal`}, {"predicate": `${name}Range`}] 43 | } 44 | ] 45 | } 46 | 47 | var ShotChartInteractionSignals = [ 48 | { 49 | "name": "scope", 50 | "init": {"width": 0}, 51 | "streams": [ 52 | {"type": "mousedown, touchstart", "expr": "eventGroup()"} 53 | ] 54 | }].concat( 55 | brushSignal('distGroup', 'dist', 'eventX', 'width', 'x', 0, 50) 56 | ).concat( 57 | brushSignal('timepassedGroup', 'timepassed', 'eventX', 'width', 'x', 0, 64) 58 | ).concat( 59 | brushSignal('xLocGroup', 'xLoc', 'eventX', 'width', 'x', -250, 250) 60 | ).concat( 61 | brushSignal('yLocGroup', 'yLoc', 'eventY', 'height', 'y', -47.5, 500) 62 | ).concat( 63 | brushSignal('shotChart', 'chartX', 'eventX', 'width', 'x', -250, 250) 64 | ).concat( 65 | brushSignal('shotChart', 'chartY', 'eventY', 'height', 'y', -47.5, 500)) 66 | 67 | var ShotChartInteractionPredicates = brushPredicate('dist', 'x').concat( 68 | brushPredicate('timepassed', 'x') 69 | ).concat( 70 | brushPredicate('xLoc', 'x') 71 | ).concat( 72 | brushPredicate('yLoc', 'y') 73 | ).concat( 74 | brushPredicate('chartX', 'x') 75 | ).concat( 76 | brushPredicate('chartY', 'y') 77 | ).concat( 78 | [ 79 | { 80 | "name": "chartEqual", 81 | "type": "&&", 82 | "operands": [{"predicate": "chartXEqual"}, {"predicate": "chartYEqual"}] 83 | }, 84 | { 85 | "name": "chartInRange", 86 | "type": "&&", 87 | "operands": [ 88 | {"predicate": "chartXRange"}, 89 | {"predicate": "chartYRange"} 90 | ] 91 | }, 92 | { 93 | "name": "chartBrush", 94 | "type": "or", 95 | "operands": [{"predicate": "chartEqual"}, {"predicate": "chartInRange"}] 96 | } 97 | ]) 98 | 99 | var ShotChartInteractionFilters = { 100 | "brush": "(minchartX == maxchartX || (datum.LOC_X >= minchartX && datum.LOC_X <= maxchartX)) && (minchartY == maxchartY || (datum.LOC_Y >= minchartY && datum.LOC_Y <= maxchartY))" 101 | } 102 | 103 | function shotFilter(...variable_signals) { 104 | return variable_signals.map((variable_signal) => { 105 | var [variable, signal] = variable_signal 106 | return `(min${signal} == max${signal} || (datum.${variable} >= min${signal} && datum.${variable} <= max${signal}))` 107 | }).join(" && ") 108 | } 109 | 110 | function crossFilter(crosses) { 111 | return function(currentFilter) { 112 | return shotFilter.apply(null, crosses.filter((value) => value[0] !== currentFilter)) 113 | } 114 | } 115 | 116 | var filterExclude = crossFilter([ 117 | ['LOC_X', 'xLoc'], ['LOC_Y', 'yLoc'], ['hoopdistance', 'dist'], ['timepassed', 'timepassed'] 118 | ]) 119 | 120 | export {ShotChartInteractionSignals, ShotChartInteractionPredicates, ShotChartInteractionFilters, filterExclude} 121 | -------------------------------------------------------------------------------- /src/explorer/components/TableContainer.js: -------------------------------------------------------------------------------- 1 | import className from 'classnames' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import { TableLayout } from './TableLayout' 5 | import Scrollbar from 'fixed-data-table/internal/Scrollbar.react' 6 | 7 | const { findDOMNode } = ReactDOM 8 | const { div, pre } = React.DOM 9 | const BORDER_HEIGHT = 1 10 | 11 | export class TableContainer extends React.Component { 12 | getTableSettings(axes) { 13 | if (null == axes) return {} 14 | let result = { 15 | rowsCount: axes.row.length, 16 | hasRowNumericAxes: !!axes.row[0].field, 17 | rowHeight: axes.row[0].field ? 300 : 30, 18 | colWidth: axes.col[0].field ? 300 : axes.row[0].field ? 60 : 100, 19 | headerHeight: Math.max(axes.row[0].key.length > 0 ? 30 : 0, axes.col[0].key.length * 30), 20 | footerHeight: axes.col[0].field ? 60 : 0, 21 | fixedQuantAxisWidth: 120, 22 | fixedOrdinalAxisWidth: 200 23 | } 24 | return _.extend(result, { 25 | bodyHeight: result.rowsCount * result.rowHeight, 26 | bodyWidth: axes.col.length * result.colWidth, 27 | fixedWidth: axes.row[0].key.length * result.fixedOrdinalAxisWidth + (axes.row[0].field ? result.fixedQuantAxisWidth : 0) 28 | }) 29 | } 30 | 31 | render() { 32 | let { axes } = this.props 33 | let tableSettings = this.getTableSettings(axes) 34 | return 35 | } 36 | } 37 | 38 | export class TableResizeWrapper extends React.Component { 39 | constructor(props) { 40 | super(props) 41 | this.state = { renderTable: false, width: 500, height: 500 } 42 | _.extend(this, _(this).pick('_update', '_onResize').mapValues(f => f.bind(this)).value()) 43 | } 44 | 45 | componentDidMount() { 46 | this._update() 47 | window.addEventListener('resize', this._onResize, false) 48 | } 49 | 50 | componentWillUnmount() { 51 | clearTimeout(this._updateTimer) 52 | window.removeEventListener('resize', this._onResize) 53 | } 54 | 55 | _onResize() { 56 | clearTimeout(this._updateTimer) 57 | this._updateTimer = setTimeout(this._update, 16) 58 | } 59 | 60 | _update() { 61 | let elem = findDOMNode(this) 62 | let { offsetWidth: width, offsetHeight: height } = elem 63 | this.setState({ 64 | renderTable: true, 65 | width: width, 66 | height: this.getTableHeight(width, height) 67 | }) 68 | } 69 | 70 | getTableHeight(containerWidth, containerHeight) { 71 | let { headerHeight, footerHeight, bodyHeight, fixedWidth, bodyWidth } = this.props 72 | if (bodyHeight == null) return containerHeight 73 | let height = headerHeight + bodyHeight + footerHeight + 2 * BORDER_HEIGHT 74 | let width = fixedWidth + bodyWidth 75 | if (containerWidth < width) height += Scrollbar.SIZE 76 | if (height > containerHeight) height = containerHeight 77 | return height 78 | } 79 | 80 | getScaleFunction(shelf, scale, key) { 81 | if ('__default__' == key) return (d) => scale['default'] 82 | let d3Scale = 'time' == scale.type ? d3.time.scale().domain( 83 | _.map(scale.domain, d => new Date(d))) : d3.scale[scale.type]().domain(scale.domain) 84 | switch(shelf) { 85 | case 'row': 86 | return d3Scale.range([this.props.rowHeight, 0]) 87 | case 'col': 88 | return d3Scale.range([0, this.props.colWidth]) 89 | default: 90 | return d3Scale.range(scale.range) 91 | } 92 | } 93 | 94 | render() { 95 | let visScales = _.mapValues( 96 | this.props.scales, (scales, shelf) => 97 | _.mapValues(scales, (scale, key) => 98 | this.getScaleFunction(shelf, scale, key))) 99 | let fieldScales = visScales ? _(this.props.queryspec).map( 100 | (fields, shelf) => { 101 | return _.map(fields, (field) => { 102 | return { field, shelf, scale: visScales[shelf][field.accessor] } 103 | }) 104 | }).flatten().value() : null 105 | 106 | return div({className: className("container-flex-fill", { 107 | 'table-no-header': 0 == this.props.headerHeight, 108 | 'table-no-footer': 0 == this.props.footerHeight, 109 | 'table-row-bottom-border': this.props.hasRowNumericAxes 110 | })}, ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/explorer/css/components/field.scss: -------------------------------------------------------------------------------- 1 | @import '../../../css/flexbox.scss'; 2 | @import '../../../css/utilities.scss'; 3 | 4 | @mixin draggable-field { 5 | cursor: pointer; 6 | cursor: -webkit-grab; 7 | cursor: -moz-grab; 8 | 9 | &:hover, 10 | &:focus { 11 | box-shadow: 0 1px 3px #dfdfdf; 12 | } 13 | &:active { 14 | box-shadow: 0 2px 3px rgba(0,0,0,.8); 15 | cursor: move; 16 | cursor: -webkit-grabbing; 17 | cursor: -moz-grabbing; 18 | } 19 | } 20 | 21 | .func-wrap { 22 | padding-left: 6px; 23 | text-transform: uppercase; 24 | text-shadow: rgb(255, 255, 255) 0px 1px 0px; 25 | color: #222; 26 | font-weight: 600; 27 | @include overflow-ellipsis; 28 | @include flex(1 0 auto); 29 | 30 | &:empty { 31 | display: none; 32 | } 33 | } 34 | 35 | .icon-wrap { 36 | text-align: center; 37 | padding-left: 3px; 38 | padding-right: 3px; 39 | border-right: 1px solid #dfdfdf; 40 | } 41 | 42 | .name-wrap { 43 | position: relative; 44 | padding-left: 6px; 45 | padding-right: 6px; 46 | @include overflow-ellipsis; 47 | 48 | &.has-func { 49 | padding-left: 5px; 50 | padding-right: 5px; 51 | 52 | &:before, 53 | &:after { 54 | position: absolute; 55 | top: -1px; 56 | } 57 | 58 | &:before { 59 | content: "("; 60 | left: 0px; 61 | } 62 | 63 | &:after { 64 | content: ")"; 65 | right: 0px; 66 | } 67 | } 68 | } 69 | 70 | .option-wrap i.fa.fa-trash-o { 71 | padding: 0px 4px 0 3px; 72 | } 73 | 74 | .field-wrap { 75 | position: relative; 76 | border: 1px solid #dfdfdf; 77 | border-radius: 1px; 78 | background-color: #f0f0f0; 79 | overflow: hidden; 80 | 81 | @include flex(0 0 auto); 82 | @include flexbox; 83 | @include flex-flow(row); 84 | @include draggable-field; 85 | 86 | &.querybuilder-field { 87 | margin-right: 6px; 88 | @include flex(0 0 auto); 89 | 90 | .icon-wrap { 91 | @include flex(1 0 auto); 92 | max-width: 60px; 93 | 94 | i.fa.fa-long-arrow-right { 95 | padding: 0 5px; 96 | } 97 | } 98 | 99 | .name-wrap { 100 | max-width: 125px; 101 | @include flex(0 1 auto); 102 | } 103 | 104 | .option-wrap { 105 | padding: 0px 3px; 106 | cursor: pointer; 107 | 108 | i.fa.fa-caret-down { 109 | padding: 0px 4px 0 3px; 110 | color: #aaa; 111 | text-shadow: rgb(255, 255, 255) 0px 1px 0px; 112 | border-radius: 2px; 113 | } 114 | 115 | i.fa.fa-times.remove-link { 116 | text-shadow: rgb(255, 255, 255) 0px 1px 0px; 117 | display: none; 118 | } 119 | 120 | &:hover, 121 | &:active { 122 | i.fa.fa-caret-down { 123 | text-shadow: none; 124 | color: #ddd; 125 | background-color: #bbb; 126 | } 127 | } 128 | 129 | &:active { 130 | i.fa.fa-caret-down { 131 | background-color: #aaa; 132 | } 133 | } 134 | } 135 | 136 | &:hover .option-wrap i.fa.fa-times.remove-link { 137 | display: initial; 138 | } 139 | } 140 | } 141 | 142 | .field-drag-layer { 143 | position: fixed; 144 | pointer-events: none; 145 | left: 0; 146 | top: 0; 147 | width: 100%; 148 | height: 100%; 149 | z-index: 100; 150 | } 151 | 152 | .field-drag-layer .field-drag-wrap { 153 | @include inline-flex; 154 | 155 | .field-wrap { 156 | opacity: 0.92; 157 | box-shadow: 0 2px 3px rgba(0,0,0,.8); 158 | cursor: move; 159 | cursor: -webkit-grabbing; 160 | cursor: -moz-grabbing; 161 | 162 | &.remove { 163 | background-color: lighten(#f18794, 5%); 164 | border: 1px solid #f18794; 165 | opacity: 0.5; 166 | .icon-wrap { 167 | border-right: 1px solid #f18794; 168 | } 169 | } 170 | } 171 | } 172 | 173 | .field-contained:before { 174 | display: none; 175 | } 176 | 177 | .field-empty:before { 178 | display: block; 179 | } 180 | 181 | .field-drop-target { 182 | background-color: lighten(#58C153, 40%); 183 | 184 | &:before { 185 | border-color: #999; 186 | font-weight: 600; 187 | } 188 | } 189 | 190 | .field-drop-over { 191 | background-color: lighten(#58C153, 30%); 192 | 193 | &:before { 194 | border-color: #999; 195 | font-weight: 600; 196 | background-color: lighten(#58C153, 20%); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/components/basketball/aggregates.js: -------------------------------------------------------------------------------- 1 | var ShotAggregates = { 2 | "type": "group", 3 | "properties": { 4 | "update": { 5 | "x": { "value": 0 }, 6 | "y": { "value": 2.5 }, 7 | "width": { "value": 500 }, 8 | "height": { "value": 50 } 9 | } 10 | }, 11 | "scales": [ 12 | { 13 | "name": "xfgp", 14 | "type": "linear", 15 | "range": [250, 350], 16 | // "reverse" : true, // hoop on bottom view 17 | "domain": [0, 1] 18 | }, 19 | { 20 | "name": "xppa", 21 | "type": "linear", 22 | "range": [375, 475], 23 | // "reverse" : true, // hoop on bottom view 24 | "domain": [0, 3] 25 | } 26 | ], 27 | "marks": [ 28 | { 29 | "type": "rect", 30 | "from": {"data": "rects"}, 31 | "properties": { 32 | "update": { 33 | "width": { "field": "width" }, 34 | "height": { "field": "height" }, 35 | "x": { "field": "x"}, 36 | "fill": { 37 | "value": { 38 | "id": "rgb", 39 | "x1": 0.0, 40 | "y1": 0.0, 41 | "x2": 1.0, 42 | "y2": 0.0, 43 | "stops": [ 44 | {"color": "#e6550d", "offset": 0.0}, 45 | {"color": "#31a354", "offset": 0.5}, 46 | {"color": "#31a354", "offset": 1}, 47 | ] 48 | } 49 | } 50 | } 51 | } 52 | }, 53 | { 54 | "from": {"data": "rects"}, 55 | "type": "text", 56 | "properties": { 57 | "enter": { 58 | "x": {"field": "x"}, 59 | "y": {"value": -5}, 60 | "text": {"field": "text"}, 61 | "fill": {"value": "black"}, 62 | "fontSize": {"value": 14}, 63 | "fontWeight": {"value": "bold"} 64 | } 65 | } 66 | }, 67 | { 68 | "from": {"data": "percentages"}, 69 | "type": "text", 70 | "properties": { 71 | "update": { 72 | "x": {"value": 0}, 73 | "dx": {"value": 0}, 74 | "y": {"value": 25}, 75 | "text": { 76 | "template": "{{datum.count | number:','}}" 77 | }, 78 | "fill": {"value": "black"}, 79 | "fontSize": {"value": 30} 80 | } 81 | } 82 | }, 83 | { 84 | "from": {"data": "percentages"}, 85 | "type": "text", 86 | "properties": { 87 | "update": { 88 | "x": {"value": 125}, 89 | "dx": {"value": 0}, 90 | "y": {"value": 25}, 91 | "text": { 92 | "template": "{{datum.sum_MADE_POINTS | number:','}}" 93 | }, 94 | "fill": {"value": "black"}, 95 | "fontSize": {"value": 30} 96 | } 97 | } 98 | }, 99 | { 100 | "from": {"data": "percentages"}, 101 | "type": "text", 102 | "properties": { 103 | "update": { 104 | "x": {"value": 250}, 105 | "dx": {"value": 10}, 106 | "y": {"value": 45}, 107 | "text": { 108 | "template": "{{datum.FGP | number:'.1%'}}" 109 | }, 110 | "fill": {"value": "black"}, 111 | "fontSize": {"value": 30} 112 | } 113 | } 114 | }, 115 | { 116 | "from": {"data": "percentages"}, 117 | "type": "text", 118 | "properties": { 119 | "update": { 120 | "x": {"value": 375}, 121 | "dx": {"value": 10}, 122 | "y": {"value": 45}, 123 | "text": { 124 | "template": "{{datum.PPA | number:'.2f'}}" 125 | }, 126 | "fill": {"value": "black"}, 127 | "fontSize": {"value": 30} 128 | } 129 | } 130 | }, 131 | { 132 | "from": {"data": "percentages"}, 133 | "type": "symbol", 134 | "properties": { 135 | "update": { 136 | "shape": { "value": "triangle-up" }, 137 | "x": {"scale": "xfgp", "field": "FGP"}, 138 | "y": {"value": 15}, 139 | "size": {"value": 40}, 140 | "fill": {"value": "black"} 141 | } 142 | } 143 | }, 144 | { 145 | "from": {"data": "percentages"}, 146 | "type": "symbol", 147 | "properties": { 148 | "update": { 149 | "shape": { "value": "triangle-up" }, 150 | "x": {"scale": "xppa", "field": "PPA"}, 151 | "y": {"value": 15}, 152 | "size": {"value": 40}, 153 | "fill": {"value": "black"} 154 | } 155 | } 156 | } 157 | ] 158 | } 159 | 160 | export { ShotAggregates } 161 | -------------------------------------------------------------------------------- /src/explorer/components/TableLayout.js: -------------------------------------------------------------------------------- 1 | import 'fixed-data-table/dist/fixed-data-table.css' 2 | import '../css/components/table.scss' 3 | 4 | import React from 'react' 5 | import _ from 'lodash' 6 | import FixedDataTable from 'fixed-data-table' 7 | import { getAccessorName } from '../helpers/field' 8 | const Axis = React.createFactory(require('./vis/Axis').Axis) 9 | const Pane = React.createFactory(require('./vis/Pane').Pane) 10 | 11 | const { Table: TableWrapper, Column: ColumnWrapper, Cell: CellWrapper } = FixedDataTable 12 | const [Table, Column, Cell] = [React.createFactory(TableWrapper), React.createFactory(ColumnWrapper), React.createFactory(CellWrapper)] 13 | const { div } = React.DOM 14 | const EMPTY_RENDER = () => '' 15 | const EMPTY_DATA = () => {} 16 | 17 | const RowAxisCell = ({axisIndex, rowAxis, scaleLookup, markType, ...props}) => { 18 | return ({rowIndex, height, width, ...cellProps}) => { 19 | if ('Q' == axisIndex) { 20 | let field = rowAxis[rowIndex].field 21 | let name = field.accessor 22 | return Axis({orient: 'left', scale: scaleLookup[name], name, field, height, width, markType}) 23 | } 24 | return Cell({}, div({className: "table-row-label"}, rowAxis[rowIndex].key[axisIndex])) 25 | } 26 | } 27 | 28 | const PaneCell = ({paneData, colAxis, colIndex, rowAxisLookup, scales, fieldScales, markType, ...props}) => { 29 | return ({rowIndex, height, width, ...cellProps}) => { 30 | const cellData = paneData[rowIndex][colIndex] 31 | if(null == cellData) return div({}) 32 | return Pane({ 33 | paneData: cellData, 34 | rowAxis: rowAxisLookup[rowIndex], 35 | colAxis, 36 | width, 37 | height, 38 | markType: markType, 39 | fieldScales: fieldScales, 40 | scales: scales 41 | }) 42 | } 43 | } 44 | 45 | const FooterCell = ({height, colAxis, colScaleLookup, markType, ...props}) => { 46 | return ({rowIndex, width, ...cellProps}) => { 47 | let field = colAxis.field 48 | let name = field.accessor 49 | return Axis({scale: colScaleLookup[name], name, field, height, width, markType}) 50 | } 51 | } 52 | 53 | export class TableLayout extends React.Component { 54 | getFixedColumns(axis, props) { 55 | return axis.map((field, r) => { 56 | let isOrdinal = 'O' == field.algebraType 57 | return Column({ 58 | fixed: true, 59 | key: getAccessorName(field), 60 | width: isOrdinal ? props.fixedOrdinalAxisWidth : props.fixedQuantAxisWidth, 61 | header: Cell({}, div({}, isOrdinal ? field.name : '')), 62 | cell: RowAxisCell({ 63 | markType: props.visualspec.table.type, 64 | axisIndex: r, 65 | rowAxis: props.axes.row, 66 | scaleLookup: props.visScales.row 67 | }), 68 | footer: EMPTY_RENDER 69 | }) 70 | }) 71 | } 72 | 73 | getScrollableColumns(cols, props) { 74 | return _.map(cols, (axis, c) => { 75 | return Column({ 76 | fixed: false, key: c, 77 | width: props.colWidth, 78 | height: props.rowHeight, 79 | header: Cell({}, div({}, _.map(axis.key, name => div({key: name}, name)))), 80 | cell: PaneCell({ 81 | paneData: props.panes, 82 | colIndex: c, 83 | colAxis: axis, 84 | rowAxisLookup: props.axes.row, 85 | markType: props.visualspec.table.type, 86 | scales: props.visScales, 87 | fieldScales: props.fieldScales 88 | }), 89 | footer: FooterCell({ 90 | colAxis: axis, 91 | colScaleLookup: props.visScales.col, 92 | height: props.footerHeight, 93 | markType: props.visualspec.table.type 94 | }), 95 | allowCellsRecycling: true 96 | }) 97 | }) 98 | } 99 | 100 | render() { 101 | const { renderTable, width, height, 102 | rowHeight, rowsCount, headerHeight, footerHeight, 103 | error, result, queryspec, axes } = this.props 104 | if (!renderTable || error || queryspec == null || result == null) { 105 | return div({}, `${error ? "Error: " : ""}No Chart`) 106 | } 107 | else { 108 | return Table( 109 | { 110 | width: width, 111 | maxHeight: height, 112 | height: height, 113 | rowHeight: rowHeight, 114 | rowsCount: rowsCount, 115 | headerHeight: headerHeight, 116 | footerHeight: footerHeight 117 | }, 118 | this.getFixedColumns(axes.row[0], this.props), 119 | this.getScrollableColumns(axes.col, this.props) 120 | ) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/explorer/components/vis/marks/Area.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import dl from 'datalib' 3 | import d3 from 'd3' 4 | import React from 'react' 5 | import { getFieldType, getAccessorName, isStackableField, isAggregateType, isBinField, isGroupByField } from '../../../helpers/field' 6 | import { stackGroupedLayout } from './layout' 7 | const { div, svg } = React.DOM 8 | 9 | export default class Area extends React.Component { 10 | getDefaultScales() { 11 | const { scales } = this.props 12 | return { 13 | stroke: scales.color.__default__, 14 | "stroke-width": 2, 15 | x: 0, 16 | y0: 0, 17 | y1: 0 18 | } 19 | } 20 | 21 | getAreaScales(props, sortedMarkData, binField) { 22 | const { width, height } = this.props 23 | const { field, scale, shelf } = props 24 | const name = getAccessorName(field) 25 | const isStackable = isStackableField(field) 26 | const isAggregate = isAggregateType(field) 27 | const isBin = isBinField(field) 28 | const isTime = 'time' == getFieldType(field) 29 | switch(shelf) { 30 | case 'row': 31 | if (isStackable) { 32 | let stacked = stackGroupedLayout(sortedMarkData, name, binField) 33 | return { 34 | y0: ([d, level], i) => scale(stacked(d, level, i)), 35 | y1: ([d, level], i) => scale(d[name]) + scale(stacked(d, level, i)) - height 36 | } 37 | } 38 | else if (isBin) { 39 | return { 40 | x: ([d, level]) => scale(d[name]) 41 | } 42 | } 43 | else { 44 | return { 45 | y0: ([d, level]) => height, 46 | y1: ([d, level]) => scale(d[name]) 47 | } 48 | } 49 | case 'col': 50 | if (isStackable) { 51 | let stacked = stackGroupedLayout(sortedMarkData, name, binField) 52 | return { 53 | y0: ([d, level], i) => width - scale(d[name]) - scale(stacked(d, level, i)), 54 | y1: ([d, level], i) => width - scale(stacked(d, level, i)), 55 | transform: `translate(${width}) rotate(90)` 56 | } 57 | } else { 58 | return { 59 | x: ([d, level]) => scale(d[name]) 60 | } 61 | } 62 | case 'color': 63 | return { 64 | stroke: (d) => scale(d[name]) 65 | } 66 | case 'size': 67 | return { 68 | // 'stroke-width': 69 | } 70 | default: 71 | return { 72 | } 73 | } 74 | } 75 | 76 | getAttributeTransforms(sortedMarkData) { 77 | const { transformFields } = this.props 78 | const binField = _.find(transformFields, fs => isBinField(fs.field)) 79 | let transforms = _.merge( 80 | this.getDefaultScales(), 81 | _.reduce(_.map(transformFields, (fs) => this.getAreaScales(fs, sortedMarkData, binField)), _.merge, {})) 82 | return transforms 83 | } 84 | 85 | _d3Render() { 86 | d3.select(this.refs.d3container).selectAll("*").remove() 87 | const sortAccessors = _.filter([ 88 | isBinField(this.props.rowAxis.field) ? this.props.rowAxis.field.accessor : null, 89 | isBinField(this.props.colAxis.field) ? this.props.colAxis.field.accessor : null]) 90 | const markSort = values => 91 | _.isEmpty(sortAccessors) ? values : _.sortByAll(values, sortAccessors) 92 | const areaGroups = _.map( 93 | dl.groupby( 94 | _.map(_.filter(this.props.transformFields, fs => !_.contains(['row', 'col'], fs.shelf)), 'field.accessor') 95 | ).execute(this.props.markData), 96 | areaGroup => _.extend({}, areaGroup, {values: markSort(areaGroup.values)})) 97 | 98 | const transforms = this.getAttributeTransforms(areaGroups) 99 | const area = d3.svg.area() 100 | .x(transforms.x) 101 | .y0(transforms.y0) 102 | .y1(transforms.y1) 103 | .interpolate('linear') 104 | 105 | let areas = d3.select(this.refs.d3container).selectAll("g.area") 106 | .data(areaGroups) // each area group 107 | 108 | areas.enter().append("g").attr("class", "area").append("path") 109 | .attr('d', (areaGroup, level) => 110 | area(_.zip(areaGroup.values,_.times(areaGroup.values.length, x => level)))) 111 | 112 | areas.selectAll("g.area path") 113 | .attr('transform', transforms.transform) 114 | .attr('stroke', transforms.stroke) 115 | .attr('stroke-width', transforms['stroke-width']) 116 | .attr('fill', transforms.stroke) 117 | areas.exit().remove() 118 | } 119 | 120 | componentDidMount() { 121 | this._d3Render() 122 | } 123 | 124 | componentDidUpdate() { 125 | this._d3Render() 126 | } 127 | 128 | render() { 129 | const { width, height } = this.props 130 | return svg({ref: 'd3container', width, height}) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/explorer/index.js: -------------------------------------------------------------------------------- 1 | import className from 'classnames' 2 | import _ from 'lodash' 3 | import React from 'react' 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import HTML5Backend from 'react-dnd-html5-backend' 7 | import { DropTarget, DragDropContext } from 'react-dnd' 8 | const { div, i: icon } = React.DOM 9 | import { getField, selectTable, connectTableIfNecessary } from './ducks/datasources' 10 | import { getFullQueryspec, clearQuery, addField, removeField, clearFields, insertFieldAtPosition, replaceFieldOnShelf, moveFieldTo, moveAndReplaceField, updateFieldTypecast, updateFieldFunction } from './ducks/queryspec' 11 | import { setTableEncoding, setPropertySetting } from './ducks/visualspec' 12 | import { DataSourceSelect, TableSchema } from './components/DataSource' 13 | import { TableLayoutSpecBuilder, TableVisualSpecBuilder } from './components/TableSpecBuilder' 14 | import { TableContainer } from './components/TableContainer' 15 | import FieldDragLayer from './components/FieldDragLayer' 16 | 17 | class Explorer extends React.Component { 18 | render() { 19 | const { dispatch, result, queryspec, visualspec, sourceIds, sources, tableId, chartspec } = this.props 20 | const { connectDropTarget, isOver, isDragging } = this.props 21 | const fieldActionCreators = bindActionCreators({ 22 | removeField, clearFields, 23 | insertFieldAtPosition, moveFieldTo, 24 | replaceFieldOnShelf, moveAndReplaceField, 25 | updateFieldTypecast, updateFieldFunction 26 | }, dispatch) 27 | const getSourceField = _.curry(getField)(sources) 28 | const getTableField = _.curry(getField)(sources, tableId) 29 | const vizActionCreators = bindActionCreators({ setTableEncoding, setPropertySetting }, dispatch) 30 | const currentData = result.cache[_.first(result.render.current)] 31 | const lastData = result.cache[_.first(result.render.last)] 32 | const isLoading = currentData && currentData.isLoading 33 | const graphicData = isLoading ? lastData : currentData 34 | const graphicChart = isLoading ? _.get(chartspec, result.render.last) : _.get(chartspec, result.render.current) 35 | return connectDropTarget( 36 | div({className: className("pane-container")}, 37 | , 38 | div({className: "pane data-pane"}, 39 | { 41 | dispatch(clearQuery()) 42 | dispatch(selectTable(tableId)) 43 | dispatch(connectTableIfNecessary(tableId)) 44 | }}/>, 45 | div({className: "datasource-table-container"}, 46 | { 48 | dispatch(addField(queryspec.row.length < queryspec.col.length ? 'row' : 'col', field)) 49 | }}/>)), 50 | div({className: "pane graphic-pane"}, 51 | div({className: "querybuilder"}, 52 | ), 55 | div({className: "container-flex-fill-wrap graphic-container"}, 56 | div({className: "loading-overlay", style: {display: isLoading ? "" : "none"}}, 57 | div({className: "loading-overlay-background"}), 58 | icon({className: "fa fa-spinner fa-pulse"})), 59 | )), 60 | 62 | )) 63 | } 64 | } 65 | 66 | function select(state) { 67 | return { 68 | sourceIds: state.datasources.IDS, 69 | sources: state.datasources.BY_ID, 70 | tableId: state.datasources.selectedTable, 71 | queryspec: state.queryspec, 72 | visualspec: state.visualspec, 73 | result: state.result, 74 | chartspec: state.chartspec 75 | } 76 | } 77 | 78 | export default (connect(select))(DragDropContext(HTML5Backend)(DropTarget( 79 | 'ShelfField', 80 | { 81 | canDrop: (props) => true, 82 | drop(props, monitor, component) { 83 | if (monitor.didDrop()) return 84 | return { ..._.pick(monitor.getItem(), 'shelf', 'position'), 85 | droppedOnBody: true, 86 | removeField: (shelf, position) => props.dispatch(removeField(shelf, position)) 87 | } 88 | } 89 | }, 90 | function (connect, monitor) { 91 | return { 92 | connectDropTarget: connect.dropTarget(), 93 | isOver: monitor.isOver({shallow: true}), 94 | isDragging: monitor.canDrop() 95 | } 96 | })(Explorer))) 97 | -------------------------------------------------------------------------------- /src/explorer/data/pane.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { getFieldQueryType, isStackableField, isBinField } from '../helpers/field' 3 | import { translateSummary } from './local' 4 | import Aggregator from 'datalib/src/aggregate/aggregator' 5 | import dl from 'datalib' 6 | 7 | function getPaneIndices(nest, rkey, ckey) { 8 | let node = nest 9 | for(let i = 0, len = rkey.length; i < len; i++) { 10 | node = node[rkey[i]] 11 | if (node == null) return null 12 | } 13 | for(let i = 0, len = ckey.length; i < len; i++) { 14 | node = node[ckey[i]] 15 | if (node == null) return null 16 | } 17 | return node 18 | } 19 | 20 | function flattenGroupedData(data) { 21 | let result = [] 22 | for(let i = 0, len = data.length; i < len; i++) { 23 | let datum = data[i] 24 | let grouped = _.omit(datum, 'values') 25 | for (let j = 0, vlen = datum.values.length; j < vlen; j++) { 26 | result.push(_.extend({}, grouped, datum.values[j])) 27 | } 28 | } 29 | return result 30 | } 31 | 32 | class PaneData { 33 | constructor(row, col, data) { 34 | this.axis = { row, col } 35 | this.data = data 36 | this.markData = null 37 | } 38 | aggregate(tableType, visualFields, mustCondense) { 39 | let axisFields = _.filter([this.axis.row.field, this.axis.col.field]) 40 | let fields = axisFields.concat(visualFields) 41 | let noHasRawValue = _.all(fields, field => 'value' != getFieldQueryType(field)) 42 | 43 | let markData = this.data 44 | 45 | // condense data to pane summarized data 46 | if (mustCondense) { 47 | let { groupby, operator, aggregate, value } = _.groupBy(fields, getFieldQueryType) 48 | let condensedData = dl.groupby(_.map(groupby, 'accessor')) 49 | .summarize(translateSummary(operator, aggregate, value)) 50 | .execute(flattenGroupedData(this.data)) 51 | this.markData = noHasRawValue ? condensedData : flattenGroupedData(condensedData) 52 | } 53 | else { 54 | this.markData = noHasRawValue ? this.data : flattenGroupedData(this.data) 55 | } 56 | return this 57 | } 58 | addDomainToAxis(tableType, didCondense) { 59 | // calculate domain for pane data, taking note of bin and stacks 60 | let isStackable = 'bar' == tableType || 'area' == tableType 61 | let groupAxis = isBinField(this.axis.row.field) ? this.axis.row : 62 | isBinField(this.axis.col.field) ? this.axis.col : null 63 | let stackAxes = _.filter([ 64 | isStackableField(this.axis.row.field) ? this.axis.row : null, 65 | isStackableField(this.axis.col.field) ? this.axis.col : null]) 66 | let domainData = isStackable && stackAxes.length > 0 ? 67 | (groupAxis ? dl.groupby([groupAxis.field.accessor]) : dl.groupby()).summarize( 68 | _.map(stackAxes, axis => { return { name: axis.field.accessor, ops: ['sum'] } }) 69 | ).execute(this.markData) : null 70 | if (domainData) { 71 | for (let i = 0, len = domainData.length; i < len; i++) { 72 | for (let s = 0, slen = stackAxes.length; s < slen; s++) { 73 | let stackAxis = stackAxes[s] 74 | stackAxis.addDomainValue(domainData[i][`sum_${stackAxis.field.accessor}`]) 75 | } 76 | } 77 | } 78 | if (didCondense) { 79 | for (let i = 0, len = this.markData.length; i < len; i++) { 80 | for (let s = 0, slen = stackAxes.length; s < slen; s++) { 81 | let stackAxis = stackAxes[s] 82 | stackAxis.addDomainValue(this.markData[i][stackAxis.field.accessor]) 83 | } 84 | } 85 | } 86 | return this 87 | } 88 | sort(sorts) { 89 | this.markData = _.sortByAll(this.markData, sorts) 90 | return this 91 | } 92 | } 93 | 94 | export function partitionPaneData(axes, nest, data) { 95 | let panes = {} 96 | for(let r = 0, rlen = axes.row.length, clen = axes.col.length; r < rlen; r++) { 97 | for(let c = 0; c < clen; c++) { 98 | let raxis = axes.row[r] 99 | let caxis = axes.col[c] 100 | if (raxis.acceptsValues && caxis.acceptsValues) { 101 | if (!panes[r]) panes[r] = {} 102 | panes[r][c] = new PaneData(raxis, caxis, data) 103 | } 104 | else { 105 | let paneDataIndices = getPaneIndices(nest, raxis.key, caxis.key) 106 | if (paneDataIndices != null) { 107 | if (!panes[r]) panes[r] = {} 108 | panes[r][c] = new PaneData(raxis, caxis, _.at(data, paneDataIndices)) 109 | } 110 | } 111 | } 112 | } 113 | return panes 114 | } 115 | 116 | export function aggregatePanes(panes, tableType, spec, mustCondense) { 117 | for (let r = 0, rkeys = Object.keys(panes), rlen = rkeys.length; r < rlen; r++) { 118 | let rkey = rkeys[r] 119 | for (let c = 0, ckeys = Object.keys(panes[rkey]), clen = ckeys.length; c < clen; c++) { 120 | let ckey = ckeys[c] 121 | panes[rkey][ckey].aggregate(tableType, spec, mustCondense) 122 | .addDomainToAxis(tableType, mustCondense) 123 | .sort(_.map(spec, 'accessor')) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/explorer/css/components/shelf.scss: -------------------------------------------------------------------------------- 1 | @import '../../../css/flexbox.scss'; 2 | 3 | @mixin border-shim($color, $border-color: #dfdfdf) { 4 | &:after { 5 | content: " "; 6 | position: absolute; 7 | left: -1px; right: -1px; 8 | bottom: -2px; 9 | height: 5px; 10 | background-color: $color; 11 | border-left: 1px solid $border-color; 12 | border-right: 1px solid $border-color; 13 | z-index: 1; 14 | } 15 | } 16 | 17 | .shelf-pane .container-flex-fill-wrap { 18 | background-color: white; 19 | border: 1px solid #dfdfdf; 20 | } 21 | 22 | .shelf-pane .container-flex-fill { 23 | @include flexbox; 24 | @include flex-flow(column); 25 | padding: 5px 6px; 26 | } 27 | 28 | .querybuilder-shelf { 29 | position: relative; 30 | @include flexbox; 31 | @include flex(1 1 0px); 32 | 33 | label { 34 | position: relative; 35 | text-shadow: rgb(255, 255, 255) 0px 1px 0px; 36 | 37 | i.fa.fa-times.remove-link { 38 | position: absolute; 39 | top: 0; 40 | right: 0; 41 | padding: 6px 5px 9px; 42 | display: none; 43 | cursor: pointer; 44 | } 45 | 46 | &:hover i.fa.fa-times.remove-link { 47 | display: block; 48 | } 49 | } 50 | 51 | .querybuilder-field-container-wrap { 52 | @include flex(1 0 auto); 53 | position: relative; 54 | overflow: hidden; 55 | 56 | &:hover { 57 | overflow: auto; 58 | } 59 | 60 | &:before { 61 | content: "Drop Fields Here"; 62 | position: absolute; 63 | margin: 3px 6px; 64 | padding: 0px 6px; 65 | border: 1px dotted #dfdfdf; 66 | border-radius: 2px; 67 | } 68 | } 69 | 70 | .querybuilder-field-container { 71 | @include flexbox; 72 | @include flex-flow(row); 73 | 74 | position: absolute; 75 | padding: 3px 6px 3px; 76 | 77 | .field-position-marker { 78 | position: absolute; 79 | background-color: #58C153; 80 | top: 0px; bottom: 0px; 81 | margin-left: -4px; 82 | width: 2px; 83 | 84 | &:before, 85 | &:after { 86 | content: " "; 87 | position: absolute; 88 | left: -2px; 89 | } 90 | 91 | &:before { 92 | top: 0px; 93 | border-top: 3px solid #58C153; 94 | border-left: 3px solid transparent; 95 | border-right: 3px solid transparent; 96 | } 97 | 98 | &:after { 99 | bottom: 0px; 100 | border-bottom: 3px solid #58C153; 101 | border-left: 3px solid transparent; 102 | border-right: 3px solid transparent; 103 | } 104 | } 105 | } 106 | } 107 | 108 | .shelf-pane .querybuilder-shelf { 109 | margin-bottom: 15px; 110 | 111 | .querybuilder-shelf-legend { 112 | overflow: auto; 113 | max-height: 200px; 114 | padding: 8px 6px 0px; 115 | } 116 | 117 | @include flex-flow(column); 118 | @include flex(0 1 auto); 119 | 120 | label { 121 | @include flexbox; 122 | @include flex(0 1 auto); 123 | @include align-items(center); 124 | 125 | i.fa.fa-caret-down { 126 | @include align-self(center); 127 | position: relative; 128 | top: -3px; 129 | padding: 3px; 130 | margin-left: auto; 131 | visibility: hidden; 132 | cursor: pointer; 133 | } 134 | 135 | &:hover, 136 | &:focus { 137 | i.fa.fa-caret-down { 138 | visibility: visible; 139 | } 140 | } 141 | } 142 | 143 | .querybuilder-field-container-wrap { 144 | height: 23px; 145 | overflow: visible; 146 | 147 | &:before { 148 | content: "Drop Field Here"; 149 | top: 0; bottom: 0; left: 0; right: 0; 150 | margin: 0; 151 | 152 | } 153 | } 154 | 155 | .querybuilder-field-container { 156 | top: 0; bottom: 0; left: 0; right: 0; 157 | padding: 0px; 158 | } 159 | 160 | .querybuilder-field.field-wrap { 161 | @include flex(0 1 auto); 162 | margin-right: 0; 163 | 164 | .name-wrap { 165 | max-width: none; 166 | } 167 | } 168 | } 169 | 170 | .shelf-pane label.tablebuilder-encoding-title { 171 | font-size: 1rem; 172 | margin-bottom: 10px; 173 | } 174 | 175 | .tablebuilder-type-select { 176 | @include flexbox; 177 | @include flex-flow(row); 178 | 179 | .tablebuilder-type-choice { 180 | @include flex(1 1 27px); 181 | position: relative; 182 | height: 29px; 183 | line-height: 29px; 184 | text-align: center; 185 | background-color: rgba(224, 224, 224, 0.5); 186 | border: 1px solid transparent; 187 | top: -1px; 188 | margin-top: 1px; 189 | cursor: pointer; 190 | 191 | &:not(:last-of-type) { 192 | margin-right: 1px; 193 | } 194 | 195 | &.active, 196 | &:not(.active):hover, 197 | &:not(.active):focus { 198 | background-color: white; 199 | border: 1px solid #dfdfdf; 200 | border-bottom: 0; 201 | margin-top: 0px; 202 | top: 0px; 203 | @include border-shim(white); 204 | } 205 | 206 | i.fa.fa-bar-chart.fa-flip-vertical-rotate-90 { 207 | @include transform(rotate(90deg) scale(-0.8, 1.2)); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /public/js/react-histogram.min.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(n){if(r[n])return r[n].exports;var o=r[n]={exports:{},id:n,loaded:!1};return t[n].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}({0:function(t,e,r){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}var o=r(5),a=n(o),i=r(6),l=n(i),u=r(7),s=n(u),c=r(318),f=n(c),p=document.getElementById("react-histogram"),d={top:10,right:30,bottom:30,left:30},m=p.offsetWidth-d.left-d.right,y=550-d.top-d.bottom,h=s["default"].range(1e3).map(s["default"].random.bates(10)),g=s["default"].format(",.0f"),x=s["default"].scale.linear().domain([0,1]).range([0,m]),b=s["default"].layout.histogram().bins(x.ticks(20))(h),v=s["default"].scale.linear().domain([0,s["default"].max(b,function(t){return t.y})]).range([y,0]),k=b.map(function(t,e){return a["default"].createElement("g",{key:e,x:"0",y:"0",className:"bar",transform:"translate("+x(t.x)+", "+v(t.y)+")"},a["default"].createElement("rect",{width:x(t.dx)-1,height:y-v(t.y)}),a["default"].createElement("text",{dy:".75em",y:2,x:x(t.dx)/2,textAnchor:"middle"},g(t.y)))});l["default"].render(a["default"].createElement("svg",{width:m+d.left+d.right,height:y+d.top+d.bottom},a["default"].createElement("g",{transform:"translate("+d.left+", "+d.top+")"},k,a["default"].createElement(f["default"],{scale:x,orient:"bottom",x:0,y:y}),a["default"].createElement(f["default"],{scale:v,orient:"left",x:0,y:0}))),p)},5:function(t,e){t.exports=React},6:function(t,e){t.exports=ReactDOM},7:function(t,e){t.exports=d3},30:function(t,e,r){var n;/*! 2 | Copyright (c) 2015 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | !function(){"use strict";function o(){for(var t="",e=0;es?"0em":".71em",textAnchor:"middle"}:{x:s*l,y:0,dy:".32em",textAnchor:0>s?"end":"start"},y={axis:!0,x:"top"===t||"bottom"===t,y:"left"===t||"right"===t},h="bottom"===t||"top"===t?c["default"].createElement("path",{className:"domain",d:"M"+u[0]+","+s*n+"V0H"+u[1]+"V"+s*n}):c["default"].createElement("path",{className:"domain",d:"M"+s*n+","+u[0]+"H0V"+u[1]+"H"+s*n}),g=a.map(function(r,n){return c["default"].createElement("g",{key:n,className:"tick",transform:"top"===t||"bottom"===t?"translate("+e(r)+",0)":"translate(0, "+e(r)+")"},c["default"].createElement("line",f),c["default"].createElement("text",i({y:"9"},m),r))});return c["default"].createElement("g",{className:(0,p["default"])(y),transform:"translate("+this.props.x+","+this.props.y+")"},g,h)}}]),e}(c["default"].Component);m.defaultProps={orient:"bottom",innerTickSize:6,outerTickSize:6,tickPadding:3,tickArguments:[10],tickValues:null,tickFormat:null},e["default"]=m,t.exports=e["default"]},319:function(t,e){"use strict";function r(t){var e=t[0],r=t[t.length-1];return r>e?[e,r]:[r,e]}function n(t){return t.rangeExtent?t.rangeExtent():r(t.range())}Object.defineProperty(e,"__esModule",{value:!0}),e.d3_scaleExtent=r,e.d3_scaleRange=n}}); --------------------------------------------------------------------------------