<|start_filename|>src/store/index.js<|end_filename|>
'use strict';

var _ = require('lodash');
var Model = require('backbone').Model;
var dispatcher = require('../dispatcher');
var dataFunctions = require('./data-functions');
var initialState = require('./initial-state');

/**
 * # Store
 *
 * This is primary data store for Data Lasso. It strives to
 * follow role prescribed to a store in a Flux pattern, containing the state
 * that is only changed by actions and never by anything else.
 *
 * Views listen for changes in this Store, and never should modify it directly.
 *
 * In its essence, it is a singleton Backbone model that both provides storage and
 * a mechanism for communicating changes to a store downstream.
 *
 * Benefit of the dispatcher: No simultaneous execution.
 */

var DataModel = Model.extend({

    defaults: initialState,

    initialize: function () {
        dispatcher.register(_.bind(this.dispatchCallback, this));
    },

    /**
     * # Dispatcher Callback
     *
     * Receives actions coming down from a dispatcher and mutates state accordingly.
     *
     * @param action {object} - Action dispatched with a dispatcher. Always has an `actionType`.
     */
    dispatchCallback: function (action) {
        switch (action.actionType) {
            case 'options-set':
                this.options = action.options;
                break;

            case 'file-uploaded':
                this.onFileUpload(action);
                break;

            case 'mode-changed':
                this.onModeChange(action);
                break;

            case 'selection-started':
                this.onSelectionStart();
                break;

            case 'selection-stopped':
                this.onSelectionStop();
                break;

            case 'selection-made':
                this.onNewSelection(action);
                break;

            case 'selection-modifier-changed':
                this.onSelectionModifierChange(action);
                break;

            case 'axis-mappings-updated':
                this.onNewMappings(action);
                break;

            case 'entry-hovered':
                this.onEntryHover(action);
                break;

            case 'zoom-in':
                this.onZoomIn();
                break;

            case 'zoom-out':
                this.onZoomOut();
                break;

            case 'source-change':
                this.onSourceChange(action);
                break;

            case 'type-change':
                this.onTypeChange(action);
                break;
        }
    },

    /**
     * New data was uploaded. Parse it to get attributes and scales.
     */
    onFileUpload: function (action) {
        if (!action.entries) {
            throw new Error('New data does not contain any entries');
        }
        var data = dataFunctions.processInput(action.entries, this.options);
        this.set({
            entries: data.entries,
            attributes: data.attributes,
            scales: data.scales,
            mappings: initialState.mappings,
            snapshots: initialState.snapshots,
        });
    },

    onModeChange: function (action) {
        if (!action.mode) {
            throw new Error('Mode is not specified');
        }
        this.set({
            mode: action.mode,
            controls: action.mode === 'view',
            selectionInProgress: false,
        });
    },

    /**
     * New selection started (user started using a selection tool).
     */
    onSelectionStart: function () {
        this.set({
            selectionInProgress: true,
        });
    },

    /**
     * Selection has stopped, or selection was finished.
     */
    onSelectionStop: function () {
        this.set({
            selectionInProgress: false,
        });
    },

    /**
     * Selection modifier was changed (add to / subtract from selection)
     */
    onSelectionModifierChange: function (action) {
        if (this.get('mode') === 'selection') {
            if (this.get('selectionInProgress')) {
                this.once('change:selectionInProgress', () => {
                    this.set({selectionModifier: action.selectionModifier});
                });
            } else {
                this.set({
                    selectionModifier: action.selectionModifier,
                });
            }
        }
    },

    /**
     * New selection was made
     */
    onNewSelection: function (action) {
        if (!action.selectedEntries) {
            throw new Error('Selected enties list is empty');
        }
        var selectedEntries = action.selectedEntries;
        var entries = _.transform(this.get('entries'), function (result, entry) {
            entry.isSelected = (selectedEntries.indexOf(entry.__id) >= 0);
            result.push(entry);
        });

        this.set({
            entries: entries,
            selectedEntries: selectedEntries,
        });
    },

    /**
     * New axis mappings were selected for the visualization
     */
    onNewMappings: function (action) {
        if (!action.mappings) {
            throw new Error('Axis mappings list is empty');
        }
        this.set({
            mappings: action.mappings,
            scales: dataFunctions.getUpdatedScales(this.get('entries'), this.options),
        });
    },

    /**
     * Entry was hovered over inside the visualization. `action.entry` can be null to
     * signal that nothing is selected after raytracing
     */
    onEntryHover: function (action) {
        this.set({
            focused: action.entry,
        });
    },

    /**
     * Selection is zoomed into
     */
    onZoomIn: function () {
        var newEntries = _.transform(this.get('entries'), function(result, entry) {
            if (entry.isSelected) {
                entry.isSelected = false;
                result.push(entry);
            }
        });

        this.saveDataSnapshot();
        this.set({
            entries: newEntries,
            selectedEntries: [],
            scales: dataFunctions.getUpdatedScales(newEntries, this.options),
        });
    },

    /**
     * Selection is zoomed out of
     */
    onZoomOut: function () {
        this.restoreLastDataSnapshot();
    },

    onSourceChange: function (action) {
        if (!action.source) {
            throw new Error('Source is not set');
        }
        this.set({
            source: action.source,
        });
    },

    onTypeChange: function (action) {
        if (!action.type) {
            throw new Error('Type is not set');
        }
        this.set({
            type: action.type,
        });
    },

    /**
     * Preserve current state that will be changed by zooming in in a snapshot
     */
    saveDataSnapshot: function () {
        var snapshots = this.get('snapshots') || [];
        snapshots.push(_.pick(this.toJSON(), ['entries', 'mappings', 'scales', 'attributes', 'selectedEntries']));
        this.set({
            snapshots: snapshots,
        });
    },

    /**
     * Get last snapshot and use it
     */
    restoreLastDataSnapshot: function () {
        var snapshots = this.get('snapshots');
        var lastSnapshot = snapshots.pop();
        this.set({
            entries: lastSnapshot.entries,
            mappings: lastSnapshot.mappings,
            scales: lastSnapshot.scales,
            attributes: lastSnapshot.attributes,
            selectedEntries: lastSnapshot.selectedEntries,
            snapshots: snapshots,
        });
    },
});

module.exports = new DataModel();


<|start_filename|>src/graph/helpers/mouse.js<|end_filename|>
'use strict';

var _ = require('lodash');
var Events = require('backbone').Events;
var THREE = require('three');

/**
 * ## Mouse Helper
 *
 * Provides an interface that makes working with mouse position
 * easier by translating DOM mouse position to a THREE vector
 *
 * @param container - container that renderer uses
 */

var Mouse = class {
    constructor (container) {
        _.extend(this, Events);

        this.container = container;
        this.vector = new THREE.Vector2();

        this.vector.x = 0;
        this.vector.y = 0;

        this.container.addEventListener('mousemove', _.bind(this.onMouseMove, this), false);
        this.container.addEventListener('mousedown', _.bind(this.onMouseDown, this), false);
    }

    /**
     * Get position vector of the mouse
     *
     * @returns {THREE.Vector2} - three.js vector for mouse cursor position
     */
    position () {
        return this.vector;
    }

    /**
     * Event listener for `mousemove`
     *
     * @param e - DOM event
     */
    onMouseMove (e) {
        this.vector.x = (e.offsetX / window.innerWidth) * 2 - 1;
        this.vector.y = -(e.offsetY / window.innerHeight) * 2 + 1;

        this.trigger('datalasso:mouse:move', {
            vector: this.vector,
            x: e.offsetX,
            y: e.offsetY,
        });
    }

    /**
     * Event listener for `mousedown`
     *
     * @param e - DOM event
     */
    onMouseDown (e) {
        this.trigger('datalasso:mouse:down', {
            button: e.button,
            vector: this.vector,
        });
    }
};

module.exports = Mouse;


<|start_filename|>src/graph/index.js<|end_filename|>
'use strict';

var _ = require('lodash');
var THREE = require('three');
var OrbitControls = require('three-orbit-controls')(THREE);
var helvetiker = require('three.regular.helvetiker');

