├── .gitignore
├── README.md
├── app
├── actions
│ └── index.js
├── components
│ ├── DataMap.jsx
│ ├── DataTable.jsx
│ ├── DataTableBox.jsx
│ ├── DataTableRow.jsx
│ ├── Navbar.jsx
│ ├── NumericInput.jsx
│ ├── SelectBox.jsx
│ └── SortableHeader.jsx
├── constants
│ └── ActionTypes.js
├── containers
│ └── App.jsx
├── data
│ ├── states-data.js
│ └── states-defaults.js
├── main.js
├── main.scss
├── reducers
│ ├── emptyRegions.js
│ ├── index.js
│ ├── regionData.js
│ └── sortState.js
└── styles
│ └── _select_box.scss
├── index.html
├── package.json
├── webpack.config.js
└── webpack.config.prod.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # simple-data-table-map
2 | http://caspg.github.io/simple-data-table-map/
3 |
4 | Editable data-map built with React, Redux and [datamaps](https://github.com/markmarkoh/datamaps) (d3.js).
5 |
6 | # running locally
7 |
8 | * clone this repo
9 | * run `npm install` from main repo directory
10 | * start webpack development server with `npm start`
11 | * go to `localhost:8080` in your browser
12 |
13 | If you want to create minified `bundle.js` file, run `npm run build`.
14 |
--------------------------------------------------------------------------------
/app/actions/index.js:
--------------------------------------------------------------------------------
1 | import { EDIT_ROW, DELETE_ROW, ADD_ROW, TOGGLE_DIRECTION } from '../constants/ActionTypes';
2 |
3 | export function editRow(regionName, value) {
4 | return { type: EDIT_ROW, regionName, value };
5 | }
6 |
7 | export function deleteRow(regionName, code) {
8 | return { type: DELETE_ROW, regionName, code };
9 | }
10 |
11 | export function addRow(regionName, code, value) {
12 | return { type: ADD_ROW, regionName, code, value };
13 | }
14 |
15 |
16 | export function toggleDirection(newSortKey) {
17 | return { type: TOGGLE_DIRECTION, newSortKey };
18 | }
19 |
--------------------------------------------------------------------------------
/app/components/DataMap.jsx:
--------------------------------------------------------------------------------
1 | import d3 from 'd3';
2 | import topojson from 'topojson';
3 | import Datamap from 'datamaps/dist/datamaps.usa.min'
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 | import statesDefaults from '../data/states-defaults';
7 | import objectAssign from 'object-assign';
8 |
9 | export default class DataMap extends React.Component {
10 | constructor(props){
11 | super(props);
12 | this.datamap = null;
13 | }
14 | linearPalleteScale(value){
15 | const dataValues = this.props.regionData.map(function(data) { return data.value });
16 | const minVal = Math.min(...dataValues);
17 | const maxVal = Math.max(...dataValues);
18 | return d3.scale.linear().domain([minVal, maxVal]).range(["#EFEFFF","#02386F"])(value);
19 | }
20 | redducedData(){
21 | const newData = this.props.regionData.reduce((object, data) => {
22 | object[data.code] = { value: data.value, fillColor: this.linearPalleteScale(data.value) };
23 | return object;
24 | }, {});
25 | return objectAssign({}, statesDefaults, newData);
26 | }
27 | renderMap(){
28 | return new Datamap({
29 | element: ReactDOM.findDOMNode(this),
30 | scope: 'usa',
31 | data: this.redducedData(),
32 | geographyConfig: {
33 | borderWidth: 0.5,
34 | highlightFillColor: '#FFCC80',
35 | popupTemplate: function(geography, data) {
36 | if (data && data.value) {
37 | return '
' + geography.properties.name + ', ' + data.value + '
';
38 | } else {
39 | return '' + geography.properties.name + '
';
40 | }
41 | }
42 | }
43 | });
44 | }
45 | currentScreenWidth(){
46 | return window.innerWidth ||
47 | document.documentElement.clientWidth ||
48 | document.body.clientWidth;
49 | }
50 | componentDidMount(){
51 | const mapContainer = d3.select('#datamap-container');
52 | const initialScreenWidth = this.currentScreenWidth();
53 | const containerWidth = (initialScreenWidth < 600) ?
54 | { width: initialScreenWidth + 'px', height: (initialScreenWidth * 0.5625) + 'px' } :
55 | { width: '600px', height: '350px' }
56 |
57 | mapContainer.style(containerWidth);
58 | this.datamap = this.renderMap();
59 | window.addEventListener('resize', () => {
60 | const currentScreenWidth = this.currentScreenWidth();
61 | const mapContainerWidth = mapContainer.style('width');
62 | if (this.currentScreenWidth() > 600 && mapContainerWidth !== '600px') {
63 | d3.select('svg').remove();
64 | mapContainer.style({
65 | width: '600px',
66 | height: '350px'
67 | });
68 | this.datamap = this.renderMap();
69 | }
70 | else if (this.currentScreenWidth() <= 600) {
71 | d3.select('svg').remove();
72 | mapContainer.style({
73 | width: currentScreenWidth + 'px',
74 | height: (currentScreenWidth * 0.5625) + 'px'
75 | });
76 | this.datamap = this.renderMap();
77 | }
78 | });
79 | }
80 | componentDidUpdate(){
81 | this.datamap.updateChoropleth(this.redducedData());
82 | }
83 | componentWillUnmount(){
84 | d3.select('svg').remove();
85 | }
86 | render() {
87 | return (
88 |
89 | );
90 | }
91 | }
92 |
93 | DataMap.propTypes = {
94 | regionData: React.PropTypes.array.isRequired
95 | };
96 |
--------------------------------------------------------------------------------
/app/components/DataTable.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DataTableRow from './DataTableRow';
3 | import SortableHeader from './SortableHeader';
4 |
5 | export default class DataTable extends React.Component {
6 | renderTableRows(){
7 | return this.props.regionData.map((data, index) => {
8 | return (
9 | this.props.onDeleteRow(data.regionName, data.code)}
15 | />
16 | );
17 | });
18 | }
19 | render() {
20 | return (
21 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
39 | {this.renderTableRows()}
40 |
41 |
42 | );
43 | }
44 | }
45 |
46 | DataTable.propTypes = {
47 | regionData: React.PropTypes.array.isRequired,
48 | onEditRow: React.PropTypes.func.isRequired,
49 | onDeleteRow: React.PropTypes.func.isRequired,
50 | toggleDirection: React.PropTypes.func.isRequired
51 | }
52 |
--------------------------------------------------------------------------------
/app/components/DataTableBox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DataTable from './DataTable';
3 | import SelectBox from './SelectBox';
4 |
5 | export default class DataTableBox extends React.Component {
6 | render() {
7 | return (
8 |
23 | );
24 | }
25 | }
26 |
27 | DataTableBox.propTypes = {
28 | regionData: React.PropTypes.array.isRequired,
29 | emptyRegions: React.PropTypes.array.isRequired,
30 | onEditRow: React.PropTypes.func.isRequired,
31 | onDeleteRow: React.PropTypes.func.isRequired,
32 | toggleDirection: React.PropTypes.func.isRequired
33 | }
34 |
--------------------------------------------------------------------------------
/app/components/DataTableRow.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NumericInput from './NumericInput';
3 |
4 | export default class DataTableRow extends React.Component {
5 | handleInputBlur(newValue){
6 | this.props.onEditRow(this.props.regionName, newValue);
7 | }
8 | render() {
9 | return (
10 |
11 |
12 | {this.props.regionName}
13 | |
14 |
15 |
19 | |
20 |
21 |
25 | |
26 |
27 | );
28 | }
29 | }
30 |
31 | DataTableRow.propTypes = {
32 | value: React.PropTypes.number.isRequired,
33 | regionName: React.PropTypes.string.isRequired,
34 | onDeleteRow: React.PropTypes.func.isRequired
35 | }
36 |
--------------------------------------------------------------------------------
/app/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class Navbar extends React.Component {
4 | constructor(props){
5 | super(props);
6 | this.handleScroll = this.handleScroll.bind(this);
7 | this.state = { visibleNav: true };
8 | this.prevPosition = false;
9 | }
10 | componentDidMount() {
11 | this.prevPosition = window.scrollY;
12 | window.addEventListener('scroll', this.handleScroll);
13 | }
14 | componentWillUnmount() {
15 | window.removeEventListener('scroll', this.handleScroll);
16 | }
17 | handleScroll(){
18 | const newPosition = window.scrollY;
19 | if (this.prevPosition === newPosition) return;
20 | const visibleNav = (this.prevPosition < newPosition) ? false : true;
21 | this.setState({ visibleNav: visibleNav });
22 | this.prevPosition = newPosition;
23 | }
24 | render() {
25 | const navStyle = {
26 | top: (this.state.visibleNav) ? 0 : -(this.refs.navbar.offsetHeight),
27 | WebkitTransition: "all .25s ease-in-out",
28 | MozTransition: "all .25s ease-in-out",
29 | OTransition: "all .25s ease-in-out",
30 | transition: "all .25s ease-in-out"
31 | };
32 |
33 | return (
34 |
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/components/NumericInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class NumericInput extends React.Component {
4 | constructor(props){
5 | super(props);
6 | this.state = { value: this.props.value || '' };
7 | this.handleOnChange = this.handleOnChange.bind(this);
8 | this.handleOnBlur = this.handleOnBlur.bind(this);
9 | }
10 | handleOnChange(event){
11 | const newValue = event.target.value;
12 | if (newValue === this.state.value) return;
13 | if (!/^[+-]?\d*(\.\d*)?$/.test(newValue)) return;
14 | this.setState({value: newValue});
15 | }
16 | handleOnBlur(event){
17 | const newValue = parseFloat(event.target.value) || 0;
18 | if (this.props.value === newValue) return;
19 | this.setState({ value: newValue });
20 | this.props.onBlur(newValue);
21 | }
22 | componentDidUpdate(prevProps){
23 | if (prevProps.value === this.props.value) return;
24 | this.setState({ value: this.props.value });
25 | }
26 | render() {
27 | return (
28 |
35 | );
36 | }
37 | }
38 |
39 | NumericInput.propTypes = {
40 | value: React.PropTypes.number,
41 | className: React.PropTypes.string,
42 | onBlur: React.PropTypes.func.isRequired
43 | }
44 |
--------------------------------------------------------------------------------
/app/components/SelectBox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Select from 'react-select';
3 | import 'react-select/dist/react-select.min.css';
4 | import NumericInput from './NumericInput';
5 |
6 | export default class SelectBox extends React.Component {
7 | constructor(props){
8 | super(props);
9 | this.handleOnChange = this.handleOnChange.bind(this);
10 | this.handleButtonClick = this.handleButtonClick.bind(this);
11 | this.handleInputBlur = this.handleInputBlur.bind(this);
12 | this.state = {
13 | selected: false,
14 | selectedValue: null,
15 | selectedOption: null,
16 | inputValue: null
17 | };
18 | }
19 | handleOnChange(value, option){
20 | const selected = value ? true : false;
21 | this.setState({
22 | selectedValue: value,
23 | selectedOption: option[0],
24 | selected: selected
25 | });
26 | }
27 | handleButtonClick(){
28 | const inputValue = this.state.inputValue || 0;
29 | this.props.onAddRow(
30 | this.state.selectedOption.regionName,
31 | this.state.selectedOption.code,
32 | inputValue
33 | );
34 | this.setState({
35 | selected: false,
36 | selectedValue: null,
37 | selectedOption: null,
38 | inputValue: null
39 | });
40 | }
41 | handleInputBlur(newValue){
42 | this.setState({ inputValue: newValue });
43 | }
44 | render() {
45 | return (
46 |
47 |
48 |
57 |
58 |
59 |
63 |
64 |
65 |
70 |
71 |
72 | );
73 | }
74 | }
75 |
76 | SelectBox.propTypes = {
77 | emptyRegions: React.PropTypes.array.isRequired,
78 | onAddRow: React.PropTypes.func.isRequired
79 | }
80 |
--------------------------------------------------------------------------------
/app/components/SortableHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class SortableHeader extends React.Component {
4 | renderSortArrow(){
5 | if (this.props.sortState.key !== this.props.sortKey) return;
6 | const glyphClass = this.props.sortState.direction === 'ASC' ?
7 | 'glyphicon glyphicon-chevron-down' : 'glyphicon glyphicon-chevron-up'
8 |
9 | return
10 | }
11 | render() {
12 | return (
13 | this.props.toggleDirection(this.props.sortKey)}>
14 | {this.props.label}
15 | {this.renderSortArrow()}
16 | |
17 | );
18 | }
19 | }
20 |
21 | SortableHeader.propTypes = {
22 | label: React.PropTypes.string.isRequired,
23 | sortState: React.PropTypes.object.isRequired,
24 | sortKey: React.PropTypes.string.isRequired,
25 | toggleDirection: React.PropTypes.func.isRequired
26 | }
27 |
--------------------------------------------------------------------------------
/app/constants/ActionTypes.js:
--------------------------------------------------------------------------------
1 | export const EDIT_ROW = 'EDIT_ROW';
2 | export const DELETE_ROW = 'DELETE_ROW';
3 | export const ADD_ROW = 'ADD_ROW';
4 |
5 | export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
6 |
--------------------------------------------------------------------------------
/app/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux'
3 | import { editRow, deleteRow, addRow, toggleDirection } from '../actions';
4 |
5 | import DataMap from '../components/DataMap';
6 | import DataTableBox from '../components/DataTableBox';
7 | import Navbar from '../components/Navbar';
8 |
9 | class App extends React.Component {
10 | constructor(props){
11 | super(props);
12 | this.handleEditRow = this.handleEditRow.bind(this);
13 | this.handleDeleteRow = this.handleDeleteRow.bind(this);
14 | this.handleAddRow = this.handleAddRow.bind(this);
15 | this.handleToggleDirection = this.handleToggleDirection.bind(this);
16 | }
17 | handleDeleteRow(regionName, code){
18 | this.props.dispatch(deleteRow(regionName, code));
19 | }
20 | handleEditRow(regionName, newValue){
21 | this.props.dispatch(editRow(regionName, newValue));
22 | }
23 | handleAddRow(regionName, code, value){
24 | this.props.dispatch(addRow(regionName, code, value));
25 | }
26 | handleToggleDirection(newSortKey){
27 | this.props.dispatch(toggleDirection(newSortKey));
28 | }
29 | render() {
30 | return (
31 |
32 |
33 |
34 |
35 |
36 |
45 |
46 | );
47 | }
48 | }
49 |
50 | App.propTypes = {
51 | regionData: React.PropTypes.array.isRequired,
52 | emptyRegions: React.PropTypes.array.isRequired,
53 | sortState: React.PropTypes.object.isRequired
54 | };
55 |
56 | function sortCollection(collection, sortState) {
57 | switch (sortState.direction) {
58 | case 'ASC':
59 | return collection.sort(function(a, b) {
60 | if (a[sortState.key] > b[sortState.key]) return 1;
61 | if (a[sortState.key] < b[sortState.key]) return -1;
62 | return 0;
63 | });
64 |
65 | case 'DESC':
66 | return collection.sort(function(a, b) {
67 | if (a[sortState.key] > b[sortState.key]) return -1;
68 | if (a[sortState.key] < b[sortState.key]) return 1;
69 | return 0;
70 | });
71 |
72 | default:
73 | return collection;
74 | }
75 | }
76 |
77 | function alphabeticOrder(collection) {
78 | return collection.sort(function(a, b) {
79 | if (a.regionName > b.regionName) return 1;
80 | if (a.regionName < b.regionName) return -1;
81 | return 0;
82 | });
83 | }
84 |
85 | function mapStateToProps(state) {
86 | return {
87 | regionData: sortCollection(state.regionData, state.sortState),
88 | emptyRegions: alphabeticOrder(state.emptyRegions),
89 | sortState: state.sortState
90 | }
91 | }
92 |
93 | export default connect(mapStateToProps)(App);
94 |
--------------------------------------------------------------------------------
/app/data/states-data.js:
--------------------------------------------------------------------------------
1 | const statesData = [
2 | { 'regionName':'Alabama','code':'AL', 'value': 89 },
3 | { 'regionName':'Alaska','code':'AK', 'value': 112 },
4 | { 'regionName':'Arizona','code':'AZ', 'value': 101 },
5 | { 'regionName':'Arkansas','code':'AR', 'value': 123 },
6 | { 'regionName':'California','code':'CA', 'value': 145 },
7 | { 'regionName':'Colorado','code':'CO', 'value': 98 },
8 | { 'regionName':'Connecticut','code':'CT', 'value': 101 },
9 | { 'regionName':'Delaware','code':'DE', 'value': 109 },
10 | // { 'regionName':'District of Columbia','code':'DC', 'value': 115 },
11 | { 'regionName':'Florida','code':'FL', 'value': 122 },
12 | { 'regionName':'Georgia','code':'GA', 'value': 91 },
13 | { 'regionName':'Hawaii','code':'HI', 'value': 131 },
14 | { 'regionName':'Idaho','code':'ID', 'value': 110 },
15 | { 'regionName':'Illinois','code':'IL', 'value': 134 },
16 | { 'regionName':'Indiana','code':'IN', 'value': 94 },
17 | { 'regionName':'Iowa','code':'IA', 'value': 106 },
18 | { 'regionName':'Kansa','code':'KS', 'value': 116 },
19 | { 'regionName':'Kentucky','code':'KY', 'value': 122 },
20 | { 'regionName':'Lousiana','code':'LA', 'value': 99 },
21 | { 'regionName':'Maine','code':'ME', 'value': 100 },
22 | { 'regionName':'Maryland','code':'MD', 'value': 101 },
23 | { 'regionName':'Massachusetts','code':'MA', 'value': 102 },
24 | { 'regionName':'Michigan','code':'MI', 'value': 104 },
25 | { 'regionName':'Minnesota','code':'MN', 'value': 112 },
26 | { 'regionName':'Mississippi','code':'MS', 'value': 105 },
27 | { 'regionName':'Missouri','code':'MO', 'value': 116 },
28 | { 'regionName':'Montana','code':'MT', 'value': 107 },
29 | { 'regionName':'Nebraska','code':'NE', 'value': 97 },
30 | { 'regionName':'Nevada','code':'NV', 'value': 108 },
31 | { 'regionName':'New Hampshire','code':'NH', 'value': 118 },
32 | { 'regionName':'New Jersey','code':'NJ', 'value': 98 },
33 | { 'regionName':'New Mexico','code':'NM', 'value': 109 },
34 | { 'regionName':'New York','code':'NY', 'value': 119 },
35 | { 'regionName':'North Carolina','code':'NC', 'value': 99 },
36 | { 'regionName':'North Dakota','code':'ND', 'value': 100 },
37 | { 'regionName':'Ohio','code':'OH', 'value': 126 },
38 | { 'regionName':'Oklahoma','code':'OK', 'value': 125 },
39 | { 'regionName':'Oregon','code':'OR', 'value': 124 },
40 | { 'regionName':'Pennsylvania','code':'PA', 'value': 122 },
41 | { 'regionName':'Rhode Island','code':'RI', 'value': 122 },
42 | { 'regionName':'South Carolina','code':'SC', 'value': 141 },
43 | { 'regionName':'South Dakota','code':'SD', 'value': 131 },
44 | { 'regionName':'Tennessee','code':'TN', 'value': 132 },
45 | { 'regionName':'Texas','code':'TX', 'value': 133 },
46 | { 'regionName':'Utah','code':'UT', 'value': 134 },
47 | { 'regionName':'Vermont','code':'VT', 'value': 121 },
48 | { 'regionName':'Virginia','code':'VA', 'value': 122 },
49 | { 'regionName':'Washington','code':'WA', 'value': 91 },
50 | { 'regionName':'West Virginia','code':'WV', 'value': 92 },
51 | { 'regionName':'Wisconsin','code':'WI', 'value': 93 },
52 | { 'regionName':'Wyoming','code':'WY', 'value': 94 }
53 | ];
54 |
55 | export default statesData;
--------------------------------------------------------------------------------
/app/data/states-defaults.js:
--------------------------------------------------------------------------------
1 | const defaultFill = '#f2f2f2';
2 |
3 | const statesDefaults = {
4 | 'AL': { fillColor: defaultFill, value: '' },
5 | 'AK': { fillColor: defaultFill, value: '' },
6 | 'AZ': { fillColor: defaultFill, value: '' },
7 | 'AR': { fillColor: defaultFill, value: '' },
8 | 'CA': { fillColor: defaultFill, value: '' },
9 | 'CO': { fillColor: defaultFill, value: '' },
10 | 'CT': { fillColor: defaultFill, value: '' },
11 | 'DE': { fillColor: defaultFill, value: '' },
12 | 'FL': { fillColor: defaultFill, value: '' },
13 | 'GA': { fillColor: defaultFill, value: '' },
14 | 'HI': { fillColor: defaultFill, value: '' },
15 | 'ID': { fillColor: defaultFill, value: '' },
16 | 'IL': { fillColor: defaultFill, value: '' },
17 | 'IN': { fillColor: defaultFill, value: '' },
18 | 'IA': { fillColor: defaultFill, value: '' },
19 | 'KS': { fillColor: defaultFill, value: '' },
20 | 'KY': { fillColor: defaultFill, value: '' },
21 | 'LA': { fillColor: defaultFill, value: '' },
22 | 'ME': { fillColor: defaultFill, value: '' },
23 | 'MD': { fillColor: defaultFill, value: '' },
24 | 'MA': { fillColor: defaultFill, value: '' },
25 | 'MI': { fillColor: defaultFill, value: '' },
26 | 'MN': { fillColor: defaultFill, value: '' },
27 | 'MS': { fillColor: defaultFill, value: '' },
28 | 'MO': { fillColor: defaultFill, value: '' },
29 | 'MT': { fillColor: defaultFill, value: '' },
30 | 'NE': { fillColor: defaultFill, value: '' },
31 | 'NV': { fillColor: defaultFill, value: '' },
32 | 'NH': { fillColor: defaultFill, value: '' },
33 | 'NJ': { fillColor: defaultFill, value: '' },
34 | 'NM': { fillColor: defaultFill, value: '' },
35 | 'NY': { fillColor: defaultFill, value: '' },
36 | 'NC': { fillColor: defaultFill, value: '' },
37 | 'ND': { fillColor: defaultFill, value: '' },
38 | 'OH': { fillColor: defaultFill, value: '' },
39 | 'OK': { fillColor: defaultFill, value: '' },
40 | 'OR': { fillColor: defaultFill, value: '' },
41 | 'PA': { fillColor: defaultFill, value: '' },
42 | 'RI': { fillColor: defaultFill, value: '' },
43 | 'SC': { fillColor: defaultFill, value: '' },
44 | 'SD': { fillColor: defaultFill, value: '' },
45 | 'TN': { fillColor: defaultFill, value: '' },
46 | 'TX': { fillColor: defaultFill, value: '' },
47 | 'UT': { fillColor: defaultFill, value: '' },
48 | 'VT': { fillColor: defaultFill, value: '' },
49 | 'VA': { fillColor: defaultFill, value: '' },
50 | 'WA': { fillColor: defaultFill, value: '' },
51 | 'WV': { fillColor: defaultFill, value: '' },
52 | 'WI': { fillColor: defaultFill, value: '' },
53 | 'WY': { fillColor: defaultFill, value: '' }
54 | };
55 |
56 | export default statesDefaults;
57 |
--------------------------------------------------------------------------------
/app/main.js:
--------------------------------------------------------------------------------
1 | require("./main.scss");
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { Provider } from 'react-redux'
5 | import { createStore } from 'redux'
6 | import rootReducer from './reducers'
7 | import App from './containers/App';
8 | import statesData from './data/states-data';
9 |
10 | const initialState = {
11 | regionData: statesData,
12 | emptyRegions: [],
13 | sortState: { key: 'regionName', direction: 'ASC' }
14 | };
15 |
16 | const store = createStore(rootReducer, initialState);
17 |
18 | ReactDOM.render(
19 |
20 |
21 | ,
22 | document.getElementById('app')
23 | );
24 |
--------------------------------------------------------------------------------
/app/main.scss:
--------------------------------------------------------------------------------
1 | $red: #B71C1C;
2 | $green: #4CAF50;
3 | $outline-blue: #08c;
4 |
5 | $borderStyle: 1px solid #ddd;
6 |
7 | @import 'styles/select_box';
8 |
9 | body {
10 | margin-top: 70px;
11 | }
12 |
13 | .navbar {
14 | border-radius: 0;
15 | height: 60px;
16 | background: rgba(239, 239, 255, 0.9);
17 | padding: 0 30px;
18 | }
19 |
20 | .navbar-header {
21 | display: inline-block;
22 | }
23 |
24 | .github-repo-link {
25 | line-height: 50px;
26 |
27 | .glyphicon {
28 | margin-right: 5px;
29 | }
30 | }
31 |
32 |
33 | .datamap-outer-conainer {
34 | padding: 0;
35 |
36 | #datamap-container {
37 | position: relative;
38 | margin: 0 auto;
39 | }
40 | }
41 |
42 | .data-table-box {
43 | padding: 0 15px;
44 |
45 | .data-table-box-outer {
46 | max-width: 650px;
47 | margin: 0 auto;
48 | }
49 | }
50 |
51 | table {
52 | width: 100%;
53 | margin-bottom: 150px;
54 | border-collapse: collapse;
55 |
56 | th {
57 | border: $borderStyle;
58 | padding: 5px 15px;
59 | font-weight: bold;
60 | cursor: pointer;
61 |
62 | h4 {
63 | display: inline-block;
64 | }
65 |
66 | .glyphicon {
67 | margin-left: 20px;
68 | }
69 | }
70 |
71 | td {
72 | border: $borderStyle;
73 | width: 45%;
74 | height: 35px;
75 | text-align: start;
76 |
77 | &:first-of-type {
78 | padding: 10px 15px;
79 | }
80 |
81 | &:last-of-type {
82 | border: none;
83 | width: 10%;
84 | }
85 | }
86 |
87 | input {
88 | border: none;
89 | width: 100%;
90 | height: 100%;
91 | padding-left: 15px;
92 | }
93 |
94 | .remove-btn {
95 | margin-left: 10px;
96 | padding: 10px;
97 | color: $red;
98 | opacity: 0;
99 | cursor: pointer;
100 | font-size: 1.7rem;
101 | }
102 |
103 | tr:hover .remove-btn {
104 | opacity: 1;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/app/reducers/emptyRegions.js:
--------------------------------------------------------------------------------
1 | import { DELETE_ROW, ADD_ROW } from '../constants/ActionTypes';
2 |
3 | export default function emptyRegions(state = [], action) {
4 | switch (action.type) {
5 | case DELETE_ROW:
6 | return [
7 | {
8 | regionName: action.regionName,
9 | code: action.code
10 | },
11 | ...state
12 | ]
13 |
14 | case ADD_ROW:
15 | return state.filter((data) => {
16 | return data.regionName !== action.regionName;
17 | })
18 |
19 | default:
20 | return state;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import sortState from './sortState';
4 | import emptyRegions from './emptyRegions';
5 | import regionData from './regionData';
6 |
7 | const rootReducer = combineReducers({
8 | regionData,
9 | emptyRegions,
10 | sortState
11 | });
12 |
13 | export default rootReducer;
14 |
--------------------------------------------------------------------------------
/app/reducers/regionData.js:
--------------------------------------------------------------------------------
1 | import { EDIT_ROW, DELETE_ROW, ADD_ROW } from '../constants/ActionTypes';
2 | import objectAssign from 'object-assign';
3 |
4 | export default function regionData(state = [], action) {
5 | switch (action.type) {
6 | case EDIT_ROW:
7 | return state.map((data) => {
8 | if (data.regionName === action.regionName) {
9 | return objectAssign({}, data, { value: action.value });
10 | } else {
11 | return data;
12 | }
13 | })
14 |
15 | case DELETE_ROW:
16 | return state.filter((data) => {
17 | return data.regionName !== action.regionName;
18 | })
19 |
20 | case ADD_ROW:
21 | return [
22 | {
23 | regionName: action.regionName,
24 | code: action.code,
25 | value: action.value
26 | },
27 | ...state
28 | ]
29 |
30 | default:
31 | return state;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/reducers/sortState.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_DIRECTION } from '../constants/ActionTypes';
2 |
3 | function newSortDirection(state, action) {
4 | if (state.key !== action.newSortKey) return state.direction;
5 | return state.direction === 'ASC' ? 'DESC' : 'ASC';
6 | }
7 |
8 | export default function sortState(state = {}, action) {
9 | switch (action.type) {
10 | case TOGGLE_DIRECTION:
11 | return {
12 | key: action.newSortKey,
13 | direction: newSortDirection(state, action)
14 | };
15 |
16 | default:
17 | return state;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/styles/_select_box.scss:
--------------------------------------------------------------------------------
1 | .select-box-container {
2 | display: flex;
3 | margin-bottom: 20px;
4 |
5 | -webkit-box-pack: start;
6 | -webkit-justify-content: flex-start;
7 | -ms-flex-pack: start;
8 | justify-content: flex-start;
9 | text-align: start;
10 |
11 | .column {
12 | display: inline-block;
13 | width: 45%;
14 | height: 36px;
15 |
16 | &:last-of-type {
17 | width: 10%;
18 | }
19 | }
20 |
21 | .numeric-input-column {
22 | input {
23 | padding-left: 15px;
24 | width: 100%;
25 | height: 100%;
26 | border: $borderStyle;
27 |
28 | &:focus {
29 | outline: none;
30 | border-color: $outline-blue lighten($outline-blue, 5%) lighten($outline-blue, 5%);
31 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px opacify($outline-blue, 0.5);
32 | }
33 | }
34 | }
35 |
36 | .Select-control {
37 | border: $borderStyle;
38 | border-radius: 0;
39 | border-right: none;
40 |
41 | .Select-menu-outer {
42 | border-color: red;
43 | }
44 | }
45 |
46 | .button-column {
47 | button {
48 | height: 100%;
49 | }
50 | }
51 |
52 | .add-button {
53 | margin-left: 10px;
54 | padding: 10px;
55 | color: $green;
56 | border: none;
57 | background: none;
58 |
59 | &:disabled {
60 | color: lighten($green, 25%);
61 | }
62 |
63 | .glyphicon {
64 | font-size: 1.7rem;
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Simple Data Table Map
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simple-data-table-map",
3 | "version": "0.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack-dev-server -d",
8 | "build": "webpack -p --config webpack.config.prod.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "babel-core": "^6.4.5",
14 | "babel-loader": "^6.2.1",
15 | "babel-preset-es2015": "^6.3.13",
16 | "babel-preset-react": "^6.3.13",
17 | "css-loader": "^0.23.1",
18 | "d3": "^3.5.13",
19 | "datamaps": "^0.4.2",
20 | "node-sass": "^3.4.2",
21 | "object-assign": "^4.0.1",
22 | "react-select": "^0.9.1",
23 | "sass-loader": "^3.1.2",
24 | "style-loader": "^0.13.0",
25 | "topojson": "^1.6.20",
26 | "webpack": "^1.12.12",
27 | "webpack-dev-server": "^1.14.1"
28 | },
29 | "dependencies": {
30 | "react": "^0.14.6",
31 | "react-dom": "^0.14.6",
32 | "react-redux": "^4.0.6",
33 | "redux": "^3.0.6"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: './app/main.js',
3 | output: { path: './', filename: 'bundle.js' },
4 | module: {
5 | loaders: [
6 | {
7 | test: /.jsx?$/,
8 | loader: 'babel-loader',
9 | exclude: /node_modules/,
10 | query: {
11 | presets: ['es2015', 'react']
12 | }
13 | },
14 | {
15 | test: /\.s*css$/,
16 | loaders: ["style", "css", "sass"]
17 | }
18 | ]
19 | },
20 | resolve: {
21 | extensions: ['', '.js', '.jsx']
22 | },
23 | plugins: []
24 | };
25 |
--------------------------------------------------------------------------------
/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const config = require('./webpack.config.js');
3 |
4 | config.plugins.push(
5 | new webpack.DefinePlugin({
6 | 'process.env': {NODE_ENV: '"production"'}
7 | })
8 | );
9 |
10 | module.exports = config;
11 |
--------------------------------------------------------------------------------