├── .gitignore
├── DataStore.js
├── Makefile
├── Utils.js
├── components
├── BarChart.js
├── ChartGlobalConfig.js
├── ChartSelector.js
├── Editor.js
├── EditorContainer.js
├── ExcelEditor.js
├── LineChart.js
├── MainContainer.js
└── SettingsContainer.js
├── dist
├── CNAME
├── app-c0a285bc3f25568b99.css
├── browser-bundle-fedfac1b3f4ee5e7d388.js
└── index.html
├── icons
├── bar-chart-icon.svg
├── line-chart-icon.svg
├── settings-icon.svg
└── svg-icons-all.svg
├── index.html.tpl
├── index.js
├── package.json
├── sampleData.json
├── styles
└── app.scss
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | node_modules
3 | .sass-cache
4 | styles/*.map
5 | styles/*.css
6 | *.bkp.json
--------------------------------------------------------------------------------
/DataStore.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 | var lz = require('lz-string');
5 | var Utils = require('./Utils');
6 | var sampleJson = require('./sampleData.json');
7 |
8 |
9 | function DataStore (key) {
10 | this.key = key;
11 | // initialize with sample data
12 | this.initialize();
13 | this._events = {};
14 | }
15 |
16 | DataStore.prototype.initialize = function () {
17 | if (this._initFromUrl()) return;
18 | if (localStore(this.key)) {
19 | this.data = localStore(this.key);
20 | }
21 | else {
22 | this._setSampleData();
23 | }
24 | };
25 |
26 | DataStore.prototype._setSampleData = function () {
27 | this.data = _.cloneDeep(sampleJson);
28 | };
29 |
30 | DataStore.prototype._initFromUrl = function () {
31 | // if share url present, init data from url and return true
32 | // if not present, return false
33 | if (!window.location.hash) return false;
34 | var m = /#\/share\/(.+)/.exec(decodeURIComponent(window.location.hash));
35 | if (m[1]) {
36 | try {
37 | var rawData = lz.decompressFromEncodedURIComponent(m[1]);
38 | this.data = JSON.parse(rawData);
39 | }
40 | catch (e) { return false; }
41 | return true;
42 | }
43 | else return false;
44 | };
45 |
46 | DataStore.prototype.subscribe = function (eventType, cb) {
47 | var events = this._events[eventType] || (this._events[eventType] = []);
48 | events.push(cb);
49 | };
50 |
51 | DataStore.prototype.inform = function (eventType) {
52 | var events;
53 | switch (eventType) {
54 | case 'reset':
55 | events = _.reduce(this._events, function (res, val) {
56 | return res.concat(val)
57 | });
58 | break;
59 |
60 | case 'change':
61 | default:
62 | events = this._events[eventType];
63 | break;
64 | }
65 |
66 | events.forEach(function (cb) {
67 | cb();
68 | });
69 | };
70 |
71 | DataStore.prototype.getData = function () {
72 | return this.data;
73 | },
74 |
75 | DataStore.prototype.update = function (newData) {
76 | this.data = newData;
77 | localStore(this.key, this.data);
78 | this.inform('change');
79 | };
80 |
81 | DataStore.prototype.resetData = function () {
82 | window.localStorage.removeItem(this.key);
83 | this._setSampleData();
84 | this.inform('reset');
85 | };
86 |
87 | module.exports = DataStore;
88 |
89 |
90 | // Helper functions
91 |
92 | // save data to localstorage – localStore('my-data')
93 | // get data from localstorage – localStore('my-data', 'foobar')
94 | function localStore (namespace, data) {
95 | if (data) {
96 | return window.localStorage.setItem(namespace, JSON.stringify(data));
97 | }
98 |
99 | var store = window.localStorage.getItem(namespace);
100 | return (store && JSON.parse(store)) || "";
101 | }
102 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | css=$(shell ls dist/*.css)
2 | js=$(shell ls dist/*.js)
3 | css_file=$(notdir $(css))
4 | js_file=$(notdir $(js))
5 | html_file=dist/index.html
6 |
7 | DOMAIN=tinychart.co
8 |
9 | all:
10 | @echo "use 'make build' or 'make deploy'"
11 |
12 | prepare-dist:
13 | @mkdir -p dist
14 | @touch dist/CNAME
15 | @echo $(DOMAIN) > dist/CNAME
16 |
17 | dev-html:
18 | @cp index.html.tpl index.html
19 | @sed -i "" "s/{{MAINCSS}}/styles\/app.css/g" index.html
20 | @sed -i "" "s/{{MAINJS}}/browser-bundle.js/g" index.html
21 |
22 | build: prepare-dist
23 | @echo "building dist version..."
24 | @cp index.html.tpl $(html_file)
25 | @echo "inserting compiled css and js into index.html..."
26 | @sed -i "" "s/{{MAINCSS}}/$(subst /,\\/,${css_file})/g" $(html_file)
27 | @sed -i "" "s/{{MAINJS}}/$(subst /,\\/,${js_file})/g" $(html_file)
28 | @say build done
29 |
30 | deploy: build
31 | @echo "deploying dist/ to gh-pages remote branch..."
32 | git subtree push --prefix dist origin gh-pages
33 | @say deploy done
34 |
35 | .PHONY: prepare-dist build deploy
36 |
--------------------------------------------------------------------------------
/Utils.js:
--------------------------------------------------------------------------------
1 | var lz = require('lz-string');
2 |
3 | module.exports = {
4 |
5 | // SVG helper to generate icon code
6 | // usage:
7 | svgHelper: function (id) {
8 | return " ";
9 | },
10 |
11 | // Chart colors for datasets
12 | colorschemes: [
13 | {
14 | fillColor: "rgba(87,198,185,0.1)",
15 | strokeColor: "rgba(87,198,185,1)",
16 | pointColor: "rgba(87,198,185,1)",
17 | pointStrokeColor: "#fff",
18 | pointHighlightFill: "#fff",
19 | pointHighlightStroke: "rgba(122,178,234,1)",
20 | },
21 | {
22 | fillColor: "rgba(163,142,243,0.1)",
23 | strokeColor: "rgba(163,142,243,1)",
24 | pointColor: "rgba(163,142,243,1)",
25 | pointStrokeColor: "#fff",
26 | pointHighlightFill: "#fff",
27 | pointHighlightStroke: "rgba(163,142,243,1)",
28 | },
29 | {
30 | fillColor: "rgba(233,102,132,0.1)",
31 | strokeColor: "rgba(233,102,132,1)",
32 | pointColor: "rgba(233,102,132,1)",
33 | pointStrokeColor: "#fff",
34 | pointHighlightFill: "#fff",
35 | pointHighlightStroke: "rgba(122,178,234,1)",
36 | },
37 | {
38 | fillColor: "rgba(122,178,234,0.1)",
39 | strokeColor: "rgba(122,178,234,1)",
40 | pointColor: "rgba(122,178,234,1)",
41 | pointStrokeColor: "#fff",
42 | pointHighlightFill: "#fff",
43 | pointHighlightStroke: "rgba(122,178,234,1)",
44 | },
45 | {
46 | fillColor: "rgba(230,187,114,0.1)",
47 | strokeColor: "rgba(230,187,114,1)",
48 | pointColor: "rgba(230,187,114,1)",
49 | pointStrokeColor: "#fff",
50 | pointHighlightFill: "#fff",
51 | pointHighlightStroke: "rgba(230,187,114,1)",
52 | }
53 | ],
54 |
55 | // assign colorschemes to datasets
56 | assignColors: function (input, colors) {
57 | var output = _.cloneDeep(input);
58 | for (var i=0; i < output.datasets.length; i++) {
59 | for (var prop in colors[i]) {
60 | output.datasets[i][prop] = colors[i][prop];
61 | }
62 | }
63 | return output;
64 | },
65 |
66 | // set url from encoded data
67 | setUrl: function (data) {
68 | if (!data) {
69 | return window.history.replaceState({}, 'clear', '/');
70 | }
71 | var compressed = lz.compressToEncodedURIComponent(JSON.stringify(data));
72 | var url = '#/share/' + compressed;
73 | window.history.replaceState({}, 'share', url);
74 | return url;
75 | }
76 |
77 | };
--------------------------------------------------------------------------------
/components/BarChart.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react');
3 | var Chart = require('chart.js/Chart');
4 | var ChartConfig = require('./ChartGlobalConfig');
5 | var Utils = require('../Utils');
6 |
7 |
8 | var BarChart = React.createClass({
9 |
10 | propTypes: {
11 | jsonData: React.PropTypes.object.isRequired,
12 | },
13 |
14 | initializeChart: function (props) {
15 | var el = this.getDOMNode();
16 | var ctx = el.getContext('2d');
17 | Chart.defaults.global = ChartConfig;
18 | var data = Utils.assignColors(props.jsonData, Utils.colorschemes);
19 | el.width = 600;
20 | el.height = 480;
21 | this.chart = new Chart(ctx).Bar(data, {});
22 | },
23 |
24 | componentDidMount: function () {
25 | this.initializeChart(this.props);
26 | },
27 |
28 | componentWillReceiveProps: function (props) {
29 | var chart = this.chart;
30 | chart.destroy();
31 | this.initializeChart(props);
32 | },
33 |
34 | componentWillUnmount: function () {
35 | var chart = this.chart;
36 | chart.destroy();
37 | },
38 |
39 | render: function () {
40 | var style = {
41 | marginTop: window.devicePixelRatio > 1 ? 100 : 50
42 | };
43 | return (
44 |
45 | );
46 | }
47 |
48 | });
49 |
50 | module.exports = BarChart;
51 |
--------------------------------------------------------------------------------
/components/ChartGlobalConfig.js:
--------------------------------------------------------------------------------
1 | // Global configuration option for Chart.js
2 | // assign to Chart.defaults.global before initializing chart
3 |
4 | module.exports = {
5 | // Boolean - Whether to animate the chart
6 | animation: true,
7 |
8 | // Number - Number of animation steps
9 | animationSteps: 60,
10 |
11 | // String - Animation easing effect
12 | animationEasing: "easeOutQuart",
13 |
14 | // Boolean - If we should show the scale at all
15 | showScale: true,
16 |
17 | // Boolean - If we want to override with a hard coded scale
18 | scaleOverride: false,
19 |
20 | // ** Required if scaleOverride is true **
21 | // Number - The number of steps in a hard coded scale
22 | scaleSteps: null,
23 | // Number - The value jump in the hard coded scale
24 | scaleStepWidth: null,
25 | // Number - The scale starting value
26 | scaleStartValue: null,
27 |
28 | // String - Colour of the scale line
29 | scaleLineColor: "rgba(0,0,0,.1)",
30 |
31 | // Number - Pixel width of the scale line
32 | scaleLineWidth: 1,
33 |
34 | // Boolean - Whether to show labels on the scale
35 | scaleShowLabels: true,
36 |
37 | // Interpolated JS string - can access value
38 | scaleLabel: "<%=value%>",
39 |
40 | // Boolean - Whether the scale should stick to integers, not floats even if drawing space is there
41 | scaleIntegersOnly: true,
42 |
43 | // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
44 | scaleBeginAtZero: false,
45 |
46 | // String - Scale label font declaration for the scale label
47 | scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
48 |
49 | // Number - Scale label font size in pixels
50 | scaleFontSize: 12,
51 |
52 | // String - Scale label font weight style
53 | scaleFontStyle: "normal",
54 |
55 | // String - Scale label font colour
56 | scaleFontColor: "#666",
57 |
58 | // Boolean - whether or not the chart should be responsive and resize when the browser does.
59 | responsive: false,
60 |
61 | // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
62 | maintainAspectRatio: true,
63 |
64 | // Boolean - Determines whether to draw tooltips on the canvas or not
65 | showTooltips: true,
66 |
67 | // Array - Array of string names to attach tooltip events
68 | tooltipEvents: ["mousemove", "touchstart", "touchmove"],
69 |
70 | // String - Tooltip background colour
71 | tooltipFillColor: "rgba(84,94,107,0.8)",
72 |
73 | // String - Tooltip label font declaration for the scale label
74 | tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
75 |
76 | // Number - Tooltip label font size in pixels
77 | tooltipFontSize: 14,
78 |
79 | // String - Tooltip font weight style
80 | tooltipFontStyle: "normal",
81 |
82 | // String - Tooltip label font colour
83 | tooltipFontColor: "#fff",
84 |
85 | // String - Tooltip title font declaration for the scale label
86 | tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
87 |
88 | // Number - Tooltip title font size in pixels
89 | tooltipTitleFontSize: 14,
90 |
91 | // String - Tooltip title font weight style
92 | tooltipTitleFontStyle: "bold",
93 |
94 | // String - Tooltip title font colour
95 | tooltipTitleFontColor: "#fff",
96 |
97 | // Number - pixel width of padding around tooltip text
98 | tooltipYPadding: 6,
99 |
100 | // Number - pixel width of padding around tooltip text
101 | tooltipXPadding: 6,
102 |
103 | // Number - Size of the caret on the tooltip
104 | tooltipCaretSize: 8,
105 |
106 | // Number - Pixel radius of the tooltip border
107 | tooltipCornerRadius: 6,
108 |
109 | // Number - Pixel offset from point x to tooltip edge
110 | tooltipXOffset: 10,
111 |
112 | // String - Template string for single tooltips
113 | tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
114 |
115 | // String - Template string for single tooltips
116 | multiTooltipTemplate: "<%= value %>",
117 |
118 | // Function - Will fire on animation progression.
119 | onAnimationProgress: function(){},
120 |
121 | // Function - Will fire on animation completion.
122 | onAnimationComplete: function(){}
123 | };
--------------------------------------------------------------------------------
/components/ChartSelector.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react');
3 | var Utils= require('../Utils');
4 |
5 | var ChartSelector = React.createClass({
6 |
7 | propTypes: {
8 | switchChartType: React.PropTypes.func.isRequired,
9 | types: React.PropTypes.array.isRequired
10 | },
11 |
12 | onSelect: function (newVal) {
13 | this.props.switchChartType(newVal);
14 | },
15 |
16 | render: function () {
17 | var cx = React.addons.classSet;
18 | var FlashMessageClasses = cx({
19 | 'ChartSelector-button': true,
20 | 'is-selected': true,
21 | });
22 |
23 | var options = this.props.types.map(function (type) {
24 | return (
25 |
28 | );
29 | }.bind(this));
30 |
31 | return (
32 |
33 | {options}
34 |
35 | );
36 | }
37 |
38 | });
39 |
40 | module.exports = ChartSelector;
--------------------------------------------------------------------------------
/components/Editor.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react');
3 |
4 | var _ = require('lodash');
5 | var ace = require('brace');
6 |
7 | require('brace/theme/tomorrow_night');
8 | require('brace/mode/json');
9 |
10 | var Editor = React.createClass({
11 |
12 | propTypes: {
13 | newData: React.PropTypes.func.isRequired,
14 | initialData: React.PropTypes.object.isRequired
15 | },
16 |
17 | componentDidMount: function () {
18 | var editor = ace.edit('editor');
19 | this.editor = editor;
20 | editor.getSession().setMode('ace/mode/json');
21 | editor.setTheme('ace/theme/tomorrow_night');
22 |
23 | // paste code in editor as JSON
24 | editor.setValue(JSON.stringify(this.props.initialData, null, '\t'), -1);
25 |
26 | this.initEvents(editor);
27 | },
28 |
29 | componentWillReceiveProps: function (nextProps) {
30 | this.editor.setValue(JSON.stringify(nextProps.initialData, null, '\t'), -1);
31 | },
32 |
33 | initEvents: function (editor) {
34 | var onNewDataDebounced = _.debounce(this.onNewData, 300);
35 | editor.getSession().on('change', function (evt) {
36 | // skip callback if event was triggered using editor.setValue()
37 | if (editor.curOp && editor.curOp.command.name){
38 | onNewDataDebounced(editor.getValue());
39 | }
40 | });
41 | },
42 |
43 | onNewData: function (data) {
44 | // get code as string save it as JS object
45 | this.props.newData(JSON.parse(data));
46 | },
47 |
48 | render: function () {
49 | return (
50 |
51 | );
52 | }
53 |
54 | });
55 |
56 | module.exports = Editor;
--------------------------------------------------------------------------------
/components/EditorContainer.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react');
3 | var Editor = require('./Editor');
4 | var ExcelEditor = require('./ExcelEditor');
5 | var sampleJson = require('../sampleData.json');
6 |
7 | var EditorContainer = React.createClass({
8 |
9 | propTypes: {
10 | store: React.PropTypes.object.isRequired,
11 | newData: React.PropTypes.func.isRequired
12 | },
13 |
14 | getInitialState: function () {
15 | return {
16 | initialData: this.props.store.getData(),
17 | currentEditorMode: 'table'
18 | }
19 | },
20 |
21 | resetEditorData: function () {
22 | this.setState({ initialData: sampleJson });
23 | this.forceUpdate();
24 | },
25 |
26 | updateState: function () {
27 | this.setState({ initialData: this.props.store.getData() });
28 | },
29 |
30 | componentDidMount: function () {
31 | this.props.store.subscribe('reset', this.resetEditorData);
32 | this.props.store.subscribe('change', this.updateState);
33 | },
34 |
35 | shouldComponentUpdate: function (nextProps, nextState) {
36 | // will not render on data change, only if a reset has been triggered
37 | if (nextState.currentEditorMode !== this.state.currentEditorMode) return true;
38 | return false;
39 | },
40 |
41 | switchEditorMode: function (nextMode) {
42 | this.setState({
43 | currentEditorMode: nextMode,
44 | initialData: this.props.store.getData()
45 | });
46 | },
47 |
48 | render: function () {
49 | var editor;
50 | if (this.state.currentEditorMode === 'table') {
51 | editor = ;
52 | }
53 | else if (this.state.currentEditorMode === 'json') {
54 | editor = ;
55 | }
56 |
57 | return (
58 |
59 |
60 |
61 | TABLE
63 | JSON
65 |
66 |
67 | { editor }
68 |
69 | );
70 | }
71 |
72 | });
73 |
74 | module.exports = EditorContainer;
75 |
--------------------------------------------------------------------------------
/components/ExcelEditor.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react');
3 |
4 | var _ = require('lodash');
5 |
6 | var ExcelEditor = React.createClass({
7 |
8 | propTypes: {
9 | newData: React.PropTypes.func.isRequired,
10 | initialData: React.PropTypes.object.isRequired
11 | },
12 |
13 | getInitialState: function () {
14 | return {
15 | nrows: this.props.initialData.datasets[0].data.length,
16 | ncols: this.props.initialData.datasets.length,
17 | data: this.props.initialData
18 | }
19 | },
20 |
21 | componentWillReceiveProps: function (nextProps) {
22 | this.setState({
23 | nrows: nextProps.initialData.datasets[0].data.length,
24 | ncols: nextProps.initialData.datasets.length,
25 | data: nextProps.initialData
26 | });
27 | },
28 |
29 | getLabels: function () {
30 | var headers = document.querySelectorAll('[scope="row"]');
31 | return Array.prototype.map.call(headers, function (th) {
32 | return th.textContent;
33 | });
34 | },
35 |
36 | getTableData: function () {
37 | var data = {};
38 | data.labels = this.getLabels();
39 | data.datasets = [];
40 |
41 | // walk the table in rows and get data
42 | var rows = this.refs.tbody.getDOMNode().querySelectorAll('tr');
43 | Array.prototype.forEach.call(rows, function (row, i) {
44 | var rowCells = row.querySelectorAll('td');
45 | return Array.prototype.map.call(rowCells, function (cell, j) {
46 | if (!data.datasets[j]) {
47 | data.datasets[j] = {};
48 | data.datasets[j].data = [];
49 | }
50 | data.datasets[j].data.push(parseInt(cell.textContent, 10));
51 | });
52 | });
53 |
54 | return data;
55 | },
56 |
57 | _keyShouldUpdate: function (keyCode) {
58 | if (keyCode >= 48 && keyCode <= 90) return true; // [0-9 a-z]
59 | if (keyCode >= 96 && keyCode <= 105) return true; // numpad 0-9
60 | if (keyCode === 8 || keyCode === 46) return true; // backspace and delete
61 | return false;
62 | },
63 |
64 | onKeyUp: function (evt) {
65 | if (this._keyShouldUpdate(evt.keyCode)) {
66 | this.onNewData();
67 | }
68 | },
69 |
70 | onNewData: function (evt) {
71 | this.props.newData(this.getTableData());
72 | },
73 |
74 | addNewRow: function () {
75 | this.setState({
76 | nrows: ++this.state.nrows,
77 | data: this.getTableData()
78 | });
79 | },
80 |
81 | deleteRow: function () {
82 | if (this.state.nrows === 2) return;
83 | this.setState({
84 | nrows: --this.state.nrows,
85 | data: this.getTableData()
86 | }, this.onNewData);
87 | },
88 |
89 | addNewCol: function () {
90 | this.setState({
91 | ncols: ++this.state.ncols,
92 | data: this.getTableData()
93 | });
94 | },
95 |
96 | deleteCol: function () {
97 | if (this.state.ncols === 1) return;
98 | this.setState({
99 | ncols: --this.state.ncols,
100 | data: this.getTableData()
101 | }, this.onNewData);
102 | },
103 |
104 | render: function () {
105 |
106 | // table header
107 | var header = [( )];
108 | for (var j=0; jDATASET {j+1});
110 | }
111 |
112 | // generate table rows from initial data
113 | var rows = [];
114 | for (var i=0; i{this.state.data.labels[i]})
117 | ];
118 | var row = this.state.data.datasets.map(function (dataset, idx) {
119 | if (idx >= this.state.ncols) return;
120 | return (
121 | // workaround with "key" to fix contentEditable issue:
122 | // when resetting, cells didnt update since with contentEditable
123 | // DOM content is different than visible content. Adding a random key
124 | // forces the cell to update every time the parent component rerenders
125 |
126 | {dataset.data[i] ? dataset.data[i] : }
127 |
128 | );
129 | }.bind(this));
130 | if (this.state.ncols > row.length) {
131 | row.push( );
132 | }
133 | rows.push({rowHeader.concat(row)} );
134 | }
135 |
136 | return (
137 |
138 |
139 |
142 |
145 |
146 |
147 |
148 |
149 |
150 | { header }
151 |
152 |
153 |
154 | { rows }
155 |
156 |
157 |
158 |
159 | );
160 | }
161 |
162 | });
163 |
164 | module.exports = ExcelEditor;
165 |
--------------------------------------------------------------------------------
/components/LineChart.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react');
3 | var Chart = require('chart.js/Chart');
4 | var ChartConfig = require('./ChartGlobalConfig');
5 | var Utils = require('../Utils');
6 |
7 |
8 | var LineChart = React.createClass({
9 |
10 | propTypes: {
11 | jsonData: React.PropTypes.object.isRequired,
12 | },
13 |
14 | initializeChart: function (props) {
15 | var el = this.getDOMNode();
16 | var ctx = el.getContext('2d');
17 | Chart.defaults.global = ChartConfig;
18 | var data = Utils.assignColors(props.jsonData, Utils.colorschemes);
19 | el.width = 600;
20 | el.height = 480;
21 | this.chart = new Chart(ctx).Line(data, {});
22 | },
23 |
24 | componentDidMount: function () {
25 | this.initializeChart(this.props);
26 | },
27 |
28 | componentWillReceiveProps: function (props) {
29 | var chart = this.chart;
30 | chart.destroy();
31 | this.initializeChart(props);
32 | },
33 |
34 | componentWillUnmount: function () {
35 | var chart = this.chart;
36 | chart.destroy();
37 | },
38 |
39 | render: function () {
40 | var style = {
41 | marginTop: window.devicePixelRatio > 1 ? 100 : 50
42 | };
43 | return (
44 |
45 | );
46 | }
47 |
48 | });
49 |
50 | module.exports = LineChart;
51 |
--------------------------------------------------------------------------------
/components/MainContainer.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react');
3 | var LineChart = require('./LineChart');
4 | var BarChart = require('./BarChart');
5 | var ChartSelector = require('./ChartSelector');
6 | var Utils = require('../Utils');
7 | var CHART_TYPES = ['line', 'bar'];
8 |
9 |
10 | var MainContainer = React.createClass({
11 |
12 | propTypes: {
13 | store: React.PropTypes.object.isRequired,
14 | currentChartType: React.PropTypes.string.isRequired,
15 | openSettings: React.PropTypes.func.isRequired
16 | },
17 |
18 | getInitialState: function () {
19 | return { data: this.props.store.getData() };
20 | },
21 |
22 | openSettings: function () {
23 | this.props.openSettings();
24 | },
25 |
26 | getStateFromStore: function () {
27 | this.setState({ data: this.props.store.getData()});
28 | },
29 |
30 | shouldComponentUpdate: function (nextProps, nextState) {
31 | if (nextState.data !== this.state.data) return true;
32 | if (nextProps.currentChartType !== this.props.currentChartType) return true;
33 | return false;
34 | },
35 |
36 | componentDidMount: function () {
37 | this.props.store.subscribe('change', this.getStateFromStore);
38 | },
39 |
40 | render: function () {
41 | var currentChart;
42 | if (this.props.currentChartType === 'line')
43 | currentChart = ;
44 | else if (this.props.currentChartType === 'bar')
45 | currentChart = ;
46 |
47 | var cx = React.addons.classSet;
48 | var FlashMessageClasses = cx({
49 | 'FlashMessage': true,
50 | 'is-visible': true,
51 | });
52 |
53 | return (
54 |
55 |
56 | Welcome to Tinychart!
57 |
60 |
61 | {currentChart}
62 |
66 |
67 | );
68 | }
69 |
70 | });
71 |
72 | module.exports = MainContainer;
--------------------------------------------------------------------------------
/components/SettingsContainer.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react/addons');
3 | var ChartSelector = require('./ChartSelector');
4 |
5 | var SettingsContainer = React.createClass({
6 |
7 | propTypes: {
8 | isOpen: React.PropTypes.bool.isRequired,
9 | isSharing: React.PropTypes.bool.isRequired,
10 | types: React.PropTypes.array.isRequired,
11 | switchChartType: React.PropTypes.func.isRequired,
12 | resetData: React.PropTypes.func.isRequired,
13 | shareUrl: React.PropTypes.func.isRequired,
14 | url: React.PropTypes.string.isRequired
15 | },
16 |
17 | componentDidUpdate: function () {
18 | setTimeout(function () {
19 | // wait for canvas rendering before getting the image url
20 | var canvas = document.querySelector('canvas');
21 | var url = canvas.toDataURL();
22 | this.refs.download.getDOMNode().href = url;
23 | }.bind(this), 1000);
24 | },
25 |
26 | shareUrl: function () {
27 | this.props.shareUrl();
28 | var shareButton = this.refs.share.getDOMNode();
29 | shareButton.classList.add('is-sharing');
30 | setTimeout(function () {
31 | shareButton.classList.remove('is-sharing');
32 | }, 2000);
33 | },
34 |
35 | render: function () {
36 | var cx = React.addons.classSet;
37 | var SettingsContainerClasses = cx({
38 | 'SettingsContainer': true,
39 | 'is-open': this.props.isOpen,
40 | 'is-sharing': this.props.isSharing
41 | });
42 | var SettingsInputClasses = cx({
43 | 'SettingsContainer-input': true,
44 | 'is-sharing': this.props.isSharing
45 | });
46 |
47 | return (
48 |
49 |
50 |
51 |
Reset Data
52 |
57 | Share link
58 |
59 |
60 |
Download Chart
61 |
62 |
63 | );
64 | }
65 |
66 | });
67 |
68 | module.exports = SettingsContainer;
--------------------------------------------------------------------------------
/dist/CNAME:
--------------------------------------------------------------------------------
1 | tinychart.co
2 |
--------------------------------------------------------------------------------
/dist/app-c0a285bc3f25568b99.css:
--------------------------------------------------------------------------------
1 | @-webkit-keyframes rotate-wheel{100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes rotate-wheel{100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes rotate-wheel{100%{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);-o-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes saved-inout{from{-webkit-transform:translateY(-3em);-moz-transform:translateY(-3em);-ms-transform:translateY(-3em);-o-transform:translateY(-3em);transform:translateY(-3em)}50%{-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}to{-webkit-transform:translateY(-3em);-moz-transform:translateY(-3em);-ms-transform:translateY(-3em);-o-transform:translateY(-3em);transform:translateY(-3em)}}@-moz-keyframes saved-inout{from{-webkit-transform:translateY(-3em);-moz-transform:translateY(-3em);-ms-transform:translateY(-3em);-o-transform:translateY(-3em);transform:translateY(-3em)}50%{-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}to{-webkit-transform:translateY(-3em);-moz-transform:translateY(-3em);-ms-transform:translateY(-3em);-o-transform:translateY(-3em);transform:translateY(-3em)}}@keyframes saved-inout{from{-webkit-transform:translateY(-3em);-moz-transform:translateY(-3em);-ms-transform:translateY(-3em);-o-transform:translateY(-3em);transform:translateY(-3em)}50%{-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}to{-webkit-transform:translateY(-3em);-moz-transform:translateY(-3em);-ms-transform:translateY(-3em);-o-transform:translateY(-3em);transform:translateY(-3em)}}@-webkit-keyframes editor-in{to{opacity:1;-webkit-transform:translateX(0);-moz-transform:translateX(0);-ms-transform:translateX(0);-o-transform:translateX(0);transform:translateX(0)}}@-moz-keyframes editor-in{to{opacity:1;-webkit-transform:translateX(0);-moz-transform:translateX(0);-ms-transform:translateX(0);-o-transform:translateX(0);transform:translateX(0)}}@keyframes editor-in{to{opacity:1;-webkit-transform:translateX(0);-moz-transform:translateX(0);-ms-transform:translateX(0);-o-transform:translateX(0);transform:translateX(0)}}html,body{width:100%;height:100%;margin:0;padding:0;font-family:"Avenir","Arial",sans-serif}h1,h2,h3,h4,h5{font-weight:300}.u-svgIcon{width:20px;height:20px;cursor:pointer;fill:#75808F;stroke:#75808F}.OuterContainer{height:100%}.App{display:-webkit-box;display:-moz-box;display:box;display:-webkit-flex;display:-moz-flex;display:-ms-flexbox;display:flex;height:100%;overflow:hidden}.App:before{content:'';position:fixed;background:black;top:0;left:0;width:100%;height:100%;z-index:3;visibility:hidden;opacity:0;-webkit-transition:all 300ms ease;-moz-transition:all 300ms ease;transition:all 300ms ease}.App.is-dimmed:before{visibility:visible;opacity:0.3}.MainContainer{-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;-webkit-flex:1;-moz-flex:1;-ms-flex:1;flex:1;z-index:2;background-color:#E0E6EC;color:#E0E6EC}.MainContainer-header{position:relative;padding:1em 1em 1em 2em;overflow:hidden}.MainContainer-footer{position:absolute;bottom:1em;margin-left:2em;color:#3B424A;cursor:pointer}.MainContainer-brand{font-size:1em;font-weight:800;margin-right:1em}.MainContainer-contact{color:#3B424A;text-decoration:none;font-size:0.9em;font-weight:100;opacity:0.7;-webkit-transition:200ms opacity ease;-moz-transition:200ms opacity ease;transition:200ms opacity ease}.MainContainer-contact:hover{opacity:1}.SettingsButton{float:right;cursor:pointer}.SettingsButton>svg{width:25px;height:25px;stroke:#75808F;fill:transparent;-webkit-transition:200ms stroke ease;-moz-transition:200ms stroke ease;transition:200ms stroke ease;pointer-events:none;-webkit-animation:rotate-wheel 3s linear infinite;-moz-animation:rotate-wheel 3s linear infinite;animation:rotate-wheel 3s linear infinite;-webkit-animation-play-state:paused;-moz-animation-play-state:paused;animation-play-state:paused}.SettingsButton:hover>svg{stroke:#545E6B;-webkit-animation-play-state:running;-moz-animation-play-state:running;animation-play-state:running}.SettingsButton:active>svg{stroke:#7AB2EA}.ChartSelector{overflow:hidden;width:100%}.ChartSelector-button{width:50%;background-image:-webkit-linear-gradient(-298deg, #D9DEE5 0%,#E2E7EE 100%);background-image:linear-gradient(28deg, #D9DEE5 0%,#E2E7EE 100%);border:1px solid #B8C6D3;box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.5);padding:1em;outline:none;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ChartSelector-button:first-child{border-radius:4px 0px 0px 4px}.ChartSelector-button:last-child{border-radius:0px 4px 4px 0px}.ChartSelector-button:hover{background-image:-webkit-linear-gradient(-298deg, #ECEEF1 0%,#DDE3EA 100%);background-image:linear-gradient(28deg, #ECEEF1 0%,#DDE3EA 100%);box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.3)}.ChartSelector-button:hover>svg{fill:#545E6B;stroke:#545E6B}.ChartSelector-button:active{background-image:-webkit-linear-gradient(-298deg, #E4E9EE 0%,#EAEEF2 100%);background-image:linear-gradient(28deg, #E4E9EE 0%,#EAEEF2 100%);box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.3)}.ChartSelector-button:active>svg{fill:#4996E3;stroke:#4996E3}.EditorContainer{height:100%;width:30%;min-width:500px;background-color:#262a2e;color:#d9dfe2}.EditorNav{height:10%;min-height:60px;display:-webkit-box;display:-moz-box;display:box;display:-webkit-flex;display:-moz-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-moz-box-orient:horizontal;box-orient:horizontal;-webkit-flex-direction:row;-moz-flex-direction:row;flex-direction:row;-ms-flex-direction:row;-webkit-box-pack:center;-moz-box-pack:center;box-pack:center;-webkit-justify-content:center;-moz-justify-content:center;-ms-justify-content:center;-o-justify-content:center;justify-content:center;-ms-flex-pack:center}.EditorNav-list{list-style:none;cursor:pointer;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.EditorNav-elem{display:inline-block;padding:4px 8px;font-size:0.8em;color:#96A6AD;background:rgba(59,66,74,0.75);background-image:-webkit-linear-gradient(-90deg, rgba(255,255,255,0.05) 0%,rgba(255,255,255,0) 100%);background-image:linear-gradient(-180deg, rgba(255,255,255,0.05) 0%,rgba(255,255,255,0) 100%);border:1px solid rgba(26,29,33,0.75);box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.02)}.EditorNav-elem:first-child{border-radius:4px 0 0 4px}.EditorNav-elem:last-child{border-radius:0 4px 4px 0}.EditorNav-elem:hover{color:#E2E7EB;background:#3B424A;background-image:-webkit-linear-gradient(-90deg, rgba(255,255,255,0.05) 0%,rgba(255,255,255,0) 100%);background-image:linear-gradient(-180deg, rgba(255,255,255,0.05) 0%,rgba(255,255,255,0) 100%);border:1px solid rgba(26,29,33,0.75);box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.02)}.EditorNav-elem:active{color:#6BB6C4;background:rgba(58,66,73,0.75);background-image:-webkit-linear-gradient(-90deg, rgba(0,0,0,0.1) 0%,rgba(255,255,255,0) 100%);background-image:linear-gradient(-180deg, rgba(0,0,0,0.1) 0%,rgba(255,255,255,0) 100%);border:1px solid rgba(26,29,33,0.75);box-shadow:inset 0px -1px 0px 0px rgba(255,255,255,0.02)}.EditorNav-elem.is-selected{color:#191C1F;background-image:-webkit-linear-gradient(-115deg, #6BB6C4 0%,#6B9AD3 100%);background-image:linear-gradient(-155deg, #6BB6C4 0%,#6B9AD3 100%);border:1px solid rgba(26,29,33,0.75);box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.12)}.JsonEditor{width:100%;height:90%;overflow:auto;opacity:0;z-index:1;-webkit-transform:translateX(-1em);-moz-transform:translateX(-1em);-ms-transform:translateX(-1em);-o-transform:translateX(-1em);transform:translateX(-1em);-webkit-animation:editor-in 600ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;-moz-animation:editor-in 600ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;animation:editor-in 600ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards}#editor.ace_editor{background-color:#262a2e}#editor .ace_gutter{background-color:#262a2e}#editor .ace_variable{color:#6BB6C4}#editor .ace_string{color:#8091C6}#editor .ace_constant.ace_numeric{color:#E2E7EB}#editor .ace_gutter{color:#3B424A}.Chart{display:block;margin:0 auto;margin-top:50px}.SettingsContainer{position:fixed;display:block;width:250px;height:306px;top:2.9em;right:1.7em;border-radius:4px;background:#E0E6EC;opacity:0;z-index:3;color:#75808F;box-shadow:0 8px 13px rgba(0,0,0,0.36),0 0 0 1px rgba(0,0,0,0.06);-webkit-transition:300ms all cubic-bezier(0.34, 1.61, 0.7, 1);-moz-transition:300ms all cubic-bezier(0.34, 1.61, 0.7, 1);transition:300ms all cubic-bezier(0.34, 1.61, 0.7, 1);-webkit-transform:scale(0.7) translateY(-25%) translateX(25%);-moz-transform:scale(0.7) translateY(-25%) translateX(25%);-ms-transform:scale(0.7) translateY(-25%) translateX(25%);-o-transform:scale(0.7) translateY(-25%) translateX(25%);transform:scale(0.7) translateY(-25%) translateX(25%)}.SettingsContainer.is-open{opacity:1;-webkit-transform:scale(1) translateY(0);-moz-transform:scale(1) translateY(0);-ms-transform:scale(1) translateY(0);-o-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}.SettingsContainer.is-sharing{height:346px}.SettingsContainer-content{display:-webkit-box;display:-moz-box;display:box;display:-webkit-flex;display:-moz-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-moz-box-orient:vertical;box-orient:vertical;-webkit-flex-direction:column;-moz-flex-direction:column;flex-direction:column;-ms-flex-direction:column;-webkit-box-pack:justify;-moz-box-pack:justify;box-pack:justify;-webkit-justify-content:space-between;-moz-justify-content:space-between;-ms-justify-content:space-between;-o-justify-content:space-between;justify-content:space-between;-ms-flex-pack:justify;-webkit-box-align:center;-moz-box-align:center;box-align:center;-webkit-align-items:center;-moz-align-items:center;-ms-align-items:center;-o-align-items:center;align-items:center;-ms-flex-align:center;position:relative;padding:2em 2em}.SettingsContainer-button{display:block;position:relative;width:100%;height:46px;line-height:46px;margin-top:1.2em;padding:0;outline:none;cursor:pointer;border-radius:4px;font-size:1em;font-family:"Avenir","Arial",sans-serif;text-decoration:none;text-align:center;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.SettingsContainer-button--default{background-image:-webkit-linear-gradient(-298deg, #D9DEE5 0%,#E2E7EE 100%);background-image:linear-gradient(28deg, #D9DEE5 0%,#E2E7EE 100%);border:1px solid #B8C6D3;box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.5);color:#75808F}.SettingsContainer-button--default:hover{background-image:-webkit-linear-gradient(-298deg, #ECEEF1 0%,#DDE3EA 100%);background-image:linear-gradient(28deg, #ECEEF1 0%,#DDE3EA 100%);box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.3);color:#545E6B}.SettingsContainer-button--default:active{background-image:-webkit-linear-gradient(-298deg, #E4E9EE 0%,#EAEEF2 100%);background-image:linear-gradient(28deg, #E4E9EE 0%,#EAEEF2 100%);box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.3);color:#4996E3}.SettingsContainer-button--blue{background-image:-webkit-linear-gradient(-115deg, #58ACD9 0%,#7BA8FF 100%);background-image:linear-gradient(-155deg, #58ACD9 0%,#7BA8FF 100%);border:1px solid #719BEC;box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.15);color:#FFFFFF}.SettingsContainer-button--blue:hover{background-image:-webkit-linear-gradient(-115deg, #58ACD9 0%,#7BA8FF 50%);background-image:linear-gradient(-155deg, #58ACD9 0%,#7BA8FF 50%);box-shadow:inset 0px 1px 0px 0px rgba(255,255,255,0.12)}.SettingsContainer-button--blue:active{background-image:-webkit-linear-gradient(-294deg, #58ACD9 0%,#7BA8FF 100%);background-image:linear-gradient(24deg, #58ACD9 0%,#7BA8FF 100%);box-shadow:inset 0px -1px 0px 0px rgba(255,255,255,0.12)}.SettingsContainer-input{box-sizing:border-box;position:relative;width:100%;height:0;opacity:0;margin-top:-3px;padding:3px 0.4em 0 0.4em;outline:none;cursor:text;font-size:0.8em;color:#75808F;font-family:"Avenir","Arial",sans-serif;background:#FFF;border:1px solid #B8C6D3;border-radius:0 0 4px 4px;-webkit-transition:400ms all cubic-bezier(0.34, 1.61, 0.7, 1);-moz-transition:400ms all cubic-bezier(0.34, 1.61, 0.7, 1);transition:400ms all cubic-bezier(0.34, 1.61, 0.7, 1)}.SettingsContainer-input.is-sharing{height:40px;opacity:1}#share>span,#share::after{display:block;-webkit-transition:-webkit-transform 0.3s,opacity 0.3s;-moz-transition:-moz-transform 0.3s,opacity 0.3s;transition:transform 0.3s,opacity 0.3s;-webkit-transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1);-moz-transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1);transition-timing-function:cubic-bezier(0.2, 1, 0.3, 1)}#share::after{content:attr(data-text);color:#57C6B9;position:absolute;top:0;width:100%;opacity:0;-webkit-transform:translateY(25%);-moz-transform:translateY(25%);-ms-transform:translateY(25%);-o-transform:translateY(25%);transform:translateY(25%)}#share.is-sharing>span{opacity:0;-webkit-transform:translateY(-25%);-moz-transform:translateY(-25%);-ms-transform:translateY(-25%);-o-transform:translateY(-25%);transform:translateY(-25%)}#share.is-sharing::after{opacity:1;-webkit-transform:translateY(0);-moz-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}.FlashMessage{color:#545E6B;float:left;-webkit-transform:translateY(-3em);-moz-transform:translateY(-3em);-ms-transform:translateY(-3em);-o-transform:translateY(-3em);transform:translateY(-3em)}.FlashMessage.is-visible{-webkit-animation:saved-inout 4s cubic-bezier(0.86, 0, 0.07, 1) 0.2s forwards;-moz-animation:saved-inout 4s cubic-bezier(0.86, 0, 0.07, 1) 0.2s forwards;animation:saved-inout 4s cubic-bezier(0.86, 0, 0.07, 1) 0.2s forwards}.ExcelEditor{width:100%;height:90%;display:block;overflow:auto;z-index:1;-webkit-transform:translateX(1em);-moz-transform:translateX(1em);-ms-transform:translateX(1em);-o-transform:translateX(1em);transform:translateX(1em);opacity:0;-webkit-animation:editor-in 600ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;-moz-animation:editor-in 600ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards;animation:editor-in 600ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards}.ExcelEditor-controls{text-align:center;margin-top:3em;margin-bottom:2em;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}.ExcelEditor-controls:hover .ExcelEditor-control{color:#E2E7EB}.ExcelEditor-control{display:inline-block;color:#96A6AD;opacity:0.6;margin:0 1em;-webkit-transition:300ms all ease;-moz-transition:300ms all ease;transition:300ms all ease}.ExcelEditor-control a{font-size:1.1em;font-weight:600;cursor:pointer}.ExcelEditor-control a:first-child{margin-right:0.4em}.ExcelEditor-control a:last-child{margin-left:0.4em}.ExcelEditor-control a:active{color:#6BB6C4}.ExcelEditor-control span{font-size:0.8em}.ExcelEditor-wrapper{display:block;text-align:center}.ExcelEditor-table{margin:0 auto;border-spacing:0.5em;color:#96A6AD}.ExcelEditor-table thead th{font-weight:300;font-size:0.7em}.ExcelEditor-header{background:#1A1D21;min-width:3.5em;border:2px solid transparent;border-radius:4px;outline:none;padding:0.2em 0.7em;font-weight:300;line-height:1.4em;-webkit-transition:200ms all ease;-moz-transition:200ms all ease;transition:200ms all ease}.ExcelEditor-header:hover{color:#E2E7EB;border:2px solid #444f58}.ExcelEditor-header:focus{color:#6BB6C4;border:2px solid #6BB6C4}.ExcelEditor-cell{min-width:3em;text-align:center;background:#1A1D21;border:2px solid transparent;border-radius:4px;outline:none;padding:0.2em 0.7em;font-weight:300;line-height:1.4em;-webkit-transition:200ms all ease;-moz-transition:200ms all ease;transition:200ms all ease}.ExcelEditor-cell:hover{color:#E2E7EB;border:2px solid #444f58}.ExcelEditor-cell:focus{color:#6BB6C4;border:2px solid #6BB6C4}
2 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tinychart – simple, ready-to-go charts
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
24 |
25 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/icons/bar-chart-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | bar-chart-icon
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/icons/line-chart-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Untitled 2
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/icons/settings-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Oval 1
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/icons/svg-icons-all.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/index.html.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tinychart – simple, ready-to-go charts
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
24 |
25 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 | var React = require('react/addons');
3 | var EditorContainer = require('./components/EditorContainer');
4 | var MainContainer = require('./components/MainContainer');
5 | var SettingsContainer = require('./components/SettingsContainer');
6 | var DataStore = require('./DataStore');
7 | var Utils = require('./Utils');
8 | var CHART_TYPES = ['line', 'bar'];
9 |
10 | var icons = require('!!raw!./icons/svg-icons-all.svg');
11 |
12 | var App = React.createClass({
13 |
14 | getInitialState: function () {
15 | return {
16 | settingsIsOpen: false,
17 | settingsIsSharing: false,
18 | currentChartType: 'line',
19 | url: ''
20 | };
21 | },
22 |
23 | newData: function (data) {
24 | this.props.store.update(data);
25 | this.setState({ settingsIsSharing: false, url: ''});
26 | Utils.setUrl('');
27 | ga('send', 'event', 'user', 'update');
28 | },
29 |
30 | resetData: function () {
31 | this.props.store.resetData();
32 | this.setState({ settingsIsSharing: false, url: ''});
33 | Utils.setUrl('');
34 | ga('send', 'event', 'user', 'reset');
35 | },
36 |
37 | shareUrl: function () {
38 | var url = Utils.setUrl(this.props.store.getData());
39 | var fullUrl = window.location.origin + '/' + url;
40 | this.setState({ settingsIsSharing: true, url: fullUrl });
41 | ga('send', 'event', 'user', 'share', url);
42 | },
43 |
44 | openSettings: function () {
45 | this.setState({ settingsIsOpen: true });
46 | },
47 |
48 | switchChartType: function (toType) {
49 | this.setState({ currentChartType: toType });
50 | },
51 |
52 | closeSettings: function (evt) {
53 | if (this.state.settingsIsOpen) this.setState({ settingsIsOpen: false });
54 | },
55 |
56 | render: function () {
57 | var cx = React.addons.classSet;
58 | var AppClasses = cx({
59 | 'App': true,
60 | 'is-dimmed': this.state.settingsIsOpen
61 | });
62 |
63 | return (
64 |
65 |
66 |
67 |
68 |
69 |
70 |
77 |
78 | );
79 | }
80 |
81 | });
82 |
83 | var store = new DataStore('chartpad');
84 |
85 | React.render( , document.body);
86 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Tinychart",
3 | "version": "0.6.0",
4 | "description": "A simple tool to create quick exportable charts",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "npm run watch-js & npm run watch-css",
8 | "clean": "rm -f browser-bundle.js index.html",
9 | "prewatch": "make dev-html",
10 | "watch": "npm run watch-js & npm run watch-css",
11 | "watch-js": "webpack --progress --colors --watch",
12 | "watch-css": "sass --watch styles/",
13 | "prebuild": "rm -rf dist/ && mkdir -p dist/",
14 | "build": "npm run build-css & npm run build-js",
15 | "build-css": "sass --style compressed --sourcemap=none styles/app.scss | hashmark -l 18 dist/app-{hash}.css --silent",
16 | "build-js": "NODE_ENV=production webpack -p --config webpack.config.js",
17 | "postbuild": "make build"
18 | },
19 | "author": "Victor Delgado",
20 | "license": "MIT",
21 | "dependencies": {
22 | "brace": "^0.4.0",
23 | "chart.js": "^1.0.1-beta.2",
24 | "lodash": "^2.4.1",
25 | "lz-string": "^1.4.1",
26 | "react": "^0.12.1"
27 | },
28 | "devDependencies": {
29 | "hashmark": "^2.0.1",
30 | "json-loader": "^0.5.1",
31 | "jsx-loader": "^0.12.2",
32 | "node-bourbon": "^1.2.3",
33 | "raw-loader": "^0.5.1",
34 | "webpack": "^1.4.13",
35 | "webpack-dev-server": "^1.7.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/sampleData.json:
--------------------------------------------------------------------------------
1 | {
2 | "labels": ["January", "February", "March", "April", "May", "June", "July"],
3 | "datasets": [
4 | {
5 | "data": [65, 59, 80, 81, 56, 55, 40]
6 | },
7 | {
8 | "data": [28, 48, 40, 19, 86, 27, 90]
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/styles/app.scss:
--------------------------------------------------------------------------------
1 | @import '../node_modules/node-bourbon/assets/stylesheets/bourbon';
2 |
3 | /* variables */
4 | $font: 'Avenir', 'Arial', sans-serif;
5 | $radius: 4px;
6 |
7 | /* colors - light side */
8 | $light-bg-grey: #E0E6EC;
9 | $dark-bg-grey: #262a2e;
10 | $purple: #8091C6;
11 | $green: #6BB6C4;
12 | $white: #FFF;
13 | $light-grey4: #E2E7EB;
14 | $light-grey2: #3B424A;
15 | $darkest-grey: #545E6B;
16 | $icon-dark-grey: #75808F;
17 | $icon-hover: #545E6B;
18 | $icon-active: #7AB2EA;
19 | $positive-green: #57C6B9;
20 |
21 | /* colors - dark side*/
22 | $black: #1A1D21;
23 | $table-text: #96A6AD;
24 | $table-text-hover: #E2E7EB;
25 | $table-text-active: #6BB6C4;
26 |
27 | /* animations */
28 | @include keyframes(rotate-wheel) { 100% { @include transform(rotate(360deg)); } }
29 | @include keyframes(saved-inout) {
30 | from { @include transform(translateY(-3em)); }
31 | 50% { @include transform(translateY(0)); }
32 | to { @include transform(translateY(-3em)); }
33 | }
34 | @include keyframes(editor-in) {
35 | to { opacity: 1; @include transform(translateX(0)); }
36 | }
37 |
38 | html, body {
39 | width: 100%;
40 | height: 100%;
41 |
42 | margin: 0;
43 | padding: 0;
44 | font-family: $font;
45 | }
46 | h1, h2, h3, h4, h5 {
47 | font-weight: 300;
48 | }
49 |
50 | .u-svgIcon {
51 | width: 20px;
52 | height: 20px;
53 | cursor: pointer;
54 | fill: #75808F;
55 | stroke: #75808F;
56 | }
57 | .OuterContainer {
58 | height: 100%;
59 | }
60 | .App {
61 | @include display(flex);
62 | height: 100%;
63 | overflow: hidden;
64 | }
65 | .App:before {
66 | content: '';
67 | position: fixed;
68 | background: black;
69 | top:0;
70 | left:0;
71 | width:100%;
72 | height:100%;
73 | z-index: 3;
74 | visibility: hidden;
75 | opacity: 0;
76 | @include transition(all 300ms ease);
77 | }
78 | .App.is-dimmed:before {
79 | visibility: visible;
80 | opacity: 0.3;
81 | }
82 |
83 | .MainContainer {
84 | @include flex(1);
85 | z-index: 2;
86 | background-color: $light-bg-grey;
87 | color: #E0E6EC;
88 | }
89 | .MainContainer-header {
90 | position: relative;
91 | padding: 1em 1em 1em 2em;
92 | overflow: hidden;
93 | }
94 | .MainContainer-footer {
95 | position: absolute;
96 | bottom: 1em;
97 | margin-left: 2em;
98 | color: $light-grey2;
99 | cursor: pointer;
100 | }
101 | .MainContainer-brand {
102 | font-size: 1em;
103 | font-weight: 800;
104 | margin-right: 1em;
105 | }
106 | .MainContainer-contact {
107 | color: $light-grey2;
108 | text-decoration: none;
109 | font-size: 0.9em;
110 | font-weight: 100;
111 | opacity: 0.7;
112 | @include transition(200ms opacity ease);
113 |
114 | &:hover {
115 | opacity: 1;
116 | }
117 | }
118 |
119 | .SettingsButton {
120 | float: right;
121 | cursor: pointer;
122 |
123 | & > svg {
124 | width: 25px;
125 | height: 25px;
126 | stroke: $icon-dark-grey;
127 | fill: transparent;
128 | @include transition(200ms stroke ease);
129 | pointer-events: none;
130 |
131 | @include animation(rotate-wheel 3s linear infinite);
132 | @include animation-play-state(paused);
133 |
134 | }
135 | &:hover > svg {
136 | stroke: $icon-hover;
137 | @include animation-play-state(running);
138 | }
139 |
140 | &:active > svg {
141 | stroke: $icon-active;
142 | }
143 | }
144 | .ChartSelector {
145 | overflow: hidden;
146 | width: 100%;
147 | }
148 | .ChartSelector-button {
149 | width: 50%;
150 | @include background-image(linear-gradient(28deg, #D9DEE5 0%, #E2E7EE 100%));
151 | border: 1px solid #B8C6D3;
152 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.50);
153 | padding: 1em;
154 | outline: none;
155 | cursor: pointer;
156 | @include user-select(none);
157 |
158 | &:first-child {
159 | border-radius: $radius 0px 0px $radius;
160 | }
161 | &:last-child {
162 | border-radius: 0px $radius $radius 0px;
163 | }
164 | &:hover {
165 | @include background-image(linear-gradient(28deg, #ECEEF1 0%, #DDE3EA 100%));
166 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.30);
167 | & > svg{
168 | fill: #545E6B;
169 | stroke: #545E6B;
170 | }
171 | }
172 | &:active {
173 | @include background-image(linear-gradient(28deg, #E4E9EE 0%, #EAEEF2 100%));
174 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.30);
175 | & > svg{
176 | fill: #4996E3;
177 | stroke: #4996E3;
178 | }
179 | }
180 | }
181 |
182 | .EditorContainer {
183 | height: 100%;
184 | width: 30%;
185 | min-width: 500px;
186 |
187 | background-color: $dark-bg-grey;
188 | color: #d9dfe2;
189 | }
190 |
191 | .EditorNav {
192 | height: 10%;
193 | min-height: 60px;
194 | @include display(flex);
195 | @include flex-direction(row);
196 | @include justify-content(center);
197 | }
198 |
199 | .EditorNav-list {
200 | list-style: none;
201 | cursor: pointer;
202 | padding: 0;
203 | @include user-select(none);
204 | }
205 |
206 | .EditorNav-elem {
207 | display: inline-block;
208 | padding: 4px 8px;
209 | font-size: 0.8em;
210 | color: $table-text;
211 | background: rgba(59,66,74,0.75);
212 | @include background-image(linear-gradient(-180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.00) 100%));
213 | border: 1px solid rgba(26,29,33,0.75);
214 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.02);
215 |
216 | &:first-child {
217 | border-radius: $radius 0 0 $radius;
218 | }
219 | &:last-child {
220 | border-radius: 0 $radius $radius 0;
221 | }
222 |
223 | &:hover {
224 | color: #E2E7EB;
225 | background: #3B424A;
226 | @include background-image(linear-gradient(-180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.00) 100%));
227 | border: 1px solid rgba(26,29,33,0.75);
228 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.02);
229 | }
230 | &:active {
231 | color: $green;
232 | background: rgba(58,66,73,0.75);
233 | @include background-image(linear-gradient(-180deg, rgba(0,0,0,0.10) 0%, rgba(255,255,255,0.00) 100%));
234 | border: 1px solid rgba(26,29,33,0.75);
235 | box-shadow: inset 0px -1px 0px 0px rgba(255,255,255,0.02);
236 | }
237 | }
238 |
239 | .EditorNav-elem.is-selected {
240 | color: #191C1F;
241 | @include background-image(linear-gradient(-155deg, #6BB6C4 0%, #6B9AD3 100%));
242 | border: 1px solid rgba(26,29,33,0.75);
243 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.12);
244 | }
245 |
246 | .JsonEditor {
247 | width: 100%;
248 | height: 90%;
249 | overflow: auto;
250 | opacity: 0;
251 | z-index: 1;
252 | @include transform(translateX(-1em));
253 | @include animation(editor-in 600ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards);
254 | }
255 |
256 | /* custom theme for ace editor */
257 | #editor.ace_editor {
258 | background-color: $dark-bg-grey;
259 | }
260 | #editor .ace_gutter {
261 | background-color: $dark-bg-grey;
262 | }
263 | #editor .ace_variable {
264 | color: $green;
265 | }
266 | #editor .ace_string {
267 | color: $purple;
268 | }
269 | #editor .ace_constant.ace_numeric {
270 | color: $light-grey4;
271 | }
272 | #editor .ace_gutter {
273 | color: $light-grey2;
274 | }
275 |
276 | .Chart {
277 | display: block;
278 | margin: 0 auto;
279 | margin-top: 50px;
280 | }
281 |
282 | .SettingsContainer {
283 | position: fixed;
284 | display: block;
285 | width: 250px;
286 | height: 306px;
287 | top: 2.9em;
288 | right: 1.7em;
289 | border-radius: $radius;
290 | background: $light-bg-grey;
291 | opacity: 0;
292 | z-index: 3;
293 | color: $icon-dark-grey;
294 | box-shadow: 0 8px 13px rgba(0,0,0,0.36),0 0 0 1px rgba(0,0,0,0.06);
295 | @include transition(300ms all cubic-bezier(0.34,1.61,0.7,1));
296 | @include transform(scale(0.7) translateY(-25%) translateX(25%));
297 | }
298 | .SettingsContainer.is-open {
299 | opacity: 1;
300 | @include transform(scale(1) translateY(0));
301 | }
302 | .SettingsContainer.is-sharing {
303 | height: 346px;
304 | }
305 | .SettingsContainer-content {
306 | @include display(flex);
307 | @include flex-direction(column);
308 | @include justify-content(space-between);
309 | @include align-items(center);
310 | position: relative;
311 | padding: 2em 2em;
312 | }
313 | .SettingsContainer-button {
314 | display: block;
315 | position: relative;
316 | width: 100%;
317 | height: 46px;
318 | line-height: 46px;
319 | margin-top: 1.2em;
320 | padding: 0;
321 | outline: none;
322 | cursor: pointer;
323 | border-radius: $radius;
324 | font-size: 1em;
325 | font-family: $font;
326 | text-decoration: none;
327 | text-align: center;
328 | box-sizing: border-box;
329 | @include user-select(none);
330 | }
331 | .SettingsContainer-button--default {
332 | @include background-image(linear-gradient(28deg, #D9DEE5 0%, #E2E7EE 100%));
333 | border: 1px solid #B8C6D3;
334 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.50);
335 | color: $icon-dark-grey;
336 | &:hover {
337 | @include background-image(linear-gradient(28deg, #ECEEF1 0%, #DDE3EA 100%));
338 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.30);
339 | color: #545E6B;
340 | }
341 | &:active {
342 | @include background-image(linear-gradient(28deg, #E4E9EE 0%, #EAEEF2 100%));
343 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.30);
344 | color: #4996E3;
345 | }
346 | }
347 | .SettingsContainer-button--blue {
348 | @include background-image(linear-gradient(-155deg, #58ACD9 0%, #7BA8FF 100%));
349 | border: 1px solid #719BEC;
350 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.15);
351 | color: #FFFFFF;
352 | &:hover {
353 | @include background-image(linear-gradient(-155deg, #58ACD9 0%, #7BA8FF 50%));
354 | box-shadow: inset 0px 1px 0px 0px rgba(255,255,255,0.12);
355 | }
356 | &:active {
357 | @include background-image(linear-gradient(24deg, #58ACD9 0%, #7BA8FF 100%));
358 | box-shadow: inset 0px -1px 0px 0px rgba(255,255,255,0.12);
359 | }
360 | }
361 | .SettingsContainer-input {
362 | box-sizing: border-box;
363 | position: relative;
364 | width: 100%;
365 | height: 0;
366 | opacity: 0;
367 | margin-top: -3px;
368 | padding: 3px 0.4em 0 0.4em;
369 | outline: none;
370 | cursor: text;
371 | font-size: 0.8em;
372 | color: $icon-dark-grey;
373 | font-family: $font;
374 | background: $white;
375 | border: 1px solid #B8C6D3;
376 | border-radius: 0 0 $radius $radius;
377 | @include transition(400ms all cubic-bezier(0.34,1.61,0.7,1));
378 |
379 | &.is-sharing {
380 | height: 40px;
381 | opacity: 1;
382 | }
383 | }
384 |
385 | #share > span,
386 | #share::after {
387 | display: block;
388 | @include transition(transform 0.3s, opacity 0.3s);
389 | @include transition-timing-function(cubic-bezier(0.2, 1, 0.3, 1));
390 | }
391 | #share::after {
392 | content: attr(data-text);
393 | color: $positive-green;
394 | position: absolute;
395 | top: 0;
396 | width: 100%;
397 | opacity: 0;
398 | @include transform(translateY(25%));
399 | }
400 | #share.is-sharing {
401 | > span {
402 | opacity: 0;
403 | @include transform(translateY(-25%));
404 | }
405 | &::after {
406 | opacity: 1;
407 | @include transform(translateY(0));
408 | }
409 | }
410 |
411 | .FlashMessage {
412 | color: $darkest-grey;
413 | float: left;
414 | @include transform(translateY(-3em));
415 |
416 | &.is-visible {
417 | @include animation(saved-inout 4s cubic-bezier(0.86, 0, 0.07, 1) 0.2s forwards);
418 | }
419 | }
420 |
421 | .ExcelEditor {
422 | width: 100%;
423 | height: 90%;
424 | display: block;
425 | overflow: auto;
426 | z-index: 1;
427 | @include transform(translateX(1em));
428 | opacity: 0;
429 | @include animation(editor-in 600ms cubic-bezier(0.215, 0.61, 0.355, 1) forwards);
430 | }
431 | .ExcelEditor-controls {
432 | text-align: center;
433 | margin-top: 3em;
434 | margin-bottom: 2em;
435 | @include user-select(none);
436 | cursor: default;
437 |
438 | &:hover .ExcelEditor-control {
439 | color: $table-text-hover;
440 | }
441 | }
442 | .ExcelEditor-control {
443 | display: inline-block;
444 | color: $table-text;
445 | opacity: 0.6;
446 | margin: 0 1em;
447 | @include transition(300ms all ease);
448 |
449 | a {
450 | font-size: 1.1em;
451 | font-weight: 600;
452 | cursor: pointer;
453 | }
454 | a:first-child {
455 | margin-right: 0.4em;
456 | }
457 | a:last-child {
458 | margin-left: 0.4em;
459 | }
460 | a:active {
461 | color: $table-text-active;
462 | }
463 | span {
464 | font-size: 0.8em;
465 | }
466 | }
467 | .ExcelEditor-wrapper {
468 | display: block;
469 | text-align: center;
470 | }
471 | .ExcelEditor-table {
472 | margin: 0 auto;
473 | border-spacing: 0.5em;
474 | color: $table-text;
475 |
476 | thead th {
477 | font-weight: 300;
478 | font-size: 0.7em;
479 | }
480 | }
481 | .ExcelEditor-header {
482 | background: $black;
483 | min-width: 3.5em;
484 | border: 2px solid transparent;
485 | border-radius: $radius;
486 | outline: none;
487 | padding: 0.2em 0.7em;
488 | font-weight: 300;
489 | line-height: 1.4em;
490 | @include transition(200ms all ease);
491 | &:hover {
492 | color: $table-text-hover;
493 | border: 2px solid rgba(68, 79, 88, 50);
494 | }
495 | &:focus {
496 | color: $table-text-active;
497 | border: 2px solid $green;
498 | }
499 | }
500 | .ExcelEditor-cell {
501 | min-width: 3em;
502 | text-align: center;
503 | background: $black;
504 | border: 2px solid transparent;
505 | border-radius: $radius;
506 | outline: none;
507 | padding: 0.2em 0.7em;
508 | font-weight: 300;
509 | line-height: 1.4em;
510 | @include transition(200ms all ease);
511 | &:hover {
512 | color: $table-text-hover;
513 | border: 2px solid rgba(68, 79, 88, 50);
514 | }
515 | &:focus {
516 | color: $table-text-active;
517 | border: 2px solid $green;
518 | }
519 | }
520 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | var plugins = [];
4 | var release = false;
5 | if (process.env.NODE_ENV === 'production') {
6 | release = true;
7 | plugins.push(
8 | new webpack.DefinePlugin({
9 | 'process.env': {
10 | NODE_ENV: JSON.stringify('production')
11 | }
12 | }),
13 | new webpack.optimize.UglifyJsPlugin(),
14 | new webpack.optimize.DedupePlugin()
15 | );
16 | }
17 |
18 | module.exports = {
19 | cache: true,
20 | entry: './index',
21 | output: {
22 | path: release ? './dist' : '.',
23 | filename: release ? 'browser-bundle-[hash].js' : 'browser-bundle.js'
24 | },
25 | module: {
26 | loaders: [
27 | {test: /\.(js|jsx)$/, loader: 'jsx-loader'},
28 | {test: /\.json$/, loader: 'json-loader'},
29 | {test: /\.svg$/, loader: 'raw-loader'}
30 | ]
31 | },
32 | resolve: {
33 | extensions: ['', '.js', '.jsx', '.json', '.svg']
34 | },
35 | plugins: plugins
36 | };
37 |
--------------------------------------------------------------------------------