var SelectionHelper = require('./helpers/selection');
var axisGeometry = require('./geometry/axis');
var Mouse = require('./helpers/mouse');
var Keyboard = require('./helpers/keyboard');
var vertexShader = require('./shaders/vertex.glsl');
var fragmentShader = require('./shaders/fragment.glsl');
var dotTexture = require('./texture/dot');
var store = require('../store');
var dispatcher = require('../dispatcher');


/**
 * ## Graph
 *
 * Handles rendering of Data Lasso. That includes:
 * - Scene, camera, lights
 * - Raycasting and collision detection
 * - Rendering
 *
 * When initiated, that class sets up three.js, creates an element to which
 * three.js will render to and returns instance of the class
 */

let graphOptions = {
    id: 'graph-container',

    // Size multiple for hovered points
    pointHoverFactor: 2,

    // Size multiple for selected points
    pointSelectionFactor: 2,
};

var Graph = class Graph {
    constructor(options) {
        _.extend(this, graphOptions);
        this.options = options;

        this.setUpElement();
        this.setUpTHREE();
        this.setUpEventListeners();

        return this;
    }

    setUpElement() {
        this.el = document.createElement('div');
        this.el.id = this.options.id;
    }

    setUpEventListeners() {
        window.addEventListener('resize', this.onWindowResize.bind(this));
        store.on('change:mode change:selectionModifier', this.onModeChange.bind(this));
    }

    onModeChange() {
        var mode = store.get('mode');
        var selectionModifier = store.get('selectionModifier');
        var classList = 'mode-' + mode;

        if (mode === 'selection') {
            if (selectionModifier) {
                classList += '-' + store.get('selectionModifier');
            }
        }

        this.el.classList = classList;
    }

    /**
     * Set up the ground work for THREE.js
     */
    setUpTHREE() {
        THREE.typeface_js.loadFace(helvetiker);

        // Scene
        this.scene = new THREE.Scene();

        // Camera
        this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, this.options.graphSize * 10);
        this.camera.position.set(this.options.graphSize * 2, this.options.graphSize * 2, this.options.graphSize * 2);

        // Mouse vector
        this.mouse = new Mouse(this.el);

        // Keyboard helper
        this.keyboard = new Keyboard();

        // Renderer
        this.renderer = new THREE.WebGLRenderer({alpha: true});
        this.renderer.setClearColor(0x32303d, 1);
        this.renderer.setSize(window.innerWidth, window.innerHeight);
        this.el.appendChild(this.renderer.domElement);

        // Orbit Controls
        this.controls = new OrbitControls(this.camera, this.el);
        this.controls.damping = 0.2;
        this.controls.addEventListener('change', _.bind(this.onControlsUpdate, this));
        this.controls.target = new THREE.Vector3(this.options.graphSize / 2, this.options.graphSize / 2, this.options.graphSize / 2);
        store.on('change:controls', () => {
            store.get('controls') ? this.enableControls() : this.disableControls();
        });

        // Lights
        var ambientLight = new THREE.AmbientLight(0xffffff);
        this.scene.add(ambientLight);

        // Materials
        // TODO: Abstract that outside
        var shaderAttributes = {
            size: {type: 'f', value: null},
            customColor: {type: 'c', value: null},
        };
        var shaderUniforms = {
            color: {type: 'c', value: new THREE.Color(0xffffff)},
            texture: {type: 't', value: dotTexture()},
        };
        this.shaderMaterial = new THREE.ShaderMaterial({
            uniforms: shaderUniforms,
            attributes: shaderAttributes,
            vertexShader: vertexShader,
            fragmentShader: fragmentShader,
            blending: THREE.AdditiveBlending,
            depthTest: false,
            transparent: true,
        });

        // Raycaster
        this.raycaster = new THREE.Raycaster();
        this.raycaster.params.PointCloud.threshold = 1;

        // Selection
        this.selectionHelper = new SelectionHelper(this.scene, this.camera, this.mouse);

        // Kick off the render loop
        this.animate();

        store.on('change:entries change:mappings change:scales change:attributes change:selectedEntries', _.debounce(_.bind(this.redrawEverything, this)));
    }

    animate() {
        requestAnimationFrame(_.bind(this.animate, this));
        this.renderFrame();
        this.controls.update();
    }

    renderFrame() {
        this.handleRaycasting();
        this.renderer.render(this.scene, this.camera);
    }

    /**
     * Event handlers
     */
    redrawEverything() {
        this.data = _.pick(store.toJSON(), ['entries', 'mappings', 'scales', 'attributes']);

        this.updateGeometry();
        this.updateAxisGeometry();
    }

    disableControls() {
        this.controls.noZoom = true;
        this.controls.noRotate = true;
        this.controls.noPan = true;
    }

    enableControls() {
        this.controls.noZoom = false;
        this.controls.noRotate = false;
        this.controls.noPan = false;
    }

    onControlsUpdate() {
        this.renderFrame();
        this.selectionHelper.updateProjectionPlane();
    }

    onWindowResize() {
        this.renderer.setSize(window.innerWidth, window.innerHeight);
        this.camera.aspect = window.innerWidth / window.innerHeight;
        this.camera.updateProjectionMatrix();
    }

    /**
     * Raycasting Method
     *
     * Raycasts from the camera position to the mouse position
     * and determines intersections with points.
     */
    handleRaycasting() {
        this.raycaster.setFromCamera(this.mouse.position(), this.camera);

        var intersections = [];

        if (this.pointCloud) {
            intersections = this.raycaster.intersectObjects([this.pointCloud]);

            var size = this.geometry.attributes.size.array;

            _.each(size, (value, index) => {
                if (this.data.entries[index].isSelected) {
                    size[index] = this.pointSize * this.pointSelectionFactor;
                } else {
                    size[index] = this.pointSize;
                }
            });

            if (intersections.length) {
                var entry = this.data.entries[intersections[0].index];

                dispatcher.dispatch({actionType: 'entry-hovered', entry: entry});

                if (entry.isSelected) {
                    size[intersections[0].index] = this.pointSize * this.pointHoverFactor * this.pointSelectionFactor;
                } else {
                    size[intersections[0].index] = this.pointSize * this.pointHoverFactor;
                }
            } else {
                dispatcher.dispatch({actionType: 'entry-hovered', entry: null});
            }

            this.geometry.attributes.size.needsUpdate = true;
        }
    }

    /**
     * Geometry
     */
    updateGeometry() {
        this.updateBufferGeometry();
    }

    /**
     * Update point cloud geometry
     *
     * Point cloud geometry is straight up an array of coordinates for every
     * single point that go directly to the GPU
     */
    updateBufferGeometry() {
        if (this.pointCloud) {
            this.scene.remove(this.pointCloud);
        }

        this.geometry = new THREE.BufferGeometry();

        var positions = new Float32Array(this.data.entries.length * 3);
        var colors = new Float32Array(this.data.entries.length * 3);
        var sizes = new Float32Array(this.data.entries.length);
        var x;
        var y;
        var z;

        var color = new THREE.Color();

        this.pointSize = this.getPointSize();
        this.raycaster.params.PointCloud.threshold = this.pointSize / 2;

        _.each(this.data.entries, (entry, index) => {
            x = this.data.mappings.x ? this.data.scales[this.data.mappings.x](entry[this.data.mappings.x]) : 0;
            y = this.data.mappings.y ? this.data.scales[this.data.mappings.y](entry[this.data.mappings.y]) : 0;
            z = this.data.mappings.z ? this.data.scales[this.data.mappings.z](entry[this.data.mappings.z]) : 0;

            positions[3 * index + 0] = x;
            positions[3 * index + 1] = y;
            positions[3 * index + 2] = z;

            this.data.entries[index].x = x;
            this.data.entries[index].y = y;
            this.data.entries[index].z = z;

            if (entry.isSelected) {
                color.setHSL(0.7, 1, 1);
            } else {
                var hue = this.data.mappings.color ? this.data.scales[this.data.mappings.color](entry[this.data.mappings.color]) / this.options.graphSize * 0.25 : 0.15;
                color.setHSL(hue, 1, 0.5);
            }

            colors[3 * index + 0] = color.r;
            colors[3 * index + 1] = color.g;
            colors[3 * index + 2] = color.b;

            sizes[index] = entry.isSelected ? this.pointSize * this.pointSelectionFactor : this.pointSize;
        });

        this.geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
        this.geometry.addAttribute('size', new THREE.BufferAttribute(sizes, 1));
        this.geometry.addAttribute('customColor', new THREE.BufferAttribute(colors, 3));
        this.geometry.computeBoundingBox();

        this.pointCloud = new THREE.PointCloud(this.geometry, this.shaderMaterial);

        this.scene.add(this.pointCloud);
    }

    /**
     * Update axis lines and legend geometry
     */
    updateAxisGeometry() {
        if (this.axisMeshes) {
            _.each(this.axisMeshes, (mesh) => {
                this.scene.remove(mesh);
            });
        }

        this.axisMeshes = axisGeometry(this.data.mappings, this.options);
        this.scene.add.apply(this.scene, this.axisMeshes);
    }

    /**
     * Calculate point size based on the size of the
     * graph and amount of entries in data. The bigger the graph,
     * the smaller the point size.
     *
     * @returns {number}
     */
    getPointSize() {
        return Math.pow(this.options.graphSize / Math.log(this.data.entries.length), 2) / 1000;
    }

    remove() {
        window.removeEventListener('resize', _.bind(this.onWindowResize, this));
    }
};

