├── src ├── htdocs │ ├── favicon.ico │ └── index.html ├── styl │ ├── core.styl │ ├── main.styl │ ├── type.styl │ ├── responsive.styl │ ├── colors.styl │ ├── fonts.styl │ ├── chart-renderer.styl │ └── layout.styl ├── README.md ├── js │ ├── charts │ │ ├── renderers.js │ │ ├── chart-type-configs.js │ │ ├── editors.js │ │ ├── cb-xy │ │ │ ├── xy-dimensions.js │ │ │ └── xy-config.js │ │ ├── cb-chart-grid │ │ │ ├── chart-grid-dimensions.js │ │ │ └── chart-grid-config.js │ │ └── ChartConfig.js │ ├── components │ │ ├── Canvas.jsx │ │ ├── series │ │ │ ├── LineMarkSeries.jsx │ │ │ ├── LineSeries.jsx │ │ │ ├── MarkSeries.jsx │ │ │ ├── BarSeries.jsx │ │ │ └── BarGroup.jsx │ │ ├── svg │ │ │ ├── BackgroundRect.jsx │ │ │ ├── ChartFooter.jsx │ │ │ └── SvgWrapper.jsx │ │ ├── shared │ │ │ ├── SeriesLabel.jsx │ │ │ ├── Chart.jsx │ │ │ ├── DataSeriesTypeSettings.jsx │ │ │ ├── HorizontalGridLines.jsx │ │ │ ├── ScaleReset.jsx │ │ │ ├── VerticalGridLines.jsx │ │ │ ├── BarLabels.jsx │ │ │ ├── BlockerRects.jsx │ │ │ ├── DataInput.jsx │ │ │ ├── VerticalAxis.jsx │ │ │ └── HorizontalAxis.jsx │ │ ├── mixins │ │ │ ├── NumericScaleMixin.js │ │ │ ├── ChartEditorMixin.js │ │ │ ├── ChartRendererMixin.js │ │ │ └── DateScaleMixin.js │ │ ├── LocalStorageTimer.jsx │ │ ├── chart-grid │ │ │ ├── ChartGridMobile.jsx │ │ │ ├── ChartGridRenderer.jsx │ │ │ └── ChartGrid_xScaleSettings.jsx │ │ ├── chart-xy │ │ │ └── XYMobile.jsx │ │ ├── ChartTypeSelector.jsx │ │ └── ChartMetadata.jsx │ ├── config │ │ ├── chart-sizes.js │ │ ├── chart-style.js │ │ └── chart-breakpoints.js │ ├── actions │ │ ├── ChartServerActions.js │ │ └── ChartViewActions.js │ ├── util │ │ ├── series-utils.js │ │ ├── validate-chart-model.js │ │ ├── parse-utils.js │ │ ├── ChartbuilderLocalStorageAPI.js │ │ ├── parse-config-values.js │ │ ├── parse-data-by-series.js │ │ ├── catch-chart-mistakes.js │ │ ├── grid-utils.js │ │ ├── error-names.js │ │ ├── scale-utils.js │ │ └── validate-data-input.js │ ├── index.js │ ├── dispatcher │ │ └── dispatcher.js │ └── stores │ │ ├── SessionStore.js │ │ ├── ErrorStore.js │ │ ├── ChartMetadataStore.js │ │ └── ChartPropertiesStore.js └── assets │ └── iphone-6.svg ├── test ├── jsx │ ├── index-jsx.js │ ├── MockComponent.jsx │ ├── chart-grid-xy.jsx │ ├── chartbuilder.jsx │ ├── chart-grid-bar.jsx │ └── xy-renderer.jsx ├── index.js ├── test-page │ ├── main.js │ ├── index.html │ └── TestPage.jsx ├── chart-generators │ ├── index.js │ ├── generate-chart-grid.js │ └── generate-xy.js ├── util │ └── util.js ├── validate-chart-model.js ├── auto-date-interval.js ├── chart-metadata-store.js ├── chart-properties-store.js ├── catch-chart-mistakes.js ├── date-parsers.js ├── timezone-adjustments.js ├── validate-input.js └── helper.js ├── .editorconfig ├── .gitignore ├── gulp ├── aws-config.json.example ├── config.js └── publish.js ├── .htaccess ├── docs ├── testing.md ├── deploying.md ├── git-workflow-forks.md └── 02-customizing-chartbuilder.md ├── LICENSE ├── tutorials ├── bar-chart-with-ranking-data.md ├── column-chart-ordinal-data.md └── basic-chart.md ├── package.json ├── CONTRIBUTING.md └── README.md /src/htdocs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quartz/Chartbuilder/HEAD/src/htdocs/favicon.ico -------------------------------------------------------------------------------- /src/styl/core.styl: -------------------------------------------------------------------------------- 1 | @import 'type' 2 | @import 'colors' 3 | @import 'chart-editor' 4 | @import 'chart-renderer' 5 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | Static assets (images, data files, etc) can be placed in here and will be 2 | automatically moved to the build directory on `gulp build`. 3 | -------------------------------------------------------------------------------- /test/jsx/index-jsx.js: -------------------------------------------------------------------------------- 1 | require("./chartbuilder.jsx"); 2 | require("./xy-renderer.jsx"); 3 | require("./chart-grid-bar.jsx"); 4 | require("./chart-grid-xy.jsx"); 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | .publish 3 | build 4 | !node_modules/stylus-normalize 5 | .DS_Store 6 | .idea 7 | .tmp 8 | .lvimrc 9 | npm-debug.log 10 | bower_components/ 11 | src/fonts 12 | 13 | config.json 14 | .awspublish-* 15 | -------------------------------------------------------------------------------- /test/jsx/MockComponent.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | var MockComponent = React.createClass({ 4 | render: function() { 5 | return ( 6 |
7 | ); 8 | } 9 | }); 10 | 11 | module.exports = MockComponent; 12 | -------------------------------------------------------------------------------- /src/js/charts/renderers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name renderers 3 | */ 4 | 5 | // Chart renderer 6 | module.exports = { 7 | xy: require("../components/chart-xy/XYRenderer.jsx"), 8 | chartgrid: require("../components/chart-grid/ChartGridRenderer.jsx") 9 | }; 10 | -------------------------------------------------------------------------------- /src/js/components/Canvas.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | var Canvas = React.createClass({ 4 | 5 | render: function() { 6 | var self = this; 7 | return ; 8 | } 9 | }); 10 | 11 | module.exports = Canvas; 12 | -------------------------------------------------------------------------------- /gulp/aws-config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "AWS": { 3 | "Credentials": { 4 | "AccessKeyId": "ABCXYZ", 5 | "SecretAccessKey": "Ab0Xyz123C" 6 | }, 7 | "Region": "us-east-1", 8 | "Bucket": "my-bucket", 9 | "FolderPath": "Path/to/destination" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/js/charts/chart-type-configs.js: -------------------------------------------------------------------------------- 1 | // Object containing chart types and their respective configs 2 | var chart_type_configs = { 3 | xy: require("./cb-xy/xy-config"), 4 | chartgrid: require("./cb-chart-grid/chart-grid-config"), 5 | }; 6 | 7 | module.exports = chart_type_configs; 8 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | 2 | FileETag None 3 | 4 | Header unset ETag 5 | Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate" 6 | Header set Pragma "no-cache" 7 | Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT" 8 | 9 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require("./chart-metadata-store"); 2 | require("./chart-properties-store"); 3 | require("./helper"); 4 | require("./parse-input"); 5 | require("./date-parsers"); 6 | require("./date-frequencies"); 7 | require("./timezone-adjustments"); 8 | require("./auto-date-interval"); 9 | require("./validate-input"); 10 | require("./catch-chart-mistakes"); 11 | -------------------------------------------------------------------------------- /src/styl/main.styl: -------------------------------------------------------------------------------- 1 | // Normalize -- https://github.com/bymathias/normalize.styl 2 | @import 'normalize' 3 | @import '../../node_modules/chartbuilder-ui/dist/styles.css' 4 | 5 | // nib -- http://visionmedia.github.io/nib/ 6 | @import 'nib' 7 | 8 | // Chartbuilder-specific css 9 | @import 'colors' 10 | @import 'responsive' 11 | @import 'fonts' 12 | @import 'type' 13 | @import 'layout' 14 | -------------------------------------------------------------------------------- /test/test-page/main.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var ReactDOM = require("react-dom") 3 | 4 | var container = document.querySelector(".chartbuilder-container"); 5 | var TestPage = require("./TestPage.jsx"); 6 | 7 | document.addEventListener("DOMContentLoaded", function() { 8 | // Initialize data from localStorage 9 | ReactDOM.render( 10 | , 11 | container); 12 | }); 13 | -------------------------------------------------------------------------------- /src/styl/type.styl: -------------------------------------------------------------------------------- 1 | // Typography 2 | 3 | // Fonts 4 | $font-sans = 'Khula-Regular',Arial,Helvetica,sans-serif 5 | $font-sans-light = 'Khula-Light',Arial,Helvetica,sans-serif 6 | $font-sans-bold = "Khula-Bold", Arial, sans-serif 7 | $font-serif = Georgia,serif 8 | $primary-font-family = $font-sans 9 | $secondary-font-family = $font-serif 10 | $monospaced-font-family = Monaco, Lucida Console, monspace 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/js/config/chart-sizes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default dimensions for non-responsive chart sizes. 3 | * @name chart_sizes 4 | * @memberof config 5 | * @static 6 | */ 7 | var chart_sizes = { 8 | medium: { 9 | width: 640, 10 | height: 390 11 | }, 12 | spotLong: { 13 | width: 320, 14 | height: 390 15 | }, 16 | spotSmall: { 17 | width: 320, 18 | height: 250 19 | } 20 | }; 21 | 22 | module.exports = chart_sizes; 23 | -------------------------------------------------------------------------------- /src/js/charts/editors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name editors 3 | */ 4 | 5 | // Editor components for chart types, as well as their mobile override editor interfaces 6 | module.exports = { 7 | xy: { 8 | Editor: require("../components/chart-xy/XYEditor.jsx"), 9 | MobileOverrides: require("../components/chart-xy/XYMobile.jsx") 10 | }, 11 | 12 | chartgrid: { 13 | Editor: require("../components/chart-grid/ChartGridEditor.jsx"), 14 | MobileOverrides: require("../components/chart-grid/ChartGridMobile.jsx") 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /test/chart-generators/index.js: -------------------------------------------------------------------------------- 1 | var util = require("../util/util"); 2 | var generateXY = require("./generate-xy"); 3 | var generateChartGrid = require("./generate-chart-grid"); 4 | 5 | var chartGenerators = { 6 | xy: generateXY, 7 | chartgrid: generateChartGrid 8 | }; 9 | 10 | function randChart() { 11 | var generator = util.randArrElement(Object.keys(chartGenerators)); 12 | return chartGenerators[generator](); 13 | } 14 | 15 | module.exports = { 16 | xy: generateXY, 17 | chartgrid: generateChartGrid, 18 | randChart: randChart 19 | }; 20 | -------------------------------------------------------------------------------- /src/htdocs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chartbuilder 3.0.5 8 | 9 | 10 | 11 | 12 |
13 |

