├── .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 |
9 |
10 | 14 | 21 |
22 |
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 |