module.exports = Graph;


<|start_filename|>src/components/DataLassoUI/index.js<|end_filename|>
'use strict';

var React = require('react');

var Uploader = require('../Uploader');
var AxisControls = require('../AxisControls');
var Hud = require('../Hud');
var ModeIndicator = require('../ModeIndicator');
var SelectionControls = require('../SelectionControls');


/**
 * # Data Lasso React component
 *
 * Wrapper component for Data Lasso UI which
 * initializes rest of the UI
 */

var DataLassoUI = React.createClass({
    render: function() {
        return (
            <div>
                <Uploader/>
                <AxisControls/>
                <Hud/>
                <ModeIndicator/>
                <SelectionControls/>
            </div>
        )
    }
});

module.exports = DataLassoUI;


<|start_filename|>src/components/Uploader/subcomponents/Toggle.js<|end_filename|>
'use strict';

var React = require('react');

/**
 * # Toggle React component
 *
 * Renders a single toggle
 */
const Toggle = React.createClass({
    propTypes: {
        isActive: React.PropTypes.bool,
        onClick: React.PropTypes.func,
        children: React.PropTypes.node.isRequired,
    },

    render: function() {
        let classes = this.props.isActive ? 'toggle active' : 'toggle';
        return (
            <button onClick={this.props.onClick} className={classes}>{this.props.children}</button>
        )
    }
});

module.exports = Toggle;


<|start_filename|>src/graph/helpers/keyboard.js<|end_filename|>
'use strict';

var dispatcher = require('../../dispatcher');

/**
 * # Keyboard Helper
 *
 * Sets up listeners for keyboard events; contains logic to update
 * store according to those events.
 */

class KeyboardHelper {
    constructor () {
        document.addEventListener('keyup', this.onDocumentKeyUp.bind(this), false);
        document.addEventListener('keydown', this.onDocumentKeyDown.bind(this), false);
    }

    onDocumentKeyUp (e) {
        switch (e.keyCode) {
            case 32: // SPACE
                this.startSelectionMode();
                break;

            case 13: // ENTER
            case 27: // ESC
                this.stopSelectionMode();
                break;

            case 16: // SHIFT
                this.stopSelectionModifier();
                break;

            case 18: // ALT
                this.stopSelectionModifier();
                break;
        }
    }

    onDocumentKeyDown (e) {
        switch (e.keyCode) {
            case 16: // SHIFT
                this.startSelectionModifier('add');
                break;

            case 18: // ALT
                this.startSelectionModifier('subtract');
                break;
        }
    }

    startSelectionModifier (modifier) {
        dispatcher.dispatch({
            actionType: 'selection-modifier-changed',
            selectionModifier: modifier,
        });
    }

    stopSelectionModifier () {
        dispatcher.dispatch({
            actionType: 'selection-modifier-changed',
            selectionModifier: null,
        });
    }

    startSelectionMode () {
        dispatcher.dispatch({
            actionType: 'mode-changed',
            mode: 'selection',
        });
    }

    stopSelectionMode () {
        dispatcher.dispatch({
            actionType: 'mode-changed',
            mode: 'view',
        });
    }
}

module.exports = KeyboardHelper;


<|start_filename|>public/modules/example-module.js<|end_filename|>
'use strict';

/**
 * # Sample Data Lasso Module
 *
 * Data Lasso functionality can be extended using modules. Below
 * is an example of a simple module that displays count of
 * attributes in the data set.
 *
 * ## Setup
 *
 * First, information about the module must be passed to Data
 * Lasso upon initialization in options hash in the form of:
 *
 * ```
 * options.modules = {
 *   nameOfTheModule: {
 *     constructor: constructorFN
 *   }
 * }
 * ```
 *
 * ## Module structure
 *
 * Attributes can be simple objects, the only requirement is for
 * a module to have `initialize` method (following Backbone View
 * convention). That `initialize `method will be called on Data
 * Lasso initialization with hash consisting of these attributes:
 *
 * - $container { jQuery object }
 *   Data Lasso container element. If your module has an interface,
 *   this is the element to append to.
 *
 * - store { Backbone.Model instance }
 *   Data Lasso store, containing current state. For a complete list
 *   of state attributes, refer to `/src/store/initialState.js`
 *
 * - dispatcher { Flux.Dispatcher instance }
 *   Action dispatcher of Data Lasso
 *
 */

var ExampleModule = Backbone.View.extend({

    className: 'sample-data-lasso-module',

    template: _.template('Data attributes count: <%= (_.keys(attributes)).length %>'),

    initialize: function (options) {
        this.dlStore = options.store;
        this.dlDispatcher = options.dispatcher;
        this.container = options.container;

        this.listenTo(this.dlStore, 'change:attributes', this._onNewInput);

        this.$el.css({
            position: 'absolute',
            top: '10px',
            right: '10px',
            color: '#ffffff',
        });

        this.render();
    },

    _onNewInput: function () {
        this.render(this.dlStore.get('attributes'));
    },

    render: function (attributes) {
        attributes || (attributes = []);
        this.$el.html(this.template({attributes: attributes}));
        this.container.appendChild(this.el);
    },
});


<|start_filename|>src/graph/helpers/selection.js<|end_filename|>
'use strict';

var _ = require('lodash');
var THREE = require('three');
var store = require('../../store');
var dispatcher = require('../../dispatcher');
var Frustum = require('../geometry/frustum');

/**
 * ## Selection Helper
 *
 * Helper module to handle lasso selection. It handles creation
 * and operation of a projection plane that is positioned
 * right in front of the camera and stays in front of the camera
 * at all times.
 *
 * When selection is being made, that projection plane servees as
 * a 'surface' where the selection is made
 *
 */


/**
 * Distance from projection plane, where selection is made,
 * to the camera itself.
 */
var planeDistanceFromCamera = 2000;

/**
 * Size of the projection plane, should be big enough to cover most
 * of the viewport, but not big enough to cause performance issues.
 */
var planeSize = 2000;

/**
 * How many points will be in selection polygon lasso
 */
var lassoPoints = 4;