Chartbuilder 3.0.5

14 |
15 |
16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/util/util.js: -------------------------------------------------------------------------------- 1 | // Helpers for test data 2 | 3 | var _ = require("lodash"); 4 | 5 | function randInt(min, max) { 6 | return Math.floor(Math.random() * (max - min + 1)) + min; 7 | } 8 | 9 | function randArrElement(arr) { 10 | return arr[Math.floor(Math.random() * arr.length)]; 11 | } 12 | 13 | function flattenLengthsToTotal(arr) { 14 | return _.reduce(arr, function(total, d) { 15 | return total + d.length; 16 | }, 0); 17 | } 18 | 19 | module.exports = { 20 | randInt: randInt, 21 | randArrElement: randArrElement, 22 | flattenLengthsToTotal: flattenLengthsToTotal 23 | }; 24 | -------------------------------------------------------------------------------- /src/js/actions/ChartServerActions.js: -------------------------------------------------------------------------------- 1 | var Dispatcher = require("../dispatcher/dispatcher"); 2 | 3 | /** 4 | * ### ChartServerActions 5 | * Send data from some external API, usually localStorage in our case 6 | */ 7 | var ChartServerActions = { 8 | 9 | /** 10 | * Update the whole chart model 11 | * @param {Object} model 12 | * @param {object} model.chartProps 13 | * @param {object} model.metadata 14 | */ 15 | receiveModel: function(chartModel) { 16 | Dispatcher.handleServerAction({ 17 | eventName: "receive-model", 18 | model: chartModel 19 | }); 20 | } 21 | 22 | }; 23 | 24 | module.exports = ChartServerActions; 25 | -------------------------------------------------------------------------------- /src/js/components/series/LineMarkSeries.jsx: -------------------------------------------------------------------------------- 1 | // Svg text elements used to describe chart 2 | var React = require("react"); 3 | var PropTypes = React.PropTypes; 4 | var line = require("d3").svg.line(); 5 | 6 | var LineSeries = require("./LineSeries.jsx") 7 | var MarkSeries = require("./MarkSeries.jsx") 8 | 9 | var LineMarkSeries = React.createClass({ 10 | 11 | propTypes: { 12 | data: PropTypes.array, 13 | xScale: PropTypes.func, 14 | yScale: PropTypes.func 15 | }, 16 | 17 | render: function() { 18 | return ( 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | }); 27 | 28 | module.exports = LineMarkSeries; 29 | -------------------------------------------------------------------------------- /test/test-page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Chartbuilder 2.0 8 | 9 | 10 | 18 | 19 | 20 |
21 |

Chartbuilder 2.0 (TEST PAGE)

