├── CODE_OF_CONDUCT.md ├── bower.json ├── src ├── filters │ ├── BaseFilter.js │ ├── DownloadButton.js │ ├── SliderInput.js │ ├── ApiButton.js │ ├── SelectButton.js │ ├── CheckboxGroup.js │ ├── factory.js │ ├── ConditionalSelectButton.js │ └── DynamicSearchInput.js ├── charts │ ├── plotly │ │ └── base.js │ ├── nvd3 │ │ ├── pieChart.js │ │ ├── base.js │ │ └── twoAxisFocus.js │ ├── datamaps │ │ └── base.js │ ├── factory.js │ ├── metricsgraphics │ │ └── base.js │ └── datatables │ │ └── base.js ├── index.js └── layouts │ └── FilterChart.js ├── README.md ├── LICENSE.txt ├── package.json └── webpack.config.js /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to keeping our community open and inclusive. 4 | 5 | **Our Code of Conduct can be found here**: 6 | http://opensource.stitchfix.com/code-of-conduct.html 7 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyxley", 3 | "version": "0.0.6", 4 | "homepage": "https://github.com/stitchfix/pyxleyJS", 5 | "description": "React components for use with pyxley python package", 6 | "main": "build/pyxley.js", 7 | "keywords": [ 8 | "react", 9 | "dashboards" 10 | ], 11 | "authors": [ 12 | "Nicholas Kridler" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "node_modules", 17 | "bower_components", 18 | "test", 19 | "tests" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/filters/BaseFilter.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.min.css'; 2 | import React from 'react'; 3 | 4 | export default class BaseFilter extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | value: null, 9 | selected: 0 10 | }; 11 | } 12 | 13 | getCurrentState() { 14 | var result = {}; 15 | result[this.props.options.alias] = this.state.value || 16 | this.props.options.default; 17 | return result; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/filters/DownloadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button} from 'react-bootstrap'; 3 | import BaseFilter from './BaseFilter'; 4 | 5 | export class DownloadButton extends BaseFilter { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | _onClick() { 11 | var params = this.props.onChange(); 12 | var url = this.props.options.url.concat("?", $.param(params)); 13 | window.location.href = url; 14 | } 15 | 16 | render() { 17 | return ( 18 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/charts/plotly/base.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | export class PlotlyAPI extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | } 8 | 9 | _update(params) { 10 | var url = this.props.options.url.concat("?", 11 | $.param(params)); 12 | $.get(url, 13 | function(result) { 14 | Plotly.newPlot( 15 | this.props.options.chartid, 16 | result.data, 17 | result.layout, 18 | result.config 19 | ) 20 | }.bind(this) 21 | ); 22 | } 23 | 24 | componentDidMount() { 25 | this._update(this.props.options.params); 26 | 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 |
33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export {ChartFactory} from './charts/factory.js'; 4 | export {FilterFactory} from './filters/factory.js'; 5 | export {FilterChart} from './layouts/FilterChart'; 6 | export {Filter} from './filters/factory.js'; 7 | export {Chart} from './charts/factory.js'; 8 | export {Table} from './charts/datatables/base.js'; 9 | export {MetricsGraphics} from './charts/metricsgraphics/base.js'; 10 | export {NVD3Chart} from './charts/nvd3/base.js'; 11 | export {Datamaps} from './charts/nvd3/base.js'; 12 | export {PlotlyAPI} from './charts/plotly/base.js'; 13 | import {SelectButton} from './filters/SelectButton'; 14 | import {ConditionalSelectButton} from './filters/ConditionalSelectButton'; 15 | import {ApiButton} from './filters/ApiButton'; 16 | import {DownloadButton} from './filters/DownloadButton'; 17 | import {SliderInput} from './filters/SliderInput'; 18 | import {DynamicSearchInput} from './filters/DynamicSearchInput'; 19 | import {CheckboxGroup} from './filters/CheckboxGroup'; 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyxley 2 | 3 | `pyxley.js` is a complement to the [pxyley](http://github.com/stitchfix/pyxley) python package. It provides a set of simple React components commonly used in developing lightweight dashboards and web applications. 4 | 5 | ## Charts & Filters 6 | The core idea of `pyxley` is that most dashboards are a simple collection of charts and filters. React provides a common interface that simplifies the integration with Python. Specifics for each chart and filter component are supplied as props in a single master component. 7 | 8 | ## Wrappers 9 | `pyxley` is predominantly a pattern. This allows us to easily integrate with existing Javascript packages. Utilizing React, `pyxley` provides a number of simple wrappers to packages such as [MetricsGraphics.js](http://metricsgraphicsjs.org/) and [React-Bootstrap](http://react-bootstrap.github.io/). 10 | 11 | ## Installation 12 | Install `pyxley` using npm 13 | ``` 14 | npm install pyxley 15 | ``` 16 | or install using Bower 17 | ``` 18 | bower install pyxley 19 | ``` 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stitch Fix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the Software), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/filters/SliderInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import BaseFilter from './BaseFilter'; 4 | 5 | export class SliderInput extends BaseFilter { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | _handleChange(e) { 11 | e.preventDefault(); 12 | var slider = ReactDOM.findDOMNode(this); 13 | this.setState({value: slider.value}); 14 | 15 | if(this.props.dynamic){ 16 | var result = { 17 | alias: this.props.options.alias, 18 | value: slider.value 19 | }; 20 | this.props.onChange([result]); 21 | 22 | } 23 | } 24 | 25 | render() { 26 | 27 | return ( 28 | 37 | ); 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/charts/nvd3/pieChart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import nv from 'nvd3'; 3 | import d3 from 'd3'; 4 | 5 | var PieChart = function() { 6 | this.chart = nv.models.pieChart(); 7 | } 8 | 9 | PieChart.prototype.initialize = function(options){ 10 | this.chart 11 | .x(function(d) { return d.label}) 12 | .y(function(d) { return d.value }) 13 | .showLabels(true) 14 | .labelType(options.labelType) 15 | .showLegend(true) 16 | .labelsOutside(false) 17 | .color(options.colors); 18 | 19 | this.chart.tooltip.contentGenerator( 20 | function(x){ 21 | return '

'+x.data.label+"

"+x.data.value*100+"%

" 22 | }); 23 | }; 24 | 25 | PieChart.prototype.get = function(chartid, url, params){ 26 | d3.json(url.concat("?", $.param(params)), 27 | function(error, result) { 28 | var svg = d3.select("#".concat(chartid, " svg")) 29 | .datum(result.data) 30 | .transition() 31 | .duration(500) 32 | .call(this.chart); 33 | nv.utils.windowResize(this.chart.update); 34 | 35 | }.bind(this)); 36 | }; 37 | 38 | export default PieChart; 39 | -------------------------------------------------------------------------------- /src/charts/datamaps/base.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export class Datamaps extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | chart: null 8 | }; 9 | } 10 | 11 | componentDidMount() { 12 | this._initialize(this.props.options.params); 13 | } 14 | 15 | _initialize(params) { 16 | var url = this.props.options.url.concat("?", $.param(params)); 17 | $.get(url, 18 | function(result){ 19 | this.state.chart = new Datamap({ 20 | element: document.getElementById(this.props.options.chartid), 21 | scope: 'usa', 22 | fills: result.fills, 23 | data: result.data 24 | }); 25 | }.bind(this)) 26 | } 27 | 28 | _update(params){ 29 | var url = this.props.options.url.concat("?", $.param(params)) 30 | $.get(url, 31 | function(result){ 32 | this.state.chart.updateChoropleth(result.data); 33 | }.bind(this)); 34 | } 35 | 36 | render() { 37 | return ( 38 |
41 |
42 | ); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/charts/factory.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {MetricsGraphics} from './metricsgraphics/base.js'; 3 | import {NVD3Chart} from './nvd3/base.js'; 4 | import {Table} from './datatables/base.js'; 5 | import {Datamaps} from './datamaps/base.js'; 6 | import {PlotlyAPI} from './plotly/base.js'; 7 | 8 | var ChartFactory = function(type) { 9 | 10 | if (typeof ChartFactory[type] != 'function'){ 11 | throw new Error(type + ' is not a valid chart type.'); 12 | } 13 | 14 | return ChartFactory[type]; 15 | }; 16 | 17 | 18 | ChartFactory.MetricsGraphics = MetricsGraphics; 19 | ChartFactory.Table = Table; 20 | ChartFactory.NVD3Chart = NVD3Chart; 21 | ChartFactory.Datamaps = Datamaps; 22 | ChartFactory.PlotlyAPI = PlotlyAPI; 23 | 24 | class Chart extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | } 28 | 29 | update(params) { 30 | this.refs.chart._update(params); 31 | } 32 | 33 | render() { 34 | var Z = this.props.factory(this.props.type); 35 | return ( 36 | 37 | ); 38 | } 39 | } 40 | Chart.defaultProps = { 41 | type: React.PropTypes.string, 42 | factory: ChartFactory, 43 | options: React.PropTypes.object 44 | }; 45 | 46 | export {Chart}; 47 | export {ChartFactory}; 48 | -------------------------------------------------------------------------------- /src/filters/ApiButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseFilter from './BaseFilter'; 3 | import {SelectButton} from './SelectButton'; 4 | 5 | export class ApiButton extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | items: [] 10 | }; 11 | } 12 | 13 | getCurrentState() { 14 | return this.refs.apiBtn.getCurrentState(); 15 | } 16 | 17 | componentDidMount() { 18 | this._fetchAPIData({}); 19 | } 20 | 21 | _fetchAPIData(params) { 22 | $.get(this.props.options.url.concat('?', $.param(params)), 23 | function(result){ 24 | this.setState({items: result.data}); 25 | }.bind(this) 26 | ); 27 | } 28 | 29 | update(params) { 30 | this._fetchAPIData(params); 31 | } 32 | 33 | render() { 34 | var opts = { 35 | items: this.state.items, 36 | alias: this.props.options.alias, 37 | default: this.props.options.default, 38 | label: this.props.options.label 39 | }; 40 | return ( 41 | 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/filters/SelectButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseFilter from './BaseFilter'; 3 | import {DropdownButton, MenuItem} from 'react-bootstrap'; 4 | 5 | export class SelectButton extends BaseFilter { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | _handleClick(index, text, e) { 11 | e.preventDefault(); 12 | this.setState({ 13 | selected: index, 14 | value: text 15 | }); 16 | 17 | if(this.props.dynamic){ 18 | var result = { 19 | alias: this.props.options.alias, 20 | value: text 21 | }; 22 | this.props.onChange([result]); 23 | } 24 | } 25 | 26 | render() { 27 | var items = this.props.options.items.map(function(item, index){ 28 | return ( 29 | 32 | {item} 33 | 34 | ); 35 | 36 | }.bind(this)); 37 | var label = this.state.value || this.props.options.label; 38 | return ( 39 | 43 | {items} 44 | 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/filters/CheckboxGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Input, Button} from 'react-bootstrap'; 3 | 4 | export class CheckboxGroup extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | } 9 | 10 | getCurrentState() { 11 | var result = {}; 12 | for(var i = 0; i < this.props.options.labels.length; i++){ 13 | result[this.props.options.aliases[i]] = 14 | this.refs["checkbox_".concat(i)].getChecked(); 15 | } 16 | return result; 17 | } 18 | 19 | _resetChecked() { 20 | for(var i = 0; i < this.props.options.labels.length; i++){ 21 | this.refs["checkbox_".concat(i)].getInputDOMNode().checked = false; 22 | } 23 | } 24 | 25 | render() { 26 | var labels = this.props.options.labels.map(function(item, index){ 27 | return ( 28 | 31 | ); 32 | }); 33 | return ( 34 |
35 |
36 | 37 | 38 |
39 | {labels} 40 |
41 | ); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyxley", 3 | "version": "0.0.6", 4 | "description": "react components for dashboards", 5 | "main": "build/pyxley.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/stitchfix/pyxleyJS.git" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "dashboard" 16 | ], 17 | "author": "Nicholas Kridler", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/stitchfix/pyxleyJS/issues" 21 | }, 22 | "homepage": "https://github.com/stitchfix/pyxleyJS", 23 | "devDependencies": { 24 | "babel-cli": "^6.5.1", 25 | "babel-core": "^6.5.0", 26 | "babel-loader": "^6.2.2", 27 | "babel-preset-es2015": "^6.5.0", 28 | "babel-preset-react": "^6.5.0", 29 | "bootstrap": "^3.3.6", 30 | "classnames": "^2.1.2", 31 | "css-loader": "^0.23.1", 32 | "datamaps": "^0.4.4", 33 | "datatables": "^1.10.7", 34 | "file-loader": "^0.8.5", 35 | "image-loader": "0.0.1", 36 | "metrics-graphics": "^2.8.0", 37 | "node-libs-browser": "^0.5.2", 38 | "node-sass": "^3.4.2", 39 | "nvd3": "^1.8.2", 40 | "plotly.js": "^1.5.2", 41 | "react": "^0.14.7", 42 | "react-bootstrap": "^0.28.2", 43 | "react-dom": "^0.14.7", 44 | "sass-loader": "^3.1.2", 45 | "style-loader": "^0.13.0", 46 | "url-loader": "^0.5.7", 47 | "webpack": "^1.12.13" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/filters/factory.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {SelectButton} from './SelectButton'; 3 | import {ConditionalSelectButton} from './ConditionalSelectButton'; 4 | import {ApiButton} from './ApiButton'; 5 | import {DownloadButton} from './DownloadButton'; 6 | import {SliderInput} from './SliderInput'; 7 | import {DynamicSearchInput} from './DynamicSearchInput'; 8 | import {CheckboxGroup} from './CheckboxGroup'; 9 | 10 | var FilterFactory = function(type) { 11 | if (typeof FilterFactory[type] != 'function'){ 12 | throw new Error(type + ' is not a valid filter.'); 13 | } 14 | 15 | return FilterFactory[type]; 16 | }; 17 | 18 | FilterFactory.SliderInput = SliderInput; 19 | FilterFactory.SelectButton = SelectButton; 20 | FilterFactory.ConditionalSelectButton = ConditionalSelectButton; 21 | FilterFactory.ApiButton = ApiButton; 22 | FilterFactory.DownloadButton = DownloadButton; 23 | FilterFactory.DynamicSearch = DynamicSearchInput; 24 | FilterFactory.CheckboxGroup = CheckboxGroup; 25 | 26 | export class Filter extends React.Component { 27 | constructor(props) { 28 | super(props); 29 | } 30 | 31 | render() { 32 | var Z = FilterFactory(this.props.type); 33 | return ( 34 | 40 | ); 41 | } 42 | } 43 | 44 | export {FilterFactory}; 45 | -------------------------------------------------------------------------------- /src/charts/nvd3/base.js: -------------------------------------------------------------------------------- 1 | import 'nvd3/build/nv.d3.min.css'; 2 | 3 | import React from 'react'; 4 | import TwoAxisFocus from './twoAxisFocus'; 5 | import PieChart from './pieChart'; 6 | 7 | var NVD3Factory = function(type, options){ 8 | var chart; 9 | 10 | if (typeof NVD3Factory[type] != 'function'){ 11 | throw new Error(type + ' is not a valid NVD3 type.'); 12 | } 13 | NVD3Factory.prototype = NVD3Factory[type].prototype; 14 | chart = new NVD3Factory[type](); 15 | chart.initialize(options); 16 | return chart; 17 | } 18 | NVD3Factory.TwoAxisFocus = TwoAxisFocus; 19 | NVD3Factory.PieChart = PieChart; 20 | 21 | export class NVD3Chart extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | chart: null 26 | }; 27 | } 28 | 29 | componentWillMount() { 30 | this.setState({ 31 | chart: this._initialize(this.state.chart) 32 | }); 33 | } 34 | 35 | _initialize(chart) { 36 | chart = NVD3Factory(this.props.options.type, 37 | this.props.options); 38 | 39 | chart.get(this.props.options.chartid, 40 | this.props.options.url, 41 | this.props.options.init_params); 42 | 43 | return chart; 44 | } 45 | 46 | _update(params) { 47 | this.state.chart.get(this.props.options.chartid, 48 | this.props.options.url, 49 | params); 50 | } 51 | 52 | render() { 53 | return ( 54 |
55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/charts/metricsgraphics/base.js: -------------------------------------------------------------------------------- 1 | 2 | import 'metrics-graphics/dist/metricsgraphics.css'; 3 | import React from 'react'; 4 | // import MG from 'metrics-graphics'; 5 | 6 | export class MetricsGraphics extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | componentDidMount() { 12 | this._initialize(this.props.options.params.init_params); 13 | } 14 | 15 | _initialize(params) { 16 | d3.json(this.props.options.url.concat("?", $.param(params)), 17 | function(error, data){ 18 | 19 | var options = this.props.options.params; 20 | if(data.date){ 21 | for(var i=0; i < data.result.length; i++){ 22 | data.result[i] = MG.convert.date(data.result[i], "x", 23 | this.props.options.date_format); 24 | } 25 | } 26 | options.data = data.result; 27 | MG.data_graphic(options); 28 | }.bind(this) 29 | ); 30 | } 31 | 32 | _update(params) { 33 | d3.json(this.props.options.url.concat("?",$.param(params)), 34 | function(error, data){ 35 | var options = this.props.options.params; 36 | if(data.date){ 37 | for(var i=0; i < data.result.length; i++){ 38 | data.result[i] = MG.convert.date(data.result[i], "x", 39 | this.props.options.date_format); 40 | } 41 | } 42 | options.data = data.result; 43 | MG.data_graphic(options); 44 | }.bind(this) 45 | ); 46 | } 47 | 48 | render() { 49 | var width = {width: this.props.options.params.width}; 50 | 51 | return ( 52 |
53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/layouts/FilterChart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Row} from 'react-bootstrap'; 3 | import {Filter} from '../filters/factory'; 4 | import {Chart} from '../charts/factory'; 5 | 6 | export class FilterChart extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | _handleClick(input) { 12 | var params = {}; 13 | for(var i = 0; i < this.props.filters.length; i++){ 14 | var vals = this.refs["filter_".concat(i)].refs.filter.getCurrentState(); 15 | for(var key in vals){ 16 | params[key] = vals[key]; 17 | } 18 | } 19 | if(input){ 20 | for(var i = 0; i < input.length; i++){ 21 | params[input[i].alias] = input[i].value; 22 | } 23 | } 24 | for(var i = 0; i < this.props.charts.length; i++){ 25 | this.refs["chart_".concat(i)].update(params); 26 | } 27 | return params; 28 | } 29 | 30 | render() { 31 | var items = this.props.filters.map(function(x, index){ 32 | return(); 39 | }.bind(this)); 40 | 41 | var charts = this.props.charts.map(function(x, index){ 42 | return(); 45 | }); 46 | 47 | 48 | return ( 49 |
50 | 51 |
52 | {items} 53 |
54 |
55 | 56 | {charts} 57 | 58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/charts/datatables/base.js: -------------------------------------------------------------------------------- 1 | import 'datatables/media/css/jquery.dataTables.min.css'; 2 | require("datatables"); 3 | import React from 'react'; 4 | $["dataTable"] = require("datatables"); 5 | 6 | export class Table extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | columns: [] 11 | }; 12 | } 13 | 14 | componentWillMount() { 15 | this._initialize(); 16 | } 17 | 18 | _initialize() { 19 | var _url = this.props.options.url.concat("?", 20 | $.param(this.props.options.params)); 21 | $.get(_url, 22 | function(result){ 23 | this.setState({columns: result.columns}); 24 | var options = this.props.options.table_options; 25 | options.data = result.data; 26 | options.columns = result.columns; 27 | options.aaData = result.data; 28 | if("columnDefs" in this.props.options.table_options){ 29 | options.columnDefs = this.props.options.table_options.columnDefs; 30 | options.columnDefs.forEach(function(x){ 31 | x.render = new Function("return '" + x.render + "';"); 32 | }); 33 | } 34 | 35 | options.initComplete = new Function("settings", "json", 36 | this.props.options.table_options.initComplete); 37 | options.drawCallback = new Function("settings", 38 | this.props.options.table_options.drawCallback); 39 | $('#'.concat(this.props.options.id)).dataTable(options); 40 | }.bind(this) 41 | ); 42 | } 43 | 44 | _update(params) { 45 | var _table = $('#'.concat(this.props.options.id)).dataTable(); 46 | _table.api().ajax.url( 47 | this.props.options.url.concat("?",$.param(params)) 48 | ).load(); 49 | 50 | } 51 | 52 | render() { 53 | var header = this.state.columns.map(function(item, index) { 54 | return ( 55 | {item.data} 56 | ); 57 | }); 58 | return ( 59 |
60 | 64 | {header} 65 |
66 |
67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports = { 4 | entry: './src/index.js', 5 | output: { 6 | path: './build', 7 | filename: 'pyxley.js', 8 | library: 'pxyley', 9 | libraryTarget: 'umd' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.js?$/, 15 | loader: 'babel', 16 | exclude: /(node_modules|bower_components)/, 17 | query: { 18 | presets: ['es2015', 'react'] 19 | } 20 | }, 21 | { 22 | test: /\.css$/, 23 | loaders: ['style', 'css', 'sass'] 24 | }, 25 | { 26 | test: /\.png$/, 27 | loader: "url-loader?mimetype=image/png" 28 | }, 29 | { 30 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 31 | loader: "file" 32 | }, 33 | { 34 | test: /\.(woff|woff2)$/, 35 | loader:"url?prefix=font/&limit=5000" 36 | }, 37 | { 38 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 39 | loader: "url?limit=10000&mimetype=application/octet-stream" 40 | }, 41 | { 42 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 43 | loader: "url?limit=10000&mimetype=image/svg+xml" 44 | } 45 | 46 | ] 47 | }, 48 | externals: [ 49 | { 50 | "plotly\.js": "Plotly" 51 | }, 52 | { 53 | "datamaps": "Datamap" 54 | }, 55 | { 56 | "metrics-graphics": "MG" 57 | }, 58 | { 59 | "d3": "d3" 60 | }, 61 | { 62 | "nvd3": "nv" 63 | }, 64 | { 65 | 'react': { 66 | root: 'React', 67 | commonjs2: 'react', 68 | commonjs: 'react', 69 | amd: 'react' 70 | } 71 | }, 72 | { 73 | 'react-dom': { 74 | root: 'ReactDOM', 75 | commonjs2: 'react-dom', 76 | commonjs: 'react-dom', 77 | amd: 'react-dom' 78 | } 79 | } 80 | ], 81 | plugins: [ 82 | new webpack.ProvidePlugin({ 83 | $: "jquery", 84 | jQuery: "jquery", 85 | "window.jQuery": "jquery", 86 | "window.$": "jquery" 87 | }), 88 | new webpack.DefinePlugin({ 89 | "process.env": { 90 | NODE_ENV: JSON.stringify("production") 91 | } 92 | }) 93 | ] 94 | 95 | }; 96 | -------------------------------------------------------------------------------- /src/filters/ConditionalSelectButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BaseFilter from './BaseFilter'; 3 | import {SelectButton} from './SelectButton'; 4 | import {DropdownButton, MenuItem} from 'react-bootstrap'; 5 | 6 | export class ConditionalSelectButton extends BaseFilter { 7 | constructor(props){ 8 | super(props); 9 | } 10 | 11 | _handleClick(index, text, e) { 12 | e.preventDefault(); 13 | this.setState({ 14 | selected: index, 15 | value: text 16 | }); 17 | 18 | if(this.props.dynamic){ 19 | var result = [ 20 | { 21 | alias: this.props.options.aliases[0], 22 | value: text 23 | }, 24 | { 25 | alias: this.props.options.aliases[1], 26 | value: "All" 27 | } 28 | ]; 29 | this.props.onChange(result); 30 | } 31 | this.refs.secondary.setState({value: "All"}); 32 | } 33 | 34 | getCurrentState() { 35 | var result = {}; 36 | result[this.props.options.aliases[0]] = this.state.value 37 | || this.props.options.defaults[0]; 38 | result[this.props.options.aliases[1]] = this.refs.secondary.state.value 39 | || this.props.options.defaults[1]; 40 | return result; 41 | } 42 | 43 | render() { 44 | var primary = this.props.options.items.map(function(item, idx){ 45 | return ( 46 | 49 | {item.primary} 50 | 51 | ); 52 | }.bind(this)); 53 | 54 | var label = this.state.value || this.props.options.labels[0]; 55 | var secondary = { 56 | items: this.props.options.items[this.state.selected].secondary, 57 | label: this.props.options.labels[1], 58 | alias: this.props.options.aliases[1], 59 | default: this.props.options.defaults[1] 60 | }; 61 | return ( 62 |
63 | 67 | {primary} 68 | 69 | 73 |
74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/filters/DynamicSearchInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Input, Dropdown, MenuItem} from 'react-bootstrap'; 4 | import classNames from 'classnames'; 5 | 6 | class InputMenu extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | value: '' 11 | }; 12 | this.onChange = e => this.setState({ 13 | value: e.target.value 14 | }); 15 | } 16 | 17 | filterChildren() { 18 | let { children } = this.props; 19 | let filtered = []; 20 | React.Children.forEach(children, child => { 21 | filtered.push(child); 22 | }); 23 | 24 | return filtered; 25 | } 26 | 27 | render() { 28 | 29 | return ( 30 | 31 | 32 |
    33 | { this.filterChildren() } 34 |
35 | 36 | ); 37 | } 38 | } 39 | 40 | export class DynamicSearchInput extends React.Component { 41 | constructor(props) { 42 | super(props); 43 | this.state = { 44 | value: null, 45 | searchString: "", 46 | items: [] 47 | }; 48 | } 49 | 50 | getCurrentState() { 51 | var result = {}; 52 | result[this.props.options.alias] = this.state.value || 53 | this.props.options.default; 54 | return result; 55 | } 56 | 57 | _fetchAPIData(params) { 58 | var url = this.props.options.url.concat("?", $.param(params)); 59 | $.get(url, function(result){ 60 | this.setState({items: result.data}); 61 | }.bind(this)); 62 | } 63 | 64 | update(params) { 65 | this._fetchAPIData(params); 66 | } 67 | 68 | componentDidMount() { 69 | this._fetchAPIData({}); 70 | } 71 | 72 | _handleItemSelect(text, e) { 73 | this.setState({ value: text }); 74 | this.dd.refs.inner.toggleOpen(false); 75 | if(this.props.dynamic){ 76 | var result = { 77 | alias: this.props.options.alias, 78 | value: text 79 | }; 80 | this.props.onChange([result]); 81 | } 82 | 83 | } 84 | 85 | _filterItems() { 86 | var items = this.state.items; 87 | var searchString = this.state.searchString.trim().toLowerCase(); 88 | if(searchString.length > 0){ 89 | items = items.filter(function(x){ 90 | return x.toLowerCase().match(searchString) 91 | }); 92 | } 93 | var nresults = items.length; 94 | var results_left = "Results Left: #"; 95 | return items.map(function(item, index){ 96 | if(index < this.props.options.max){ 97 | return ( 98 | 101 | {item} 102 | 103 | 104 | ); 105 | } else if (index == this.props.options.max){ 106 | return ( 107 | 108 | {results_left.replace("#", (nresults-index))} 109 | 110 | ); 111 | } 112 | }.bind(this)); 113 | } 114 | 115 | _handleChange(e) { 116 | e.preventDefault(); 117 | this.setState({ 118 | searchString: e.target.value, 119 | value: e.target.value 120 | }); 121 | } 122 | 123 | render() { 124 | return ( 125 | this.dd = dd}> 127 | 128 | 136 | 139 | {this._filterItems()} 140 | 141 | 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/charts/nvd3/twoAxisFocus.js: -------------------------------------------------------------------------------- 1 | // Adapted from nvd3.org Line With View Finder 2 | // http://nvd3.org/examples/lineWithFocus.html 3 | // and multichart http://nvd3.org/examples/linePlusBar.html 4 | import d3 from 'd3'; 5 | 6 | const twoAxisFocusChart = function() { 7 | "use strict"; 8 | 9 | //============================================================ 10 | // Public Variables with Default Settings 11 | //------------------------------------------------------------ 12 | 13 | var yScale1 = d3.scale.linear() 14 | , yScale2 = d3.scale.linear() 15 | , yScaleC = d3.scale.linear() 16 | , lines1 = nv.models.line().yScale(yScale1) 17 | , lines2 = nv.models.line().yScale(yScale2) 18 | , linesC = nv.models.line().yScale(yScaleC) 19 | , xAxis = nv.models.axis().orient('bottom').tickPadding(5) 20 | , y1Axis = nv.models.axis().scale(yScale1).orient('left') 21 | , y2Axis = nv.models.axis().scale(yScale2).orient('right') 22 | , xCAxis = nv.models.axis().orient('bottom').tickPadding(5) 23 | , yCAxis = nv.models.axis().scale(yScaleC).orient('left') 24 | , legend = nv.models.legend() 25 | , brush = d3.svg.brush() 26 | 27 | ; 28 | 29 | var margin = {top: 30, right: 30, bottom: 30, left: 60} 30 | , margin2 = {top: 10, right: 30, bottom: 20, left: 60} 31 | , color = nv.utils.defaultColor() 32 | , width = null 33 | , height = null 34 | , height2 = 100 35 | , x 36 | , y1 37 | , y2 38 | , xC 39 | , yC 40 | , showLegend = true 41 | , brushExtent = null 42 | , tooltips = true 43 | , tooltip = nv.models.tooltip() 44 | // , tooltip = function(key, x, y, e, graph) { 45 | // return '

' + key + '

' + 46 | // '

' + y + ' at ' + x + '

' 47 | // } 48 | , noData = "No Data Available." 49 | , dispatch = d3.dispatch('tooltipShow', 'tooltipHide', 'brush', 'stateChange', 'changeState') 50 | , transitionDuration = 250 51 | , state = nv.utils.state() 52 | , defaultState = null 53 | , yDomain1 54 | , yDomain2 55 | , yDomainC 56 | ; 57 | 58 | lines1.clipEdge(true); 59 | lines2.clipEdge(true); 60 | linesC.interactive(false).clipEdge(true); 61 | 62 | //============================================================ 63 | // Private Variables 64 | //------------------------------------------------------------ 65 | 66 | var showTooltip = function(e, offsetElement) { 67 | var left = e.pos[0] + ( offsetElement.offsetLeft || 0 ), 68 | top = e.pos[1] + ( offsetElement.offsetTop || 0), 69 | x = xAxis.tickFormat()(lines1.x()(e.point, e.pointIndex)), 70 | y = y1Axis.tickFormat()(lines1.y()(e.point, e.pointIndex)), 71 | content = tooltip(e.series.key, x, y, e, chart); 72 | }; 73 | 74 | 75 | 76 | 77 | var stateGetter = function(data) { 78 | return function(){ 79 | return { 80 | active: data.map(function(d) { return !d.disabled }) 81 | }; 82 | } 83 | }; 84 | 85 | var stateSetter = function(data) { 86 | return function(state) { 87 | if (state.active !== undefined) 88 | data.forEach(function(series,i) { 89 | series.disabled = !state.active[i]; 90 | }); 91 | } 92 | }; 93 | 94 | function chart(selection) { 95 | selection.each(function(data) { 96 | 97 | 98 | var container = d3.select(this), 99 | that = this; 100 | nv.utils.initSVG(container); 101 | var availableWidth = (width || parseInt(container.style('width')) || 960) 102 | - margin.left - margin.right, 103 | availableHeight1 = (height || parseInt(container.style('height')) || 400) 104 | - margin.top - margin.bottom - height2, 105 | availableHeight2 = height2 - margin2.top - margin2.bottom; 106 | 107 | var dataLines1 = data.filter(function(d) {return d.yAxis == 1}); 108 | var dataLines2 = data.filter(function(d) {return d.yAxis == 2}); 109 | 110 | chart.update = function() { container.transition().duration(transitionDuration).call(chart) }; 111 | chart.container = this; 112 | 113 | state 114 | .setter(stateSetter(data), chart.update) 115 | .getter(stateGetter(data)) 116 | .update(); 117 | 118 | // DEPRECATED set state.disableddisabled 119 | state.disabled = data.map(function(d) { return !!d.disabled }); 120 | 121 | if (!defaultState) { 122 | var key; 123 | defaultState = {}; 124 | for (key in state) { 125 | if (state[key] instanceof Array) 126 | defaultState[key] = state[key].slice(0); 127 | else 128 | defaultState[key] = state[key]; 129 | } 130 | } 131 | 132 | // Display No Data message if there's nothing to show. 133 | if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { 134 | var noDataText = container.selectAll('.nv-noData').data([noData]); 135 | 136 | noDataText.enter().append('text') 137 | .attr('class', 'nvd3 nv-noData') 138 | .attr('dy', '-.7em') 139 | .style('text-anchor', 'middle'); 140 | 141 | noDataText 142 | .attr('x', margin.left + availableWidth / 2) 143 | .attr('y', margin.top + availableHeight1 / 2) 144 | .text(function(d) { return d }); 145 | 146 | return chart; 147 | } else { 148 | container.selectAll('.nv-noData').remove(); 149 | } 150 | 151 | var series1 = data.filter(function(d) {return !d.disabled && d.yAxis == 1}) 152 | .map(function(d) { 153 | return d.values.map(function(d,i) { 154 | return { x: d.x, y: d.y } 155 | }) 156 | }); 157 | 158 | var series2 = data.filter(function(d) {return !d.disabled && d.yAxis == 2}) 159 | .map(function(d) { 160 | return d.values.map(function(d,i) { 161 | return { x: d.x, y: d.y } 162 | }) 163 | }); 164 | 165 | // Setup Scales 166 | x = lines1.xScale(); 167 | y1 = lines1.yScale(); 168 | y2 = lines2.yScale(); 169 | xC = linesC.xScale(); 170 | yC = linesC.yScale(); 171 | 172 | // x.domain(d3.extent(d3.merge(series1.concat(series2)), function(d) { return d.x } )) 173 | // .range([0, availableWidth]); 174 | 175 | // Setup containers and skeleton of chart 176 | var wrap = container.selectAll('g.nv-wrap.nv-twoAxisFocusChart').data([data]); 177 | var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-twoAxisFocusChart').append('g'); 178 | var g = wrap.select('g'); 179 | gEnter.append('g').attr('class', 'legendWrap'); 180 | 181 | var focusEnter = gEnter.append('g').attr('class', 'nv-focus'); 182 | focusEnter.append('g').attr('class', 'nv-x nv-axis'); 183 | focusEnter.append('g').attr('class', 'nv-y1 nv-axis'); 184 | focusEnter.append('g').attr('class', 'nv-y2 nv-axis'); 185 | focusEnter.append('g').attr('class', 'nv-lines1Wrap'); 186 | focusEnter.append('g').attr('class', 'nv-lines2Wrap'); 187 | 188 | var contextEnter = gEnter.append('g').attr('class', 'nv-context'); 189 | contextEnter.append('g').attr('class', 'nv-x nv-axis'); 190 | contextEnter.append('g').attr('class', 'nv-y nv-axis'); 191 | contextEnter.append('g').attr('class', 'nv-lines1Wrap'); 192 | contextEnter.append('g').attr('class', 'nv-brushBackground'); 193 | contextEnter.append('g').attr('class', 'nv-x nv-brush'); 194 | 195 | // Legend 196 | var color_array = data.map(function(d,i) { 197 | return data[i].color || color(d, i); 198 | }); 199 | 200 | if (showLegend) { 201 | legend.color(color_array); 202 | legend.width( availableWidth / 2 ); 203 | 204 | g.select('.legendWrap') 205 | .datum(data.map(function(series) { 206 | series.originalKey = series.originalKey === undefined ? series.key : series.originalKey; 207 | series.key = series.originalKey + (series.yAxis == 1 ? '' : ' (right axis)'); 208 | return series; 209 | })) 210 | .call(legend); 211 | 212 | if ( margin.top != legend.height()) { 213 | margin.top = legend.height(); 214 | availableHeight1 = (height || parseInt(container.style('height')) || 400) 215 | - margin.top - margin.bottom - height2; 216 | availableHeight2 = height2 - margin2.top - margin2.bottom; 217 | // availableHeight = (height || parseInt(container.style('height')) || 400) 218 | // - margin.top - margin.bottom; 219 | } 220 | 221 | g.select('.legendWrap') 222 | .attr('transform', 'translate(' + ( availableWidth / 2 ) + ',' + (-margin.top) +')'); 223 | } 224 | 225 | wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); 226 | 227 | // Main Chart Component(s) 228 | lines1 229 | .width(availableWidth) 230 | .height(availableHeight1) 231 | .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 1})); 232 | lines2 233 | .width(availableWidth) 234 | .height(availableHeight1) 235 | .color(color_array.filter(function(d,i) { return !data[i].disabled && data[i].yAxis == 2})); 236 | linesC 237 | .width(availableWidth) 238 | .height(availableHeight2) 239 | .color(color_array.filter(function(d,i) { return data[i].yAxis == 2})); 240 | 241 | g.select('.nv-context') 242 | .attr('transform', 'translate(0,' + ( availableHeight1 + margin.bottom + margin2.top) + ')') 243 | 244 | yScale1.domain(yDomain1 || d3.extent(series1, function(d) { return d.y } )) 245 | .range([0, availableHeight1]); 246 | 247 | yScale2.domain(yDomain2 || d3.extent(series2, function(d) { return d.y } )) 248 | .range([0, availableHeight1]); 249 | 250 | yScaleC.domain(yDomainC || d3.extent(series2, function(d) { return d.y } )) 251 | .range([availableHeight2, 0]); 252 | 253 | lines1.yDomain(yScale1.domain()); 254 | lines2.yDomain(yScale2.domain()); 255 | linesC.yDomain(yScaleC.domain()); 256 | 257 | var context1LinesWrap = g.select('.nv-context .nv-lines1Wrap') 258 | .datum(dataLines2); 259 | 260 | if(dataLines2.length){d3.transition(context1LinesWrap).call(linesC);} 261 | 262 | // Setup Main (Focus) Axes 263 | xAxis 264 | .scale(x) 265 | .ticks( nv.utils.calcTicksX(availableWidth/100, dataLines1) ) 266 | .tickSize(-availableHeight1, 0); 267 | 268 | y1Axis 269 | .ticks( nv.utils.calcTicksY(availableHeight1/36, dataLines1) ) 270 | .tickSize( -availableWidth, 0); 271 | 272 | y2Axis 273 | .ticks( nv.utils.calcTicksY(availableHeight1/36, dataLines2) ) 274 | .tickSize( -availableWidth, 0); 275 | 276 | g.select('.nv-focus .nv-x.nv-axis') 277 | .attr('transform', 'translate(0,' + availableHeight1 + ')'); 278 | 279 | 280 | function mouseover_line(evt) { 281 | var yaxis = data[evt.seriesIndex].yAxis === 2 ? y2Axis : y1Axis; 282 | evt.value = evt.point.x; 283 | evt.series = { 284 | value: evt.point.y, 285 | color: evt.point.color 286 | }; 287 | tooltip 288 | .duration(100) 289 | .valueFormatter(function(d, i) { 290 | return yaxis.tickFormat()(d, i); 291 | }) 292 | .data(evt) 293 | .position(evt.pos) 294 | .hidden(false); 295 | } 296 | 297 | // Setup Brush 298 | brush 299 | .x(xC) 300 | .on('brush', function() { 301 | //When brushing, turn off transitions because chart needs to change immediately. 302 | var oldTransition = chart.duration(); 303 | chart.duration(0); 304 | onBrush(); 305 | chart.duration(oldTransition); 306 | }); 307 | 308 | if (brushExtent) brush.extent(brushExtent); 309 | 310 | var brushBG = g.select('.nv-brushBackground').selectAll('g') 311 | .data([brushExtent || brush.extent()]) 312 | 313 | var brushBGenter = brushBG.enter() 314 | .append('g'); 315 | 316 | brushBGenter.append('rect') 317 | .attr('class', 'left') 318 | .attr('x', 0) 319 | .attr('y', 0) 320 | .attr('height', availableHeight2); 321 | 322 | brushBGenter.append('rect') 323 | .attr('class', 'right') 324 | .attr('x', 0) 325 | .attr('y', 0) 326 | .attr('height', availableHeight2); 327 | 328 | var gBrush = g.select('.nv-x.nv-brush') 329 | .call(brush); 330 | gBrush.selectAll('rect') 331 | //.attr('y', -5) 332 | .attr('height', availableHeight2); 333 | gBrush.selectAll('.resize').append('path').attr('d', resizePath); 334 | 335 | onBrush(); 336 | 337 | // Setup Secondary (Context) Axes 338 | xCAxis 339 | .scale(xC) 340 | .ticks( nv.utils.calcTicksX(availableWidth/100, dataLines2) ) 341 | .tickSize(-availableHeight2, 0); 342 | 343 | g.select('.nv-context .nv-x.nv-axis') 344 | .attr('transform', 'translate(0,' + yScaleC.range()[0] + ')'); 345 | d3.transition(g.select('.nv-context .nv-x.nv-axis')) 346 | .call(xCAxis); 347 | 348 | yCAxis 349 | .ticks( nv.utils.calcTicksY(availableHeight2/36, dataLines2) ) 350 | .tickSize( -availableWidth, 0); 351 | 352 | d3.transition(g.select('.nv-context .nv-y.nv-axis')) 353 | .call(yCAxis); 354 | 355 | // g.select('.nv-context .nv-y.nv-axis').transition().duration(transitionDuration) 356 | // .call(yCAxis); 357 | 358 | g.select('.nv-context .nv-x.nv-axis') 359 | .attr('transform', 'translate(0,' + yScaleC.range()[0] + ')'); 360 | 361 | //============================================================ 362 | // Event Handling/Dispatching (in chart's scope) 363 | //------------------------------------------------------------ 364 | 365 | legend.dispatch.on('stateChange', function(newState) { 366 | for (var key in newState) 367 | state[key] = newState[key]; 368 | dispatch.stateChange(state); 369 | chart.update(); 370 | }); 371 | 372 | 373 | // dispatch.on('tooltipShow', function(e) { 374 | // if (tooltips) showTooltip(e, that.parentNode); 375 | // }); 376 | 377 | dispatch.on('changeState', function(e) { 378 | if (typeof e.disabled !== 'undefined') { 379 | data.forEach(function(series,i) { 380 | series.disabled = e.disabled[i]; 381 | }); 382 | } 383 | chart.update(); 384 | }); 385 | 386 | //============================================================ 387 | // Functions 388 | //------------------------------------------------------------ 389 | 390 | // Taken from crossfilter (http://square.github.com/crossfilter/) 391 | function resizePath(d) { 392 | var e = +(d == 'e'), 393 | x = e ? 1 : -1, 394 | y = availableHeight2 / 3; 395 | return 'M' + (.5 * x) + ',' + y 396 | + 'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6) 397 | + 'V' + (2 * y - 6) 398 | + 'A6,6 0 0 ' + e + ' ' + (.5 * x) + ',' + (2 * y) 399 | + 'Z' 400 | + 'M' + (2.5 * x) + ',' + (y + 8) 401 | + 'V' + (2 * y - 8) 402 | + 'M' + (4.5 * x) + ',' + (y + 8) 403 | + 'V' + (2 * y - 8); 404 | } 405 | 406 | 407 | function updateBrushBG() { 408 | if (!brush.empty()) brush.extent(brushExtent); 409 | brushBG 410 | .data([brush.empty() ? xC.domain() : brushExtent]) 411 | .each(function(d,i) { 412 | var leftWidth = xC(d[0]) - x.range()[0], 413 | rightWidth = x.range()[1] - xC(d[1]); 414 | d3.select(this).select('.left') 415 | .attr('width', leftWidth < 0 ? 0 : leftWidth); 416 | 417 | d3.select(this).select('.right') 418 | .attr('x', xC(d[1])) 419 | .attr('width', rightWidth < 0 ? 0 : rightWidth); 420 | }); 421 | } 422 | 423 | 424 | function onBrush() { 425 | brushExtent = brush.empty() ? null : brush.extent(); 426 | var extent = brush.empty() ? xC.domain() : brush.extent(); 427 | 428 | //The brush extent cannot be less than one. If it is, don't update the line chart. 429 | if (Math.abs(extent[0] - extent[1]) <= 1) { 430 | return; 431 | } 432 | 433 | dispatch.brush({extent: extent, brush: brush}); 434 | 435 | 436 | updateBrushBG(); 437 | 438 | // Update Main (Focus) 439 | var focusLines1Wrap = g.select('.nv-focus .nv-lines1Wrap') 440 | .datum( 441 | dataLines1 442 | .filter(function(d) { return !d.disabled }) 443 | .map(function(d,i) { 444 | return { 445 | key: d.key, 446 | area: d.area, 447 | values: d.values.filter(function(d,i) { 448 | return lines1.x()(d,i) >= extent[0] && lines1.x()(d,i) <= extent[1]; 449 | }) 450 | } 451 | }) 452 | ); 453 | 454 | var focusLines2Wrap = g.select('.nv-focus .nv-lines2Wrap') 455 | .datum( 456 | dataLines2 457 | .filter(function(d) { return !d.disabled }) 458 | .map(function(d,i) { 459 | return { 460 | key: d.key, 461 | area: d.area, 462 | values: d.values.filter(function(d,i) { 463 | return lines2.x()(d,i) >= extent[0] && lines2.x()(d,i) <= extent[1]; 464 | }) 465 | } 466 | }) 467 | ); 468 | focusLines1Wrap.transition().duration(transitionDuration).call(lines1); 469 | focusLines2Wrap.transition().duration(transitionDuration).call(lines2); 470 | 471 | 472 | // Update Main (Focus) Axes 473 | g.select('.nv-focus .nv-x.nv-axis').transition().duration(transitionDuration) 474 | .call(xAxis); 475 | g.select('.nv-focus .nv-y1.nv-axis').transition().duration(transitionDuration) 476 | .call(y1Axis); 477 | g.select('.nv-focus .nv-y2.nv-axis') 478 | .attr('transform', 'translate(' + x.range()[1] + ',0)') 479 | .transition().duration(transitionDuration) 480 | .call(y2Axis); 481 | } 482 | lines1.dispatch.on('elementMouseover.tooltip', mouseover_line); 483 | lines2.dispatch.on('elementMouseover.tooltip', mouseover_line); 484 | lines1.dispatch.on('elementMouseout.tooltip', function(evt) { 485 | tooltip.hidden(true) 486 | }); 487 | lines2.dispatch.on('elementMouseout.tooltip', function(evt) { 488 | tooltip.hidden(true) 489 | }); 490 | }); 491 | 492 | return chart; 493 | } 494 | 495 | //============================================================ 496 | // Event Handling/Dispatching (out of chart's scope) 497 | //------------------------------------------------------------ 498 | 499 | // lines1.dispatch.on('elementMouseover.tooltip', function(e) { 500 | // e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; 501 | // dispatch.tooltipShow(e); 502 | // }); 503 | 504 | // lines1.dispatch.on('elementMouseout.tooltip', function(e) { 505 | // dispatch.tooltipHide(e); 506 | // }); 507 | 508 | // lines2.dispatch.on('elementMouseover.tooltip', function(e) { 509 | // e.pos = [e.pos[0] + margin.left, e.pos[1] + margin.top]; 510 | // dispatch.tooltipShow(e); 511 | // }); 512 | 513 | // lines2.dispatch.on('elementMouseout.tooltip', function(e) { 514 | // dispatch.tooltipHide(e); 515 | // }); 516 | 517 | // dispatch.on('tooltipHide', function() { 518 | // if (tooltips) nv.tooltip.cleanup(); 519 | // }); 520 | 521 | //============================================================ 522 | // Expose Public Variables 523 | //------------------------------------------------------------ 524 | 525 | // expose chart's sub-components 526 | chart.dispatch = dispatch; 527 | chart.legend = legend; 528 | chart.lines1 = lines1; 529 | chart.lines2 = lines2; 530 | chart.linesC = linesC; 531 | chart.xAxis = xAxis; 532 | chart.y1Axis = y1Axis; 533 | chart.y2Axis = y2Axis; 534 | chart.xCAxis = xCAxis; 535 | chart.yCAxis = yCAxis; 536 | 537 | chart.options = nv.utils.optionsFunc.bind(chart); 538 | 539 | chart._options = Object.create({}, { 540 | // simple options, just get/set the necessary values 541 | width: {get: function(){return width;}, set: function(_){width=_;}}, 542 | height: {get: function(){return height;}, set: function(_){height=_;}}, 543 | focusHeight: {get: function(){return height2;}, set: function(_){height2=_;}}, 544 | showLegend: {get: function(){return showLegend;}, set: function(_){showLegend=_;}}, 545 | yDomain1: {get: function(){return yDomain1;}, set: function(_){yDomain1=_;}}, 546 | yDomain2: {get: function(){return yDomain2;}, set: function(_){yDomain2=_;}}, 547 | yDomainC: {get: function(){return yDomainC;}, set: function(_){yDomainC=_;}}, 548 | brushExtent: {get: function(){return brushExtent;}, set: function(_){brushExtent=_;}}, 549 | tooltips: {get: function(){return tooltips;}, set: function(_){tooltips=_;}}, 550 | tooltipContent: {get: function(){return tooltip;}, set: function(_){tooltip=_;}}, 551 | defaultState: {get: function(){return defaultState;}, set: function(_){defaultState=_;}}, 552 | noData: {get: function(){return noData;}, set: function(_){noData=_;}}, 553 | 554 | // options that require extra logic in the setter 555 | margin: {get: function(){return margin;}, set: function(_){ 556 | margin.top = _.top !== undefined ? _.top : margin.top; 557 | margin.right = _.right !== undefined ? _.right : margin.right; 558 | margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; 559 | margin.left = _.left !== undefined ? _.left : margin.left; 560 | }}, 561 | color: {get: function(){return color;}, set: function(_){ 562 | color = nv.utils.getColor(_); 563 | legend.color(color); 564 | // line color is handled above? 565 | }}, 566 | interpolate: {get: function(){return lines1.interpolate();}, set: function(_){ 567 | lines1.interpolate(_); 568 | lines2.interpolate(_); 569 | linesC.interpolate(_); 570 | }}, 571 | xTickFormat: {get: function(){return xAxis.xTickFormat();}, set: function(_){ 572 | xAxis.xTickFormat(_); 573 | xCAxis.xTickFormat(_); 574 | }}, 575 | yTickFormat: {get: function(){return y1Axis.yTickFormat();}, set: function(_){ 576 | y1Axis.yTickFormat(_); 577 | y2Axis.yTickFormat(_); 578 | yCAxis.yTickFormat(_); 579 | }}, 580 | duration: {get: function(){return transitionDuration;}, set: function(_){ 581 | transitionDuration=_; 582 | y1Axis.duration(transitionDuration); 583 | y2Axis.duration(transitionDuration); 584 | yCAxis.duration(transitionDuration); 585 | xAxis.duration(transitionDuration); 586 | }}, 587 | x: {get: function(){return lines.x();}, set: function(_){ 588 | lines1.x(_); 589 | lines2.x(_); 590 | linesC.x(_); 591 | }}, 592 | y: {get: function(){return lines.y();}, set: function(_){ 593 | lines1.y(_); 594 | lines2.y(_); 595 | linesC.y(_); 596 | }} 597 | }); 598 | 599 | nv.utils.initOptions(chart); 600 | 601 | return chart; 602 | }; 603 | 604 | var TwoAxisFocus = function() { 605 | this.chart = twoAxisFocusChart(); 606 | }; 607 | 608 | TwoAxisFocus.prototype.initialize = function(options){ 609 | this.chart.margin(options.margin) 610 | .color(options.colors); 611 | 612 | this.chart.lines1.interpolate('monotone'); 613 | this.chart.lines2.interpolate('monotone'); 614 | this.chart.linesC.interpolate('monotone'); 615 | 616 | this.chart.xAxis 617 | .showMaxMin(false) 618 | .axisLabel(options.labels.xAxis) 619 | .tickFormat(function(d){return d3.time.format('%X')(new Date(d));}); 620 | 621 | this.chart.xCAxis 622 | .showMaxMin(false) 623 | .axisLabel(options.labels.xAxis) 624 | .tickFormat(function(d){return d3.time.format('%X')(new Date(d));}); 625 | 626 | this.chart.y1Axis 627 | .axisLabel(options.labels.yAxis1) 628 | .tickFormat(d3.format(',.2f')); 629 | 630 | this.chart.y2Axis 631 | .axisLabel(options.labels.yAxis2) 632 | .tickFormat(d3.format(',.2f')); 633 | 634 | this.chart.yCAxis 635 | .axisLabel(options.labels.yAxis2) 636 | .tickFormat(d3.format(',.2f')); 637 | }; 638 | 639 | TwoAxisFocus.prototype.get = function(chartid, url, params){ 640 | d3.json(url.concat("?", $.param(params)), 641 | function(error, result) { 642 | result.data.forEach(function(d){ 643 | d.values.forEach(function(v){ 644 | v.x = new Date(v.x * 1000); 645 | }); 646 | }); 647 | this.chart.yDomain1(result.yAxis1.bounds); 648 | this.chart.yDomain2(result.yAxis2.bounds); 649 | this.chart.yDomainC(result.yAxis2.bounds); 650 | d3.select("#".concat(chartid, " svg")) 651 | .datum(result.data) 652 | .call(this.chart); 653 | nv.utils.windowResize(this.chart.update); 654 | 655 | }.bind(this)); 656 | }; 657 | 658 | export default TwoAxisFocus; 659 | --------------------------------------------------------------------------------