var SelectionHelper = class {
    constructor (scene, camera, mouse) {
        this.camera = camera;
        this.scene = scene;
        this.mouse = mouse;

        this.mouse.on('datalasso:mouse:move', this.onMouseMove.bind(this));
        this.mouse.on('datalasso:mouse:down', this.onMouseDown.bind(this));

        store.on('change:mode', this.onModeChange.bind(this));

        this.lassoPoints = [];
        this.lassoLineSegments = [];
    }

    onModeChange () {
        if (store.get('mode') === 'view') {
            this.cancelSelection();
        }
    }

    /**
     * Update the projection plane - an invisible
     * plane between the camera and the graph
     *
     * Called by the graph when 3d space is moved, rotated or scaled.
     */
    updateProjectionPlane () {
        if (!this.projectionPlane) {
            var planeMaterial = new THREE.MeshBasicMaterial({color: 0xFFFFFF, transparent: true, opacity: 0.0});
            var planeGeometry = new THREE.PlaneGeometry(planeSize, planeSize, 1);

            this.projectionPlane = new THREE.Mesh(planeGeometry, planeMaterial);
            this.scene.add(this.projectionPlane);
        }

        var vector = new THREE.Vector3(0, 0, -1);

        vector.applyQuaternion(this.camera.quaternion);
        vector.multiplyScalar(planeDistanceFromCamera);

        this.planePosition = new THREE.Vector3().addVectors(this.camera.position, vector);

        this.projectionPlane.position.copy(this.planePosition);
        this.projectionPlane.rotation.setFromQuaternion(this.camera.quaternion);
    }

    /**
     * Based on mouse position, get point on projection
     * plane that is essentially an invisible wall between
     * camera and graph
     */
    getPointOnProjectionPlane (mouseVector) {
        var raycaster = new THREE.Raycaster();

        raycaster.setFromCamera(mouseVector, this.camera);

        var intersections = raycaster.intersectObject(this.projectionPlane);

        if (intersections.length) {
            return intersections[0].point;
        }

        return false;
    }

    onMouseDown (e) {
        switch (e.button) {
            case THREE.MOUSE.LEFT:
                if (store.get('mode') === 'selection') {
                    if (!store.get('selectionInProgress')) {
                        this.lassoPoints = [];
                        this.lassoLineSegments = [];
                        dispatcher.dispatch({actionType: 'selection-started'});
                    }
                    this.addLassoPoint(e.vector);
                }
                break;
        }
    }

    onMouseMove (e) {
        if (store.get('mode') === 'selection') {
            this.drawPreviewLine(e.vector);
        }
    }

    /**
     * ### Draw Preview Line
     *
     * Preview line is a lasso selection line that follows
     * mouse cursor when selection is being made
     *
     * @param mouseVector
     */
    drawPreviewLine (mouseVector) {
        this.clearPreviewLine();

        if (this.lassoPoints.length) {
            this.previewLine = this.getLineMesh(this.lassoPoints[this.lassoPoints.length - 1], this.getPointOnProjectionPlane(mouseVector));

            this.scene.add(this.previewLine);
        }
    }

    /**
     * ### Clear Preview Line
     */
    clearPreviewLine () {
        if (this.previewLine) {
            this.scene.remove(this.previewLine);
        }
    }

    /**
     * ### Get Line Mesh
     *
     * Helper method to draw a line from point A to point B
     *
     * @param pointA
     * @param pointB
     * @returns {mesh} - line mesh
     */
    getLineMesh (pointA, pointB) {
        var material = new THREE.LineBasicMaterial({color: 0xffffff, trasparent: true});
        material.opacity = 0.25;

        var geometry = new THREE.Geometry();

        geometry.vertices.push(pointA);
        geometry.vertices.push(pointB);

        return new THREE.Line(geometry, material);
    }

    /**
     * ### Add Lasso Point
     *
     * When new point is selected for polygonal lasso selection,
     * store it and progress drawing of lasso tool
     *
     * @param mouseVector {vector3}
     */
    addLassoPoint (mouseVector) {
        var newPoint = this.getPointOnProjectionPlane(mouseVector);

        if (newPoint) {
            this.lassoPoints.push(newPoint);

            // If a point was already selected, connect them
            if (this.lassoPoints.length > 1) {
                this.addLassoSegment(newPoint, this.lassoPoints[this.lassoPoints.length - 2]);
            }

            // All points are placed, selection is completed
            if (this.lassoPoints.length === lassoPoints) {
                this.addLassoSegment(newPoint, this.lassoPoints[0]);
                this.finalizeSelection();
            }
        }
    }

    /**
     * ### Cancel Selection
     *
     * Clean up if selection ended mid flight
     */
    cancelSelection () {
        this.clearPreviewLine();
        this.removeLassoLineSegments();

    }

    /**
     * ### Finalize Selection
     *
     * Coordinate end of selection. Remove leftover
     * visual helpers, stop selection mode and initialize
     * calculation of what was selected
     */
    finalizeSelection () {
        this.clearPreviewLine();

        // Remove the drawn lasso 100ms later to give user visual feedback
        _.delay(_.bind(this.removeLassoLineSegments, this), 100);

        this.performSelection(store.get('selectionModifier'));
        dispatcher.dispatch({actionType: 'selection-stopped'});
    }

    /**
     * ### Add Lasso Segment
     *
     * Draw a line of lasso selection tool based on two points in space
     */
    addLassoSegment (pointA, pointB) {
        var line = this.getLineMesh(pointA, pointB);
        this.scene.add(line);
        this.lassoLineSegments.push(line);
    }

    /**
     * ### Remove Lasso Segment
     *
     * Remove polygonal lasso that was drawn during selection
     */
    removeLassoLineSegments () {
        _.forEach(this.lassoLineSegments, (segment) => {
            this.scene.remove(segment);
        });
        this.lassoPoints = [];
        this.lassoLineSegments = [];
    }

    /**
     * ### Perform Selection
     *
     * This is THE thing that does the selection.
     * Perform selection on the graph using lasso selection from the user.
     *
     * If initially selection yields no results, construct reversed frustum
     * and try selection again.
     *
     * @param selectionModifier - Selection modifier at the time of the selection
     */
    performSelection (selectionModifier) {
        var result = [];

        switch (selectionModifier) {
            case 'add':
                var addedEntries = this.getSelectedEntries(store.get('entries'));
                result = _.uniq(_.concat(store.get('selectedEntries'), addedEntries));
                break;

            case 'subtract':
                var removedEntries = this.getSelectedEntries(store.get('entries'));
                var originalSelection = store.get('selectedEntries');
                result = _.without(originalSelection, ...removedEntries);
                break;

            default:
                result = this.getSelectedEntries(store.get('entries'));
        }

        dispatcher.dispatch({
            actionType: 'selection-made',
            selectedEntries: result,
        });
    }

    /**
     * ## Get Selected Entries
     *
     * Perform selection in a set of items
     *
     * @param {Array} entries - Array of entries which we will be checking against matching the selection
     */
    getSelectedEntries (entries) {
        var frustum = new Frustum(this.camera.position, this.lassoPoints);
        var result = frustum.findEntriesInsideFrustum(entries);
        if (!result.length) {
            frustum.invert();
        }
        result = frustum.findEntriesInsideFrustum(entries);
        return result;
    }
};

module.exports = SelectionHelper;


<|start_filename|>src/dispatcher/index.js<|end_filename|>
'use strict';

/**
 * # Dispatcher
 *
 * This is a dispatcher for Data Lasso, as in Dispatcher from Flux pattern.
 * It's task is to accept dispatched events and pass them to registered callbacks.
 */

var Dispatcher = require('flux').Dispatcher;

module.exports = new Dispatcher();


<|start_filename|>src/components/Uploader/subcomponents/FileForm.js<|end_filename|>
'use strict';

const React = require('react');

/**
 * # File Form React component
 *
 * Renders a form for file upload and handles reading uploaded file
 */