22 |
23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/js/util/series-utils.js: -------------------------------------------------------------------------------- 1 | var createFactory = require("react").createFactory; 2 | var BarGroup = require("../components/series/BarGroup.jsx"); 3 | var LineMarkSeries = require("../components/series/LineMarkSeries.jsx"); 4 | var LineSeries = require("../components/series/LineSeries.jsx"); 5 | var MarkSeries = require("../components/series/MarkSeries.jsx"); 6 | 7 | var series_components = { 8 | "line": createFactory(LineSeries), 9 | "lineMark": createFactory(LineMarkSeries), 10 | "column": createFactory(BarGroup), 11 | "scatterPlot": createFactory(MarkSeries) 12 | }; 13 | 14 | function create_series(type, props) { 15 | return series_components[type](props); 16 | } 17 | 18 | module.exports = { 19 | createSeries: create_series 20 | }; 21 | -------------------------------------------------------------------------------- /src/js/util/validate-chart-model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test whether a string is a valid Chartbuilder model 3 | * @param {string} modelStr - unparsed string of chart model JSON 4 | * @returns {object or null} parsed - parsed object 5 | * @static 6 | * @memberof helper 7 | */ 8 | function validate_chart_model(modelStr) { 9 | var parsed; 10 | 11 | try { 12 | parsed = JSON.parse(modelStr); 13 | } catch (e) { 14 | throw new TypeError("Chart model is not valid JSON"); 15 | } 16 | 17 | var isValidChartModel = (parsed.hasOwnProperty("chartProps") && parsed.hasOwnProperty("metadata")); 18 | if (isValidChartModel) { 19 | return parsed; 20 | } else { 21 | throw new TypeError("Not a valid Chartbuilder model"); 22 | } 23 | 24 | } 25 | 26 | module.exports = validate_chart_model; 27 | -------------------------------------------------------------------------------- /src/js/util/parse-utils.js: -------------------------------------------------------------------------------- 1 | var tabRegex = /[^\t]/gi; 2 | var datePattern = /^(date|time|year)$/i; 3 | 4 | function escapeRegExp(str) { 5 | return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 6 | } 7 | 8 | // Detect delimiter of input 9 | // algorithm: if numtabs >= numrows then TSV, else CSV 10 | function detectDelimiter(input) { 11 | var numRows = input.split(/\r\n|\r|\n/).length; 12 | var numTabs = input.replace(tabRegex, "").length; 13 | 14 | if (numTabs >= numRows - 1) { 15 | return "\t"; 16 | } else { 17 | return ","; 18 | } 19 | } 20 | 21 | function matchDatePattern(str) { 22 | return datePattern.test(str); 23 | } 24 | 25 | module.exports = { 26 | escapeRegExp: escapeRegExp, 27 | detectDelimiter: detectDelimiter, 28 | matchDatePattern: matchDatePattern 29 | }; 30 | -------------------------------------------------------------------------------- /src/styl/responsive.styl: -------------------------------------------------------------------------------- 1 | // Default breakpoints 2 | 3 | $width-xsmall = 320px 4 | $width-small = 480px 5 | $width-tablet = 650px 6 | $width-medium = 1024px 7 | $width-large = 1200px 8 | 9 | $single_column_breakpoint = 800px 10 | 11 | 12 | //@media only screen and (max-width: $width-xsmall) 13 | //// Old iPhones, iPhone Retina 14 | 15 | //@media only screen and (min-width: $width-small) and (max-width: $width-tablet) 16 | //// Extra-small devices, phones 17 | 18 | //@media only screen and (min-width: $width-tablet) and (max-width: $width-medium) 19 | //// Small devices, tablets 20 | 21 | //@media only screen and (min-width: $width-medium) and (max-width: $width-large) 22 | //// Medium devices, desktops 23 | 24 | //@media only screen and (min-width: $width-large) 25 | //// Large devices, wide screens 26 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Chartbuilder tests 2 | 3 | ### Unit tests 4 | 5 | To run the unit tests, do: 6 | 7 | npm test 8 | 9 | Tests are written using [tape](https://github.com/substack/tape) which has its 10 | output piped to [smokestack](https://github.com/hughsk/smokestack). Pure 11 | JavaScript and JSX-reliant modules can be tested independently, with `npm run 12 | test:js` and `npm run test:jsx`. 13 | 14 | If you make a change, consider adding a test! 15 | 16 | ### Chart test page 17 | 18 | There is also a chart test page that renders various charts at different sizes. 19 | This is helpful if you need to quickly see how your changes look or if they are 20 | breaking anything. To run this page, do: 21 | 22 | npm run test-page 23 | 24 | And open your browser to the URL that appears in the console, usually 25 | `http://localhost:3000`. 26 | -------------------------------------------------------------------------------- /src/js/components/svg/BackgroundRect.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var PropTypes = React.PropTypes; 3 | 4 | var BackgroundRect = React.createClass({ 5 | 6 | propTypes: { 7 | dimensions: PropTypes.shape({ 8 | width: PropTypes.number, 9 | height: PropTypes.number 10 | }), 11 | x: PropTypes.number, 12 | y: PropTypes.number 13 | }, 14 | 15 | getDefaultProps: function() { 16 | return { 17 | x: 0, 18 | y: 0 19 | } 20 | }, 21 | 22 | render: function() { 23 | var props = this.props; 24 | return ( 25 | 26 | 33 | 34 | ); 35 | } 36 | 37 | }); 38 | 39 | module.exports = BackgroundRect; 40 | -------------------------------------------------------------------------------- /test/validate-chart-model.js: -------------------------------------------------------------------------------- 1 | var test = require("tape"); 2 | 3 | var validateChartModel = require("../src/js/util/validate-chart-model"); 4 | var sample_model = require("./util/sample_model.json"); 5 | 6 | var incorrect_json = { 7 | chart: { 8 | data: [0,1,2,3,4,5] 9 | }, 10 | meta: { 11 | title: "Incorrect" 12 | } 13 | }; 14 | 15 | test("validate chart model", function(t) { 16 | t.plan(3); 17 | 18 | t.throws(function() { 19 | validateChartModel("NOT JSON"); 20 | }, TypeError, "Throw if chart model is not valid JSON"); 21 | 22 | t.throws(function() { 23 | validateChartModel(incorrect_json); 24 | }, TypeError, "Throw if chart model is JSON but not a valid Chartbuilder model"); 25 | 26 | var validated = validateChartModel(JSON.stringify(sample_model)); 27 | t.deepEqual(validated, sample_model, "Validator returns model if valid."); 28 | }); 29 | -------------------------------------------------------------------------------- /src/js/util/ChartbuilderLocalStorageAPI.js: -------------------------------------------------------------------------------- 1 | // Get data from and save to local storage 2 | 3 | var ChartServerActions = require("../actions/ChartServerActions"); 4 | var defaultInput = require("../config/default-input"); 5 | var chartConfig = require("../charts/chart-type-configs"); 6 | //var testInput = require("../../../test/util/test-input"); 7 | 8 | module.exports = { 9 | defaultChart: function() { 10 | var default_model = chartConfig.xy.defaultProps; 11 | default_model.chartProps.input = { 12 | //raw: testInput.init_data_time 13 | raw: defaultInput 14 | }; 15 | 16 | ChartServerActions.receiveModel(default_model); 17 | }, 18 | 19 | getChart: function() { 20 | ChartServerActions.receiveModel(JSON.parse(localStorage.getItem("model"))); 21 | }, 22 | 23 | saveChart: function(model) { 24 | localStorage.setItem("model", JSON.stringify(model)); 25 | } 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV == "dev") { 2 | // Include React as a global variable if we are in dev environment. 3 | // This makes the app useable with React dev tools 4 | global.React = require("react"); 5 | } 6 | 7 | var React = require("react"); 8 | var ReactDOM = require("react-dom"); 9 | var ChartbuilderLocalStorageAPI = require("./util/ChartbuilderLocalStorageAPI"); 10 | var Chartbuilder = require("./components/Chartbuilder.jsx"); 11 | var container = document.querySelector(".chartbuilder-container"); 12 | 13 | document.addEventListener("DOMContentLoaded", function() { 14 | document.cookie = "authed=yes"; 15 | // Initialize data from localStorage 16 | ChartbuilderLocalStorageAPI.defaultChart(); 17 | // Render parent chartbuilder component 18 | ReactDOM.render( 19 | , 23 | container ); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /src/js/components/series/LineSeries.jsx: -------------------------------------------------------------------------------- 1 | // Svg text elements used to describe chart 2 | var React = require("react"); 3 | var PropTypes = React.PropTypes; 4 | var line = require("d3").svg.line(); 5 | var ordinalAdjust = require("../../util/scale-utils").ordinalAdjust; 6 | 7 | var LineSeries = React.createClass({ 8 | 9 | propTypes: { 10 | data: PropTypes.array, 11 | xScale: PropTypes.func, 12 | yScale: PropTypes.func 13 | }, 14 | 15 | render: function() { 16 | var props = this.props; 17 | 18 | var lineFunc = line 19 | .x(function(d) { return ordinalAdjust(props.xScale, d.entry); }) 20 | .y(function(d) { return props.yScale(d.value); }); 21 | 22 | return ( 23 | 24 | 28 | 29 | ); 30 | } 31 | 32 | }); 33 | 34 | module.exports = LineSeries; 35 | -------------------------------------------------------------------------------- /gulp/config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var dirs = { 4 | build: './build', 5 | tmp: './.tmp', 6 | src: './src', 7 | dist: './dist' 8 | }; 9 | 10 | var paths = { 11 | src: { 12 | img: dirs.src + '/img', 13 | htdocs: dirs.src + '/htdocs', 14 | js: dirs.src + '/js', 15 | styl: dirs.src + '/styl', 16 | fonts: dirs.src + '/fonts', 17 | assets: dirs.src + '/assets' 18 | }, 19 | tmp: { 20 | css: dirs.tmp + '/css', 21 | img: dirs.tmp + '/img', 22 | js: dirs.tmp + '/js', 23 | fonts: dirs.tmp + '/fonts', 24 | assets: dirs.tmp + '/assets' 25 | }, 26 | build: { 27 | css: dirs.build + '/css', 28 | img: dirs.build + '/img', 29 | js: dirs.build + '/js', 30 | fonts: dirs.build + '/fonts', 31 | assets: dirs.build + '/assets' 32 | }, 33 | dist: { 34 | css: dirs.dist + '/css' 35 | } 36 | }; 37 | 38 | var server = { 39 | port: '8080', 40 | root: path.resolve('./'), 41 | }; 42 | 43 | module.exports = { 44 | dirs: dirs, 45 | paths: paths, 46 | server: server 47 | }; 48 | -------------------------------------------------------------------------------- /gulp/publish.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | rename = require('gulp-rename'), 3 | awspublish = require('gulp-awspublish'); 4 | 5 | const debugMode = false; 6 | 7 | gulp.task('publish',['build'], function(done) { 8 | 9 | var aws; 10 | try { 11 | aws = require('./aws-config.json').AWS; 12 | } 13 | catch (ex) { 14 | console.log('>>> no config.json found', ex); 15 | done(); 16 | } 17 | 18 | var publisher = awspublish.create({ 19 | region: aws.Region, 20 | params: {Bucket: aws.Bucket}, 21 | accessKeyId: aws.Credentials.AccessKeyId, 22 | secretAccessKey: aws.Credentials.SecretAccessKey 23 | }); 24 | 25 | return gulp.src(['build/**/*.{html,css,js}']) 26 | .pipe(rename(function (path) { 27 | if(aws.FolderPath) { 28 | path.dirname = '/'+ aws.FolderPath +'/'+ path.dirname; 29 | } 30 | })) 31 | .pipe(awspublish.gzip()) 32 | .pipe(publisher.publish({}, {simulate: debugMode, createOnly: false})) 33 | .pipe(publisher.cache()) 34 | .pipe(awspublish.reporter()); 35 | }); 36 | -------------------------------------------------------------------------------- /src/js/components/shared/SeriesLabel.jsx: -------------------------------------------------------------------------------- 1 | // Svg text elements used to describe chart 2 | var React = require("react"); 3 | var PropTypes = React.PropTypes; 4 | var isNumber = require("lodash/isNumber"); 5 | 6 | var SeriesLabel = React.createClass({ 7 | 8 | propTypes: { 9 | text: PropTypes.string, 10 | translate: PropTypes.array, 11 | colorIndex: PropTypes.number 12 | }, 13 | 14 | getDefaultProps: function() { 15 | return { 16 | translate: [0, 0], 17 | text: "SeriesLabel", 18 | colorIndex: 0, 19 | xVal: 0 20 | }; 21 | }, 22 | 23 | render: function() { 24 | var props = this.props; 25 | var x; 26 | 27 | if (isNumber(props.x)) { 28 | x = props.x; 29 | } else { 30 | x = props.xScale(props.xVal); 31 | } 32 | 33 | return ( 34 | 39 | {props.text} 40 | 41 | ); 42 | } 43 | 44 | }); 45 | 46 | module.exports = SeriesLabel; 47 | -------------------------------------------------------------------------------- /src/js/util/parse-config-values.js: -------------------------------------------------------------------------------- 1 | function convertConfig(item, info, emSize, chartWidth) { 2 | var out; 3 | var o; 4 | 5 | if(!info) { 6 | info = {}; 7 | info.em = emSize; 8 | info.width = chartWidth; 9 | } 10 | 11 | if(item === null) { 12 | out = item; 13 | } 14 | else if(Array.isArray(item)) { 15 | out = []; 16 | for (var i = item.length - 1; i >= 0; i--) { 17 | out[i] = convertConfig(item[i], info); 18 | } 19 | } 20 | else if (typeof item === "object") { 21 | out = {}; 22 | for(var prop in item) { 23 | out[prop] = convertConfig(item[prop], info); 24 | } 25 | } 26 | else { 27 | out = item; 28 | 29 | if(typeof item === "string") { 30 | var floated = parseFloat(item); 31 | if(floated + "em" == item || floated + "rem" == item) { 32 | out = info.em * floated; 33 | } 34 | else if(floated + "%" == item) { 35 | out = info.width * floated/100; 36 | } 37 | else if(floated + "px" == item) { 38 | out = floated; 39 | } 40 | } 41 | } 42 | 43 | return out; 44 | 45 | } 46 | 47 | 48 | 49 | module.exports = convertConfig; 50 | -------------------------------------------------------------------------------- /src/js/components/mixins/NumericScaleMixin.js: -------------------------------------------------------------------------------- 1 | var clone = require("lodash/clone"); 2 | var map = require("lodash/map"); 3 | var reduce = require("lodash/reduce"); 4 | var help = require("../../util/helper.js"); 5 | 6 | /** 7 | * ### Mixin for renderers that require construction of a numeric scale 8 | * @instance 9 | * @memberof renderers 10 | */ 11 | var NumericScaleMixin = { 12 | 13 | /** 14 | * generateNumericScale 15 | * Create a numeric scale given data, scale, and dimensions settings 16 | * @param props 17 | * @return {object} - `{ numericTicks: ..., domain: ..., numericFormatter: ...}` 18 | */ 19 | generateNumericScale: function(props) { 20 | // TODO: auto-generate some kind of "good" numeric scale? For now just 21 | // return the settings 22 | var cur = props.chartProps.scale.numericSettings; 23 | 24 | return { 25 | custom: cur.custom, 26 | domain: cur.domain, 27 | precision: cur.precision, 28 | prefix: cur.prefix, 29 | suffix: cur.suffix, 30 | tickValues: cur.tickValues, 31 | ticks: cur.ticks 32 | } 33 | } 34 | }; 35 | 36 | module.exports = NumericScaleMixin; 37 | -------------------------------------------------------------------------------- /src/js/config/chart-style.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global config not specific to a chart type 3 | * @name config 4 | */ 5 | 6 | /** 7 | * Global style config that is not specific to any one chart type 8 | * @name chart_style 9 | * @property {Nem|number} overtick_top 10 | * @property {Nem|number} overtick_bottom 11 | * @property {number} numColors 12 | * @property {Nem|number} xOvertick - Font size at this breakpoint. This is used to 13 | * @property {Nem|number} creditMargin - Distance btwn credit and the logo/text beside it 14 | * @memberof config 15 | * @static 16 | */ 17 | var chart_style = { 18 | overtick_top: "0.8em", 19 | overtick_bottom: "0.8em", 20 | fontFamilies: { // necessary for calculating text width before render 21 | axes: "Khula-Light", 22 | labels: "Khula-Light" 23 | }, 24 | fontSizes: { 25 | large: "1.2em", 26 | medium: "1em", 27 | small: "0.8em" 28 | }, 29 | dotRadiusFactor: 0.007, // size of dot as % of width 30 | numColors: 11, 31 | xOverTick: "1em", // horizontal the distance between the yAxes and xAxis 32 | creditMargin: "0.6em" 33 | }; 34 | 35 | module.exports = chart_style; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Quartz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/js/components/series/MarkSeries.jsx: -------------------------------------------------------------------------------- 1 | // Svg text elements used to describe chart 2 | var React = require("react"); 3 | var PropTypes = React.PropTypes; 4 | var map = require("lodash/map"); 5 | var ordinalAdjust = require("../../util/scale-utils").ordinalAdjust; 6 | 7 | var MarkSeries = React.createClass({ 8 | 9 | propTypes: { 10 | data: PropTypes.array, 11 | dimensions: PropTypes.object, 12 | dotRadiusFactor: PropTypes.number, 13 | xScale: PropTypes.func, 14 | yScale: PropTypes.func 15 | }, 16 | 17 | getDefaultProps: function() { 18 | return { dotRadiusFactor: 0.007 } 19 | }, 20 | 21 | render: function() { 22 | var props = this.props; 23 | var radius = props.dimensions.width * props.dotRadiusFactor; 24 | var marks = map(props.data, function(d, i) { 25 | return ( 26 | 33 | ); 34 | }); 35 | 36 | return ( 37 | {marks} 38 | ); 39 | 40 | } 41 | 42 | }); 43 | 44 | module.exports = MarkSeries; 45 | -------------------------------------------------------------------------------- /test/auto-date-interval.js: -------------------------------------------------------------------------------- 1 | var test = require("tape"); 2 | require("sugar-date"); // sugar is used for date parsing 3 | var processDates = require("../src/js/util/process-dates"); 4 | var auto = processDates.autoDateFormatAndFrequency; 5 | var width = 640; 6 | var minDate; 7 | var maxDate; 8 | var ff; 9 | 10 | test("auto date interval:", function(t) { 11 | t.plan(4); 12 | 13 | minDate = new Date(2010, 0, 1); 14 | maxDate = new Date(2010, 0, 2); 15 | ff = auto(minDate, maxDate, "auto", width); 16 | t.equal(ff.format, "h", "return hours for proper interval"); 17 | 18 | minDate = new Date(2010, 0, 1); 19 | maxDate = new Date(2010, 2, 1); 20 | ff = auto(minDate, maxDate, "auto", width); 21 | t.equal(ff.format, "M1d", "return months/days for proper interval"); 22 | 23 | minDate = new Date(2010, 0, 1); 24 | maxDate = new Date(2010, 12, 1); 25 | ff = auto(minDate, maxDate, "auto", width); 26 | t.equal(ff.format, "M", "return months for proper interval"); 27 | 28 | minDate = new Date(2010, 0, 1); 29 | maxDate = new Date(2015, 12, 1); 30 | ff = auto(minDate, maxDate, "auto", width); 31 | t.equal(ff.format, "yy", "return years for proper interval"); 32 | 33 | t.end(); 34 | }); 35 | -------------------------------------------------------------------------------- /test/chart-metadata-store.js: -------------------------------------------------------------------------------- 1 | var test = require("tape"); 2 | var chartGenerators = require("./chart-generators"); 3 | var ChartServerActions = require("../src/js/actions/ChartServerActions"); 4 | var ChartViewActions = require("../src/js/actions/ChartViewActions"); 5 | var ChartMetadataStore = require("../src/js/stores/ChartMetadataStore"); 6 | var registeredCallback; 7 | 8 | function setupStore(callback) { 9 | ChartMetadataStore.clear(); 10 | callback(ChartMetadataStore); 11 | } 12 | 13 | test("Chart metadata store", function(t) { 14 | t.plan(3); 15 | 16 | setupStore(function(store) { 17 | t.deepEqual(store.getAll(), {}, "initial store returns an empty object"); 18 | }); 19 | 20 | setupStore(function(store) { 21 | ChartViewActions.updateMetadata("test_key", "test_value"); 22 | t.equal(store.get("test_key"), "test_value", "set a single prop and get the same value from the store"); 23 | }); 24 | 25 | setupStore(function(store) { 26 | var randChart = chartGenerators.randChart(); 27 | ChartServerActions.receiveModel(randChart); 28 | var _all = store.getAll(); 29 | t.deepEqual(_all, randChart.metadata, "load random model into the metadata store"); 30 | t.end(); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/chart-properties-store.js: -------------------------------------------------------------------------------- 1 | var test = require("tape"); 2 | var chartGenerators = require("./chart-generators"); 3 | var ChartServerActions = require("../src/js/actions/ChartServerActions"); 4 | var ChartViewActions = require("../src/js/actions/ChartViewActions"); 5 | var ChartPropertiesStore = require("../src/js/stores/ChartPropertiesStore"); 6 | 7 | function setupStore(callback) { 8 | ChartPropertiesStore.clear(); 9 | callback(ChartPropertiesStore); 10 | } 11 | 12 | test("Chart properties store", function(t) { 13 | t.plan(3); 14 | 15 | setupStore(function(store) { 16 | t.deepEqual(store.getAll(), {}, "initial store returns an empty object"); 17 | }); 18 | 19 | setupStore(function(store) { 20 | ChartViewActions.updateChartProp("test_key", "test_value"); 21 | t.equal(store.get("test_key"), "test_value", "set a single prop and get the same value from the store"); 22 | }); 23 | 24 | setupStore(function(store) { 25 | var randChart = chartGenerators.randChart(); 26 | 27 | ChartServerActions.receiveModel(randChart); 28 | var _all = store.getAll(); 29 | t.ok(_all.hasOwnProperty("data"), "load model into the properties store and parse input"); 30 | }); 31 | 32 | t.end(); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /src/js/config/chart-breakpoints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration of breakpoints for Chartbuilder renderers. 3 | * @property {string} class_name - Applied to the renderer at this break point 4 | * @property {number} min_size - Minimum value (most likely width) at which to 5 | * apply this breakpoint 6 | * @property {number} em_size - Font size at this breakpoint. This is used to 7 | * calculate relative positioning 8 | * @memberof config 9 | * @static 10 | */ 11 | var breakpoints = [ 12 | { 13 | "class_name": "large", 14 | "min_size": 900, 15 | "em_size": 20 16 | }, 17 | { 18 | "class_name": "medium", 19 | "min_size": 480, 20 | "em_size": 20 21 | }, 22 | { 23 | "class_name":"small", 24 | "min_size": 0, 25 | "em_size": 12 26 | } 27 | ]; 28 | 29 | breakpoints.sort(function(a, b) { 30 | return b.min_size - a.min_size; 31 | }); 32 | 33 | function getBreakpointObj(enableResponsive, width) { 34 | if (enableResponsive || !width) { 35 | return breakpoints.filter(function(bp) { 36 | return width > bp.min_size; 37 | })[0]; 38 | } else { 39 | return breakpoints[1]; 40 | } 41 | } 42 | 43 | module.exports = { 44 | breakpoints: breakpoints, 45 | getBreakpointObj: getBreakpointObj 46 | }; 47 | -------------------------------------------------------------------------------- /src/js/charts/cb-xy/xy-dimensions.js: -------------------------------------------------------------------------------- 1 | var chartSizes = require("../../config/chart-sizes"); 2 | 3 | // TODO: jsDocify this if it works 4 | 5 | /** 6 | * see [ChartConfig#calculateDimensions](#chartconfig/calculatedimensions) 7 | * @see ChartConfig#calculateDimensions 8 | * @instance 9 | * @memberof xy_config 10 | */ 11 | function calculate_xy_dimensions(width, opts) { 12 | var height; 13 | var aspectRatio = opts.displayConfig.aspectRatio; 14 | var metadata = opts.metadata; 15 | 16 | if (metadata.size == "auto" || opts.enableResponsive) { 17 | // use current width 18 | } else { 19 | width = chartSizes[metadata.size].width; 20 | } 21 | 22 | switch (metadata.size) { 23 | case "auto": 24 | height = width * aspectRatio.wide; 25 | break; 26 | 27 | case 'medium': 28 | height = width * aspectRatio.wide; 29 | break; 30 | 31 | case "spotLong": 32 | height = width * aspectRatio.longSpot; 33 | break; 34 | 35 | case "spotSmall": 36 | height = width * aspectRatio.smallSpot; 37 | break; 38 | 39 | default: 40 | height = width * aspectRatio.wide; 41 | } 42 | 43 | return { 44 | width: width, 45 | height: height 46 | }; 47 | } 48 | 49 | module.exports = calculate_xy_dimensions; 50 | -------------------------------------------------------------------------------- /src/js/components/shared/Chart.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var PropTypes = React.PropTypes; 3 | var map = require("lodash/map"); 4 | var assign = require("lodash/assign"); 5 | 6 | // Wrapper class for charts. Clone all children assigning them the properties of 7 | // this component so that chart configuration is passed down 8 | var Chart = React.createClass({ 9 | 10 | propTypes: { 11 | xScale: PropTypes.func, 12 | yScale: PropTypes.func, 13 | dimensions: PropTypes.object, 14 | chartType: PropTypes.string, 15 | metadata: PropTypes.object, 16 | translate: PropTypes.array 17 | }, 18 | 19 | getDefaultProps: function() { 20 | return { 21 | translate: [0, 0], 22 | tickTextHeight: 0 23 | } 24 | }, 25 | 26 | render: function() { 27 | var props = this.props; 28 | var children = React.Children.toArray(props.children); 29 | var childrenWithProps = map(children, function(child) { 30 | var childProps = assign({}, props, child.props); 31 | return React.cloneElement(child, childProps); 32 | }); 33 | 34 | return ( 35 | 39 | {childrenWithProps} 40 | 41 | ); 42 | } 43 | 44 | }); 45 | 46 | module.exports = Chart; 47 | -------------------------------------------------------------------------------- /src/js/components/mixins/ChartEditorMixin.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var update = require("react-addons-update"); 3 | 4 | // Flux actions 5 | var ChartViewActions = require("../../actions/ChartViewActions"); 6 | 7 | /** 8 | * ### Functions common to chart editors 9 | * @instance 10 | * @memberof editors 11 | */ 12 | var ChartEditorMixin = { 13 | 14 | /** 15 | * _handlePropUpdate 16 | * Initiate a flux action that updates a prop, that doesn't require reparsing 17 | * @param k - `chartProp` key 18 | * @param v - `chartProp` value 19 | */ 20 | _handlePropUpdate: function(k, v) { 21 | ChartViewActions.updateChartProp(k, v); 22 | }, 23 | 24 | /** 25 | * _handlePropAndReparse 26 | * Initiate a flux action that updates a prop and then triggers a reparse 27 | * @param k - `chartProp` key 28 | * @param v - `chartProp` value 29 | */ 30 | _handlePropAndReparse: function(k, v) { 31 | ChartViewActions.updateAndReparse(k, v); 32 | }, 33 | 34 | /** 35 | * _handleStateUpdate 36 | * Update a key in the editor component's state 37 | * @param k - `this.state` key 38 | * @param v - `this.state` value 39 | */ 40 | _handleStateUpdate: function(k, v) { 41 | var newValue = {}; 42 | newValue[k] = v; 43 | this.setState(update(this.state, { $merge: newValue })); 44 | }, 45 | 46 | }; 47 | 48 | module.exports = ChartEditorMixin; 49 | -------------------------------------------------------------------------------- /src/js/dispatcher/dispatcher.js: -------------------------------------------------------------------------------- 1 | var Dispatcher = require("flux").Dispatcher; 2 | var assign = require("lodash/assign"); 3 | 4 | /** 5 | * Flux dispatcher handles incoming payloads and sends them to flux stores. 6 | * Usually data come from the UI, but can also come from localStorage or a 7 | * server 8 | * @class 9 | * @name ChartbuilderDispatcher 10 | */ 11 | var ChartbuilderDispatcher = assign(new Dispatcher(), { 12 | 13 | /** 14 | * Incoming server action. Normally a localStorage object 15 | * See `./actions/ChartServerActions.js` 16 | * @param {Object} action { eventName: "string", : } 17 | * @instance 18 | * @memberof ChartbuilderDispatcher 19 | */ 20 | handleServerAction: function(action) { 21 | var payload = { 22 | source: "server-action", 23 | action: action 24 | }; 25 | ChartbuilderDispatcher.dispatch(payload); 26 | }, 27 | 28 | /** 29 | * Incoming view action. Normally comes from a React component. 30 | * See `./actions/ChartPropertiesActions.js` 31 | * @param {Object} action { eventName: "string", : } 32 | * @instance 33 | * @memberof ChartbuilderDispatcher 34 | */ 35 | handleViewAction: function(action) { 36 | var payload = { 37 | source: "view-action", 38 | action: action 39 | }; 40 | ChartbuilderDispatcher.dispatch(payload); 41 | } 42 | 43 | }); 44 | 45 | module.exports = ChartbuilderDispatcher; 46 | -------------------------------------------------------------------------------- /src/js/charts/cb-chart-grid/chart-grid-dimensions.js: -------------------------------------------------------------------------------- 1 | var chartSizes = require("../../config/chart-sizes"); 2 | 3 | /** 4 | * see [ChartConfig#calculateDimensions](#chartconfig/calculatedimensions) 5 | * @see ChartConfig#calculateDimensions 6 | * @instance 7 | * @memberof chart_grid_config 8 | */ 9 | function chartGridDimensions(width, opts) { 10 | var height; 11 | var metadata = opts.metadata; 12 | var grid = opts.grid; 13 | 14 | if (metadata.size == "auto" || opts.enableResponsive) { 15 | // use current width 16 | } else { 17 | width = chartSizes[metadata.size].width; 18 | } 19 | 20 | if (grid.type == "bar") { 21 | var numDataPoints = opts.data[0].values.length; 22 | height = calculate_bar_height(numDataPoints, grid, opts.displayConfig); 23 | } else { 24 | height = calculate_cartesian_height(width, grid, opts.displayConfig); 25 | } 26 | 27 | if (!opts.showMetadata) { 28 | height -= opts.displayConfig.padding.bottom; 29 | } 30 | 31 | return { 32 | width: width, 33 | height: height 34 | }; 35 | } 36 | 37 | function calculate_bar_height(numDataPoints, grid, displayConfig) { 38 | return displayConfig.barHeight * numDataPoints * grid.rows; 39 | } 40 | 41 | function calculate_cartesian_height(width, grid, displayConfig, extraHeight) { 42 | var height = ( 43 | grid.rows * 44 | ((width / grid.cols) * 45 | displayConfig.xy.aspectRatio.wide) 46 | ); 47 | return height; 48 | } 49 | 50 | module.exports = chartGridDimensions; 51 | -------------------------------------------------------------------------------- /src/js/components/shared/DataSeriesTypeSettings.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var PropTypes = React.PropTypes; 3 | var chartbuilderUI = require("chartbuilder-ui"); 4 | var ButtonGroup = chartbuilderUI.ButtonGroup; 5 | 6 | var dataTypeOptions = [ 7 | { title: "Dates", content: "Dates", value: "date" }, 8 | { title: "Names", content: "Names", value: "ordinal" }, 9 | { title: "Numbers", content: "Numbers", value: "numeric" } 10 | ]; 11 | 12 | 13 | var DataSeriesTypeSettings = React.createClass({ 14 | propTypes: { 15 | onUpdate: PropTypes.func, 16 | chartProps: PropTypes.object 17 | }, 18 | 19 | _handleSeriesTypeUpdate: function(ix,k,v) { 20 | var chartProps = this.props.chartProps; 21 | chartProps.input.type = v; 22 | this.props.onUpdate(chartProps.input); 23 | }, 24 | 25 | render: function() { 26 | var chartProps = this.props.chartProps; 27 | return ( 28 |
29 |

