--force
80 | ```
81 |
82 | #### 3. Submit your pull request!
83 |
84 | *Why a single commit? Many people out there have slightly modified forks/versions of Chartbuilder.
85 | That means that new features can cause ugly merge conflicts since some parts of
86 | the code are meant to differ across forks. The best way for new features
87 | to work well with the [recommended workflow](docs/git-workflow-forks.md) for Chartbuilder
88 | forks is to create a single commit that can be pulled in with `git cherry-pick`.
89 |
--------------------------------------------------------------------------------
/src/js/stores/ErrorStore.js:
--------------------------------------------------------------------------------
1 | var assign = require("lodash/assign");
2 | var clone = require("lodash/clone");
3 | var some = require("lodash/some");
4 | var filter = require("lodash/filter");
5 | var EventEmitter = require("events").EventEmitter;
6 |
7 | /* Flux dispatcher */
8 | var Dispatcher = require("../dispatcher/dispatcher");
9 |
10 | var errorNames = require("../util/error-names");
11 | var validateDataInput = require("../util/validate-data-input");
12 | var ChartPropertiesStore = require("./ChartPropertiesStore");
13 |
14 | /* Singleton that houses errors */
15 | var _errors = { valid: true, messages: [] };
16 | var CHANGE_EVENT = "change";
17 |
18 | /**
19 | * ### ErrorStore.js
20 | * Store for errors/warnings to users about the bad/dumb things they are
21 | * probably doing
22 | */
23 | var ErrorStore = assign({}, EventEmitter.prototype, {
24 |
25 | emitChange: function() {
26 | this.emit(CHANGE_EVENT);
27 | },
28 |
29 | addChangeListener: function(callback) {
30 | this.on(CHANGE_EVENT, callback);
31 | },
32 |
33 | removeChangeListener: function(callback) {
34 | this.removeListener(CHANGE_EVENT, callback);
35 | },
36 |
37 | /**
38 | * get
39 | * @param k
40 | * @return {any} - Return value at key `k`
41 | * @instance
42 | * @memberof ErrorStore
43 | */
44 | get: function(k) {
45 | return _errors[k];
46 | },
47 |
48 | /**
49 | * getAll
50 | * @return {object} - Return all errors
51 | * @instance
52 | * @memberof ErrorStore
53 | */
54 | getAll: function() {
55 | return clone(_errors);
56 | },
57 |
58 | /**
59 | * clear
60 | * Set errors to empty
61 | * @instance
62 | * @memberof ErrorStore
63 | */
64 | clear: function() {
65 | _errors = {};
66 | }
67 |
68 | });
69 |
70 | /* Respond to actions coming from the dispatcher */
71 | function registeredCallback(payload) {
72 | var action = payload.action;
73 | var chartProps;
74 | var error_messages;
75 | var input_errors;
76 |
77 | switch(action.eventName) {
78 | /* *
79 | * Data input updated or reparse called
80 | * */
81 | case "update-data-input":
82 | case "update-and-reparse":
83 |
84 | Dispatcher.waitFor([ChartPropertiesStore.dispatchToken]);
85 | chartProps = ChartPropertiesStore.getAll();
86 |
87 | error_messages = [];
88 | input_errors = validateDataInput(chartProps);
89 | error_messages = error_messages.concat(input_errors);
90 |
91 | _errors.messages = error_messages.map(function(err_name) {
92 | return errorNames[err_name];
93 | });
94 |
95 | var isInvalid = some(_errors.messages, { type: "error" } );
96 | _errors.valid = !isInvalid;
97 |
98 | ErrorStore.emitChange();
99 | break;
100 |
101 | default:
102 | // do nothing
103 | }
104 |
105 | return true;
106 |
107 | }
108 |
109 | /* Respond to actions coming from the dispatcher */
110 | ErrorStore.dispatchToken = Dispatcher.register(registeredCallback);
111 | module.exports = ErrorStore;
112 |
--------------------------------------------------------------------------------
/src/js/components/chart-grid/ChartGridMobile.jsx:
--------------------------------------------------------------------------------
1 | var React = require("react");
2 | var PropTypes = React.PropTypes;
3 | var clone = require("lodash/clone");
4 | var update = require("react-addons-update");
5 |
6 | var ChartEditorMixin = require("../mixins/ChartEditorMixin");
7 | var XY_yScaleSettings = require("../shared/XY_yScaleSettings.jsx");
8 | var ChartGrid_xScaleSettings = require("./ChartGrid_xScaleSettings.jsx");
9 |
10 | // Flux stores
11 | //var ChartViewActions = require("../../actions/ChartViewActions");
12 |
13 | // Chartbuilder UI components
14 | var chartbuilderUI = require("chartbuilder-ui");
15 | var ButtonGroup = chartbuilderUI.ButtonGroup;
16 | var LabelledTangle = chartbuilderUI.LabelledTangle;
17 | var TextInput = chartbuilderUI.TextInput;
18 |
19 | var ChartGridMobile = React.createClass({
20 |
21 | mixins: [ ChartEditorMixin ],
22 |
23 | getInitialState: function() {
24 | return {
25 | scale: this.props.chartProps.mobile.scale || this.props.chartProps.scale
26 | };
27 | },
28 |
29 | _handleUpdate: function(k, v) {
30 | var newSetting = {};
31 | newSetting[k] = v;
32 | var newMobile = update(this.props.chartProps.mobile, { $merge: newSetting });
33 | this._handlePropAndReparse("mobile", newMobile);
34 | },
35 |
36 | _handleScaleUpdate: function(k, v) {
37 | var scale = clone(this.state.scale, true);
38 | scale.primaryScale[k] = v;
39 | var newMobile = update(this.props.chartProps.mobile, {
40 | $merge: { scale: scale }
41 | });
42 | this._handlePropAndReparse("mobile", newMobile);
43 | },
44 |
45 | _handleScaleReset: function() {
46 | var mobile = clone(this.props.chartProps.mobile);
47 | delete mobile.scale;
48 | this._handlePropAndReparse("mobile", mobile);
49 | },
50 |
51 | componentWillReceiveProps: function(nextProps) {
52 | var scale = nextProps.chartProps.mobile.scale || nextProps.chartProps.scale;
53 | this.setState({ scale: scale });
54 | },
55 |
56 | render: function() {
57 | var chartProps = this.props.chartProps;
58 |
59 | var scaleSettings = [];
60 | scaleSettings.push(
61 | ,
68 |
76 | );
77 |
78 | return (
79 |
80 |
81 | ✭
82 | Mobile settings
83 |
84 |
90 | {scaleSettings}
91 |
92 | );
93 | }
94 | });
95 |
96 | module.exports = ChartGridMobile;
97 |
--------------------------------------------------------------------------------
/src/js/components/chart-xy/XYMobile.jsx:
--------------------------------------------------------------------------------
1 | var React = require("react");
2 | var PropTypes = React.PropTypes;
3 | var update = require("react-addons-update");
4 | var clone = require("lodash/clone");
5 |
6 | var ChartEditorMixin = require("../mixins/ChartEditorMixin");
7 | var XY_yScaleSettings = require("../shared/XY_yScaleSettings.jsx");
8 |
9 | // Flux stores
10 | //var ChartViewActions = require("../../actions/ChartViewActions");
11 |
12 | // Chartbuilder UI components
13 | var chartbuilderUI = require("chartbuilder-ui");
14 | var ButtonGroup = chartbuilderUI.ButtonGroup;
15 | var LabelledTangle = chartbuilderUI.LabelledTangle;
16 | var TextInput = chartbuilderUI.TextInput;
17 |
18 | var XYMobile = React.createClass({
19 |
20 | mixins: [ChartEditorMixin],
21 |
22 | _handleUpdate: function(k, v) {
23 | var newSetting = {};
24 | newSetting[k] = v;
25 | var newMobile = update(this.props.chartProps.mobile, { $merge: newSetting });
26 | this._handlePropUpdate("mobile", newMobile);
27 | },
28 |
29 | _handleScaleUpdate: function(k, v) {
30 | var newSetting = {};
31 | newSetting[k] = v;
32 | var newMobile = update(this.props.chartProps.mobile, { $merge: newSetting });
33 | this._handlePropAndReparse("mobile", newMobile);
34 | },
35 |
36 | _handleScaleReset: function() {
37 | var mobile = clone(this.props.chartProps.mobile);
38 | delete mobile.scale;
39 | this._handlePropAndReparse("mobile", mobile);
40 | },
41 |
42 | render: function() {
43 | var chartProps = this.props.chartProps;
44 | var scaleSettings = [];
45 | var scale = chartProps.mobile.scale || chartProps.scale;
46 |
47 | /* Y scale settings */
48 | scaleSettings.push(
49 |
60 | );
61 |
62 | /* render a second y scale component if altAxis is specified */
63 | if (chartProps._numSecondaryAxis > 0) {
64 | scaleSettings.push(
65 |
76 | );
77 | }
78 |
79 | return (
80 |
81 |
82 | ✭
83 | Mobile settings
84 |
85 |
91 | {scaleSettings}
92 |
93 | );
94 | }
95 | });
96 |
97 | module.exports = XYMobile;
98 |
--------------------------------------------------------------------------------
/test/jsx/xy-renderer.jsx:
--------------------------------------------------------------------------------
1 | var test = require("tape");
2 |
3 | var React = require("react");
4 | var ReactDOM = require("react-dom");
5 |
6 | var d3 = require("d3");
7 | var _ = require("lodash");
8 | var TU = require("react-addons-test-utils");
9 | var util = require("../util/util");
10 |
11 | var RendererWrapper = require("../../src/js/components/RendererWrapper.jsx");
12 | var lineDotsThresholdSingle = 10;
13 | var lineDotsThresholdTotal = 30;
14 |
15 | var test_charts = require("../test-page/test_charts.json");
16 | var xy_charts = _.filter(test_charts, function(chart) {
17 | return chart.metadata.chartType === "xy";
18 | });
19 | var randXY = util.randArrElement(xy_charts);
20 |
21 | test("Renderer: XY chart", function(t) {
22 | var rw = TU.renderIntoDocument(
23 |
30 | );
31 | t.plan(5);
32 |
33 | var svg = TU.findRenderedDOMComponentWithTag(
34 | rw,
35 | "svg"
36 | );
37 |
38 | t.ok(TU.isDOMComponent(svg), "svg rendered to DOM");
39 |
40 | var svg_dom = ReactDOM.findDOMNode(svg);
41 | var d3svg = d3.select(svg_dom);
42 |
43 | t.equal(d3svg.select(".chartArea").attr("class"), "chartArea", "chartArea rendered to DOM");
44 |
45 | var xy_types = _.map(randXY.chartProps.chartSettings, function(d) {
46 | return d.type;
47 | });
48 |
49 | var type_counts = {
50 | column: 0,
51 | line: 0,
52 | scatterPlot: 0
53 | };
54 |
55 | _.each(xy_types, function(type) {
56 | type_counts[type] = type_counts[type] + 1;
57 | });
58 |
59 | var num_vals = randXY.chartProps.data[0].values.length;
60 |
61 | var cols = d3svg.select("g.bars");
62 | var lines = d3svg.select("g.lines");
63 | var dots = d3svg.select("g.circles");
64 |
65 | var num_cols = cols.selectAll("g")[0].length;
66 | var num_lines = lines.selectAll("g")[0].length;
67 | var num_dots = dots.selectAll("g.value")[0].length;
68 | var expected_cols = (type_counts.column === 0) ? 0 : num_vals;
69 |
70 | var singleLineDotThresh = false;
71 |
72 | // Expected number of dots increases if we are within set limits
73 | var totalLinePoints = _.map(randXY.chartProps.data, function(d) {
74 | if (d.values.length < lineDotsThresholdSingle) {
75 | singleLineDotThresh = true;
76 | }
77 | return d.values;
78 | }).reduce(function(a, b) {
79 | return a.concat(b);
80 | }).length;
81 |
82 | var totalLineDotThresh = (totalLinePoints < lineDotsThresholdTotal);
83 | var renderLineDots = (singleLineDotThresh && totalLineDotThresh);
84 | if (renderLineDots && type_counts.line > 0) {
85 | type_counts.scatterPlot = type_counts.scatterPlot + type_counts.line;
86 | }
87 |
88 | t.equal(num_cols, expected_cols, "number of rendered column groups matches data");
89 | t.equal(num_lines, type_counts.line, "number of rendered line groups matches data");
90 | t.equal(num_dots, type_counts.scatterPlot, "number of rendered dot groups matches data");
91 |
92 | // Remove test RendererWrapper
93 | ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(rw).parentNode);
94 | t.end();
95 | });
96 |
97 |
--------------------------------------------------------------------------------
/src/js/components/ChartTypeSelector.jsx:
--------------------------------------------------------------------------------
1 | // Select one of an array of chart types.
2 | // Delete chartProps that are "private" to certain chart types (namespaced with `_`)
3 | // and apply setting that carry over to the new type
4 |
5 | var React = require("react");
6 | var clone = require("lodash/clone");
7 | var map = require("lodash/map");
8 | var keys = require("lodash/keys");
9 | var helper = require("../util/helper");
10 |
11 | // Flux actions
12 | var ChartServerActions = require("../actions/ChartServerActions");
13 |
14 | // Chartbuilder UI components
15 | var chartbuilderUI = require("chartbuilder-ui");
16 | var ButtonGroup = chartbuilderUI.ButtonGroup;
17 |
18 | var chartConfig = require("../charts/chart-type-configs");
19 |
20 | /**
21 | * Select a new chart type, copying shared settings over to the new type.
22 | * @instance
23 | * @memberof editors
24 | */
25 | var ChartTypeSelctor = React.createClass({
26 |
27 | /* Generate values for each chart type that can be used to create buttons */
28 | getInitialState: function() {
29 | var chartTypeButtons = map(keys(chartConfig), function(chartTypeKey) {
30 | return {
31 | title: chartConfig[chartTypeKey].displayName,
32 | content: chartConfig[chartTypeKey].displayName,
33 | value: chartTypeKey
34 | };
35 | });
36 | return { chartConfig: chartTypeButtons };
37 | },
38 |
39 | /*
40 | * Change the chart type
41 | * @param {string} chartType - the new chart type
42 | */
43 | _handleChartTypeChange: function(chartType) {
44 | /* Dont rerender if the chart type is the same */
45 | if (chartType === this.props.metadata.chartType) {
46 | return;
47 | }
48 | var metadata = clone(this.props.metadata);
49 | /* Set the new chart type in metadata */
50 | metadata.chartType = chartType;
51 |
52 | var prevProps = this.props.chartProps;
53 | var newDefaultProps = chartConfig[chartType].defaultProps.chartProps;
54 | var prevSettings = prevProps.chartSettings;
55 | var newDefaultSettings = newDefaultProps.chartSettings[0];
56 | var prevKeys = keys(prevSettings[0]);
57 |
58 | /* Apply any settings that carry over, otherwise ignore them */
59 | var newProps = helper.mergeOrApply(newDefaultProps, prevProps);
60 |
61 | /*
62 | * For each data series, check whether a `chartSetting` has already been
63 | * defined by another chart type. If so, apply it. If not, use the new
64 | * type's default
65 | */
66 | newProps.chartSettings = map(prevProps.data, function(d, i) {
67 | return helper.mergeOrApply(newDefaultSettings, prevSettings[i]);
68 | });
69 |
70 | /* Dispatch the new model to the flux stores */
71 | ChartServerActions.receiveModel({
72 | chartProps: newProps,
73 | metadata: metadata
74 | });
75 | },
76 |
77 | render: function() {
78 | return (
79 |
80 |
81 | 1
82 | Select chart type
83 |
84 |
90 |
91 | );
92 | }
93 |
94 | });
95 |
96 | module.exports = ChartTypeSelctor;
97 |
--------------------------------------------------------------------------------
/test/validate-input.js:
--------------------------------------------------------------------------------
1 | var test = require("tape");
2 | var _ = require("lodash");
3 |
4 | var testInput = require("./util/test-input");
5 | var validateDataInput = require("../src/js/util/validate-data-input");
6 | var parseDataBySeries = require("../src/js/util/parse-data-by-series");
7 | var primaryScale = {
8 | tickValues: [0,20,40,60,80,100],
9 | domain: [0, 100]
10 | };
11 |
12 | test("validate data input", function(t) {
13 | t.plan(8);
14 |
15 | var validateResult;
16 | var parsed;
17 |
18 | parsed = parseDataBySeries(testInput.init_data_ordinal, { checkForDate: true} );
19 | validateResult = validateDataInput({
20 | input: { raw: testInput.init_data_ordinal },
21 | data: parsed.series,
22 | scale: {
23 | hasDate: parsed.hasDate,
24 | primaryScale: primaryScale
25 | }
26 | });
27 | t.deepEqual(validateResult, [], "valid input returns empty array");
28 |
29 | validateResult = validateDataInput({
30 | input: { raw: "" },
31 | data: "",
32 | scale: {
33 | hasDate: false,
34 | primaryScale: primaryScale
35 | }
36 | });
37 | t.deepEqual(validateResult, ["EMPTY"], "empty input returns EMPTY");
38 |
39 | parsed = parseDataBySeries(testInput.uneven_series, { checkForDate: true} );
40 | validateResult = validateDataInput({
41 | input: { raw: testInput.uneven_series },
42 | data: parsed.series,
43 | scale: {
44 | hasDate: parsed.hasDate,
45 | primaryScale: primaryScale
46 | }
47 | });
48 | t.deepEqual(validateResult, ["UNEVEN_SERIES"], "uneven series return UNEVEN_SERIES");
49 |
50 | parsed = parseDataBySeries(testInput.too_many_series, { checkForDate: true} );
51 | validateResult = validateDataInput({
52 | input: { raw: testInput.too_many_series },
53 | data: parsed.series,
54 | scale: {
55 | hasDate: parsed.hasDate,
56 | primaryScale: primaryScale
57 | }
58 | });
59 | t.deepEqual(validateResult, ["TOO_MANY_SERIES"], "12+ columns returns TOO_MANY_SERIES");
60 |
61 | parsed = parseDataBySeries(testInput.too_few_series, { checkForDate: true} );
62 | validateResult = validateDataInput({
63 | input: { raw: testInput.too_few_series },
64 | data: parsed.series,
65 | scale: {
66 | hasDate: parsed.hasDate,
67 | primaryScale: primaryScale
68 | }
69 | });
70 | t.deepEqual(validateResult, ["TOO_FEW_SERIES"], "one column returns TOO_FEW_SERIES");
71 |
72 | parsed = parseDataBySeries(testInput.nan_values, { checkForDate: true} );
73 | validateResult = validateDataInput({
74 | input: { raw: testInput.nan_values },
75 | data: parsed.series,
76 | scale: {
77 | hasDate: parsed.hasDate,
78 | primaryScale: primaryScale
79 | }
80 | });
81 | t.deepEqual(validateResult, ["NAN_VALUES"], "column with NaN value returns NAN_VALUES");
82 |
83 | parsed = parseDataBySeries(testInput.not_dates, { checkForDate: true} );
84 | validateResult = validateDataInput({
85 | input: { raw: testInput.not_dates, type: "date" },
86 | data: parsed.series,
87 | scale: {
88 | hasDate: parsed.hasDate,
89 | primaryScale: primaryScale
90 | }
91 | });
92 | t.deepEqual(validateResult, ["NOT_DATES"], "date column with non-date returns NOT_DATES");
93 |
94 | parsed = parseDataBySeries(testInput.multiple_errors, { checkForDate: true} );
95 | validateResult = validateDataInput({
96 | input: { raw: testInput.multiple_errors },
97 | data: parsed.series,
98 | scale: {
99 | hasDate: parsed.hasDate,
100 | primaryScale: primaryScale
101 | }
102 | });
103 | t.deepEqual(validateResult, ["UNEVEN_SERIES", "NAN_VALUES", "CANT_AUTO_TYPE"], "input with several errors returns them all");
104 |
105 | t.end();
106 | });
107 |
--------------------------------------------------------------------------------
/src/styl/chart-renderer.styl:
--------------------------------------------------------------------------------
1 | $em_size = 20px
2 | $em_size_small = 14px
3 | $em_size_large = 36px
4 |
5 | // OUTER DIV
6 | div.renderer-wrapper
7 | font-smoothing antialiased
8 | width 100%
9 | margin 0
10 | padding 0
11 |
12 | // SVG-WIDE
13 | svg
14 | &.renderer-svg
15 | fill $color-bg
16 | text-rendering optimizeLegibility
17 | box-sizing border-box
18 | tspan
19 | height 1em
20 | line-height 1em
21 | .svg-background
22 | fill $color-bg
23 |
24 | // EM SIZE USED TO CONVERT JSON CONFIG TO
25 | // PROPORTIONAL SIZES BASED ON VIEWPORT
26 | .em-size-wrapper
27 | width 100%
28 | .em-size
29 | font-size $em_size
30 | position absolute
31 | visibility hidden
32 | top 0
33 | line-height 1em
34 | margin 0 0 0 -1000px
35 | padding 0
36 | width 1em
37 | height 1em
38 |
39 | .small .em-size
40 | font-size $em_size_small
41 |
42 | // SVG TEXT
43 | .svg-text
44 | font-size $em_size
45 |
46 | tspan
47 | &.em
48 | font-style italic
49 |
50 | &.strong
51 | font-family $font-sans-bold
52 |
53 | &.strong .em, &.em .strong
54 | font-family $font-sans-bold
55 | font-style italic
56 |
57 | &.svg-text-credit
58 | fill $color-chart-meta
59 | text-anchor start
60 | &.svg-text-source
61 | fill $color-chart-meta
62 | &.left
63 | text-anchor start
64 | &.right
65 | text-anchor end
66 | &.svg-text-title
67 | fill $color-body-text
68 |
69 | .medium .svg-text
70 | font-size $em_size
71 | &.svg-text-credit
72 | font-size $em_size*0.6
73 | &.svg-text-source
74 | font-size $em_size*0.6
75 | &.svg-text-title
76 | font-size $em_size
77 |
78 | .small .svg-text
79 | font-size $em_size_small
80 | &.svg-text-credit
81 | font-size $em_size*0.5
82 | &.svg-text-source
83 | font-size $em_size*0.5
84 | &.svg-text-pipe
85 | text
86 | dominant-baseline central
87 | font-size $em_size*0.6
88 | &.svg-text-title
89 | font-size $em_size_small
90 |
91 | // LABEL TEXT
92 | .svg-label-text
93 | &.draggable
94 | cursor move
95 |
96 | .small .svg-label-text
97 | font-size $em_size_small
98 |
99 | // THIS IS RENDERED FOR MEASUREMENTS BUT NOT SHOWN
100 | .hidden-svg
101 | visibility hidden
102 |
103 | // LINE SERIES
104 | .series-line-path
105 | fill none
106 | stroke-linecap round
107 | stroke-linejoin round
108 |
109 | .large .series-line-path
110 | stroke-width 4px
111 |
112 | .medium .series-line-path
113 | stroke-width 3px
114 |
115 | .small .series-line-path
116 | stroke-width 2px
117 |
118 | // AXIS
119 | .axis
120 | text.tick
121 | fill $color-chart-axis-text
122 | shape-rendering crispEdges
123 | &[data-anchor="middle"]
124 | text
125 | text-anchor middle
126 | &.orient-left
127 | text-anchor start
128 | &.orient-right
129 | text-anchor end
130 | &.hidden-svg
131 | visibility hidden
132 |
133 | // GRID LINES
134 | .grid-lines
135 | line.tick
136 | stroke $color-chart-gridline
137 | stroke-width 1px
138 | shape-rendering crispEdges
139 | line.zero
140 | stroke $color-chart-zeroline
141 | shape-rendering crispEdges
142 |
143 | .concealer-label
144 | text
145 | fill $color-chart-axis-text
146 | rect
147 | fill $color-bg
148 |
149 | // CHART GRID
150 | .blocker-rect
151 | fill $color-bg
152 |
153 | .series-label
154 | font-size $em_size
155 |
156 | .small .series-label
157 | font-size $em_size_small
158 |
159 | // MISC ELEMENTS
160 | .svg-source-pipe
161 | stroke $color-chart-meta
162 | fill none
163 | stroke-width 0.8px
164 |
165 | .crosshair
166 | line
167 | stroke-width 1
168 | shape-rendering crispEdges
169 | stroke black
170 |
171 | .cb-credit-logo
172 | fill $color-chart-meta
173 |
--------------------------------------------------------------------------------
/src/js/stores/ChartMetadataStore.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Store the Chart's metadata. These are properties used to describe the chart
3 | * and are unrelated to the actual rendering of the chart.
4 | */
5 | var assign = require("lodash/assign");
6 | var clone = require("lodash/clone");
7 | var EventEmitter = require("events").EventEmitter;
8 |
9 | /* Flux dispatcher */
10 | var Dispatcher = require("../dispatcher/dispatcher");
11 |
12 | /* Require the `ChartProptiesStore so that we can wait for it to update */
13 | var ChartPropertiesStore = require("./ChartPropertiesStore");
14 |
15 | /* Singleton that houses metadata */
16 | var _metadata = {};
17 | var titleDirty = false;
18 | var CHANGE_EVENT = "change";
19 |
20 | /**
21 | * ### ChartMetadataStore.js
22 | * Flux store for chart metadata such as title, source, size, etc.
23 | */
24 | var ChartMetadataStore = assign({}, EventEmitter.prototype, {
25 |
26 | emitChange: function() {
27 | this.emit(CHANGE_EVENT);
28 | },
29 |
30 | addChangeListener: function(callback) {
31 | this.on(CHANGE_EVENT, callback);
32 | },
33 |
34 | removeChangeListener: function(callback) {
35 | this.removeListener(CHANGE_EVENT, callback);
36 | },
37 |
38 | /**
39 | * get
40 | * @param k
41 | * @return {any} - Return value at key `k`
42 | * @instance
43 | * @memberof ChartMetadataStore
44 | */
45 | get: function(k) {
46 | return _metadata[k];
47 | },
48 |
49 | /**
50 | * getAll
51 | * @return {object} - Return all metadata
52 | * @instance
53 | * @memberof ChartMetadataStore
54 | */
55 | getAll: function() {
56 | return clone(_metadata);
57 | },
58 |
59 | /**
60 | * clear
61 | * Set metadata to empty
62 | * @instance
63 | * @memberof ChartMetadataStore
64 | */
65 | clear: function() {
66 | _metadata = {};
67 | }
68 |
69 | });
70 |
71 | /* Respond to actions coming from the dispatcher */
72 | function registeredCallback(payload) {
73 | var action = payload.action;
74 | var data;
75 |
76 | switch(action.eventName) {
77 | /*
78 | * New chart model is received. Respond by first waiting for
79 | * `ChartProptiesStore`
80 | */
81 | case "receive-model":
82 | Dispatcher.waitFor([ChartPropertiesStore.dispatchToken]);
83 | _metadata = action.model.metadata;
84 | data = ChartPropertiesStore.get("data");
85 | _metadata.title = defaultTitle(data);
86 | ChartMetadataStore.emitChange();
87 | break;
88 |
89 | /* Metadata alone is being updated */
90 | case "update-metadata":
91 | _metadata[action.key] = action.value;
92 | // if title is edited, set dirty to true and dont generate default anymore
93 | // TODO: we don't need to do this every time
94 | if (action.key == "title") {
95 | titleDirty = true;
96 | }
97 | ChartMetadataStore.emitChange();
98 | break;
99 |
100 | case "update-and-reparse":
101 | if (!titleDirty) {
102 | data = ChartPropertiesStore.get("data");
103 | _metadata.title = defaultTitle(data);
104 | ChartMetadataStore.emitChange();
105 | }
106 | break;
107 |
108 | case "update-data-input":
109 | if (!titleDirty) {
110 | data = ChartPropertiesStore.get("data");
111 | _metadata.title = defaultTitle(data);
112 | ChartMetadataStore.emitChange();
113 | }
114 | break;
115 |
116 | default:
117 | // do nothing
118 | }
119 |
120 | return true;
121 |
122 | }
123 |
124 | //Dispatcher.register(registeredCallback);
125 | /* Respond to actions coming from the dispatcher */
126 | ChartMetadataStore.dispatchToken = Dispatcher.register(registeredCallback);
127 |
128 | function defaultTitle(data) {
129 | if (data.length === 1 && _metadata.title === "") {
130 | return data[0].name;
131 | } else {
132 | return _metadata.title;
133 | }
134 | }
135 |
136 | module.exports = ChartMetadataStore;
137 |
--------------------------------------------------------------------------------
/src/js/components/series/BarGroup.jsx:
--------------------------------------------------------------------------------
1 | var React = require("react");
2 | var PropTypes = React.PropTypes;
3 | var d3scale = require("d3-scale");
4 | var assign = require("lodash/assign");
5 | var map = require("lodash/map");
6 | var keys = require("lodash/keys");
7 | var reduce = require("lodash/reduce");
8 | var range = require("lodash/range");
9 | var isArray = require("lodash/isArray");
10 | var Rect = React.createFactory('rect');
11 | var G = React.createFactory('g');
12 |
13 | // parse props differently if bar is horizontal/vertical
14 | var orientation_map = {
15 | vertical: {
16 | "ordinalScale": "xScale",
17 | "ordinalVal": "x",
18 | "ordinalSize": "width",
19 | "linearScale": "yScale",
20 | "linearVal": "y",
21 | "linearSize": "height",
22 | "linearCalculation": Math.max.bind(null, 0),
23 | "groupTransform": function(x) { return "translate(" + x + ",0)"; }
24 | },
25 | horizontal: {
26 | "ordinalScale": "yScale",
27 | "ordinalVal": "y",
28 | "ordinalSize": "height",
29 | "linearScale": "xScale",
30 | "linearVal": "x",
31 | "linearSize": "width",
32 | "linearCalculation": Math.min.bind(null, 0),
33 | "groupTransform": function(y) { return "translate(0," + y + ")"; }
34 | },
35 | };
36 |
37 | var BarGroup = React.createClass({
38 |
39 | propTypes: {
40 | dimensions: PropTypes.object,
41 | xScale: PropTypes.func,
42 | bars: PropTypes.array,
43 | orientation: PropTypes.oneOf(["vertical", "horizontal"])
44 | },
45 |
46 | getDefaultProps: function() {
47 | return {
48 | groupPadding: 0.2,
49 | orientation: "vertical"
50 | }
51 | },
52 |
53 | _makeBarProps: function(bar, i, mapping, linearScale, ordinalScale, size, offset) {
54 | var props = this.props;
55 | var barProps = { key: i, colorIndex: bar.colorIndex };
56 | barProps[mapping.ordinalVal] = ordinalScale(bar.entry);
57 | barProps[mapping.ordinalSize] = size;
58 |
59 | // linearVal needs to be negative if number is neg else 0
60 | // see https://bl.ocks.org/mbostock/2368837
61 | barProps[mapping.linearVal] = linearScale(mapping.linearCalculation(bar.value));
62 | barProps[mapping.linearSize] = Math.abs(linearScale(bar.value) - linearScale(0));
63 | return barProps;
64 | },
65 |
66 | render: function() {
67 | var props = this.props;
68 | var mapping = orientation_map[props.orientation];
69 | var numDataPoints = props.bars[0].data.length;
70 | var makeBarProps = this._makeBarProps;
71 | var groupInnerPadding = Math.max(0.2, (props.displayConfig.columnInnerPadding / numDataPoints));
72 | var outerScale = props[mapping.ordinalScale];
73 | var isOrdinal = outerScale.hasOwnProperty("bandwidth");
74 | var offset = 0;
75 | var innserSize;
76 |
77 | if (isOrdinal) {
78 | var innerSize = outerScale.bandwidth();
79 | } else {
80 | var innerSize = props.dimensions[mapping.ordinalSize] / numDataPoints;
81 | }
82 |
83 | var innerScale = d3scale.scaleBand().domain(range(props.bars.length))
84 | .rangeRound([0, innerSize], 0.2, groupInnerPadding);
85 |
86 | var rectSize = innerScale.bandwidth();
87 |
88 | if (!isOrdinal) { offset = innerSize / -2; }
89 |
90 | var groups = map(props.bars, function(bar, ix) {
91 | var groupProps = { "key": ix, className: "bar-series" };
92 | groupProps["transform"] = mapping.groupTransform(innerScale(ix) + offset);
93 |
94 | var rects = map(bar.data, function(d, i) {
95 | var linearScale = bar[mapping.linearScale] || props[mapping.linearScale];
96 | var ordinalOffset = innerScale(ix);
97 | var barProps = makeBarProps(d, i, mapping, linearScale, outerScale, rectSize, offset);
98 | barProps.className = "color-index-" + bar.colorIndex;
99 |
100 | return Rect(barProps);
101 | });
102 |
103 | return G(groupProps, rects);
104 | });
105 |
106 | return {groups};
107 | }
108 |
109 | });
110 |
111 | module.exports = BarGroup;
112 |
--------------------------------------------------------------------------------
/src/js/components/chart-grid/ChartGridRenderer.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * ### ChartGrid Renderer
3 | * Render a grid of N columns by N rows of the same kind of chart
4 | * This is split up into two different sub-components, one for rendering a grid
5 | * of bar (row) charts, and another for a grid of XY (line, dot, column) charts
6 | */
7 |
8 | var React = require("react");
9 | var PropTypes = React.PropTypes;
10 | var update = require("react-addons-update");
11 | var chartSizes = require("../../config/chart-sizes");
12 | var clone = require("lodash/clone");
13 | var assign = require("lodash/assign");
14 | var each = require("lodash/each");
15 | var gridDimensions = require("../../charts/cb-chart-grid/chart-grid-dimensions");
16 |
17 | /* Chart grid types */
18 | var ChartGridBars = require("./ChartGridBars.jsx");
19 | var ChartGridXY = require("./ChartGridXY.jsx");
20 |
21 | /**
22 | * ### Component that decides which grid (small multiples) type to render and
23 | * passes props to that renderer
24 | * @property {boolean} editable - Allow the rendered component to interacted with and edited
25 | * @property {object} displayConfig - Parsed visual display configuration for chart grid
26 | * @property {object} chartProps - Properties used to draw this chart
27 | * @instance
28 | * @memberof renderers
29 | */
30 | var ChartGridRenderer = React.createClass({
31 |
32 | propTypes: {
33 | editable: PropTypes.bool.isRequired,
34 | displayConfig: PropTypes.shape({
35 | margin: PropTypes.obj,
36 | padding: PropTypes.obj
37 | }).isRequired,
38 | chartProps: PropTypes.shape({
39 | chartSettings: PropTypes.array.isRequired,
40 | data: PropTypes.array.isRequired,
41 | scale: PropTypes.object.isRequired,
42 | _grid: PropTypes.object.isRequired
43 | }).isRequired,
44 | showMetadata: PropTypes.bool,
45 | metadata: PropTypes.object
46 | },
47 |
48 | _createMobileScale: function(_chartProps) {
49 | var mobile = clone(_chartProps.mobile.scale, true);
50 | var scale = assign({}, _chartProps.scale, mobile);
51 | each(["prefix", "suffix"], function(text) {
52 | if (!mobile.primaryScale[text] || mobile.primaryScale[text] === "") {
53 | scale.primaryScale[text] = _chartProps.scale.primaryScale[text];
54 | }
55 | });
56 | return scale;
57 | },
58 |
59 | render: function() {
60 | var props = this.props;
61 | var _chartProps = this.props.chartProps;
62 | var gridTypeRenderer;
63 | var dimensions = gridDimensions(props.width, {
64 | metadata: props.metadata,
65 | grid: _chartProps._grid,
66 | data: _chartProps.data,
67 | displayConfig: props.displayConfig,
68 | showMetadata: props.showMetadata
69 | });
70 |
71 | var scale;
72 | if (this.props.enableResponsive && _chartProps.hasOwnProperty("mobile") && this.props.svgSizeClass === "small") {
73 | if (_chartProps.mobile.scale) {
74 | scale = this._createMobileScale(_chartProps);
75 | } else {
76 | scale = _chartProps.scale;
77 | }
78 | } else {
79 | scale = _chartProps.scale;
80 | }
81 |
82 | var chartProps = update(_chartProps, { $merge: { scale: scale }});
83 |
84 | /* Pass a boolean that detects whether there is a title */
85 | var hasTitle = (this.props.metadata.title.length > 0 && this.props.showMetadata);
86 |
87 | /* Choose between grid of bars and grid of XY, and transfer all props to
88 | * relevant component
89 | */
90 | if (this.props.chartProps._grid.type == "bar") {
91 | gridTypeRenderer = (
92 |
98 | );
99 | } else {
100 | gridTypeRenderer = (
101 |
107 | );
108 | }
109 | return gridTypeRenderer;
110 | }
111 | });
112 |
113 |
114 | module.exports = ChartGridRenderer;
115 |
--------------------------------------------------------------------------------
/tutorials/basic-chart.md:
--------------------------------------------------------------------------------
1 | # Making chart using Chartbuilder
2 |
3 | ## 1. Get your data into the correct format
4 |
5 | Chartbuilder expects your data to be in a specific format, which looks something like this:
6 |
7 | 
8 |
9 | For this tutorial we'll use [this data](https://docs.google.com/a/qz.com/spreadsheets/d/1xScjLJvFk1a0RjRWedi4ICmpeAMJ4t3s0HCZ3ROeSbg/edit#gid=0)
10 |
11 | Every row after should represent one moment in the data and every column after should represent one series of data. The first column and the first row are used as your axis and series labels.
12 |
13 | The design of the charts that Chartbuilder creates works best if your numbers are less than 1,000. For instance instead of "2,345,000,000" reduce it to 2.345 (you'll add a "billions" label in step 4)
14 |
15 | ## 2. Paste it into Chartbuilder from your spreadsheet software
16 |
17 | Open up Chartbuilder in your browser, it should look something like this
18 |
19 | 
20 |
21 | By copy and pasting your data from software like Microsoft Excel or Google Drive it should appear as tab-separated in Chartbuilder
22 |
23 | If your data is in the proper format, you should see a green confirmation below the input area—otherwise there will be a message in red alerting you to why Chartbuilder is having a hard time with your data.
24 |
25 | Using the data from above Chartbuilder should now look like this
26 | 
27 |
28 | ## 3. Select the way you want to display your data
29 |
30 | For every series you have there is an option for how you want to have it displayed. You can select lines, columns, or dots.
31 |
32 | 
33 |
34 | You can select which type of display you want by clicking the designated button. Lets leave it on the default: "Lines"
35 |
36 | Clicking on the colored circle will reveal the available colors in your version of chartbuilder, clicking on one will change the color of the series associated with it.
37 |
38 | 
39 |
40 | ## 4. Set your units and adjust axis
41 |
42 | Chartbuilder gives you the option to label your axes inline. Input text into the "prefix" field to add something like a currency unit to before the top most axis item.
43 |
44 | Input text into the "suffix" field to add something like the magnitude (" millions") or unit (" per capita") of your data to after the top most axis item
45 |
46 | For this data lets leave the prefix blank and set " billion people" as the suffix.
47 |
48 | Lets also set the Axis minimum to 0, the maximum to 1.5 and the ticks to 7.
49 |
50 | Your Chartbuilder should now look like this
51 | 
52 |
53 | ## 5. Add a Title and the source of your data
54 |
55 | Want to add a title to your chart? Add one by typing into the "add a Title" filed
56 |
57 | How about "The three most populous countries"
58 |
59 | You'll notice that as you type the chart updates
60 |
61 | Also, add the source of your data into the "Add a source field"
62 |
63 | In this case it's "University of Groningen, University of California, Davis, and University of Pennsylvania"
64 |
65 | ## 6. Select a size and export
66 |
67 | What size do you want your chart to be? You can select "Medium" "Long and Skinny" or "spot"
68 |
69 | Lets keep it as "Medium"
70 |
71 | Click the "Download Image" button
72 |
73 | If you're using Chrome or Firefox this image should download
74 |
75 | 
76 |
77 | If you're using Safari it will show up in your browser. Right click and save to download it.
78 |
79 | ### Next tutorial: [Charting a column chart using ordinal data](column-chart-ordinal-data.md)
--------------------------------------------------------------------------------
/src/js/components/shared/DataInput.jsx:
--------------------------------------------------------------------------------
1 | var React = require("react");
2 | var PropTypes = React.PropTypes;
3 | var update = require("react-addons-update");
4 |
5 | // Flux actions
6 | var ChartViewActions = require("../../actions/ChartViewActions");
7 | var ChartServerActions = require("../../actions/ChartServerActions");
8 |
9 | var errorNames = require("../../util/error-names");
10 | var validateChartModel = require("../../util/validate-chart-model");
11 |
12 | var chartbuilderUI = require("chartbuilder-ui");
13 | var TextArea = chartbuilderUI.TextArea;
14 | var AlertGroup = chartbuilderUI.AlertGroup;
15 | var DataSeriesTypeSettings = require("../shared/DataSeriesTypeSettings.jsx");
16 |
17 | /**
18 | * ### Text area component and error messaging for data input
19 | * @instance
20 | * @memberof editors
21 | */
22 | var DataInput = React.createClass({
23 |
24 | propTypes: {
25 | errors: PropTypes.array.isRequired,
26 | chartProps: PropTypes.shape({
27 | chartSettings: PropTypes.array,
28 | data: PropTypes.array,
29 | scale: PropTypes.object,
30 | input: PropTypes.object
31 | }).isRequired,
32 | className: PropTypes.string
33 | },
34 |
35 | getInitialState: function() {
36 | return {
37 | alertType: "default",
38 | alertText: "Waiting for data...",
39 | boldText: "",
40 | dropping: false
41 | };
42 | },
43 |
44 | _handleReparseUpdate: function(k, v) {
45 | if (k == "input") {
46 | input = update(this.props.chartProps.input, { $merge: {
47 | raw: v,
48 | type: undefined
49 | }});
50 | ChartViewActions.updateInput(k, input);
51 | } else if (k == "type") {
52 | input = update(this.props.chartProps.input, { $set: {
53 | raw: v.raw,
54 | type: v.type
55 | }});
56 | ChartViewActions.updateAndReparse("input", input);
57 | } else {
58 | return;
59 | }
60 | },
61 |
62 | _toggleDropState: function(e) {
63 | this.setState({ dropping: !this.state.dropping });
64 | },
65 |
66 | onFileUpload: function(e) {
67 | var reader = new FileReader();
68 | reader.onload = function() {
69 | parsedModel = validateChartModel(this.result);
70 | if (parsedModel) {
71 | // Update flux store with incoming model
72 | ChartServerActions.receiveModel(parsedModel);
73 | }
74 | };
75 | this._toggleDropState();
76 | reader.readAsText(e.target.files[0]);
77 | },
78 |
79 | // Render only the dropover area
80 | _renderDropArea: function() {
81 | return (
82 |
86 |
87 |
Drop configuration file here
88 |
89 |
90 |
91 | );
92 | },
93 |
94 | _renderErrors: function() {
95 | if (this.props.errors.length === 0) return null;
96 |
97 | return (
98 |
101 | );
102 | },
103 |
104 | // Render the data input text area and indicator
105 | _renderDataInput: function() {
106 |
107 | var errors = this._renderErrors();
108 |
109 | return (
110 |
113 |
121 | {errors}
122 |
126 |
127 | );
128 | },
129 |
130 | render: function() {
131 | if (this.state.dropping) {
132 | return this._renderDropArea();
133 | } else {
134 | return this._renderDataInput();
135 | }
136 | }
137 |
138 | });
139 |
140 | module.exports = DataInput;
141 |
--------------------------------------------------------------------------------
/src/styl/layout.styl:
--------------------------------------------------------------------------------
1 | $header-height = 3em
2 | $chart-area-width = 640px
3 | $chart-area-padding = 20px
4 |
5 | // Page layout setup
6 |
7 | html, body
8 | min-width 200px
9 | color $color-body-text
10 | max-width 100%
11 | overflow-x none
12 |
13 | h1, h2, h4, h5
14 | font-family $font-sans-bold
15 | color $color-body-text
16 |
17 | h1
18 | line-height 50px
19 | margin-bottom 10px
20 |
21 | h2
22 | font-size 24px
23 | line-height 24px
24 | font-style bold
25 | margin 40px 0px
26 |
27 | h3
28 | font-size 20px
29 | font-family $font-serif
30 | font-weight bold
31 |
32 | p
33 | position relative
34 | margin 0 0 0 0
35 | padding 16px 0 16px 0
36 | line-height 1.6
37 |
38 | // Base elements
39 |
40 | body
41 | background-color #f3f3f3
42 | font-family $primary-font-family
43 | font-size 12px
44 |
45 | h1
46 | font-size 2em
47 | line-height 1
48 | font-family $primary-font-family
49 |
50 | h2
51 | font-size 1.5em
52 | line-height 1.5em
53 | margin-top 0.5em
54 | margin-bottom 0.25em
55 | font-family $primary-font-family
56 |
57 | h3
58 | font-size 1.25em
59 | font-family $primary-font-family
60 |
61 | h4
62 | font-size 1em
63 | primary-font-family $primary-font-family
64 |
65 | table
66 | width 100%
67 |
68 | cite
69 | font-size 1em
70 | font-family $secondary-font-family
71 | font-style normal
72 |
73 | textarea, input, select
74 | width 100%
75 | box-sizing border-box
76 | margin-bottom 1em
77 | color #666
78 | border 1px solid #ccc
79 | border-radius 0
80 | font-family $primary-font-family
81 | background-color #fff
82 |
83 | textarea, input
84 | appearance none
85 |
86 |
87 | textarea:focus,
88 | select:focus,
89 | input:focus
90 | outline #999 1px solid
91 |
92 | textarea
93 | font-size 0.75em
94 |
95 | select
96 | height 3em
97 |
98 | canvas
99 | display none
100 |
101 | .hide
102 | display none
103 |
104 | .clearfix
105 | clear both
106 |
107 | *
108 | -ms-overflow-style none
109 |
110 | /**
111 | * Layout
112 | */
113 |
114 | .header
115 | background-color #333
116 | margin-top 0
117 | top 0
118 | position fixed
119 | height $header-height
120 | width 100%
121 | z-index 10000
122 | padding 0.5em 0 0 0.25em
123 | h1
124 | color #777
125 | margin 0
126 | line-height 1
127 | padding 0
128 |
129 | .chartbuilder-container
130 | margin-top $header-height + 0.75
131 | position relative
132 | overflow hidden
133 |
134 | .chartbuilder-renderer
135 | width $chart-area-width
136 | left ($chart-area-padding / 2)
137 | top $header-height
138 | overflow-y scroll
139 | overflow-x hidden
140 | height 100%
141 | max-width 100%
142 | position fixed
143 | margin-top $chart-area-padding
144 | .desktop
145 | width $chart-area-width
146 | position relative
147 |
148 | .renderer-wrapper
149 | width auto
150 | margin-left auto
151 | margin-right auto
152 |
153 | .mobile
154 | max-width 429px
155 | height 1000px
156 | margin-left auto
157 | margin-right auto
158 | margin-top 2em
159 | position relative
160 |
161 | .phone-wrap
162 | background-image url('/assets/iphone-6.svg')
163 | background-size contain
164 | transform scale(1)
165 | transform-origin top
166 | height 881px
167 |
168 | .phone-frame
169 | overflow-y scroll
170 | position relative
171 | height 669px
172 | width 372px
173 | top 116px
174 | left 29px
175 |
176 | @import 'chart-renderer.styl'
177 |
178 | .chartbuilder-editor
179 | margin-left $chart-area-width + ($chart-area-padding * 2)
180 | margin-bottom 30px
181 | display inline-block
182 | width 40%
183 | padding 0 10px
184 | min-width 100px
185 | @import 'chart-editor.styl'
186 |
187 | @media screen and (max-width: $single_column_breakpoint)
188 | .header
189 | position relative
190 | .chartbuilder-container
191 | margin-top 10px
192 | .chartbuilder-renderer
193 | display block
194 | position relative
195 | margin-top 10px
196 | width 100%
197 | top 0
198 | overflow-x scroll
199 | .desktop
200 | margin-left auto
201 | margin-right auto
202 | .chartbuilder-editor
203 | margin-top 20px
204 | box-sizing border-box
205 | margin-left 0
206 | display block
207 | width 100%
208 |
--------------------------------------------------------------------------------
/src/js/components/ChartMetadata.jsx:
--------------------------------------------------------------------------------
1 | // Component that handles global metadata, ie data that is universal regardless
2 | // of chart type. Eg title, source, credit, size.
3 |
4 | var React = require("react");
5 | var PropTypes = React.PropTypes;
6 | var PureRenderMixin = require("react-addons-pure-render-mixin");
7 | var clone = require("lodash/clone");
8 |
9 | // Flux stores
10 | var ChartMetadataStore = require("../stores/ChartMetadataStore");
11 | var ChartViewActions = require("../actions/ChartViewActions");
12 |
13 | // Chartbuilder UI components
14 | var chartbuilderUI = require("chartbuilder-ui");
15 | var ButtonGroup = chartbuilderUI.ButtonGroup;
16 | var TextInput = chartbuilderUI.TextInput;
17 |
18 | // Give chart sizes friendly names
19 | var chart_sizes = [
20 | {
21 | title: "Auto",
22 | content: "Auto",
23 | value: "auto"
24 | },
25 | {
26 | title: "Medium",
27 | content: "Medium",
28 | value: "medium"
29 | },
30 | {
31 | title: "Long spot chart",
32 | content: "Long spot chart",
33 | value: "spotLong"
34 | },
35 | {
36 | title: "Small spot chart",
37 | content: "Small spot chart",
38 | value: "spotSmall"
39 | }
40 | ];
41 |
42 | var text_input_values = [
43 | { name: "title", content: "Title" },
44 | { name: "credit", content: "Credit" },
45 | { name: "source", content: "Source" }
46 | ];
47 |
48 | /**
49 | * Edit a chart's metadata
50 | * @property {object} metadata - Current metadata
51 | * @property {string} stepNumber - Step in the editing process
52 | * @property {[components]} additionalComponents - Additional React components.
53 | * Anything passed here will be given a callback that updates the `metadata`
54 | * field. This is useful for adding custom input fields not provided.
55 | * @instance
56 | * @memberof editors
57 | */
58 | var ChartMetadata = React.createClass({
59 |
60 | propTypes: {
61 | metadata: PropTypes.shape({
62 | chartType: PropTypes.string.isRequired,
63 | size: PropTypes.string.isRequired,
64 | source: PropTypes.string,
65 | credit: PropTypes.string,
66 | title: PropTypes.string
67 | }),
68 | stepNumber: PropTypes.string,
69 | additionalComponents: PropTypes.array
70 | },
71 |
72 | // Get text input types from state
73 | getInitialState: function() {
74 | return {
75 | };
76 | },
77 |
78 | // Update metadata store with new settings
79 | _handleMetadataUpdate: function(k, v) {
80 | ChartViewActions.updateMetadata(k, v);
81 | },
82 |
83 | render: function() {
84 | var metadata = this.props.metadata;
85 |
86 | if (this.props.additionalComponents.length > 0) {
87 | this.props.additionalComponents.forEach(function(c, i) {
88 | c.props.onUpdate = this._handleMetadataUpdate;
89 | c.props.value = metadata[c.key] || "";
90 | }, this);
91 | }
92 | // Create text input field for each metadata textInput
93 | var textInputs = text_input_values.map(function(textInput) {
94 | return
101 | }, this);
102 |
103 | return (
104 |
105 |
106 | {this.props.stepNumber}
107 | Set title, source, credit and size
108 |
109 | {textInputs}
110 | {this.props.additionalComponents}
111 |
116 |
117 | );
118 | }
119 | });
120 |
121 | // Small wrapper arount TextInput component specific to metadata
122 | var ChartMetadataText = React.createClass({
123 |
124 | mixins: [ PureRenderMixin ],
125 |
126 | render: function() {
127 | return (
128 |
129 |
135 |
136 | );
137 | }
138 | });
139 |
140 | module.exports = ChartMetadata;
141 |
--------------------------------------------------------------------------------
/test/helper.js:
--------------------------------------------------------------------------------
1 | var test = require("tape");
2 | var help = require("../src/js/util/helper");
3 | var util = require("./util/util");
4 |
5 | function isInteger(x) {
6 | return x % 1 === 0;
7 | }
8 |
9 | test("helper: exact ticks", function(t) {
10 | t.plan(2);
11 |
12 | var et = help.exactTicks;
13 | var domain_no_zero = [util.randInt(1, 10), util.randInt(11, 1000)];
14 | var domain_pass_zero = [util.randInt(-1000, -500), util.randInt(500, 1000)];
15 | var numTicks = 5;
16 |
17 | t.equal(et(domain_no_zero, numTicks).length, numTicks, "length of tick array matches number given");
18 |
19 | var passZero = et(domain_pass_zero, numTicks);
20 | t.ok((passZero.indexOf(0) > -1), "domain that passes zero contains tick of zero");
21 | t.end();
22 | });
23 |
24 | test("helper: round to precision", function(t) {
25 | t.plan(3);
26 |
27 | var rtp = help.roundToPrecision;
28 | var rand_float = util.randInt(1, 200) + Math.random();
29 | var precision_float = util.randInt(1, 4);
30 | var decimalPoints;
31 |
32 | t.equal(rtp(0, util.randInt(1, 4)), "0", "if passed 0 return \"0\" regardless of precision");
33 |
34 | t.ok(isInteger(rtp(rand_float, 0)), "precision 0 returns an integer");
35 |
36 | // using this method instead of ours as we have to test ours
37 | digits_after_dec = rtp(rand_float, precision_float).split(".")[1].length;
38 | t.equal(digits_after_dec, precision_float, "number of digits following decimal point matches argument");
39 | });
40 |
41 | test("helper: compute scale domain", function(t) {
42 | t.plan(6);
43 |
44 | var csd = help.computeScaleDomain;
45 | var scaleObj = {};
46 | var expect;
47 | var processScale;
48 |
49 | var no_opts = [1, 25, 50, 75, 99];
50 | expect = { domain: [1, 99], custom: false };
51 | t.deepEqual(csd(scaleObj, no_opts), expect, "returns [ data[0], data[data.length - 1] ] if no options given");
52 |
53 | var nice = [1, 25, 50, 75, 99];
54 | expect = { domain: [0, 100], custom: false };
55 | t.deepEqual(csd(scaleObj, nice, { nice: true }), expect, "returns niced values if { nice: true } set");
56 |
57 | var force_zero = [10, 20, 30, 40];
58 | t.equal(csd(scaleObj, force_zero, { minZero: true }).domain[0], 0, "sets domain[0] to 0 if { minZero: true } set");
59 |
60 | var negative = [-50, -25, 0, 25, 50];
61 | t.equal(csd(scaleObj, negative, { minZero: true }).domain[0], -50, "sets domain[0] to negative number even if { minZero: true } set");
62 |
63 | scaleObj = { domain: [5, 95], custom: true };
64 | processScale = csd(scaleObj, nice, { nice: true });
65 | t.ok(processScale.custom, "respects custom values even if { nice: true } set");
66 |
67 | scaleObj = { domain: [0, 100], custom: false };
68 | var new_data = [0, 100, 200, 300, 400, 500];
69 | expect = { domain: [0, 500], custom: false };
70 | processScale = csd(scaleObj, new_data, { nice: true });
71 | t.deepEqual(processScale, expect, "updates domain if new data extent entered and scale is not custom");
72 |
73 | t.end();
74 | });
75 |
76 | test("helper: detect precision", function(t) {
77 | t.plan(1);
78 | var p = help.precision;
79 | var rand_float = util.randInt(1, 10) + Math.random();
80 | var rand_precision = util.randInt(1, 6);
81 | var to_fixed = parseFloat(rand_float.toFixed(rand_precision));
82 | digits_after_dec = to_fixed.toString().split(".")[1].length;
83 | t.equal(p(to_fixed), digits_after_dec, "precision returns same value passed to toFixed");
84 | t.end();
85 | });
86 |
87 | test("helper: precision of NaN is 0", function(t) {
88 | t.plan(1);
89 | var p = help.precision;
90 | t.equal(p(0 / 0), 0);
91 | t.end();
92 | });
93 |
94 | test("helper: convert timezone strings", function(t) {
95 | t.plan(8);
96 | var p = help.TZOffsetToMinutes
97 | t.equal(p("Z"),0,"Z timezone returns offset of 0 mintues")
98 | t.equal(p("-00:00"), 0, "+0000 tz offset returns 0 minutes")
99 | t.equal(p("+00:00"), 0, "+0000 tz offset returns 0 minutes")
100 | t.equal(p("-08:00"), -480, "-0800 tz offset returns -480 minutes")
101 | t.equal(p("+05:00"), 300, "+0500 tz offset returns 600 minutes")
102 | t.equal(p("+04:30"), 270, "+0430 tz offset returns 270 minutes")
103 | t.equal(p("-04:30"), -270, "-0430 tz offset returns -270 minutes")
104 | t.equal(p("+10:00"), 600, "+1030 tz offset returns 600 minutes")
105 | t.end();
106 | })
107 |
--------------------------------------------------------------------------------
/docs/02-customizing-chartbuilder.md:
--------------------------------------------------------------------------------
1 | ## Customizing Chartbuilder
2 |
3 | You may want to alter some of Chartbuilder's basic style and settings for your organization.
4 | That's what we'll cover here.
5 |
6 | As noted in the [introduction to Chartbuilder's internals](01-introduction.md), we
7 | made as many style options as possible configurable through CSS. Nearly all
8 | other settings can be configured in one of two places in JavaScript:
9 | 1. The global config files in the `src/js/config` directory
10 | 2. The configuration file for each chart type, for example `xy-config.js` in `src/js/charts/cb-xy`.
11 |
12 | ### chart-style.js
13 |
14 | Style configuration that is global to all chart types is defined in
15 | `src/js/config/chart-style.js`. In order for your chart to render correctly, you
16 | will need to set `fontFamilies` and `fontSizes` in this file. Some aspects of
17 | the chart require measuring the size of rendered text, and defining it in
18 | javascript here allows us to calculate that size before drawing the text, much
19 | faster than drawing it first then checking and drawing again.
20 |
21 | ### Colors and other CSS
22 |
23 | CSS will be able to cover most of the prominent aesthetic features of your
24 | charts: the color palette, typography, appearance of lines and dots, and more. Let's
25 | look at editing the colors.
26 |
27 | Nearly all the colors used anywhere in Chartbuilder are defined in
28 | `styl/colors.styl`. There's an array of colors defined there, `$chart-colors`.
29 | Changing these values will update the color palette used for your charts,
30 | including the interface. There's one thing to note, however: our JavaScript has
31 | no way of knowing how many colors it ought to render, so you'll need to make
32 | sure the `numColors` value in your chart config matches the number of colors
33 | defined in `colors.styl`. The number of colors is set per chart type, in case
34 | some types have different requirements.
35 |
36 | The `colors.styl` file also contains variables for various other colors used in
37 | Chartbuilder, so edit them at will. You can find typography definitions in
38 | `type.styl`. The visual elements of the charts themselves are defined in
39 | `chart-renderer.styl`.
40 |
41 | ### Chart properties
42 |
43 | Each chart type has its own config file. Currently in Chartbuilder there are
44 | such files for an XY chart (line, column, dots, or any combination of these) and
45 | a Chart Grid (repeated small multiples). Each config file has the following
46 | properties to keep in mind:
47 |
48 | * `displayName`: Name used on the button for selecting this chart
49 | * `parser`: A function that processes the input and parses it for use with the
50 | chart editor and renderer.
51 | * `config`: Settings that are specific to this chart type, and are static,
52 | meaning they don't change over time. Settings that are set in "ems" will be
53 | calculated based on the rendered size of a chart's type.
54 | * `defaultProps`: Where the default properties for your chart model are
55 | set.
56 |
57 | ### The chart model
58 |
59 | A chart consists of `chartProps` and `metadata`. The former
60 | tell the chart renderer how to draw the chart itself, while `metadata` relates to the
61 | area around the chart, such as the title, credit, and source. By convention,
62 | `chartProps` that are specific to a certain chart type start with an
63 | underscore, like `chartProps._grid` in chart grid. Properties that are shared
64 | across chart types will be carried over when changing types.
65 |
66 | Here is further explanation of these properties (a sample is shown below):
67 |
68 | `input`: An object containing the `raw` input as well as a status code
69 | indicating whether there have been any parse errors.
70 |
71 | `scale`: Settings related to the chart's scales. The `primaryScale` should be
72 | used for the main linear scale in order for settings to carry over from one
73 | chart type to another. This isolates the primary scale from others, such as
74 | dates or a secondary scale for a chart with multiple y axes.
75 |
76 | `chartSettings`: Any additional configuration that should be changeable via a
77 | chart editor. These are mostly used for properties specific to each column of a
78 | dataset, such as the color or display type.
79 |
80 | Some properties need to be calculated; the `config` file merely sets the
81 | defaults. It is up to the chart parser to do any necessary calculations. For
82 | example, [here is](misc/sample_chart_model.json) a model of an XY chart after it has
83 | been parsed by `js/charts/cb-xy/parse-xy.js`.
84 |
85 |
--------------------------------------------------------------------------------
/src/js/components/shared/VerticalAxis.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 max = require("lodash/max");
6 | var help = require("../../util/helper.js");
7 | var ordinalAdjust = require("../../util/scale-utils").ordinalAdjust;
8 |
9 | var DY = "0.32em";
10 |
11 | var VerticalAxis = React.createClass({
12 |
13 | propTypes: {
14 | orient: PropTypes.string,
15 | width: PropTypes.number,
16 | yScale: PropTypes.func,
17 | offset: PropTypes.shape({
18 | x: PropTypes.number,
19 | y: PropTypes.number
20 | }),
21 | tickValues: PropTypes.array,
22 | tickFormat: PropTypes.func,
23 | prefix: PropTypes.string,
24 | suffix: PropTypes.string,
25 | tickWidths: PropTypes.shape({
26 | widths: PropTypes.array,
27 | max: PropTypes.number
28 | }),
29 | colorIndex: PropTypes.number,
30 | tickTextHeight: PropTypes.number
31 | },
32 |
33 | getDefaultProps: function() {
34 | return {
35 | orient: "left",
36 | offset: {
37 | x: 0,
38 | y: 0
39 | },
40 | tickFormat: function(d) { return d; },
41 | textAlign: "outside"
42 | }
43 | },
44 |
45 | _orientation: {
46 | "left": {
47 | transformTextX: function(width, textAlign, tickWidth, maxTickWidth) {
48 | if (textAlign === "inside") {
49 | return tickWidth * -1 + maxTickWidth;
50 | } else {
51 | return 0;
52 | }
53 | },
54 | textAnchor: {
55 | "inside": "end",
56 | "outside": "start",
57 | },
58 | transformRectX: function(textAlign, tickWidth, maxTickWidth) {
59 | return 0;
60 | }
61 | },
62 | "right": {
63 | transformTextX: function(width, textAlign, tickWidth, maxTickWidth) {
64 | if (textAlign === "inside") {
65 | return maxTickWidth * -1;
66 | } else {
67 | return tickWidth * -1;
68 | }
69 | },
70 | textAnchor: {
71 | "inside": "start",
72 | "outside": "end"
73 | },
74 | transformRectX: function(textAlign, tickWidth, maxTickWidth) {
75 | if (textAlign === "inside") {
76 | return maxTickWidth * -1;
77 | } else {
78 | return tickWidth * -1;
79 | }
80 | }
81 | }
82 | },
83 |
84 | _generateText: function(props) {
85 | var numTicks = props.tickValues.length;
86 | var concealerHeight = props.tickTextHeight + props.displayConfig.blockerRectOffset;
87 | var orientation = this._orientation[props.orient]
88 | var textAnchor = orientation.textAnchor[props.textAlign];
89 |
90 | return map(props.tickValues, function(tickValue, i) {
91 | var formatted = props.tickFormat(tickValue);
92 | var currTickWidth = props.tickWidths.widths[i];
93 | var maxTickWidth = Math.max(currTickWidth, props.tickWidths.max);
94 |
95 | var rectWidth;
96 | if (props.textAlign === "inside") {
97 | rectWidth = maxTickWidth + props.displayConfig.blockerRectOffset;
98 | } else {
99 | rectWidth = currTickWidth + props.displayConfig.blockerRectOffset
100 | }
101 |
102 | var textX = orientation.transformTextX(props.width, props.textAlign, currTickWidth, maxTickWidth);
103 |
104 | var text;
105 | if (i === (numTicks - 1)) {
106 | text = [props.prefix, formatted, props.suffix].join("");
107 | } else {
108 | text = formatted;
109 | }
110 |
111 | var transformY = ordinalAdjust(props.yScale, tickValue);
112 |
113 | return (
114 |
119 |
126 |
130 | {text}
131 |
132 |
133 | )
134 |
135 | });
136 | },
137 |
138 | render: function() {
139 | var props = this.props;
140 | var text = this._generateText(props);
141 | var transformGroup = (props.orient === "left") ? 0 : props.width;
142 |
143 | return (
144 |
149 | {text}
150 |
151 | );
152 | }
153 |
154 | });
155 |
156 | module.exports = VerticalAxis;
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Chartbuilder :chart_with_upwards_trend:
2 | ============
3 |
4 | Chartbuilder is a front-end charting application that facilitates easy creation of simple beautiful charts.
5 |
6 | Chartbuilder is the user and export interface.
7 | Chartbuilder powers all chart creation on [Atlas](http://atlas.qz.com), a
8 | charting platform developed by Quartz.
9 |
10 | What's new in Chartbuilder 2.0
11 | -------------------------
12 | * The Chart Grid type. Use it to create small multiples of bars, lines, dots, or columns.
13 | * The app has been rewritten in React.js, making it easier to add new chart types or use
14 | third-party rendering libraries.
15 | * Chart edits are automatically saved to localStorage, and a chart can be
16 | recovered by clicking the "load previous chart" button on loading the page.
17 | * Data input now accepts csv formatted data as well as tsv formated data
18 | * All UI elements belong to [Chartbuilder UI](https://github.com/Quartz/chartbuilder-ui),
19 | a framework of flexible React components that you can use in other projects.
20 |
21 | What Chartbuilder is not
22 | -------------------------
23 | + A replacement for Excel
24 | + A replacement for Google Spreadsheet
25 | + A data analysis tool
26 | + A data transformation tool
27 |
28 | What Chartbuilder is
29 | --------------------
30 | Chartbuilder is the final step in charting to create charts in a consistent predefined style. Paste data into it and export the code to draw a mobile friendly responsive chart or a static svg or png chart.
31 |
32 |
33 | Who is using Chartbuilder
34 | --------------------
35 | Other than Quartz, customized Chartbuilder created charts have been seen in many publications:
36 | + [NPR](http://www.npr.org/blogs/parallels/2013/10/24/240493422/in-most-every-european-country-bikes-are-outselling-cars)
37 | + [The Wall Street Journal](http://blogs.wsj.com/korearealtime/2014/03/07/for-korean-kids-mobile-chat-rules/)
38 | + [CNBC](http://www.cnbc.com/id/101009116)
39 | + [The New Yorker](http://www.newyorker.com/online/blogs/currency/2013/12/2013-the-year-in-charts.html)
40 | + [The Press-Enterprise](http://blog.pe.com/political-empire/2013/07/31/ppic-poll-global-warming-a-concern-for-inland-voters/)
41 | + [New Hampshire Public Radio](http://nhpr.org/post/water-cleanup-commences-beede-story-shows-superfund-laws-flaws)
42 | + [CFO Magazine](http://ww2.cfo.com/the-economy/2013/11/retail-sales-growth-stalls/)
43 | + [Australian Broadcasting Corporation](http://www.abc.net.au/news/2013-10-11/nobel-prize3a-why-2001-was-the-best-year-to-win/5016010)
44 | + [Digiday](http://digiday.com/publishers/5-charts-tell-state-digital-publishing/)
45 |
46 |
47 | Getting started with Chartbuilder
48 | ---------------------------------
49 | If you are not interested in customizing the styles of your charts use the hosted version: http://quartz.github.io/Chartbuilder
50 |
51 | To work on the Chartbuilder code, first download the project and install
52 | dependencies:
53 |
54 | #### Download via github
55 | 1. Make sure you have a recent version of [node.js](https://github.com/joyent/node/wiki/Installation) (0.12 or above) (or [io.js](https://iojs.org/en/index.html))
56 | 2. [Download source](https://github.com/Quartz/Chartbuilder/archive/master.zip) (and unzip or clone using git)
57 | 3. from the terminal navigate to the source folder (on a Mac: `cd ~/Downloads/Chartbuilder-master/`)
58 | 4. Install the dependencies automatically by running `npm install`
59 | 5. Start the included web server by running `npm run dev`
60 | 6. Point your browser to [http://localhost:3000/](http://localhost:3000/)
61 | 7. When you're done developing, [build and deploy](docs/deploying.md) your Chartbuilder!
62 |
63 | #### Making a chart with Charbuilder
64 | * [How to make a line chart with time series data](tutorials/basic-chart.md)
65 | * [How to make a bar chart with ranking data](tutorials/bar-chart-with-ranking-data.md)
66 | * [How to make a column chart with ordinal data](tutorials/column-chart-ordinal-data.md)
67 |
68 | #### Customizing your Chartbuilder
69 | * [Getting to know the Chartbuilder code](docs/01-introduction.md)
70 | * [Customizing chartbuilder](docs/02-customizing-chartbuilder.md)
71 | * [Test things out](docs/testing.md)
72 | * When you're done developing, [build and deploy](docs/deploying.md) your Chartbuilder!
73 | * Keep your customized version [in sync with the master](docs/git-workflow-forks.md)
74 |
75 | ### Documentation
76 |
77 | * The [Chartbuilder API docs](http://quartz.github.io/Chartbuilder/api-docs/)
78 | document most of the React components, classes, and utilities in the code base.
79 |
80 | ##### Documentation for Chartbuilder's dependencies:
81 | * [D3](https://github.com/mbostock/d3/wiki)
82 | * [React](https://facebook.github.io/react/docs/getting-started.html)
83 |
84 |
--------------------------------------------------------------------------------
/src/js/charts/cb-chart-grid/chart-grid-config.js:
--------------------------------------------------------------------------------
1 | var ChartConfig = require("../ChartConfig");
2 |
3 | /**
4 | * ### Configuration of a Chart grid
5 | * @name chart_grid_config
6 | */
7 |
8 | /**
9 | * display
10 | * @static
11 | * @memberof chart_grid_config
12 | * @property {Nem|number} afterTitle - Distance btwn top of title and top of legend or chart
13 | * @property {Nem|number} afterLegend - Distance btwn top of legend and top of chart
14 | * @property {Nem|number} blockerRectOffset - Distance btwn text of axis and its background blocker
15 | * @property {Nem|number} paddingBerBar - Space btwn two bars in a bar grid
16 | * @property {Nem|number} barHeight - Height of an individual bar in a bar grid
17 | * @property {Nem|number} afterXYBottom - Vert distance btwn two chart grids that are stacked
18 | * @property {Nem|number} afterXYRight - Horiz distance btwn two chart grids that are next to each other
19 | * @property {Nem|number} columnExtraPadding - Extra padding given if a chart grid XY has columns
20 | * @property {Nem|number} bottomPaddingWithoutFooter - Bottom padding if footer is not drawn
21 | * @property {Nem|number} bottomPaddingWithoutFooter - Bottom padding if footer is not drawn
22 | * @property {object} xy - Copy of `xy_config.display`, used in XY chart grids
23 | * @property {object} margin - Distances btwn outer chart elements and container
24 | * @property {object} padding - Distances btwn inner chart elements and container
25 | */
26 | var display = {
27 | afterTitle: "1.6em", // distance between top of title and top of legend or chart
28 | afterLegend: "0.8em", // distance between top of legend and top of chart
29 | blockerRectOffset: "0.25em", // distance between text and background blocker rect
30 | barHeight: "1.9em", // height of each bars
31 | columnExtraPadding: "0.5em",
32 | barInnerPadding: 0.4, // % of col group width to pad btwn each
33 | barOuterPadding: 0.1, // % of col group width to pad btwn each
34 | bottomPaddingWithoutFooter: "0.5em",
35 | gridPadding: {
36 | xInnerPadding: 0.05,
37 | xOuterPadding: 0,
38 | yInnerPadding: 0.05,
39 | yOuterPadding: 0
40 | },
41 | xy: require("../cb-xy/xy-config").display,
42 | margin: {
43 | top: "0.8em",
44 | right: "0.25em",
45 | bottom: "0.15em",
46 | left: "0.25em"
47 | },
48 | padding: {
49 | top: "0.4em",
50 | right: 0,
51 | bottom: "1em",
52 | left: 0
53 | }
54 | };
55 | /**
56 | * @name chart_grid_defaultProps
57 | * @static
58 | * @memberof chart_grid_config
59 | */
60 | var defaultProps = {
61 | /**
62 | * @name chartProps
63 | * @property {object} scale - Default settings for date and primary scales
64 | * @property {object} input
65 | * @property {array} chartSettings - Default settings for a given series (column) of data
66 | * @property {object} extraPadding - Additional padding. This is a dynamic
67 | * value and is mostly changed within the component itself
68 | * @property {object} _grid - Grid settings
69 | * @property {number} _grid.rows - Number of rows in the grid
70 | * @property {number} _grid.cols - Number of columns in the grid
71 | * @property {string} _grid.type - Grid type `(bars|lines|dots|columns)`
72 | * @property {object} mobile - Mobile-specific override settings
73 | * @static
74 | * @memberof chart_grid_defaultProps
75 | */
76 | chartProps: {
77 | input: {},
78 | extraPadding: {
79 | top: 0,
80 | right: 0,
81 | bottom: 0,
82 | left: 0
83 | },
84 | scale: {
85 | primaryScale: {
86 | ticks: 5,
87 | precision: 0,
88 | prefix: "",
89 | suffix: ""
90 | },
91 | dateSettings: {
92 | dateFrequency: "auto",
93 | dateFormat: "auto",
94 | inputTZ: null,
95 | displayTZ: "as-entered"
96 | },
97 | numericSettings: {
98 | ticks: 5,
99 | precision: 0,
100 | prefix: "",
101 | suffix: ""
102 | }
103 | },
104 | chartSettings: [
105 | {
106 | colorIndex: 0,
107 | barHeight: "0.85em"
108 | }
109 | ],
110 | _grid: {
111 | rows: 1,
112 | type: null
113 | },
114 | mobile: {}
115 | },
116 | /**
117 | * @name metadata
118 | * @property {string} chartType
119 | * @property {string} size
120 | * @static
121 | * @memberof chart_grid_defaultProps
122 | */
123 | metadata: {
124 | id: null,
125 | chartType: "chartgrid",
126 | title: "",
127 | source: "",
128 | credit: "Made with Chartbuilder",
129 | size: "auto"
130 | }
131 | };
132 |
133 | var chart_grid_config = new ChartConfig({
134 | displayName: "Chart grid",
135 | parser: require("./parse-chart-grid"),
136 | calculateDimensions: require("./chart-grid-dimensions"),
137 | display: display,
138 | defaultProps: defaultProps
139 | });
140 |
141 | module.exports = chart_grid_config;
142 |
--------------------------------------------------------------------------------
/src/js/stores/ChartPropertiesStore.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Store the Chart's properties. These are properties relevant to the
3 | * rendering of the chart.
4 | */
5 |
6 | var assign = require("lodash/assign");
7 | var EventEmitter = require("events").EventEmitter;
8 |
9 | /* Flux dispatcher */
10 | var Dispatcher = require("../dispatcher/dispatcher");
11 | var SessionStore = require("./SessionStore");
12 |
13 | /*
14 | * Each chart type has an associated parser, defined in its chartConfig
15 | * settings. The `ChartProptiesStore` is resposible for parsing the input before
16 | * sending parsed data back to the app, so we require the configs here.
17 | */
18 | var chartConfig = require("../charts/chart-type-configs");
19 |
20 | /* Singleton that houses chart props */
21 | var _chartProps = {};
22 | var CHANGE_EVENT = "change";
23 | var chartType;
24 | var newLineRegex = /\r\n|\r|\n/;
25 |
26 | /**
27 | * ### ChartProptiesStore.js
28 | * Flux store for chart properties such as data, settings, scale
29 | */
30 | var ChartPropertiesStore = assign({}, EventEmitter.prototype, {
31 |
32 | emitChange: function() {
33 | this.emit(CHANGE_EVENT);
34 | },
35 |
36 | addChangeListener: function(callback) {
37 | this.on(CHANGE_EVENT, callback);
38 | },
39 |
40 | removeChangeListener: function(callback) {
41 | this.removeListener(CHANGE_EVENT, callback);
42 | },
43 |
44 | /**
45 | * get
46 | * @param k
47 | * @return {any} - Return value at key `k`
48 | * @instance
49 | * @memberof ChartPropertiesStore
50 | */
51 | get: function(k) {
52 | return _chartProps[k];
53 | },
54 |
55 | /**
56 | * getAll
57 | * @return {object} - Return all chartProps
58 | * @instance
59 | * @memberof ChartPropertiesStore
60 | */
61 | getAll: function() {
62 | return _chartProps;
63 | },
64 |
65 | /**
66 | * clear
67 | * Set chartProps to empty
68 | * @instance
69 | * @memberof ChartPropertiesStore
70 | */
71 | clear: function() {
72 | _chartProps = {};
73 | }
74 |
75 | });
76 |
77 | function registeredCallback(payload) {
78 | var action = payload.action;
79 | var parser;
80 | var config;
81 |
82 | switch(action.eventName) {
83 | /*
84 | * Receive a new model, which includes metadata. Respond by parsing input and
85 | * setting current `chartType`.
86 | */
87 | case "receive-model":
88 | Dispatcher.waitFor([SessionStore.dispatchToken]);
89 | chartType = action.model.metadata.chartType;
90 | config = chartConfig[chartType];
91 | parser = chartConfig[chartType].parser;
92 | _chartProps = parser(config, action.model.chartProps);
93 | break;
94 |
95 | /*
96 | * Update all `chartProps`, assuming incoming payload is the entire object
97 | */
98 | case "update-all-chart-props":
99 | parser = chartConfig[chartType].parser;
100 | config = chartConfig[chartType];
101 | parser(config, action.chartProps, function(newProps) {
102 | _chartProps = newProps;
103 | ChartPropertiesStore.emitChange();
104 | });
105 | break;
106 |
107 | /*
108 | * Update a single key in the chartProps. As it is deeply nested the payload
109 | * is likely still an object
110 | */
111 | case "update-chart-prop":
112 | _chartProps[action.key] = action.newProp;
113 | ChartPropertiesStore.emitChange();
114 | break;
115 |
116 | /*
117 | * Update a single key in the chartProps, and also reparse the input and
118 | * send back to the UI
119 | */
120 | case "update-and-reparse":
121 | parser = chartConfig[chartType].parser;
122 | config = chartConfig[chartType];
123 | _chartProps[action.key] = action.newProp;
124 | parser(config, _chartProps, function(newProps) {
125 | _chartProps = newProps;
126 | ChartPropertiesStore.emitChange();
127 | });
128 | break;
129 |
130 | case "update-data-input":
131 | parser = chartConfig[chartType].parser;
132 | config = chartConfig[chartType];
133 |
134 | checkColumnChange(action.newProp.raw, function(columnsChanged) {
135 | _chartProps[action.key] = action.newProp;
136 | var parseOpts = { columnsChanged: columnsChanged };
137 | parser(config, _chartProps, function(newProps) {
138 | _chartProps = newProps;
139 | ChartPropertiesStore.emitChange();
140 | }, parseOpts);
141 | });
142 | break;
143 |
144 | default:
145 | // do nothing
146 | }
147 | return true;
148 | }
149 |
150 | function checkColumnChange(newInput, callback) {
151 | var newCols = newInput.split(newLineRegex)[0];
152 | var oldCols = _chartProps.input.raw.split(newLineRegex)[0];
153 | callback((newCols !== oldCols));
154 | }
155 |
156 | /* Respond to actions coming from the dispatcher */
157 | ChartPropertiesStore.dispatchToken = Dispatcher.register(registeredCallback);
158 |
159 | module.exports = ChartPropertiesStore;
160 |
--------------------------------------------------------------------------------
/src/js/components/shared/HorizontalAxis.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 help = require("../../util/helper.js");
6 | var ordinalAdjust = require("../../util/scale-utils").ordinalAdjust;
7 |
8 | var DY = "0.32em"
9 |
10 | var HorizontalAxis = React.createClass({
11 |
12 | propTypes: {
13 | prefix: PropTypes.string,
14 | suffix: PropTypes.string,
15 | orient: PropTypes.string,
16 | dimensions: PropTypes.object,
17 | xScale: PropTypes.func,
18 | tickValues: PropTypes.array,
19 | tickFormat: PropTypes.func,
20 | textAnchor: PropTypes.string
21 | },
22 |
23 | getInitialState: function() {
24 | return {
25 | lastTickWidth: 0,
26 | firstTickWidth: 0
27 | }
28 | },
29 |
30 | componentDidMount: function() {
31 | this._setTickWidths(this.props);
32 | },
33 |
34 | componentWillReceiveProps: function(nextProps) {
35 | this._setTickWidths(nextProps);
36 | },
37 |
38 | getDefaultProps: function() {
39 | return {
40 | orient: "bottom",
41 | tickFormat: function(d) { return d; },
42 | textAnchor: "middle",
43 | prefix: "",
44 | suffix: ""
45 | }
46 | },
47 |
48 | _setTickWidths: function(props) {
49 | var tickValues = props.tickValues;
50 | var lastTick = props.tickFormat(tickValues[tickValues.length - 1]);
51 | var firstTick = props.prefix + props.tickFormat(tickValues[0]);
52 | var lastTickWidth;
53 | var firstTickWidth;
54 |
55 | switch (props.textAnchor) {
56 | case 'middle':
57 | lastTickWidth = help.computeTextWidth(lastTick, props.tickFont) / 2;
58 | firstTickWidth = help.computeTextWidth(firstTick, props.tickFont) / 2;
59 | break;
60 | case 'start':
61 | lastTickWidth = help.computeTextWidth(lastTick, props.tickFont);
62 | firstTickWidth = 0;
63 | break;
64 | case 'end':
65 | lastTickWidth = 0;
66 | firstTickWidth = help.computeTextWidth(firstTick, props.tickFont);
67 | break;
68 | default:
69 | lastTickWidth = 0;
70 | firstTickWidth = 0;
71 | break;
72 | }
73 |
74 | if ((lastTickWidth !== this.state.lastTickWidth) || (firstTickWidth !== this.state.firstTickWidth)) {
75 | this.setState({
76 | lastTickWidth: lastTickWidth,
77 | firstTickWidth: firstTickWidth
78 | });
79 | };
80 | },
81 |
82 | _getTransformY: function(orient, height, yScale) {
83 | var yRange;
84 | if (yScale.rangeExtent) {
85 | yRange = yScale.rangeExtent();
86 | } else {
87 | yRange = yScale.range();
88 | }
89 |
90 | if (orient === "top") {
91 | return yRange[1];
92 | } else if (orient === "bottom") {
93 | return yRange[0];
94 | }
95 | },
96 |
97 | _generateTicks: function(props) {
98 | var lastTickWidth = this.state.lastTickWidth;
99 |
100 | return map(props.tickValues, function(tickValue, i) {
101 | var text;
102 | var formatted = props.tickFormat(tickValue)
103 | var xVal = ordinalAdjust(props.xScale, tickValue);
104 |
105 | // offset a tick label that is over the edge
106 | if (xVal + lastTickWidth > props.dimensions.width) {
107 | xVal += (props.dimensions.width - (xVal + lastTickWidth));
108 | }
109 |
110 | if (i === 0) {
111 | text = [props.prefix, formatted].join("");
112 | } else {
113 | text = formatted;
114 | }
115 |
116 | return (
117 |
125 | {text}
126 |
127 | )
128 | });
129 | },
130 |
131 | _generateSuffix: function(props) {
132 | if (props.suffix !== "") {
133 | var suffX = props.xScale(props.tickValues[0]);
134 | var suffY = props.tickTextHeight + 10; // TODO: remove hardcodes
135 | return (
136 |
144 | {props.suffix}
145 |
146 | )
147 | } else {
148 | return null;
149 | }
150 | },
151 |
152 | render: function() {
153 | var props = this.props;
154 | var ticks = this._generateTicks(props);
155 | var suffix = this._generateSuffix(props);
156 | var transformY = this._getTransformY(props.orient, props.dimensions.height, props.yScale);
157 |
158 | return (
159 |
164 | {ticks}
165 | {suffix}
166 |
167 | );
168 | }
169 |
170 | });
171 |
172 | module.exports = HorizontalAxis;
173 |
--------------------------------------------------------------------------------
/src/js/util/scale-utils.js:
--------------------------------------------------------------------------------
1 | var d3 = require("d3");
2 | var d3scale = require("d3-scale");
3 | var clone = require("lodash/clone");
4 | var assign = require("lodash/assign");
5 | var reduce = require("lodash/reduce");
6 | var map = require("lodash/map");
7 | var processDates = require("./process-dates");
8 | var help = require("./helper");
9 |
10 | var scale_types = {
11 | "linear": _linearScale,
12 | "ordinal": _ordinalScale,
13 | "time": _timeScale
14 | };
15 |
16 | var defaultPadding = {
17 | inner: 0.2,
18 | outer: 0.1
19 | };
20 |
21 | /**
22 | * generateScale
23 | *
24 | * @param type
25 | * @param scaleOptions
26 | * @param data
27 | * @param range
28 | * @returns {scale: d3 scale, tickValues: array, tickFormat: format func}
29 | */
30 | function generate_scale(type, scaleOptions, data, range, additionalOpts) {
31 | if (!scaleOptions) return {};
32 | return scale_types[type](scaleOptions, data, range, additionalOpts);
33 | }
34 |
35 | function _timeScale(scaleOptions, data, range) {
36 | // Return the ticks used for a time scale based on the time span and settings
37 | var formatAndFreq = {};
38 | var dateFormat = scaleOptions.dateFormat;
39 | var dateFrequency = scaleOptions.dateFrequency;
40 |
41 | // grab the first series to get the first/last dates
42 | var firstSeries = data[0].values;
43 | // make sure dates are in chronological order
44 | var dateRange = [
45 | firstSeries[0].entry,
46 | firstSeries[firstSeries.length - 1].entry
47 | ].sort(d3.ascending);
48 |
49 | var minDate = dateRange[0];
50 | var maxDate = dateRange[1];
51 |
52 | if (dateFrequency === "auto" || dateFormat === "auto") {
53 | var autoSettings = processDates.autoDateFormatAndFrequency(minDate, maxDate, dateFormat, range[1]);
54 | }
55 |
56 | if (dateFrequency !== "auto") {
57 | var freqSettings = processDates.dateFrequencies[dateFrequency];
58 | formatAndFreq.frequency = freqSettings(minDate, maxDate);
59 | } else {
60 | formatAndFreq.frequency = autoSettings.frequency;
61 | }
62 | if (dateFormat !== "auto") {
63 | formatAndFreq.format = dateFormat;
64 | } else {
65 | formatAndFreq.format = autoSettings.format;
66 | }
67 |
68 | // use min/max dates if our auto calc comes up with empty array
69 | if (formatAndFreq.frequency.length === 0) {
70 | formatAndFreq.frequency = [minDate, maxDate];
71 | }
72 |
73 | return {
74 | scale: d3.time.scale().range(range).domain([minDate, maxDate]),
75 | tickValues: formatAndFreq.frequency,
76 | tickFormat: processDates.dateParsers[formatAndFreq.format]
77 | };
78 | }
79 |
80 | function _linearScale(scaleOptions, data, range) {
81 | return {
82 | scale: d3.scale.linear().domain(scaleOptions.domain).range(range),
83 | tickValues: scaleOptions.tickValues,
84 | tickFormat: function(d) {
85 | return help.roundToPrecision(d, scaleOptions.precision);
86 | }
87 | };
88 | }
89 |
90 | function _ordinalScale(scaleOptions, data, range, _padding) {
91 | var padding = assign({}, defaultPadding, _padding);
92 |
93 | var entries = map(data[0].values, function(value) {
94 | return value.entry;
95 | });
96 |
97 | var scale = d3scale.scaleBand()
98 | .domain(entries)
99 | .range(range)
100 | .paddingInner(padding.inner)
101 | .paddingOuter(padding.outer);
102 |
103 | return {
104 | scale: scale,
105 | tickValues: entries,
106 | };
107 | }
108 |
109 | //TODO: make this keyed funcs that accept a `type` param
110 | //(like "ordinal", "time", "linear") so that we dont have to check every time
111 | function _ordinalAdjust(scale, value) {
112 | var isOrdinal = scale.hasOwnProperty("bandwidth");
113 | if (isOrdinal) {
114 | return scale(value) + scale.bandwidth() / 2;
115 | } else {
116 | return scale(value);
117 | }
118 | }
119 |
120 | /**
121 | * get_tick_widths
122 | *
123 | * @param scaleOptions object
124 | * @param font string
125 | * @returns {width: [number], max: number}
126 | */
127 | function get_tick_widths(scaleOptions, font) {
128 | if (!scaleOptions) return { width: [], max: 0};
129 |
130 | var numTicks = scaleOptions.tickValues.length - 1;
131 | var formattedTicks = reduce(scaleOptions.tickValues, function(prev, tick, i) {
132 | if (i === numTicks) {
133 | return prev.concat([
134 | scaleOptions.prefix,
135 | help.roundToPrecision(tick, scaleOptions.precision),
136 | scaleOptions.suffix
137 | ].join(""));
138 | } else {
139 | return prev.concat(help.roundToPrecision(tick, scaleOptions.precision));
140 | }
141 | }, []);
142 |
143 | var widths = map(formattedTicks, function(text) {
144 | return help.computeTextWidth(text, font);
145 | });
146 |
147 | return {
148 | widths: widths,
149 | max: d3.max(widths.slice(0, -1)) // ignore the top tick
150 | };
151 | }
152 |
153 | module.exports = {
154 | generateScale: generate_scale,
155 | getTickWidths: get_tick_widths,
156 | ordinalAdjust: _ordinalAdjust
157 | };
158 |
--------------------------------------------------------------------------------
/src/js/util/validate-data-input.js:
--------------------------------------------------------------------------------
1 | // Return input with validation statuses. These get sent to the UI in order
2 | // to prevent drawing and give error messages.
3 |
4 | var map = require("lodash/map");
5 | var max = require("lodash/max");
6 | var filter = require("lodash/filter");
7 | var some = require("lodash/some");
8 | var unique = require("lodash/uniq");
9 | var catchChartMistakes = require("./catch-chart-mistakes");
10 |
11 | types = {
12 | "number": "numeric",
13 | "object": "date",
14 | "string": "ordinal"
15 | };
16 |
17 | var MAX_BYTES = 400000; // Max 400k for chartProps
18 |
19 | function validateDataInput(chartProps) {
20 | var input = chartProps.input.raw;
21 | var series = chartProps.data;
22 | var hasDate = chartProps.scale.hasDate;
23 | var isNumeric = chartProps.scale.isNumeric;
24 | var type = chartProps.input.type;
25 | var scale = chartProps.scale;
26 |
27 | var inputErrors = [];
28 |
29 | // Check whether we have input
30 | if (input.length === 0) {
31 | inputErrors.push("EMPTY");
32 | return inputErrors;
33 | }
34 |
35 | if (series.length && !series[0].values[0].entry) {
36 | // Check that we have at least 1 value col (i.e. minimum header + 1 data col)
37 | inputErrors.push("TOO_FEW_SERIES");
38 | } else if (series.length > 12) {
39 | // Check whether there are too many series
40 | inputErrors.push("TOO_MANY_SERIES");
41 | }
42 |
43 | // Whether a column has a different number of values
44 | var unevenSeries = dataPointTest(
45 | series,
46 | function(val) { return val.value !== null ? (val.value === undefined || val.value.length === 0) : false;},
47 | function(empty,vals) { return empty.length !== vals[0].length;}
48 | );
49 |
50 | if (unevenSeries) {
51 | inputErrors.push("UNEVEN_SERIES");
52 | }
53 |
54 | // Whether a column has something that is NaN but is not nothing (blank) or `null`
55 | var nanSeries = somePointTest(
56 | series,
57 | function(val) {
58 | return (isNaN(val.value) && val.value !== undefined && val.value !== "");
59 | }
60 | );
61 |
62 | if (nanSeries) {
63 | inputErrors.push("NAN_VALUES");
64 | }
65 |
66 | // Are there multiple types of axis entries
67 | var entryTypes = unique(series[0].values.map(function(d){return typeof d.entry;}));
68 | if(entryTypes.length > 1 && !chartProps.input.type) {
69 | inputErrors.push("CANT_AUTO_TYPE");
70 | }
71 |
72 | //Whether an entry column that is supposed to be a Number is not in fact a number
73 | if(isNumeric || chartProps.input.type == "numeric") {
74 | var badNumSeries = somePointTest(
75 | series,
76 | function(val) { return isNaN(val.entry); }
77 | );
78 |
79 | if (badNumSeries) {
80 | inputErrors.push("NAN_VALUES");
81 | }
82 | }
83 |
84 | // Whether an entry column that is supposed to be a date is not in fact a date
85 | if(hasDate || chartProps.input.type == "date") {
86 | var badDateSeries = somePointTest(
87 | series,
88 | function(val) { return !val.entry.getTime || isNaN(val.entry.getTime()); }
89 | );
90 |
91 | if (badDateSeries) {
92 | inputErrors.push("NOT_DATES");
93 | }
94 |
95 | var tz_pattern = /([+-]\d\d:*\d\d)/gi;
96 | var found_timezones = input.match(tz_pattern);
97 | if(found_timezones && found_timezones.length != series[0].values.length) {
98 | inputErrors.push("UNEVEN_TZ");
99 | }
100 | }
101 |
102 | // Whether a column has numbers that should be divided
103 | var largeNumbers = somePointTest(
104 | series,
105 | function(val) { return Math.floor(val.value).toString().length > 4; },
106 | function(largeNums, vals) { return largeNums.length > 0;}
107 | );
108 |
109 | if (largeNumbers) {
110 | inputErrors.push("LARGE_NUMBERS");
111 | }
112 |
113 | // Whether the number of bytes in chartProps exceeds our defined maximum
114 | if (catchChartMistakes.tooMuchData(chartProps)) {
115 | inputErrors.push("TOO_MUCH_DATA");
116 | }
117 |
118 | // Whether axis ticks divide evenly
119 | if (!catchChartMistakes.axisTicksEven(scale.primaryScale)) {
120 | inputErrors.push("UNEVEN_TICKS");
121 | }
122 |
123 | // Whether axis is missing pref and suf
124 | if (catchChartMistakes.noPrefixSuffix(scale.primaryScale)) {
125 | inputErrors.push("NO_PREFIX_SUFFIX");
126 | }
127 |
128 | return inputErrors;
129 |
130 | }
131 |
132 | // Func that checks wheter a single data point failes a test.
133 | // Will return as soon as failure is found
134 | function somePointTest(series, someTest) {
135 | return some(series, function(s) {
136 | return some(s.values, someTest);
137 | });
138 | }
139 |
140 | // Func that checks wheter all data points pass a test
141 | function dataPointTest(series, filterTest, someTest) {
142 | var vals = map(series, function(d,i) {
143 | return filter(d.values, filterTest);
144 | });
145 |
146 | return some(vals, function(n) {
147 | return someTest(n,vals);
148 | });
149 | }
150 |
151 | module.exports = validateDataInput;
152 |
--------------------------------------------------------------------------------
/src/js/charts/cb-xy/xy-config.js:
--------------------------------------------------------------------------------
1 | var ChartConfig = require("../ChartConfig");
2 | var now = new Date();
3 |
4 | /**
5 | * ### Configuration of an XY chart
6 | * @name xy_config
7 | */
8 |
9 | /**
10 | * display
11 | * @static
12 | * @memberof xy_config
13 | * @property {Nem|number} labelRectSize - Size of the legend label rectangle
14 | * @property {Nem|number} labelXMargin - Horiz distance btwn labels
15 | * @property {Nem|number} labelTextMargin - Horiz distance btwn label rect and text
16 | * @property {Nem|number} labelRowHeight - Vert distance btwn rows of labels
17 | * items with colors the appropriate indexed CSS class
18 | * @property {Nem|number} afterTitle - Distance btwn top of title and top of legend or chart
19 | * @property {Nem|number} afterLegend - Distance btwn top of legend and top of chart
20 | * @property {Nem|number} blockerRectOffset - Distance btwn text of axis and its background blocker
21 | * @property {Nem|number} columnPaddingCoefficient - Distance relative to
22 | * width that column charts should be from edge of the chart
23 | * @property {Nem|number} minPaddingOuter - Minimum distance between the
24 | * outside of a chart and a graphical element
25 | * @property {Nem|number} bottomPaddingWithoutFooter - Bottom padding if footer is not drawn
26 | * @property {object} aspectRatio
27 | * @property {number|fraction} aspectRatio.wide
28 | * @property {number|fraction} aspectRatio.longSpot
29 | * @property {number|fraction} aspectRatio.smallSpot
30 | * @property {object} margin - Distances btwn outer chart elements and container
31 | * @property {object} padding - Distances btwn inner chart elements and container
32 | */
33 |
34 | var display = {
35 | labelRectSize: "0.6em",
36 | labelXMargin: "0.6em",
37 | labelTextMargin: "0.3em",
38 | labelRowHeight: "1.2em",
39 | afterTitle: "1.4em",
40 | afterLegend: "1em",
41 | blockerRectOffset: "0.2em",
42 | lineMarkThreshold: 10, // render marks (dots) on lines if data < N
43 | columnOuterPadding: 0.01, // % of width to pad for columns
44 | columnInnerPadding: 0, // % of col group width to pad btwn each
45 | minPaddingOuter: "1em",
46 | bottomPaddingWithoutFooter: "3em",
47 | yAxisOrient: {
48 | primaryScale: "left",
49 | secondaryScale: "right",
50 | },
51 | aspectRatio: {
52 | wide: (9 / 16),
53 | longSpot: (4 / 3),
54 | smallSpot: (3 / 4)
55 | },
56 | margin: {
57 | top: "0.8em",
58 | right: "0.25em",
59 | bottom: "0.15em",
60 | left: "0.25em"
61 | },
62 | padding: {
63 | top: 0,
64 | right: 0,
65 | bottom: "3.5em",
66 | left: 0
67 | }
68 | };
69 |
70 | /**
71 | * @name xy_defaultProps
72 | * @static
73 | * @memberof xy_config
74 | */
75 | var defaultProps = {
76 | /**
77 | * @name chartProps
78 | * @property {object} scale - Default settings for date and primary scales
79 | * @property {array} data
80 | * @property {object} input
81 | * @property {object[]} chartSettings - Default settings for a given series (column) of data
82 | * @property {object} extraPadding - Additional padding. This is a dynamic
83 | * value and is mostly changed within the component itself
84 | * @property {object} _annotations - Additional informative graphical elements
85 | * @property {object} _annotations.labels - If labels are dragged, their
86 | * position settings are saved here
87 | * @property {object[]} _annotations.labels.values - Array of settings for
88 | * dragged labels
89 | * @property {object} mobile - Mobile-specific override settings
90 | * @static
91 | * @memberof xy_defaultProps
92 | */
93 | chartProps: {
94 | scale: {
95 | primaryScale: {
96 | ticks: 5,
97 | precision: 0,
98 | prefix: "",
99 | suffix: ""
100 | },
101 | dateSettings: {
102 | dateFrequency: "auto",
103 | dateFormat: "auto",
104 | inputTZ: null,
105 | displayTZ: "as-entered"
106 | },
107 | numericSettings: {
108 | ticks: null,
109 | precision: 0,
110 | prefix: "",
111 | suffix: ""
112 | }
113 | },
114 | data: [],
115 | input: {},
116 | chartSettings: [
117 | {
118 | altAxis: false,
119 | type: "line",
120 | colorIndex: 0
121 | }
122 | ],
123 | extraPadding: {
124 | top: 0,
125 | right: 0,
126 | bottom: 0,
127 | left: 0
128 | },
129 | _annotations: {
130 | labels: {
131 | hasDragged: false,
132 | values: []
133 | }
134 | },
135 | mobile: {}
136 | },
137 | /**
138 | * @name metadata
139 | * @property {string} chartType
140 | * @property {string} size
141 | * @static
142 | * @memberof xy_defaultProps
143 | */
144 | metadata: {
145 | chartType: 'xy',
146 | title: "",
147 | source: "",
148 | credit: "Made with Chartbuilder",
149 | size: "auto"
150 | }
151 | };
152 |
153 | var xy_config = new ChartConfig({
154 | displayName: "XY Chart",
155 | parser: require("./parse-xy"),
156 | calculateDimensions: require("./xy-dimensions"),
157 | display: display,
158 | defaultProps: defaultProps
159 | });
160 |
161 | module.exports = xy_config;
162 |
--------------------------------------------------------------------------------
/src/js/components/chart-grid/ChartGrid_xScaleSettings.jsx:
--------------------------------------------------------------------------------
1 | var React = require("react");
2 | var PropTypes = React.PropTypes;
3 | var update = require("react-addons-update");
4 |
5 | var clone = require("lodash/clone");
6 |
7 | var chartbuilderUI = require("chartbuilder-ui");
8 | var LabelledTangle = chartbuilderUI.LabelledTangle;
9 | var TextInput = chartbuilderUI.TextInput;
10 | var ScaleReset = require("../shared/ScaleReset.jsx");
11 |
12 | /**
13 | * ### Chart grid xScale settings parent component
14 | * @name ChartGrid_xScaleSettings
15 | * @class
16 | * @property {object} scale - `chartProps.scale` object of the current chart.
17 | * See this component's PropTypes
18 | * @property {function} onUpdate - Pass the updated scale back to the parent
19 | * @property {string} className - CSS class to apply to this component
20 | * @property {string} stepNumber - Number to display in Editor interface
21 | * @example
22 | *
29 | */
30 | var ChartGrid_xScaleSettings = React.createClass({
31 |
32 | propTypes: {
33 | scale: PropTypes.shape({
34 | primaryScale: PropTypes.shape({
35 | domain: PropTypes.arrayOf(React.PropTypes.number),
36 | precision: PropTypes.number,
37 | ticks: PropTypes.number,
38 | prefix: PropTypes.string.isRequired,
39 | suffix: PropTypes.string.isRequired
40 | }),
41 | }).isRequired
42 | },
43 |
44 | /**
45 | * _handleScaleUpdate
46 | * Apply new values to the `scale` object and pass it to the parent's callback
47 | *
48 | * @param {string} k - New scale property's key
49 | * @param {*} v - New scale proptery's value
50 | * @instance
51 | * @memberof ChartGrid_xScaleSettings
52 | */
53 | _handleScaleUpdate: function(k, v) {
54 | var primaryScale = clone(this.props.scale.primaryScale);
55 | primaryScale[k] = v;
56 | var scale = update(this.props.scale, {
57 | $merge: { primaryScale: primaryScale }
58 | });
59 | this.props.onUpdate(scale);
60 | },
61 |
62 | /**
63 | * _handleDomainUpdate
64 | * Update the domain with a new custom maximum or mimimum. Like
65 | * `_handleScaleUpdate` this passes an updated scale object to the parent
66 | *
67 | * @param {string} k - Key of the domain object. Must be `"max"` or `"min"`
68 | * @param {number} v - New domain value
69 | * @instance
70 | * @memberof ChartGrid_xScaleSettings
71 | */
72 | _handleDomainUpdate: function(k, v) {
73 | var scale = clone(this.props.scale, true);
74 | scale.primaryScale.custom = true;
75 | if (k == "min") {
76 | scale.primaryScale.domain[0] = v;
77 | } else if (k == "max") {
78 | scale.primaryScale.domain[1] = v;
79 | }
80 | this.props.onUpdate(scale);
81 | },
82 |
83 | render: function() {
84 | var currScale = this.props.scale.primaryScale;
85 | var domain = currScale.domain;
86 |
87 | var tangleInputs = [
88 | ,
97 |
106 | ];
107 |
108 | var title_block = (
109 |
110 | {this.props.stepNumber}
111 | Set the units, max, and min of the {this.props.axis} axis
112 |
113 | )
114 |
115 | if(this.props.stepNumber == "") {
116 | title_block = (
117 |
118 | Set the units, max, and min of the {this.props.axis} axis
119 |
120 | )
121 | }
122 |
123 | return (
124 |
125 | {title_block}
126 |
132 |
139 |
140 | {tangleInputs}
141 |
142 |
148 |
149 | );
150 | }
151 | });
152 |
153 | module.exports = ChartGrid_xScaleSettings;
154 |
--------------------------------------------------------------------------------