const FileForm = React.createClass({
    propTypes: {
        handleDataLoad: React.PropTypes.func.isRequired, // Function to call with uploaded data
    },

    getInitialState: function() {
        return {
            loading: false,
            error: false,
        }
    },

    /**
     * Handles submission of file upload form. Reads the file and passes data up
     * to a parent component
     */
    handleFormSubmission: function(e) {
        e.preventDefault();

        var file = e.target[0].files[0];
        if (file) {
            var reader = new FileReader();

            this.setState({
                loading: true,
            });

            reader.onload = (e) => {
                this.props.handleDataLoad(e.target.result, this.dataLoadCallback);
            };
            reader.readAsText(file);
        }
    },

    /**
     * Callback from parent component that is called when data is done
     * being processed
     *
     * @param {string} [error] - Error, if one happened during data processing
     */
    dataLoadCallback: function(error) {
        this.setState({
            loading: false,
            error: error,
        });
    },

    render: function() {
        let inputStyle = {
            border: (!!this.state.error) ? '1px solid red' : '',
        };
        return (
            <form onSubmit={this.handleFormSubmission} className="file-upload-form">
                <div className="row">
                    <input style={inputStyle} type="file" className="file-upload-input"/>
                </div>
                <div className="row">
                    <button type="submit" className="button red">{this.state.loading ? 'LOADING...' : 'UPLOAD'}</button>
                </div>
            </form>
        )
    }
});


module.exports = FileForm;


<|start_filename|>src/store/data-functions.js<|end_filename|>
'use strict';

var _ = require('lodash');
var d3 = require('d3');

/**
 * ## Data Helper
 *
 * Helper module to handle processing of incoming data
 *
 */

var DataHelper = {

    processInput: function (data, options) {
        var entries = this._preProcessKnownFields(data);
        var attributes = this._processAttributes(entries);
        var scales = this._computeScales(attributes, options.graphSize);

        return {
            entries: entries,
            attributes: attributes,
            scales: scales,
        };
    },

    getUpdatedScales: function (entries, options) {
        var attributes = this._processAttributes(entries);
        var scales = this._computeScales(attributes, options.graphSize);

        return scales;
    },

    /**
     * Process certain known fields into more useful ones.
     *
     * - Break down IP field into subnets (`ip`)
     * - Extract domain from eligible email fields (`email`)
     * - Break down geo onto city and country (`geo`)
     */
    _preProcessKnownFields: function (entries) {
        _.each(entries, function (entry, index) {
            entry.__id = index;

            if (entry.ip) {
                try {
                    var ipSegments = entry.ip.split('.');
                    delete entries[index].ip;
                    entries[index].ip4 = ipSegments.slice(0, 4).join('.');
                    entries[index].ip3 = ipSegments.slice(0, 3).join('.');
                    entries[index].ip2 = ipSegments.slice(0, 2).join('.');
                } catch (e) {}
            }

            if (entry.email) {
                try {
                    entries[index].emailDomain = entry.email.split('@')[1];
                } catch (e) {}
            }

            if (entry.geo) {
                try {
                    entries[index].country = entry.geo.split('~')[0];
                    entries[index].city = entry.geo.split('-')[1];
                } catch (e) {}
            }
        }, this);

        return entries;
    },


    /**
     * Break down incoming raw data into structured data
     * to be used in graphing; calculate possible variations
     * and extreme values for each attribute
     */
    _processAttributes: function (entries) {
        var attributes = {};

        /**
         * Look at the first row of data and set up attributes hash
         * based on that.
         *
         * - If value looks like a number, attribute will be deemed numeric,
         *   and min/max values will be added to it
         *
         * - If value does not look like a number, attribute will be considered
         *   categorical, and a list of categories will be added.
         */
        _.each(entries[0], function (value, name) {
            attributes[name] = {};
            if (/^\d+$/.test(value)) {
                value = parseInt(value, 10);
                attributes[name].type = 'numeric';
                attributes[name].min = value;
                attributes[name].max = value;
            } else {
                attributes[name].type = 'categorical';
                attributes[name].categories = {};
            }
        });

        /**
         * Go over the rest of the entries and calculate min/max values for
         * numeric attributes, and count instances of each category for categorical
         * attributes.
         */
        _.each(entries, function (entry) {
            _.each(entry, function (value, name) {
                if (attributes[name].type === 'categorical') {
                    if (!attributes[name].categories[value]) {
                        attributes[name].categories[value] = 1;
                    } else {
                        attributes[name].categories[value] += 1;
                    }
                } else if (attributes[name].type === 'numeric') {
                    value = parseInt(value, 10);
                    if (value < attributes[name].min) {
                        attributes[name].min = value;
                    }
                    if (value > attributes[name].max) {
                        attributes[name].max = value;
                    }
                }
            });
        });

        // To save computation time, categories are processed after the initial parsing
        var categories;

        _.each(attributes, function (attribute, name) {
            if (attribute.categories) {
                categories = [];

                _.each(attribute.categories, function (count, category) {
                    categories.push(category);
                }, this);

                attributes[name].categories = categories;
            }
        });

        // Sort category arrays
        _.each(attributes, function (attribute, name) {
            if (attribute.categories) {
                attributes[name].categories.sort();
            }
        });

        return attributes;
    },


    /**
     * Compute D3 scales for attributes and values in data
     */
    _computeScales: function (attributes, size) {
        var scales = {};

        _.each(attributes, function (attribute, name) {
            if (attribute.type === 'categorical') {
                scales[name] = d3.scale.ordinal()
                    .domain(attribute.categories)
                    .rangePoints([0, size]);
            } else if (attribute.type === 'numeric') {
                scales[name] = d3.scale.linear()
                    .domain([attribute.min, attribute.max])
                    .range([0, size]);
            }
        }, this);

        return scales;
    },
};

module.exports = DataHelper;


<|start_filename|>src/graph/geometry/frustum.js<|end_filename|>
'use strict';

var _ = require('lodash');
var THREE = require('three');

/**
 * # Frustum
 *
 * Frustum-like structure that consists of four planes containing the selection.
 *
 * It is described by 5 points - one for a camera that is the tip of the selection
 * and 4 points that are in the base of the frustum.
 * 
 *                [camera]
 *                   ▼
 *
 *                  /-\\
 *                 /---\ \
 *                /-----\  \
 *               /-------\   \
 *              /---------\    \
 *             /-----------\     \
 *            /-------------\      \
 *           /---------------\       \
 *          /-----------------\        \
 *         /-------------------\         \
 *        /---------------------\          \ p3
 *       /-----------------------\        /
 *      /-------------------------\     /
 *     /---------------------------\   /
 *    /-----------------------------\/
 *  p1                              p2
 *
 *
 * @param {Vector3} cameraPosition - Current position of the camera
 * @param {[Vector3]} points - Array of vectors for every point of frustum's bottom
 */
class Frustum {
    constructor(cameraPosition, points) {
        this.cameraPosition = cameraPosition;
        this.points = points;
        this.planes = this.createFrustumPlanes();
    }

    /**
     * ## Invert
     *
     * Inverts frustum planes so that their normals are reversed
     */
    invert() {
        this.planes = this.createFrustumPlanes(true);
    }

    /**
     * ## Create Frustum Planes
     *
     * Creates an array of planes that make up the frustum
     *
     * @param {bool} inverse - whether to create planes in reversed order
     */
    createFrustumPlanes (inverse = false) {
        var planes = [];
        if (!inverse) {
            planes.push(new THREE.Plane().setFromCoplanarPoints(this.points[0], this.cameraPosition, this.points[1]));
            planes.push(new THREE.Plane().setFromCoplanarPoints(this.points[1], this.cameraPosition, this.points[2]));
            planes.push(new THREE.Plane().setFromCoplanarPoints(this.points[2], this.cameraPosition, this.points[3]));
            planes.push(new THREE.Plane().setFromCoplanarPoints(this.points[3], this.cameraPosition, this.points[0]));
        } else {
            planes.push(new THREE.Plane().setFromCoplanarPoints(this.points[1], this.cameraPosition, this.points[0]));
            planes.push(new THREE.Plane().setFromCoplanarPoints(this.points[2], this.cameraPosition, this.points[1]));
            planes.push(new THREE.Plane().setFromCoplanarPoints(this.points[3], this.cameraPosition, this.points[2]));
            planes.push(new THREE.Plane().setFromCoplanarPoints(this.points[0], this.cameraPosition, this.points[3]));
        }
        return planes;
    }