Your first column is

30 | 42 |
43 | ) 44 | } 45 | }); 46 | 47 | module.exports = DataSeriesTypeSettings; -------------------------------------------------------------------------------- /src/js/components/mixins/ChartRendererMixin.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var update = require("react-addons-update"); 3 | var assign = require("lodash/assign"); 4 | var map = require("lodash/map"); 5 | 6 | /** 7 | * ### Functions common to chart renderers 8 | * @instance 9 | * @memberof renderers 10 | */ 11 | var ChartRendererMixin = { 12 | 13 | /** 14 | * _applySettingsToData 15 | * Our d4 chart renderers expect any additional settings to be in the data 16 | * that is passed to it, so we merge them in (from a separate 17 | * `chartSettings` object). An optional `additional` parameter adds an 18 | * arbitray object to this 19 | * @param _chartProps - Current data and series settings 20 | * @param additional - Optional additional object to apply 21 | * @return {object} - Data with settings applied 22 | */ 23 | _applySettingsToData: function(_chartProps, additional) { 24 | return map(_chartProps.data, function(d, i) { 25 | var series = {}; 26 | series.key = d.name; 27 | if (additional) { 28 | series = assign(series, additional); 29 | } 30 | return assign(series, d, _chartProps.chartSettings[i]); 31 | }); 32 | }, 33 | 34 | /** 35 | * _handleStateUpdate 36 | * Update a key in the renderer component's state 37 | * @param k - `this.state` key 38 | * @param v - `this.state` value 39 | */ 40 | _handleStateUpdate: function(k, v) { 41 | var newValue = {}; 42 | newValue[k] = v; 43 | this.setState(update(this.state, { $merge: newValue })); 44 | } 45 | 46 | }; 47 | 48 | module.exports = ChartRendererMixin; 49 | -------------------------------------------------------------------------------- /src/styl/colors.styl: -------------------------------------------------------------------------------- 1 | // Colors 2 | 3 | $color-bg = #fff 4 | $color-body-text = #666 5 | $color-chart-gridline = #ddd 6 | $color-chart-meta = #CCCCCC 7 | $color-chart-axis-text = #666 8 | $color-chart-title-text = #333 9 | $color-chart-zeroline = #333 10 | 11 | $color-editor-border = #e2e2e2 12 | $color-editor-fill = #F1F1F1 13 | $color-editor-disabled = #BEBEBE 14 | $color-editor-text = #333333 15 | $color-editor-placeholder = #CACACA 16 | 17 | $chart-colors = \ 18 | #a50026 \ 19 | #fdae61 \ 20 | #d73027 \ 21 | #abd9e9 \ 22 | #f46d43 \ 23 | #74add1 \ 24 | #4575b4 \ 25 | #313695 \ 26 | #999 \ 27 | #666 \ 28 | #ccc 29 | 30 | for c, i in $chart-colors 31 | [data-color-index=\"{i}\"] 32 | path 33 | stroke (c) 34 | rect 35 | fill (c) 36 | circle 37 | stroke (c) 38 | stroke-width 2px 39 | fill (c) 40 | 41 | .color-index-{i} 42 | fill (c) 43 | 44 | rect.color-index-{i} 45 | fill (c) 46 | 47 | path.color-index-{i} 48 | stroke (c) 49 | 50 | circle.color-index-{i} 51 | fill (c) 52 | 53 | rect[data-color-index=\"{i}\"] 54 | fill (c) 55 | 56 | text[data-color-index=\"{i}\"] 57 | fill (c) 58 | 59 | g.axis.color-index-{i} 60 | text 61 | fill (c) 62 | 63 | .cb-colorpicker-color-{i} 64 | background-color (c) 65 | 66 | .series-label-{i} 67 | color (c) 68 | 69 | .series-label-input-{i} 70 | border-bottom 2px solid (c) 71 | &:hover, &:focus 72 | border none 73 | border-bottom 2px solid (c) 74 | input 75 | color (c) 76 | 77 | .cb-toggle.toggled.toggle-{i} 78 | .cb-toggle-container 79 | background-color (c) 80 | 81 | -------------------------------------------------------------------------------- /docs/deploying.md: -------------------------------------------------------------------------------- 1 | ### To Github pages 2 | 3 | If your Chartbuilder is on a Github repo, you can deploy it to github pages 4 | using the command: 5 | 6 | npm run gh-pages 7 | 8 | The resulting page will also contain the Chartbuilder API docs at `/api-docs`, 9 | with Chartbuilder at the root `index.html`. 10 | 11 | ### Deploying your Chartbuilder 12 | 13 | Once you're done [customizing](02-customizing-chartbuilder.md), you'll want to 14 | build the source files so that you can upload them to your hosted location. 15 | 16 | Deployment is easy! Just do: 17 | 18 | npm run build 19 | 20 | This will use [gulp](http://gulpjs.com/) to concatenate and minify your 21 | javascript. (The javascript you get from `npm run dev` is not minified, if you 22 | need to check what that looks like.) The build task will also convert all of the 23 | fonts defined in your CSS base64, which makes image export more robust. 24 | 25 | Once that completes, the finished product will live in the `build` folder inside 26 | of your project directory. You can just move the contents of `build` to a server 27 | using FTP or however else. Or you might create a simple auto-deploy script like so: 28 | 29 | #!/bin/sh 30 | echo "BUILDING CHARTBUILDER..." 31 | npm run build 32 | echo "SYNCING BUILD WITH REMOTE FILES..." 33 | rsync -rav --progress build/* 34 | 35 | #### AWS 36 | 37 | Chartbuilder can easily be automatically deployed to Amazon Web Services. Just 38 | rename the file at `gulp/aws-config.json.example` to `gulp/aws-config.json`, and 39 | add your credentials. Then you can run the `npm run aws` process to deploy. 40 | -------------------------------------------------------------------------------- /src/js/components/svg/ChartFooter.jsx: -------------------------------------------------------------------------------- 1 | // Footer of the chart, which contains the credit and source text. 2 | 3 | var React = require("react"); 4 | var ReactDom = require("react-dom") 5 | var PropTypes = React.PropTypes; 6 | var SvgText = require("./SvgText.jsx"); 7 | var update = require("react-addons-update"); 8 | 9 | /** 10 | * Render a footer with the chart credit and source 11 | * @instance 12 | * @memberof RendererWrapper 13 | */ 14 | var ChartFooter = React.createClass({ 15 | 16 | propTypes: { 17 | metadata: PropTypes.object, 18 | translate: PropTypes.object, 19 | chartWidth: PropTypes.number 20 | }, 21 | 22 | _createSourceLine: function() { 23 | var sourceLine; 24 | if (this.props.metadata.source && this.props.metadata.source !== "") { 25 | sourceLine = "Data: " + this.props.metadata.source; 26 | } else { 27 | sourceLine = ""; 28 | } 29 | 30 | if (this.props.metadata.notes && this.props.metadata.notes !== "") { 31 | sourceLine = [sourceLine, this.props.metadata.notes].join(" | "); 32 | } 33 | 34 | return sourceLine; 35 | }, 36 | 37 | render: function() { 38 | var sourceLineText = this._createSourceLine(); 39 | return ( 40 | 41 | 46 | 51 | 52 | ); 53 | } 54 | 55 | }); 56 | 57 | module.exports = ChartFooter; 58 | -------------------------------------------------------------------------------- /src/js/util/parse-data-by-series.js: -------------------------------------------------------------------------------- 1 | // Separate a flat array of `{ column: value }` objects into a multi-array, one 2 | // for each column. Each object contains a `values` array, with each entry 3 | // looking like: 4 | // ``` 5 | // { entry: , value: } 6 | // ``` 7 | 8 | var datePattern = /date|time|year/i; 9 | var parseDelimInput = require("./parse-delimited-input").parser; 10 | 11 | // Parse data by series. Options: 12 | // checkForDate: bool | tell parser to return dates if key column is date/time/year 13 | function dataBySeries(input, opts) { 14 | var series; 15 | opts = opts || {}; 16 | 17 | var parsedInput = parseDelimInput(input, { 18 | checkForDate: opts.checkForDate, 19 | type: opts.type 20 | }); 21 | 22 | var columnNames = parsedInput.columnNames; 23 | var keyColumn = columnNames.shift(); 24 | 25 | if (columnNames.length === 0) { 26 | series = [{ 27 | name: keyColumn, 28 | values: parsedInput.data.map(function(d) { 29 | return { 30 | name: keyColumn, 31 | value: d[keyColumn] 32 | }; 33 | }) 34 | }]; 35 | } else { 36 | series = columnNames.map(function(header, i) { 37 | return { 38 | name: header, 39 | values: parsedInput.data.map(function(d) { 40 | return { 41 | name: header, 42 | entry: d[keyColumn], 43 | value: d[header] 44 | }; 45 | }) 46 | }; 47 | }); 48 | } 49 | 50 | return { 51 | series: series, 52 | input: { raw: input, type: opts.type }, 53 | hasDate: parsedInput.hasDate && (!opts.type || opts.type == "date"), 54 | isNumeric: parsedInput.isNumeric && (!opts.type || opts.type == "numeric") 55 | }; 56 | } 57 | 58 | module.exports = dataBySeries; 59 | -------------------------------------------------------------------------------- /src/js/components/LocalStorageTimer.jsx: -------------------------------------------------------------------------------- 1 | var SessionStore = require("../stores/SessionStore"); 2 | var ChartViewActions = require("../actions/ChartViewActions"); 3 | var ChartbuilderLocalStorageAPI = require("../util/ChartbuilderLocalStorageAPI"); 4 | 5 | /* Node modules */ 6 | var React = require("react"); 7 | var cx = require("classnames"); 8 | var PropTypes = React.PropTypes; 9 | 10 | /* Chartbuilder UI components */ 11 | var chartbuilderUI = require("chartbuilder-ui"); 12 | var Button = chartbuilderUI.Button; 13 | var timer; 14 | var TIMER_DURATION = 30000; 15 | 16 | /** 17 | * Button that persists for `TIMER_DURATION` and allows user to re-load the 18 | * chart currently saved in `localStorage`. On click, it updates the 19 | * `SessionStore`. 20 | */ 21 | var LocalStorageTimer = React.createClass({ 22 | propTypes: { 23 | timerOn: PropTypes.bool.isRequired 24 | }, 25 | 26 | _disableTimer: function() { 27 | clearTimeout(timer); 28 | ChartViewActions.stopTimer(); 29 | }, 30 | 31 | _handleLoadChart: function() { 32 | ChartbuilderLocalStorageAPI.getChart(); 33 | this._disableTimer(); 34 | }, 35 | 36 | componentWillMount: function() { 37 | if (this.props.timerOn) { 38 | timer = setTimeout(function() { 39 | this._disableTimer(); 40 | }.bind(this), TIMER_DURATION); 41 | ChartViewActions.startTimer(); 42 | } 43 | }, 44 | 45 | render: function() { 46 | var className = cx({ 47 | "load-localstorage": true, 48 | "active": this.props.timerOn 49 | }); 50 | 51 | return ( 52 |