├── 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 |
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 | {{ page.date | date_to_string }}
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 | Please enable JavaScript to view the comments powered by Disqus.
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 |
9 |
14 |
15 | {{ post.date | date_to_string }}
16 |
17 | {{ post.content }}
18 |
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(
72 | {shotChartUrls.map(function(player) {
73 | return {player.name}
74 | })}
75 | , 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}});
--------------------------------------------------------------------------------