    /**
     * ## Find Entries Inside Frustum
     *
     * Go over the graph's entries and check what is inside the frustum of users's selection
     *
     * @param entries - List of entries to check
     * @returns {Array} of selected entry ids
     */
    findEntriesInsideFrustum (entries) {
        var selected = [];

        _.forEach(entries, (entry) => {
            if (this.isPointInsideFrustum(new THREE.Vector3(entry.x, entry.y, entry.z))) {
                selected.push(entry.__id);
            }
        });

        return selected;
    }

    /**
     * ## Is Point Inside Frustum
     *
     * Check if entry is inside the frustum by checking distance to every frustum plane.
     *
     * Distance from a point to a plain is negative if the point is
     * 'inside' the plane (on the back side of the plane)
     *
     * @param point - Point to check
     */
    isPointInsideFrustum (point) {
        for (var i = 0; i < this.planes.length; i++) {
            var distance = this.planes[i].distanceToPoint(point);

            if (distance > 0) {
                return false;
            }
        }

        return true;
    }
}

module.exports = Frustum;


<|start_filename|>src/components/hud/index.js<|end_filename|>
'use strict';

var _ = require('lodash');
var store = require('../../store');
var React = require('react');

// Margin from HUD element to selected entry
var margin = 20;

/**
 * ## Heads Up Display Component
 *
 * Handles HUD that is displayed when you hover over
 * the entry on the graph.
 */
var Hud = React.createClass({
    getInitialState: function() {
        return {
            focused: store.get('focused'),
            mappings: store.get('mappings'),
            attributesInUse: [],
            top: 0,
            left: 0,
        }
    },

    componentDidMount: function() {
        store.on('change:focused', this.handleFocusedChange);
        store.on('change:mappings', this.handleMappingsChange);

        document.addEventListener('mousemove', _.throttle(_.bind(this.handleMouseMove, this), 50));
    },

    /**
     * Update internal state with entry that is currently in focus
     */
    handleFocusedChange: function() {
        this.setState({
            focused: store.get('focused'),
        })
    },

    /**
     * Update internal state with new axis mappings that were selected
     */
    handleMappingsChange: function() {
        let attributesInUse = _.filter(_.map(store.get('mappings'), (axis) => axis));
        this.setState({
            attributesInUse: attributesInUse || []
        });
    },

    /**
     * Update internal state with pointer coordinates
     */
    handleMouseMove: function (e) {
        this.setState({
            top: e.y + margin,
            left: e.x + margin,
        });
    },

    /**
     * Render the HUD if something is in focus and some attributes were mapped to axis
     * @returns {*}
     */
    render: function() {
        if (this.state.focused && this.state.attributesInUse.length) {
            let attributes = _.map(this.state.attributesInUse, (attribute) => {
                return {
                    name: attribute,
                    value: this.state.focused[attribute],
                }
            });
            let style = {
                top: this.state.top,
                left: this.state.left,
            };
            return (
                <div className="hud-container" style={style}>
                    {_.map(attributes, (attribute) => {
                        return (
                            <div key={attribute.name} className='attribute'>
                                <label className="attribute-name">{attribute.name}</label>
                                <span className="attribute-value">{attribute.value}</span>
                            </div>
                        )
                    })}
                </div>
            )
        } else {
            return null;
        }
    }
});

module.exports = Hud;


<|start_filename|>src/store/initial-state.js<|end_filename|>
'use strict';

/**
 * # Initial State
 *
 * This object contains initial state of the data store.
 */
module.exports = {

    /**
     * List of entries (records) to visualize
     */
    entries: [],

    /**
     * List D3 scales - one scale per axis
     */
    scales: [],

    /**
     * List of attributes in the data set. For numerical attributes also contains min/max
     * values, for categorical attributes also contains list of factors
     */
    attributes: [],

    /**
     * List of IDs of selected entries
     */
    selectedEntries: [],

    /**
     * List of snapshots of data that changes when zooming is performed
     */
    snapshots: [],

    /**
     * Flag that determines whether controls (pan, zoom, rotate) are on or off
     */
    controls: true,

    /**
     * UI mode. Currently has two possible values:
     *  - 'view'
     *  - 'selection'
     */
    mode: 'view',

    /**
     * Selection modifier - adding or subtracting from current selection
     *  - 'add'
     *  - 'subtract'
     */
    selectionModifier: null,

    /**
     * Selection in progress flag
     */
    selectionInProgress: false,

    /**
     * Object containing mappings of attributes to axes
     */
    mappings: {
        x: null,
        y: null,
        z: null,
        color: null,
    },

    source: 'file',

    type: 'csv',
};


<|start_filename|>src/components/Uploader/index.js<|end_filename|>
'use strict';

var d3 = require('d3');
var store = require('../../store');
var dispatcher = require('../../dispatcher');
var React = require('react');

var Toggle = require('./subcomponents/Toggle');
var FileForm = require('./subcomponents/FileForm');
var UrlForm = require('./subcomponents/UrlForm');

/**
 * ## Upload Form Component
 *
 * Handles UI and logic of loading data into Data Lasso
 * either using file or by requesting dataset from a URL
 */
var Uploader = React.createClass({
    getInitialState: function() {
        return {
            source: store.get('source'),
            type: store.get('type'),
        };
    },

    componentDidMount: function() {
        store.on('change:source', () => {this.setState({source: store.get('source')})});
        store.on('change:type', () => {this.setState({type: store.get('type')})});
    },

    /**
     * Handles incoming data either from file uploader form or from
     * URL form
     *
     * @param {string} data - Data as a string, coming from file or URL forms
     * @param {fn} callback - Callback function for when data was loaded
     */
    handleDataLoad: function(data, callback) {
        if (data) {
            var parsedData;

            try {
                parsedData = this.parseFile(data);
            } catch (e) {
                callback('Error parsing incoming data');
            }

            if (parsedData) {
                store.once('change:entries', () => {
                    callback();
                });
                dispatcher.dispatch({actionType: 'file-uploaded', entries: parsedData});
            } else {
                callback('No entries in the data');
            }
        } else {
            callback('Data to load is empty');
        }
    },

    /**
     * Parses incoming data from a string to an object based on the file type provided
     * @param {string} data - Data as a string, coming from file or URL forms
     * @returns {object} - Parsed data
     */
    parseFile: function (data) {
        switch (this.state.type) {
            case 'csv':
                return d3.csv.parse(data);
            case 'tsv':
                return d3.tsv.parse(data);
            case 'json':
                return JSON.parse(data);
        }
    },

    /**
     * Renders Uploader UI, with either file upload form, or URL upload form
     */
    render: function() {
        let form = this.state.source === 'file' ? (<FileForm handleDataLoad={this.handleDataLoad}/>) : (<UrlForm handleDataLoad={this.handleDataLoad}/>);
        return (
            <div className='uploader'>
                <Toggles source={this.state.source} type={this.state.type} />
                {form}
            </div>
        )
    }
});

/**
 * ## Toggles React component
 *
 * Renders a set of toggles that are used in the Uploader
 */
const Toggles = React.createClass({
    handleSourceChange: function(newSource) {
        dispatcher.dispatch({
            actionType: 'source-change',
            source: newSource,
        });
    },

    handleTypeChange: function(newType) {
        dispatcher.dispatch({
            actionType: 'type-change',
            type: newType,
        });
    },

    render: function() {
        return (
            <div>
                <div className="row">DATA SOURCE</div>

                <div className="row extra-spacing">
                    <div className="column">
                        <Toggle onClick={this.handleSourceChange.bind(this, 'file')} isActive={this.props.source === 'file'}>FILE</Toggle>
                    </div>
                    <div className="column">
                        <Toggle onClick={this.handleSourceChange.bind(this, 'url')} isActive={this.props.source === 'url'}>URL</Toggle>
                    </div>
                </div>

                <div className="row extra-spacing">
                    <div className="column">
                        <Toggle onClick={this.handleTypeChange.bind(this, 'csv')} isActive={this.props.type === 'csv'}>CSV</Toggle>
                    </div>
                    <div className="column">
                        <Toggle onClick={this.handleTypeChange.bind(this, 'tsv')} isActive={this.props.type === 'tsv'}>TSV</Toggle>
                    </div>
                    <div className="column">
                        <Toggle onClick={this.handleTypeChange.bind(this, 'json')} isActive={this.props.type === 'json'}>JSON</Toggle>
                    </div>
                </div>
            </div>
        )
    }
});

module.exports = Uploader;


<|start_filename|>src/components/SelectionControls/index.js<|end_filename|>
'use strict';

var _ = require('lodash');
var store = require('../../store');
var dispatcher = require('../../dispatcher');
var React = require('react');

/**
 * ## Selection UI Component
 *
 * Displays information on currently selected entries and zooming in and out of a dataset
 */
var SelectionControls = React.createClass({
    getInitialState: function() {
        return {
            entriesTotal: store.get('entries').length,
            selectedEntriesTotal: _.filter(store.get('entries'), 'isSelected').length,
            snapshotCount: store.get('snapshots').length,
        }
    },

    componentDidMount: function() {
        store.on('change:selectedEntries', this.handleDataUpdate);
        store.on('change:snapshots', this.handleDataUpdate);
    },

    /**
     * Update internal state when selection or snapshots change
     */
    handleDataUpdate: function() {
        this.setState({
            entriesTotal: store.get('entries').length,
            selectedEntriesTotal: _.filter(store.get('entries'), 'isSelected').length,
            snapshotCount: store.get('snapshots').length,
        });
    },

    handleZoomOutClick: function() {
        dispatcher.dispatch({actionType: 'zoom-out'});
    },

    handleZoomInClick: function() {
        dispatcher.dispatch({actionType: 'zoom-in'});
    },

    /**
     * Renders the selection UI if something is selected, or zooming was performed earlier
     */
    render: function() {
        if (this.state.selectedEntriesTotal || this.state.snapshotCount) {
            return (
                <div className="selection-controls">
                    <div className="selected-indicator">
                        <div className="total">
                            <div className="count-label">Total</div>
                            <div className="count">{this.state.entriesTotal}</div>
                        </div>
                        <div className="selected">
                            <div className="count-label">Selected</div>
                            <div className="count">{this.state.selectedEntriesTotal}</div>
                        </div>
                    </div>

                    <div className="buttons-container">
                        <ZoomControlButton onClick={this.handleZoomOutClick} className="back" isActive={this.state.snapshotCount > 0}>◀</ZoomControlButton>
                        <ZoomControlButton onClick={this.handleZoomInClick} className="zoom" isActive={this.state.selectedEntriesTotal > 0}>Zoom in</ZoomControlButton>
                    </div>


                    <DownloadButton isActive={this.state.selectedEntriesTotal}>Download selected as csv</DownloadButton>
                </div>
            )
        } else {
            return null;
        }
    },
});

/**
 * ## Zoom Control button helper React component
 *
 * Buttons that control zoom
 */
var ZoomControlButton = React.createClass({
    propTypes: {
        onClick: React.PropTypes.func,
        isActive: React.PropTypes.bool,
    },

    render: function() {
        let classList = ['button', 'black'];
        this.props.className && classList.push(this.props.className);
        classList.push(this.props.isActive ? 'active' : 'disabled');
        return (
            <button onClick={this.props.onClick} className={classList.join(' ')}>{this.props.children}</button>
        )
    }
});

/**
 * ## Selection Download button helper React component
 *
 * Renders button for downloading selection as a CSV. Also handles generating that CSV and opening it
 * for download
 */
var DownloadButton = React.createClass({
    downloadSelected: function() {
        var csvContent = 'data:text/csv;charset=utf-8,';
        csvContent += _.keys(store.get('attributes')).join(',');

        _.forEach(store.get('entries'), (entry, index) => {
            if (entry.isSelected) {
                csvContent += _.values(entry).join(',');
                csvContent += (index < store.get('selectedEntries').length) ? '\n' : '';
            }
        });
        window.open(encodeURI(csvContent));
    },

    render: function() {
        if (this.props.isActive) {
            return (
                <div onClick={this.downloadSelected} className="download-link">
                    {this.props.children}
                </div>
            )
        } else {
            return null;
        }
    }
});

module.exports = SelectionControls;


<|start_filename|>src/components/AxisControls/index.js<|end_filename|>
'use strict';

var $ = require('jquery');
var _ = require('lodash');
var store = require('../../store');
var dispatcher = require('../../dispatcher');
var React = require('react');

/**
 * ## Axis Controls Component
 *
 * Component that handles mapping of attributes to axis.
 * Displays mappings selectors based on data received from upload process.
 */
var AxisControls = React.createClass({
    getInitialState: function() {
        return {
            mappings: store.get('mappings'),
            attributes: store.get('attributes'),
        }
    },

    componentDidMount: function () {
        store.on('change:attributes', () => {
            this.setState({
                attributes: store.get('attributes'),
                mappings: store.get('mappings'),
            });
        });
    },

    /**
     * Handles setting of new axis mappings. Makes a new axis mappings hash
     * and passes it on to the Dispatcher
     */
    submitHandler: function(e) {
        e.preventDefault();
        let mappings = _.clone(this.state.mappings);

        _.forEach(this.state.mappings, (mapping, axis) => {
            var selectValue = $(e.target).find('.axis-selector[name="' + axis + '"]').val();
            mappings[axis] = selectValue ? selectValue : null;
        });

        dispatcher.dispatch({actionType: 'axis-mappings-updated', mappings: mappings});
    },

    /**
     * Renders the form for Axis Mappings Form. If state does not have attributes (nothing was
     * uploaded yet), renders nothing
     */
    render: function() {
        if (!_.isEmpty(this.state.attributes)) {
            return (
                <form onSubmit={this.submitHandler} className="axis-controls controls-form">
                    {_.map(this.state.mappings, (mappedAttribute, axisName) => {
                        return (
                            <Axis key={axisName} attributes={this.state.attributes} mappedAttribute={mappedAttribute} name={axisName}/>
                        )
                    })}

                    <button className="button red">Go</button>
                </form>
            )
        } else {
            return null;
        }
    }
});

/**
 * ## Axis React Component
 *
 * Helper component that renders label and options drop down for a given axis
 */
var Axis = React.createClass({
    propTypes: {
        attributes: React.PropTypes.object.isRequired, // Array of attributes
        mappedAttribute: React.PropTypes.string, // Attribute name currently mapped to this axis
        name: React.PropTypes.string.isRequired, // Name of the axis
    },

    render: function() {
        let attributes = _.filter(_.keys(this.props.attributes), (attributeName) => !_.startsWith(attributeName, '_'));
        return (
            <div>
                <label className="axis-label" for={this.props.name}>{this.props.name}</label>
                <select name={this.props.name} id={this.props.name} className="axis-selector" defaultValue={this.props.mappedAttribute || ""}>
                    <option value="">not mapped</option>

                    {_.map(attributes, (attributeName) => {
                        return <option key={attributeName} value={attributeName}>{attributeName}</option>
                    })}
                </select>
            </div>
        )
    }
});

module.exports = AxisControls;


<|start_filename|>src/components/ModeIndicator/index.js<|end_filename|>
'use strict';

var React = require('react');
var store = require('../../store');

/**
 * ## Mode Indicator Component
 *
 * Handles visual feedback when mode is changed between normal mode and selection mode.
 */
var ModeIndicator = React.createClass({
    getInitialState: function() {
        return {
            mode: store.get('mode'),
        }
    },

    componentDidMount: function() {
        store.on('change:mode', () => {
            this.setState({
                mode: store.get('mode'),
            })
        });
    },

    /**
     * Contains text that should be displayed for each mode
     * @param {string} mode for which to get copy
     * @returns {object} hash of copy containing header and a hint
     */
    getCopyForMode: function(mode) {
        switch (mode) {
            case 'view':
                return {
                    header: 'View mode',
                    hint: '(SPACEBAR for selection mode)',
                };
            case 'selection':
                return {
                    header: 'Selection mode',
                    hint: '(ESC for view mode, LMB to select a 4-point rectangle)',
                };
        }
    },

    render: function() {
        let copy = this.getCopyForMode(this.state.mode);
        return (
            <div className="mode-indicator">
                <div className={this.state.mode + '-mode'}>
                    <strong>{copy.header}</strong>
                    <small>{copy.hint}</small>
                </div>
            </div>
        )
    }
});

module.exports = ModeIndicator;


<|start_filename|>src/components/Uploader/subcomponents/UrlForm.js<|end_filename|>
'use strict';

const $ = require('jquery');
const React = require('react');

/**
 * # Url Upload Form React component
 *
 * Renders a form for downloading data set from a given URL
 */
const UrlForm = React.createClass({
    propTypes: {
        handleDataLoad: React.PropTypes.func.isRequired, // Function to call with uploaded data
    },

    getInitialState: function() {
        return {
            loading: false,
            error: false,
        }
    },

    /**
     * Handles submission of URL data set download form. Fires off XHR request and provides
     * callbacks for it's completion
     */
    handleFormSubmission: function(e) {
        e.preventDefault();

        var url = e.target[1].value;
        if (url) {
            this.setState({
                loading: true,
            });

            $.get(url)
                .then((data) => {this.props.handleDataLoad(data, this.dataLoadCallback)})
                .fail((jqXHR, status, error) => {
                    if (status === 'error' && !error) {
                        error = 'Error while making a request';
                    }
                    this.dataLoadCallback(error);
                });
        }
    },

    /**
     * Callback from parent component that is called when data is done
     * being processed
     *
     * @param {string} [error] - Error, if one happened during data processing
     */
    dataLoadCallback: function(error) {
        this.setState({
            loading: false,
            error: error,
        });
    },

    render: function() {
        return (
            <form onSubmit={this.handleFormSubmission} className="url-form">
                <UrlField error={this.state.error} />
                <div className="row">
                    <button className="button red">{this.state.loading ? 'LOADING...' : 'UPLOAD'}</button>
                </div>
            </form>
        )
    }
});

/**
 * ## UrlField React Component
 *
 * Helps with rendering the URL input field, as well as logic
 * to pre-populate that field with a test dataset
 */
const UrlField = React.createClass({
    getInitialState: function() {
        return {
            url: '',
        }
    },

    handleTestDatasetClick: function() {
        this.setState({
            url: '//tumblr.github.io/data-lasso/samples/UCS_Satellite_Database.csv',
        });
    },

    handleValueChange: function (e) {
        this.setState({
            url: e.target.value,
        });
    },

    render: function() {
        let inputStyle = {
            border: this.props.error ? '1px solid red' : '',
        };
        return (
            <div>
                <button className="button transparent" onClick={this.handleTestDatasetClick}>Use sample dataset</button>
                <input style={inputStyle} onChange={this.handleValueChange} value={this.state.url} type="text" placeholder="URL to a file" className="text-input url-input"/>
            </div>
        )
    }
});

module.exports = UrlForm;


<|start_filename|>src/graph/geometry/axis.js<|end_filename|>
'use strict';

var _ = require('lodash');
var THREE = require('three');

/**
 * ## Axis Geometry
 *
 * Contains functions to make geometry for chart axis and axis legends.
 * Call is as a function to get back a list of meshes.
 *
 */


// Height of the text geometry material
var textGeometryHeight = 1;

// Segments per curve of the legent text geometry
var textCurveSegments = 2;

// Opacity of the legend text
var legendOpacity = 0.9;

// Offset of legend text relative to axis
var legendPositionOffset = 100;

// Color of the axis line, specified as array of R,G,B
var axisLineColor = [0.15, 0.14, 0.21];


var makeAxisTextGeometry = function (mappings, options) {
    var labelMeshes = [];

    _.each(mappings, function (axis, name) {
        var geometry = new THREE.TextGeometry(name + (axis ? ' : ' + axis : ''), {
            size: options.legendSize,
            height: textGeometryHeight,
            curveSegments: textCurveSegments,
        });

        var material = new THREE.MeshBasicMaterial({
            color: options.legendColor,
            transparent: true,
        });

        material.opacity = legendOpacity;

        var mesh = new THREE.Mesh(geometry, material);

        switch (name) {
            case 'x':
                mesh.rotation.x = -Math.PI / 2;
                mesh.position.x = options.graphSize + legendPositionOffset;
                break;
            case 'y':
                mesh.position.y = options.graphSize + legendPositionOffset;
                break;
            case 'z':
                mesh.rotation.x = -Math.PI / 2;
                mesh.rotation.z = Math.PI / 2;
                mesh.position.z = options.graphSize + legendPositionOffset;
                break;
        }

        labelMeshes.push(mesh);
    }, this);

    return labelMeshes;
};

var makeAxisLinesGeometry = function (options) {
    var size = options.graphSize;

    var vertices = new Float32Array([
        0, 0, 0,  size, 0, 0,
        0, 0, 0,  0, size, 0,
        0, 0, 0,  0, 0, size,
    ]);

    var colors = new Float32Array([
        axisLineColor[0], axisLineColor[1], axisLineColor[2],   axisLineColor[0], axisLineColor[1], axisLineColor[2],
        axisLineColor[0], axisLineColor[1], axisLineColor[2],   axisLineColor[0], axisLineColor[1], axisLineColor[2],
        axisLineColor[0], axisLineColor[1], axisLineColor[2],   axisLineColor[0], axisLineColor[1], axisLineColor[2],
    ]);

    var geometry = new THREE.BufferGeometry();
    geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
    geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));

    var material = new THREE.LineBasicMaterial({vertexColors: THREE.VertexColors});

    return new THREE.Line(geometry, material);
};

module.exports = function (mappings, options) {
    var meshes = [];

    meshes.push(makeAxisLinesGeometry(options));
    meshes.push(makeAxisTextGeometry(mappings, options));

    return _.flatten(meshes);
};


<|start_filename|>src/index.js<|end_filename|>
'use strict';

var $ = require('jquery');
var _ = require('lodash');
var React = require('react');
var ReactDom = require('react-dom');

var styles = require('./styles/index.scss');
var dispatcher = require('./dispatcher');
var store = require('./store');

var Graph = require('./graph');
var DataLassoUI = require('./components/DataLassoUI');

/**
 * Default options that Data Lasso is initialized with that can be overwritten
 */
var defaults = {
    /** Size of the graph */
    graphSize: 2000,
    /** Size of the axis legend text */
    legendSize: 50,
    /** Color of the legend text */
    legendColor: 0xffffff,
};

/**
 * # Data Lasso Class
 *
 * Class responsible for initialization of Data Lasso. Receives options,
 * creates DOM element for Data Lasso and initializes the rest of the components
 *
 */
var DataLasso = class DataLasso {
    constructor (options) {
        this.modules = options.modules;
        this.options = _.defaults(defaults, _.omit(options, 'modules'), this.defaults);

        dispatcher.dispatch({actionType: 'options-set', options: this.options});

        this.createElement();

        this.render();

        if (this.modules) {
            this.initializeModules(this.modules);
        }
    }

    /**
     * Creates DOM element for Data Lasso
     */
    createElement () {
        this.el = document.createElement('div');
        this.el.className = 'datalasso-container'
    }

    /**
     * Initialize modules that can be plugged into
     * Data Lasso.
     *
     * Each module coming to Data Lasso is defined by a name
     * and a constructor function. That constructor function will
     * be called for every module with a hash containing:
     *
     *  - Container element
     *  - Store
     *  - Dispatcher
     *
     * @param {array} modules: Data Lasso modules to use
     */
    initializeModules (modules) {
        this.modules = _.transform(modules, (result, module, name) => {
            result[name] = new module.constructor({
                store: store,
                dispatcher: dispatcher,
                container: this.el,
            });
        });
    }

    /**
     * Render Data Lasso:
     *  - Attach styles to the DOM
     *  - Render UI
     *  - Set up and render 3d space
     *
     * @returns {HTMLElement} - Element containing Data Lasso
     */
    render () {
        ReactDom.render(<DataLassoUI/>, this.el);
        this.graph = new Graph(this.options);
        this.el.appendChild(this.graph.el);
    }
};

module.exports = DataLasso;

window.DataLasso = module.exports;
