├── .nvmrc ├── .babelrc ├── img ├── crash-mapper-lg.jpg └── checkpeds_logo@2x.jpg ├── scss ├── components │ ├── _share-options.scss │ ├── _loading-indicator.scss │ ├── _app-options-filters.scss │ ├── _filter-by-type.scss │ ├── _download-data.scss │ ├── _options-filter-footer.scss │ ├── _about-copy.scss │ ├── _stats-counter.scss │ ├── _app-stats-legend.scss │ ├── _help-copy.scss │ ├── _filter-by-boundary.scss │ ├── _zoom-controls.scss │ ├── _options-container.scss │ ├── _small-device-message.scss │ ├── _filter-button.scss │ ├── _stats-contributing-factors-list.scss │ ├── _app-header.scss │ ├── _app-menu.scss │ ├── _stats-legend-container.scss │ ├── _filter-by-date.scss │ ├── _modal.scss │ └── _leaflet-map.scss ├── skeleton │ ├── base │ │ ├── _utils.scss │ │ ├── _functions.scss │ │ ├── _base-styles.scss │ │ ├── _typography.scss │ │ ├── _variables.scss │ │ └── _normalize.scss │ ├── modules │ │ ├── _tables.scss │ │ ├── _spacing.scss │ │ ├── _code.scss │ │ ├── _lists.scss │ │ ├── _media-queries.scss │ │ ├── _forms.scss │ │ ├── _buttons.scss │ │ └── _grid.scss │ └── skeleton.scss ├── _helpers.scss ├── main.scss ├── _variables.scss └── _app.scss ├── src ├── components │ ├── Modal │ │ ├── Help.js │ │ ├── About.js │ │ ├── Disclaimer.js │ │ ├── Copyright.js │ │ ├── ShareFB.js │ │ ├── ShareTwitter.js │ │ ├── ShareURL.js │ │ ├── ModalContent.js │ │ ├── DownloadData.js │ │ └── index.js │ ├── StatsLegend │ │ ├── DateRange.js │ │ ├── TotalCrashCounter.js │ │ ├── ContributingFactorsList.js │ │ ├── StatsCounter.js │ │ ├── LegendContainer.js │ │ └── index.js │ ├── SmallDeviceMessage.js │ ├── LeafletMap │ │ ├── ZoomControls.js │ │ └── customFilter.js │ ├── AppHeader.js │ ├── OptionsFilters │ │ ├── DownloadData.js │ │ ├── ShareOptions.js │ │ ├── FooterOptions.js │ │ ├── FilterByAreaMessage.js │ │ ├── FilterButton.js │ │ ├── MonthYearSelector.js │ │ ├── index.js │ │ ├── OptionsContainer.js │ │ ├── FilterByType.js │ │ ├── FilterByDate.js │ │ ├── FilterByVehicle.js │ │ └── FilterByArea.js │ ├── LoadingIndicator.js │ ├── Menu.js │ ├── HelpCopy.js │ ├── App.js │ └── AboutCopy.js ├── actions │ ├── filter_contributing_factor_actions.js │ ├── modal_actions.js │ ├── filter_by_date_actions.js │ ├── filter_by_type_actions.js │ ├── filter_by_area_actions.js │ ├── index.js │ ├── filter_by_vehicle_actions.js │ └── async_actions.js ├── reducers │ ├── filter_contributing_factor_reducer.js │ ├── filter_by_date.js │ ├── modal_reducer.js │ ├── crash_stats_reducer.js │ ├── stats_contributing_factors_reducer.js │ ├── year_range_reducer.js │ ├── crashes_max_date_reducer.js │ ├── crashes_date_range_reducer.js │ ├── filter_by_type_reducer.js │ ├── index.js │ ├── filter_by_area_reducer.js │ └── filter_by_vehicle_reducer.js ├── middleware.js ├── containers │ ├── FilterByDateConnected.js │ ├── OptionsFiltersConnected.js │ ├── ModalConnected.js │ ├── FilterByAreaConnected.js │ ├── FilterByTypeConnected.js │ ├── StatsLegendConnected.js │ ├── FilterByVehicleConnected.js │ ├── AppConnected.js │ ├── LeafletMapConnected.js │ └── MenuConnected.js ├── store.js ├── index.js ├── constants │ ├── cartocss.js │ ├── app_config.js │ ├── action_types.js │ └── api.js └── index.html ├── deploy_gh_pages.sh ├── .editorconfig ├── .gitignore ├── LICENSE ├── .eslintrc ├── webpack.config.js ├── package.json ├── sql └── 2016_data_update.sql └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.7.0 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-env", "react", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /img/crash-mapper-lg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreenInfo-Network/nyc-crash-mapper/HEAD/img/crash-mapper-lg.jpg -------------------------------------------------------------------------------- /img/checkpeds_logo@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreenInfo-Network/nyc-crash-mapper/HEAD/img/checkpeds_logo@2x.jpg -------------------------------------------------------------------------------- /scss/components/_share-options.scss: -------------------------------------------------------------------------------- 1 | .share-options { 2 | 3 | .filter-options-button { 4 | margin-left: 10px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scss/components/_loading-indicator.scss: -------------------------------------------------------------------------------- 1 | .loading-indicator { 2 | position: absolute; 3 | top: 183px; 4 | left: 25px; 5 | width: 30px; 6 | height: 30px; 7 | z-index: 5; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Modal/Help.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HelpCopy from '../HelpCopy'; 3 | 4 | export default () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /src/components/Modal/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AboutCopy from '../AboutCopy'; 3 | 4 | export default () => ( 5 |
6 | 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /src/actions/filter_contributing_factor_actions.js: -------------------------------------------------------------------------------- 1 | import { FILTER_BY_CONTRIBUTING_FACTOR } from '../constants/action_types'; 2 | 3 | export default (factor = '') => ({ 4 | type: FILTER_BY_CONTRIBUTING_FACTOR, 5 | factor 6 | }); 7 | -------------------------------------------------------------------------------- /src/components/Modal/Disclaimer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

6 | Crash Mapper has no legal bearing and is intended for informational purposes only. 7 |

8 |
9 | ); 10 | -------------------------------------------------------------------------------- /scss/components/_app-options-filters.scss: -------------------------------------------------------------------------------- 1 | .app-options-filters { 2 | height: auto; //$app-options-filters-height; 3 | width: $app-options-filters-width; 4 | bottom: 0; 5 | padding-top: 0; 6 | padding-right: 0; 7 | padding-bottom: 0; 8 | overflow-y: hidden; 9 | } 10 | -------------------------------------------------------------------------------- /scss/components/_filter-by-type.scss: -------------------------------------------------------------------------------- 1 | .filter-by-type { 2 | display: table; 3 | width: 100%; 4 | 5 | .filter-list { 6 | display: table-cell; 7 | vertical-align: top; 8 | width: 50%; 9 | 10 | &:nth-of-type(2) { 11 | padding-left: 0; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/actions/modal_actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | MODAL_OPENED, 3 | MODAL_CLOSED, 4 | } from '../constants/action_types'; 5 | 6 | export const openModal = modalType => ({ 7 | type: MODAL_OPENED, 8 | modalType, 9 | }); 10 | 11 | export const closeModal = () => ({ 12 | type: MODAL_CLOSED 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/Modal/Copyright.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | const year = (new Date()).getFullYear(); 5 | 6 | return ( 7 |
8 |

9 | NYC Crash Mapper © CHECKPEDS {year} 10 |

11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/reducers/filter_contributing_factor_reducer.js: -------------------------------------------------------------------------------- 1 | import { FILTER_BY_CONTRIBUTING_FACTOR } from '../constants/action_types'; 2 | 3 | export default (state = 'ALL', action) => { 4 | switch (action.type) { 5 | case FILTER_BY_CONTRIBUTING_FACTOR: 6 | return action.factor; 7 | default: 8 | return state; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/actions/filter_by_date_actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | START_DATE_CHANGE, 3 | END_DATE_CHANGE 4 | } from '../constants/action_types'; 5 | 6 | export const startDateChange = (startDate = '') => ({ 7 | type: START_DATE_CHANGE, 8 | startDate 9 | }); 10 | 11 | export const endDateChange = (endDate = '') => ({ 12 | type: END_DATE_CHANGE, 13 | endDate 14 | }); 15 | -------------------------------------------------------------------------------- /scss/skeleton/base/_utils.scss: -------------------------------------------------------------------------------- 1 | // Utilities 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | .u-full-width { 5 | width: 100%; 6 | box-sizing: border-box; 7 | } 8 | 9 | .u-max-full-width { 10 | max-width: 100%; 11 | box-sizing: border-box; 12 | } 13 | 14 | .u-pull-right { 15 | float: right; 16 | } 17 | 18 | .u-pull-left { 19 | float: left; 20 | } 21 | -------------------------------------------------------------------------------- /deploy_gh_pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # deploys contents of the dist dir to Github Pages 3 | 4 | git checkout -B gh-pages 5 | git add -f dist 6 | git mv img/ dist/img/ 7 | 8 | echo "crashmapper.org" > dist/CNAME 9 | git add dist/CNAME 10 | 11 | git commit -am "Rebuild website" 12 | git push origin :gh-pages 13 | git subtree push --prefix dist origin gh-pages 14 | git checkout - 15 | -------------------------------------------------------------------------------- /scss/skeleton/modules/_tables.scss: -------------------------------------------------------------------------------- 1 | // Tables 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | th, 5 | td { 6 | padding: 12px 15px; 7 | text-align: left; 8 | border-bottom: 1px solid $light-grey; 9 | } 10 | 11 | th:first-child, 12 | td:first-child { 13 | padding-left: 0; 14 | } 15 | 16 | th:last-child, 17 | td:last-child { 18 | padding-right: 0; 19 | } 20 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | import thunkMiddleware from 'redux-thunk'; 2 | import createLogger from 'redux-logger'; 3 | 4 | const middleware = [thunkMiddleware]; 5 | 6 | if (typeof window !== 'undefined' && process.env.NODE_ENV !== 'production') { 7 | // redux-logger only works in a browser environment 8 | middleware.push(createLogger()); 9 | } 10 | 11 | export default middleware; 12 | -------------------------------------------------------------------------------- /scss/components/_download-data.scss: -------------------------------------------------------------------------------- 1 | .download-data { 2 | display: table; 3 | width: 100%; 4 | padding-bottom: $padding-10; 5 | margin-left: $margin-10; 6 | 7 | .filter-options-button, 8 | .data-last-updated { 9 | display: table-cell; 10 | vertical-align: top; 11 | } 12 | 13 | .data-last-updated { 14 | font-size: 1.1rem; 15 | padding-left: $padding-25; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scss/skeleton/modules/_spacing.scss: -------------------------------------------------------------------------------- 1 | // Spacing 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | button, 5 | .button { 6 | margin-bottom: 1rem; 7 | } 8 | 9 | input, 10 | textarea, 11 | select, 12 | fieldset { 13 | margin-bottom: 1.5rem; 14 | } 15 | 16 | pre, 17 | blockquote, 18 | dl, 19 | figure, 20 | table, 21 | p, 22 | ul, 23 | ol, 24 | form { 25 | margin-bottom: 2.5rem; 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # This file is for unifying the coding style for different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | indent_style = space 13 | indent_size = 2 14 | max_line_length = 80 15 | 16 | [{*.md,*.json}] 17 | max_line_length = null 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /scss/components/_options-filter-footer.scss: -------------------------------------------------------------------------------- 1 | .options-filters-footer { 2 | height: $options-footer-height; 3 | border-top: 1px solid $hr-border; 4 | 5 | .filter-options-button { 6 | width: calc(33.333333333% - 2px); 7 | font-size: 1rem; 8 | text-align: center; 9 | border-right: 1px solid $hr-border; 10 | margin-bottom: 0; 11 | 12 | &:last-child { 13 | border-right: none; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /scss/skeleton/modules/_code.scss: -------------------------------------------------------------------------------- 1 | // Code 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | code { 5 | padding: .2rem .5rem; 6 | margin: 0 .2rem; 7 | font-size: 90%; 8 | white-space: nowrap; 9 | background: lighten($light-grey, 6.4%); 10 | border: 1px solid $light-grey; 11 | border-radius: $global-radius; 12 | } 13 | 14 | pre > code { 15 | display: block; 16 | padding: 1rem 1.5rem; 17 | white-space: pre; 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # node-waf configuration 12 | .lock-wscript 13 | 14 | # Compiled binary addons (http://nodejs.org/api/addons.html) 15 | build/Release 16 | 17 | # Dependency directories 18 | node_modules 19 | 20 | # Optional npm cache directory 21 | .npm 22 | 23 | # Optional REPL history 24 | .node_repl_history 25 | 26 | # OS X 27 | .DS_Store 28 | 29 | # env vars 30 | .env 31 | .env~ 32 | -------------------------------------------------------------------------------- /src/components/StatsLegend/DateRange.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const DateRange = (props) => { 4 | const { startDate, endDate } = props; 5 | 6 | return ( 7 |
8 |
{`From ${startDate} – ${endDate}`}
9 |
10 | ); 11 | }; 12 | 13 | DateRange.propTypes = { 14 | startDate: PropTypes.string.isRequired, 15 | endDate: PropTypes.string.isRequired 16 | }; 17 | 18 | export default DateRange; 19 | -------------------------------------------------------------------------------- /src/reducers/filter_by_date.js: -------------------------------------------------------------------------------- 1 | import { 2 | START_DATE_CHANGE, 3 | END_DATE_CHANGE 4 | } from '../constants/action_types'; 5 | 6 | export default (state = {}, action) => { 7 | switch (action.type) { 8 | case START_DATE_CHANGE: 9 | return { 10 | ...state, 11 | startDate: action.startDate 12 | }; 13 | case END_DATE_CHANGE: 14 | return { 15 | ...state, 16 | endDate: action.endDate 17 | }; 18 | default: 19 | return state; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Modal/ShareFB.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Placeholder 4 | export default () => { 5 | const location = window.encodeURIComponent(window.location.href); 6 | const url = `https://www.facebook.com/sharer/sharer.php?u=${location}`; 7 | 8 | return ( 9 |
10 |

11 | 12 | Share NYC Crash Mapper on Facebook 13 | 14 |

15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/SmallDeviceMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AboutCopy from './AboutCopy'; 3 | 4 | const SmallDeviceMessage = () => ( 5 |
6 |
7 |

NYC CRASH MAPPER

8 |

9 | Mobile version under development. Please come back on your laptop! 10 |

11 | 12 |
13 |
14 | ); 15 | 16 | export default SmallDeviceMessage; 17 | -------------------------------------------------------------------------------- /src/actions/filter_by_type_actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | FILTER_BY_TYPE_INJURY, 3 | FILTER_BY_TYPE_FATALITY, 4 | FILTER_BY_NO_INJURY_FATALITY, 5 | } from '../constants/action_types'; 6 | 7 | export const filterByTypeInjury = personType => ({ 8 | type: FILTER_BY_TYPE_INJURY, 9 | personType, 10 | }); 11 | 12 | export const filterByTypeFatality = personType => ({ 13 | type: FILTER_BY_TYPE_FATALITY, 14 | personType, 15 | }); 16 | 17 | export const filterByNoInjFat = () => ({ 18 | type: FILTER_BY_NO_INJURY_FATALITY 19 | }); 20 | -------------------------------------------------------------------------------- /scss/skeleton/base/_functions.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/9/2014 8 | * Sass Version by Seth Coelen https://github.com/whatsnewsaes 9 | */ 10 | 11 | @function grid-column-width($n) { 12 | @return $column-width * $n - ($column-margin*($total-columns - $n)/$total-columns); 13 | } 14 | 15 | @function grid-offset-length($n) { 16 | @return grid-column-width($n) + $column-margin; 17 | } 18 | -------------------------------------------------------------------------------- /src/containers/FilterByDateConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import FilterByDate from '../components/OptionsFilters/FilterByDate'; 4 | import { startDateChange, endDateChange } from '../actions/'; 5 | 6 | const mapStateToProps = ({ filterDate: { startDate, endDate }, yearRange, crashesDateRange }) => ({ 7 | startDate, 8 | endDate, 9 | crashesDateRange, 10 | years: yearRange.years || [], 11 | }); 12 | 13 | export default connect(mapStateToProps, { 14 | startDateChange, 15 | endDateChange, 16 | })(FilterByDate); 17 | -------------------------------------------------------------------------------- /scss/components/_about-copy.scss: -------------------------------------------------------------------------------- 1 | .about-crash-mapper { 2 | margin-bottom: $margin-25; 3 | 4 | h6 { 5 | font-weight: bolder; 6 | text-transform: uppercase; 7 | font-size: 1.5rem; 8 | } 9 | 10 | p { 11 | padding: 0 0 $padding-25 0; 12 | } 13 | 14 | ul { 15 | list-style: disc inside none; 16 | } 17 | 18 | p, 19 | li { 20 | color: $marine; 21 | font-size: 1.4rem; 22 | } 23 | 24 | p { 25 | padding-bottom: 20px; 26 | } 27 | 28 | li > p { 29 | display: inline; 30 | padding-bottom: 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/reducers/modal_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | MODAL_OPENED, 3 | MODAL_CLOSED, 4 | } from '../constants/action_types'; 5 | 6 | export default (state = {}, action) => { 7 | switch (action.type) { 8 | case MODAL_OPENED: 9 | return { 10 | ...state, 11 | modalType: action.modalType, 12 | showModal: true, 13 | }; 14 | 15 | case MODAL_CLOSED: 16 | return { 17 | ...state, 18 | modalType: '', 19 | showModal: false, 20 | }; 21 | 22 | default: 23 | return state; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /scss/skeleton/modules/_lists.scss: -------------------------------------------------------------------------------- 1 | // Lists 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | ul { 5 | list-style: circle inside; 6 | } 7 | 8 | ol { 9 | list-style: decimal inside; 10 | padding-left: 0; 11 | margin-top: 0; 12 | } 13 | 14 | ul { 15 | padding-left: 0; 16 | margin-top: 0; 17 | ul, ol { 18 | margin: 1.5rem 0 1.5rem 3rem; 19 | font-size: 90%; 20 | } 21 | } 22 | 23 | ol { 24 | ol, ul { 25 | margin: 1.5rem 0 1.5rem 3rem; 26 | font-size: 90%; 27 | } 28 | } 29 | 30 | li { 31 | margin-bottom: 1rem; 32 | } 33 | -------------------------------------------------------------------------------- /src/containers/OptionsFiltersConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { openModal } from '../actions/'; 4 | import OptionsFilters from '../components/OptionsFilters/'; 5 | 6 | const mapStateToProps = ({ browser, crashesMaxDate: { maxDate }, filterArea }) => { 7 | const { height } = browser; 8 | const { geo } = filterArea; 9 | return { 10 | geo, 11 | height, 12 | maxDate: maxDate ? maxDate.format('MM/DD/YYYY') : '' 13 | }; 14 | }; 15 | 16 | export default connect(mapStateToProps, { 17 | openModal 18 | })(OptionsFilters); 19 | -------------------------------------------------------------------------------- /scss/components/_stats-counter.scss: -------------------------------------------------------------------------------- 1 | .stats-counter { 2 | display: inline-block; 3 | width: 50%; 4 | border-top: 1px solid $hr-border; 5 | 6 | .stats-title { 7 | text-transform: uppercase; 8 | } 9 | 10 | &:nth-of-type(2n) { 11 | border-left: 1px solid $hr-border; 12 | width: calc(50% - 1px - 20px); 13 | padding-left: $padding-10; 14 | margin-right: $padding-10; 15 | } 16 | 17 | .count { 18 | display: inline-block; 19 | width: 20%; 20 | float: left; 21 | } 22 | 23 | p { 24 | height: 25px; 25 | text-align: center; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/LeafletMap/ZoomControls.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const ZoomControls = (props) => { 4 | const { handleZoomIn, handleZoomOut } = props; 5 | 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | }; 13 | 14 | ZoomControls.propTypes = { 15 | handleZoomIn: PropTypes.func.isRequired, 16 | handleZoomOut: PropTypes.func.isRequired, 17 | }; 18 | 19 | export default ZoomControls; 20 | -------------------------------------------------------------------------------- /src/containers/ModalConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { closeModal } from '../actions/'; 4 | import Modal from '../components/Modal/'; 5 | 6 | const mapStateToProps = ({ modal, filterType, filterArea, filterVehicle, filterDate }) => { 7 | const { showModal, modalType } = modal; 8 | const { startDate, endDate } = filterDate; 9 | return { 10 | showModal, 11 | modalType, 12 | filterArea, 13 | filterType, 14 | filterVehicle, 15 | startDate, 16 | endDate, 17 | }; 18 | }; 19 | 20 | export default connect(mapStateToProps, { 21 | closeModal, 22 | })(Modal); 23 | -------------------------------------------------------------------------------- /scss/components/_app-stats-legend.scss: -------------------------------------------------------------------------------- 1 | .app-stats-legend { 2 | height: $app-stats-legend-height; 3 | width: $app-stats-legend-width; 4 | 5 | p { 6 | font-size: 1.3rem; 7 | } 8 | 9 | .stats-title { 10 | @extend .roboto-medium; 11 | text-transform: uppercase; 12 | font-size: 1.4rem; 13 | padding-bottom: 0; 14 | padding-top: $padding-5; 15 | } 16 | 17 | .stats-date-range, 18 | .stats-total-crash-counter { 19 | width: 50%; 20 | float: left; 21 | display: inline-block; 22 | } 23 | 24 | .stats-total-crash-counter h6 { 25 | @extend .roboto-bold; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/containers/FilterByAreaConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { 3 | filterByAreaType, 4 | filterByAreaIdentifier, 5 | filterByAreaCustom, 6 | toggleCustomAreaDraw, 7 | } from '../actions/'; 8 | import FilterByArea from '../components/OptionsFilters/FilterByArea'; 9 | 10 | const mapStateToProps = ({ filterArea: { geo, identifier, drawEnabeled } }) => ({ 11 | drawEnabeled, 12 | geo, 13 | identifier, 14 | }); 15 | 16 | export default connect(mapStateToProps, { 17 | filterByAreaType, 18 | filterByAreaIdentifier, 19 | filterByAreaCustom, 20 | toggleCustomAreaDraw, 21 | })(FilterByArea); 22 | -------------------------------------------------------------------------------- /src/containers/FilterByTypeConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { 4 | filterByTypeInjury, 5 | filterByTypeFatality, 6 | filterByNoInjFat 7 | } from '../actions/'; 8 | import FilterByType from '../components/OptionsFilters/FilterByType'; 9 | 10 | const mapStateToProps = ({ filterType }) => { 11 | const { injury, fatality, noInjuryFatality } = filterType; 12 | return { 13 | injury, 14 | fatality, 15 | noInjuryFatality, 16 | }; 17 | }; 18 | 19 | export default connect(mapStateToProps, { 20 | filterByTypeFatality, 21 | filterByTypeInjury, 22 | filterByNoInjFat, 23 | })(FilterByType); 24 | -------------------------------------------------------------------------------- /src/components/StatsLegend/TotalCrashCounter.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const TotalCrashCounter = (props) => { 4 | const { totalCount } = props; 5 | const crash = (totalCount > 1 || totalCount === 0) ? 'Crashes' : 'Crash'; 6 | 7 | return ( 8 |
9 |
{`${totalCount.toLocaleString()} Total ${crash}`}
10 |
11 | ); 12 | }; 13 | 14 | TotalCrashCounter.defaultProps = { 15 | totalCount: 0 16 | }; 17 | 18 | TotalCrashCounter.propTypes = { 19 | totalCount: PropTypes.number.isRequired 20 | }; 21 | 22 | export default TotalCrashCounter; 23 | -------------------------------------------------------------------------------- /src/actions/filter_by_area_actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | FILTER_BY_AREA_TYPE, 3 | FILTER_BY_AREA_IDENTIFIER, 4 | FILTER_BY_AREA_CUSTOM, 5 | TOGGLE_CUSTOM_AREA_DRAW, 6 | } from '../constants/action_types'; 7 | 8 | export const filterByAreaType = (geo = 'citywide') => ({ 9 | type: FILTER_BY_AREA_TYPE, 10 | geo, 11 | }); 12 | 13 | export const filterByAreaIdentifier = identifier => ({ 14 | type: FILTER_BY_AREA_IDENTIFIER, 15 | identifier 16 | }); 17 | 18 | export const toggleCustomAreaDraw = () => ({ 19 | type: TOGGLE_CUSTOM_AREA_DRAW, 20 | }); 21 | 22 | export const filterByAreaCustom = lngLats => ({ 23 | type: FILTER_BY_AREA_CUSTOM, 24 | lngLats 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/Modal/ShareTwitter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Placeholder 4 | export default () => { 5 | // including the query string would make the tweet more than 150 chars 6 | const href = window.location.href; 7 | const arr = href.split('/'); 8 | const location = window.encodeURIComponent(`${arr[0]}//${arr[2]}/${arr[3]}`); 9 | const url = `https://twitter.com/home?status=${location}`; 10 | 11 | return ( 12 |
13 |

14 | 15 | Share NYC Crash Mapper on Twitter 16 | 17 |

18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /scss/components/_help-copy.scss: -------------------------------------------------------------------------------- 1 | .help-panel { 2 | max-height: 80vh; 3 | overflow: scroll; 4 | 5 | td { 6 | vertical-align: top; 7 | } 8 | 9 | td:last-child { 10 | padding-left: 10px; 11 | } 12 | 13 | h6 { 14 | font-weight: bolder; 15 | text-transform: uppercase; 16 | font-size: 1.5rem; 17 | } 18 | 19 | p { 20 | padding: 0 0 $padding-25 0; 21 | } 22 | 23 | ul { 24 | list-style: disc inside none; 25 | } 26 | 27 | p, 28 | li { 29 | color: $marine; 30 | font-size: 1.4rem; 31 | } 32 | 33 | p { 34 | padding-bottom: 20px; 35 | } 36 | 37 | li > p { 38 | display: inline; 39 | padding-bottom: 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scss/components/_filter-by-boundary.scss: -------------------------------------------------------------------------------- 1 | .filter-by-boundary { 2 | button { 3 | @extend .filter-options-button; 4 | 5 | &.cancel-drawing, 6 | &.draw-again { 7 | margin-left: $margin-10; 8 | } 9 | 10 | &.deselect-identifier { 11 | height: 20px; 12 | padding: 0 5px; 13 | margin: 0 0 0 $margin-10; 14 | font-size: 11px; 15 | line-height: 11px; 16 | } 17 | } 18 | 19 | p.identifier-label { 20 | display: inline-block; 21 | margin-left: $margin-10; 22 | font-size: 11px; 23 | } 24 | } 25 | 26 | .filter-by-boundary-message { 27 | font-size: 1.2rem; 28 | font-weight: 500; 29 | 30 | margin-bottom: $margin-10; 31 | margin-left: $margin-10; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/AppHeader.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | import MenuConnected from '../containers/MenuConnected'; 4 | 5 | const AppHeader = () => ( 6 |
7 |
8 |
9 | 10 | chekpeds logo 11 | 12 |
13 |

NYC Crash Mapper

14 |
15 | 16 |
17 | ); 18 | 19 | AppHeader.propTypes = { 20 | openModal: PropTypes.func.isRequired, 21 | }; 22 | 23 | export default AppHeader; 24 | -------------------------------------------------------------------------------- /src/containers/StatsLegendConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import StatsLegend from '../components/StatsLegend/'; 4 | 5 | const mapStateToProps = (state) => { 6 | const { filterDate, crashStats, contributingFactors, filterType, 7 | filterArea } = state; 8 | const { startDate, endDate } = filterDate; 9 | const { typeStats } = crashStats; 10 | const { factors } = contributingFactors; 11 | const { identifier, geo, lngLats } = filterArea; 12 | return { 13 | startDate, 14 | endDate, 15 | ...typeStats, 16 | contributingFactors: factors, 17 | filterType, 18 | identifier, 19 | geo, 20 | lngLats 21 | }; 22 | }; 23 | 24 | export default connect(mapStateToProps, {})(StatsLegend); 25 | -------------------------------------------------------------------------------- /scss/skeleton/skeleton.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/9/2014 8 | * Sass Version by Seth Coelen https://github.com/whatsnewsaes 9 | */ 10 | 11 | /* Base files. */ 12 | // @import "base/normalize"; 13 | @import "base/variables"; 14 | @import "base/functions"; 15 | @import "base/base-styles"; 16 | @import "base/utils"; 17 | @import "base/typography"; 18 | 19 | /* Modules */ 20 | @import "modules/grid"; 21 | @import "modules/buttons"; 22 | @import "modules/forms"; 23 | @import "modules/lists"; 24 | // @import "modules/code"; 25 | // @import "modules/tables"; 26 | @import "modules/spacing"; 27 | @import "modules/media-queries"; 28 | -------------------------------------------------------------------------------- /scss/components/_zoom-controls.scss: -------------------------------------------------------------------------------- 1 | .zoom-controls { 2 | width: $zoom-controls-width; 3 | height: $zoom-controls-height; 4 | left: $margin-25; 5 | top: $margin-5 + $app-header-height; 6 | padding: 0; 7 | color: $marine-light; 8 | 9 | .zoom-in, 10 | .zoom-out { 11 | width: $zoom-controls-width; 12 | height: calc(#{$zoom-controls-height} / 2); 13 | padding: 0; 14 | margin: 0; 15 | border-radius: 0; 16 | background-color: $btn-bg; 17 | border-color: $marine-light; 18 | color: $marine-light; 19 | font-size: 20px; 20 | line-height: 25px; 21 | 22 | &:hover { 23 | background-color: $btn-hover; 24 | border-color: $marine; 25 | color: $marine; 26 | } 27 | } 28 | 29 | .zoom-in { 30 | border-bottom: none; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/DownloadData.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | import FilterButton from './FilterButton'; 4 | 5 | const DownloadData = (props) => { 6 | const { lastUpdated, openModal } = props; 7 | return ( 8 |
9 | 15 |
16 |

Data last updated on:

17 |

{lastUpdated}

18 |
19 |
20 | ); 21 | }; 22 | 23 | DownloadData.propTypes = { 24 | openModal: PropTypes.func.isRequired, 25 | lastUpdated: PropTypes.string.isRequired, 26 | }; 27 | 28 | export default DownloadData; 29 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, compose } from 'redux'; 2 | import { createResponsiveStoreEnhancer } from 'redux-responsive'; 3 | 4 | import rootReducer from './reducers/'; 5 | import middleware from './middleware'; 6 | 7 | export default function makeStore(initialState) { 8 | const store = createStore( 9 | rootReducer, 10 | initialState, 11 | compose( 12 | createResponsiveStoreEnhancer(500), // throttle time 13 | applyMiddleware(...middleware) 14 | ) 15 | ); 16 | 17 | if (module.hot) { 18 | // enable hot module replacement 19 | module.hot.accept('./reducers/index.js', () => { 20 | const nextReducer = System.import('./reducers/index.js'); 21 | store.replaceReducer(nextReducer); 22 | }); 23 | } 24 | 25 | return store; 26 | } 27 | -------------------------------------------------------------------------------- /scss/components/_options-container.scss: -------------------------------------------------------------------------------- 1 | .options-container { 2 | 3 | &.opened { 4 | padding-bottom: $padding-10; 5 | } 6 | 7 | &.no-padding-bottom { 8 | padding-bottom: 0; 9 | } 10 | 11 | .options-container-header { 12 | padding-top: $padding-10; 13 | cursor: default; 14 | text-transform: uppercase; 15 | cursor: pointer; 16 | } 17 | 18 | .collapsable .options-container-header { 19 | } 20 | 21 | .options-container-title { 22 | @extend .roboto-medium; 23 | display: inline-block; 24 | } 25 | 26 | .options-container-collapse-icon { 27 | display: inline-block; 28 | float: right; 29 | margin-right: 10px; 30 | } 31 | 32 | .options-container-collapsable.scroll { 33 | overflow-y: auto !important; 34 | overflow-x: hidden; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scss/components/_small-device-message.scss: -------------------------------------------------------------------------------- 1 | .small-device-message { 2 | @media screen and (#{$bp-larger-than-desktop}) and (orientation: portrait) { 3 | display: none; 4 | } 5 | 6 | display: block; 7 | position: relative; 8 | width: 100vw; 9 | height: 100vh; 10 | 11 | .centered { 12 | height: 100%; 13 | position: relative; 14 | margin: 0 auto; 15 | padding: $padding-25; 16 | overflow-y: auto; 17 | 18 | h1, h2, h3, h4, h5, h6, p { 19 | color: $marine; 20 | } 21 | } 22 | 23 | .title, h2 { 24 | text-align: center; 25 | } 26 | 27 | .title { 28 | @extend %roboto-medium; 29 | } 30 | 31 | h2 { 32 | margin: 25px 0; 33 | } 34 | 35 | a { 36 | text-decoration: none; 37 | } 38 | 39 | p { 40 | padding-bottom: $padding-25; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /scss/skeleton/modules/_media-queries.scss: -------------------------------------------------------------------------------- 1 | // Media Queries 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | // Note: The best way to structure the use of media queries is to create the queries 5 | // near the relevant code. For example, if you wanted to change the styles for buttons 6 | // on small devices, paste the mobile query code up in the buttons section and style it 7 | // there. 8 | 9 | // Larger than mobile 10 | @media (#{$bp-larger-than-mobile}) {} 11 | 12 | // Larger than phablet (also point when grid becomes active) 13 | @media (#{$bp-larger-than-phablet}) {} 14 | 15 | // Larger than tablet 16 | @media (#{$bp-larger-than-tablet}) {} 17 | 18 | // Larger than desktop 19 | @media (#{$bp-larger-than-desktop}) {} 20 | 21 | // Larger than Desktop HD 22 | @media (#{$bp-larger-than-desktophd}) {} 23 | -------------------------------------------------------------------------------- /src/reducers/crash_stats_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | CRASHES_ALL_REQUEST, 3 | CRASHES_ALL_SUCCESS, 4 | CRASHES_ALL_ERROR 5 | } from '../constants/action_types'; 6 | 7 | export default (state = {}, action) => { 8 | switch (action.type) { 9 | case CRASHES_ALL_REQUEST: 10 | return { 11 | ...state, 12 | _fetchingCrashStatsTypes: true, 13 | typeStats: undefined 14 | }; 15 | case CRASHES_ALL_SUCCESS: 16 | return { 17 | ...state, 18 | _fetchingCrashStatsTypes: false, 19 | typeStats: action.json 20 | }; 21 | case CRASHES_ALL_ERROR: 22 | return { 23 | ...state, 24 | _fetchingCrashStatsTypes: false, 25 | error: action.error, 26 | typeStats: undefined 27 | }; 28 | default: 29 | return state; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/reducers/stats_contributing_factors_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | CONTRIBUTING_FACTORS_REQUEST, 3 | CONTRIBUTING_FACTORS_SUCCESS, 4 | CONTRIBUTING_FACTORS_ERROR, 5 | } from '../constants/action_types'; 6 | 7 | export default (state = {}, action) => { 8 | switch (action.type) { 9 | case CONTRIBUTING_FACTORS_REQUEST: 10 | return { 11 | ...state, 12 | _fetchingContributingFactors: true, 13 | }; 14 | case CONTRIBUTING_FACTORS_SUCCESS: 15 | return { 16 | ...state, 17 | _fetchingContributingFactors: false, 18 | factors: action.json 19 | }; 20 | case CONTRIBUTING_FACTORS_ERROR: 21 | return { 22 | ...state, 23 | _fetchingContributingFactors: false, 24 | error: action.error 25 | }; 26 | default: 27 | return state; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/reducers/year_range_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | CRASHES_YEAR_RANGE_ERROR, 3 | CRASHES_YEAR_RANGE_REQUEST, 4 | CRASHES_YEAR_RANGE_SUCCESS, 5 | } from '../constants/action_types'; 6 | 7 | export default (state = {}, action) => { 8 | switch (action.type) { 9 | case CRASHES_YEAR_RANGE_REQUEST: 10 | return { 11 | ...state, 12 | _fetchingCrashesYearRange: true, 13 | }; 14 | 15 | case CRASHES_YEAR_RANGE_SUCCESS: 16 | return { 17 | ...state, 18 | _fetchingCrashesYearRange: false, 19 | years: action.json.map(obj => obj.year), 20 | }; 21 | 22 | case CRASHES_YEAR_RANGE_ERROR: 23 | return { 24 | ...state, 25 | _fetchingCrashesYearRange: false, 26 | error: action.error 27 | }; 28 | 29 | default: 30 | return state; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /scss/_helpers.scss: -------------------------------------------------------------------------------- 1 | // helper classes 2 | .uppercase { 3 | text-transform: uppercase; 4 | } 5 | 6 | .title-case { 7 | text-transform: title-case; 8 | } 9 | 10 | .roboto-bold { 11 | @extend %roboto-bold; 12 | } 13 | 14 | .roboto-regular { 15 | @extend %roboto-regular; 16 | } 17 | 18 | .roboto-medium { 19 | @extend %roboto-medium; 20 | } 21 | 22 | // scrollbar styling 23 | .scroll { 24 | overflow-y: auto; 25 | overflow-x: hidden; 26 | 27 | &::-webkit-scrollbar { 28 | width: 10px; /* for vertical scrollbars */ 29 | } 30 | 31 | &::-webkit-scrollbar-track { 32 | background: rgba(0, 0, 0, 0.1); 33 | } 34 | 35 | &::-webkit-scrollbar-thumb { 36 | background: rgba(0, 0, 0, 0.5); 37 | } 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | visibility: hidden; 43 | } 44 | 45 | .hidden-sneaky { 46 | opacity: 0; 47 | z-index: -9999; 48 | } 49 | -------------------------------------------------------------------------------- /src/containers/FilterByVehicleConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { 4 | filterByVehicleCar, 5 | filterByVehicleTruck, 6 | filterByVehicleMotorcycle, 7 | filterByVehicleBicycle, 8 | filterByVehicleSuv, 9 | filterByVehicleBusVan, 10 | filterByVehicleScooter, 11 | filterByVehicleOther 12 | } from '../actions/'; 13 | import FilterByVehicle from '../components/OptionsFilters/FilterByVehicle'; 14 | 15 | const mapStateToProps = ({ filterVehicle }) => { 16 | const { vehicle } = filterVehicle; 17 | return { 18 | vehicle, 19 | }; 20 | }; 21 | 22 | export default connect(mapStateToProps, { 23 | filterByVehicleCar, 24 | filterByVehicleTruck, 25 | filterByVehicleMotorcycle, 26 | filterByVehicleBicycle, 27 | filterByVehicleSuv, 28 | filterByVehicleBusVan, 29 | filterByVehicleScooter, 30 | filterByVehicleOther, 31 | })(FilterByVehicle); 32 | -------------------------------------------------------------------------------- /src/reducers/crashes_max_date_reducer.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { 3 | CRASHES_MAX_DATE_ERROR, 4 | CRASHES_MAX_DATE_REQUEST, 5 | CRASHES_MAX_DATE_RESPONSE 6 | } from '../constants/action_types'; 7 | 8 | export default (state = {}, action) => { 9 | switch (action.type) { 10 | case CRASHES_MAX_DATE_REQUEST: 11 | return { 12 | ...state, 13 | _fetchingCrashesMaxDate: true, 14 | }; 15 | 16 | case CRASHES_MAX_DATE_RESPONSE: 17 | return { 18 | ...state, 19 | _fetchingCrashesMaxDate: false, 20 | maxDate: moment(action.json[0].max_date, 'YYYY-MM-DDTHH:mm:ssZ', true) 21 | }; 22 | 23 | case CRASHES_MAX_DATE_ERROR: 24 | return { 25 | ...state, 26 | _fetchingCrashesMaxDate: false, 27 | error: action.error, 28 | }; 29 | 30 | default: 31 | return state; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/ShareOptions.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | import FilterButton from './FilterButton'; 4 | 5 | const ShareOptions = (props) => { 6 | const { openModal } = props; 7 | 8 | return ( 9 |
10 | 16 | 22 | 28 |
29 | ); 30 | }; 31 | 32 | ShareOptions.propTypes = { 33 | openModal: PropTypes.func.isRequired 34 | }; 35 | 36 | export default ShareOptions; 37 | -------------------------------------------------------------------------------- /scss/skeleton/base/_base-styles.scss: -------------------------------------------------------------------------------- 1 | // Base Styles 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | // NOTE 4 | // html is set to 62.5% so that all the REM measurements throughout Skeleton 5 | // are based on 10px sizing. So basically 1.5rem = 15px :) 6 | 7 | html { 8 | font-size: 62.5%; 9 | } 10 | 11 | body { 12 | font-size: 1.5em; // currently ems cause chrome bug misinterpreting rems on body element 13 | line-height: 1.6; 14 | font-weight: 400; 15 | font-family: $font-family; 16 | color: $font-color; 17 | } 18 | 19 | // Links 20 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 21 | 22 | a { 23 | color: $link-color; 24 | &:hover { 25 | color: darken($link-color, 5%); 26 | } 27 | } 28 | 29 | // Misc 30 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 31 | 32 | hr { 33 | margin-top: 3rem; 34 | margin-bottom: 3.5rem; 35 | border-width: 0; 36 | border-top: 1px solid $light-grey; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/FooterOptions.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | import FilterButton from './FilterButton'; 4 | 5 | const FooterOptions = (props) => { 6 | const { openModal } = props; 7 | 8 | return ( 9 |
10 | 16 | 22 | 28 |
29 | ); 30 | }; 31 | 32 | FooterOptions.propTypes = { 33 | openModal: PropTypes.func.isRequired, 34 | }; 35 | 36 | export default FooterOptions; 37 | -------------------------------------------------------------------------------- /src/reducers/crashes_date_range_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | CRASHES_DATE_RANGE_REQUEST, 3 | CRASHES_DATE_RANGE_SUCCESS, 4 | CRASHES_DATE_RANGE_ERROR, 5 | } from '../constants/action_types'; 6 | 7 | import { momentize } from '../constants/api'; 8 | 9 | export default (state = {}, action) => { 10 | switch (action.type) { 11 | case CRASHES_DATE_RANGE_REQUEST: 12 | return { 13 | ...state, 14 | _fetchingCrashesDateRange: true, 15 | }; 16 | 17 | case CRASHES_DATE_RANGE_SUCCESS: 18 | return { 19 | ...state, 20 | _fetchingCrashesDateRange: false, 21 | minDate: momentize(action.json[0].min), 22 | maxDate: momentize(action.json[0].max), 23 | }; 24 | 25 | case CRASHES_DATE_RANGE_ERROR: 26 | return { 27 | ...state, 28 | _fetchingCrashesDateRange: false, 29 | error: action.error, 30 | }; 31 | 32 | default: 33 | return state; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/FilterByAreaMessage.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const FilterByAreaMessage = (props) => { 4 | const { geo } = props; 5 | 6 | // the stock message... or maybe not 7 | let message = 'Choose a boundary type, then click an area on the map to filter crashes within that area.'; 8 | 9 | switch (geo) { 10 | case 'custom': 11 | message = 'Draw a custom area on the map.'; 12 | break; 13 | case 'intersection': 14 | message = 'The 500 most dangerous intersections over last 2 years. Zoom in to see 90-foot radius for each.'; 15 | break; 16 | default: 17 | break; 18 | } 19 | 20 | return ( 21 |

22 | {message} 23 |

24 | ); 25 | }; 26 | 27 | FilterByAreaMessage.defaultProps = { 28 | }; 29 | 30 | FilterByAreaMessage.propTypes = { 31 | geo: PropTypes.string.isRequired, 32 | }; 33 | 34 | export default FilterByAreaMessage; 35 | -------------------------------------------------------------------------------- /scss/components/_filter-button.scss: -------------------------------------------------------------------------------- 1 | .filter-options-button { 2 | height: 25px; 3 | line-height: 25px; 4 | background-color: $btn-bg; 5 | border-color: rgba(0,0,0,0); 6 | color: $marine-light; 7 | padding: 0 10px; 8 | text-align: left; 9 | text-transform: capitalize; 10 | letter-spacing: 0.5px; 11 | border: none; 12 | 13 | &:hover, 14 | &.active { 15 | color: $marine; 16 | border: none; 17 | } 18 | 19 | &:hover { 20 | background-color: $btn-hover; 21 | } 22 | 23 | &.active { 24 | background-color: $btn-active; 25 | } 26 | 27 | &.wide { 28 | width: 135px; 29 | } 30 | 31 | &.med { 32 | width: 125px; 33 | } 34 | 35 | &.narrow { 36 | width: 90px; 37 | } 38 | 39 | &.auto { 40 | width: auto; 41 | } 42 | 43 | &.link { 44 | border: none; 45 | border-radius: 0; 46 | background-color: $transparent; 47 | color: $btn-bg; 48 | 49 | &:hover { 50 | color: $btn-hover; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/containers/AppConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { 4 | fetchCrashStatsData, 5 | fetchCrashesYearRange, 6 | fetchCrashesMaxDate, 7 | fetchCrashesDateRange, 8 | fetchContributingFactors, 9 | openModal, 10 | } from '../actions/'; 11 | 12 | import App from '../components/App'; 13 | 14 | const mapStateToProps = ({ browser, filterDate, filterType, filterVehicle, filterArea }) => { 15 | const { startDate, endDate } = filterDate; 16 | const { identifier, geo, lngLats } = filterArea; 17 | const { height, width } = browser; 18 | return { 19 | startDate, 20 | endDate, 21 | filterType, 22 | filterVehicle, 23 | identifier, 24 | geo, 25 | lngLats, 26 | height, 27 | width, 28 | }; 29 | }; 30 | 31 | export default connect(mapStateToProps, { 32 | fetchCrashStatsData, 33 | fetchCrashesDateRange, 34 | fetchCrashesMaxDate, 35 | fetchCrashesYearRange, 36 | fetchContributingFactors, 37 | openModal, 38 | })(App); 39 | -------------------------------------------------------------------------------- /scss/components/_stats-contributing-factors-list.scss: -------------------------------------------------------------------------------- 1 | .contributing-factors-list { 2 | height: calc(100% - #{$padding-5}); 3 | border: 1px solid $hr-border; 4 | 5 | ul, li { 6 | padding: 0; 7 | margin: 0; 8 | list-style-type: none; 9 | } 10 | 11 | li { 12 | width: calc(100% - 10px); 13 | margin: 0 5px 0 5px; 14 | border-bottom: 0.5px dashed $hr-border; 15 | 16 | &:last-of-type { 17 | border: none; 18 | } 19 | } 20 | 21 | p { 22 | padding: 2px 5px; 23 | height: 100%; 24 | font-size: 1.3rem; 25 | } 26 | 27 | .factor-count, 28 | .factor-type { 29 | display: inline-flex; 30 | flex-direction: row; 31 | justify-content: flex-start; 32 | align-items: center; 33 | } 34 | 35 | .factor-count { 36 | width: 60px; 37 | max-width: 60px; 38 | text-align: right; 39 | } 40 | 41 | .factor-type { 42 | max-width: calc(100% - 60px - 20px); 43 | padding-left: $padding-5; 44 | overflow-x: hidden; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scss/skeleton/base/_typography.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | h1, h2, h3, h4, h5, h6 { 5 | margin-top: 0; 6 | margin-bottom: 2rem; 7 | font-weight: 300; 8 | } 9 | 10 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem; } 11 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 12 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 13 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 14 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 15 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 16 | 17 | // Larger than phablet 18 | @media only screen 19 | and (#{$bp-larger-than-phablet}) { 20 | h1 { font-size: 5.0rem; } 21 | h2 { font-size: 4.2rem; } 22 | h3 { font-size: 3.6rem; } 23 | h4 { font-size: 3.0rem; } 24 | h5 { font-size: 2.4rem; } 25 | h6 { font-size: 1.5rem; } 26 | } 27 | 28 | p { 29 | margin-top: 0; 30 | } 31 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | fetchCrashStatsData, 3 | fetchContributingFactors, 4 | fetchCrashesYearRange, 5 | fetchCrashesDateRange, 6 | fetchCrashesMaxDate, 7 | fetchGeoPolygons, 8 | } from './async_actions'; 9 | export { startDateChange, endDateChange } from './filter_by_date_actions'; 10 | export { 11 | filterByTypeInjury, 12 | filterByTypeFatality, 13 | filterByNoInjFat 14 | } from './filter_by_type_actions'; 15 | export { 16 | filterByVehicleCar, 17 | filterByVehicleTruck, 18 | filterByVehicleMotorcycle, 19 | filterByVehicleBicycle, 20 | filterByVehicleSuv, 21 | filterByVehicleBusVan, 22 | filterByVehicleScooter, 23 | filterByVehicleOther 24 | } from './filter_by_vehicle_actions'; 25 | export { 26 | filterByAreaType, 27 | filterByAreaIdentifier, 28 | filterByAreaCustom, 29 | toggleCustomAreaDraw, 30 | } from './filter_by_area_actions'; 31 | export filterByContributingFactor from './filter_contributing_factor_actions'; 32 | export { openModal, closeModal } from './modal_actions'; 33 | -------------------------------------------------------------------------------- /src/containers/LeafletMapConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { filterByAreaIdentifier, filterByAreaCustom, fetchGeoPolygons } from '../actions/'; 4 | import LeafletMap from '../components/LeafletMap/'; 5 | 6 | const mapStateToProps = ({ filterDate, filterType, filterVehicle, filterArea }, ownProps) => { 7 | const { startDate, endDate } = filterDate; 8 | const { location: { query } } = ownProps; 9 | const { lat, lng, zoom } = query; 10 | const { geo, geojson, identifier, lngLats, drawEnabeled } = filterArea; 11 | return { 12 | zoom: zoom ? Number(zoom) : undefined, 13 | lat: lat ? Number(lat) : undefined, 14 | lng: lng ? Number(lng) : undefined, 15 | startDate, 16 | endDate, 17 | filterType, 18 | filterVehicle, 19 | drawEnabeled, 20 | geo, 21 | geojson, 22 | identifier, 23 | lngLats, 24 | }; 25 | }; 26 | 27 | export default connect(mapStateToProps, { 28 | filterByAreaIdentifier, 29 | filterByAreaCustom, 30 | fetchGeoPolygons, 31 | })(LeafletMap); 32 | -------------------------------------------------------------------------------- /src/components/Modal/ShareURL.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CopyToClipboard from 'react-copy-to-clipboard'; 3 | 4 | // TO DO: create dl button using URL & SQL API query, e.g. 5 | // https://username.cartodb.com/api/v2/sql?q=SELECT%20*%20FROM%20table_name&format=CSV 6 | class ShareURL extends Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | copied: false, 11 | value: window.location.href 12 | }; 13 | } 14 | 15 | 16 | render() { 17 | const { value, copied } = this.state; 18 | 19 | return ( 20 |
21 |

The current map view and any active filters may be shared using this URL:

22 | e.target.select()} readOnly /> 23 | this.setState({ copied: true })}> 24 | 25 | 26 | 27 | { copied ? 'Copied!' : '' } 28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | export default ShareURL; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chris Henrick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/StatsLegend/ContributingFactorsList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const ContributingFactorsList = (props) => { 4 | const { factors } = props; 5 | 6 | const factorsList = () => 7 | factors.map((f) => { 8 | const { factor, count_factor } = f; 9 | 10 | return ( 11 |
  • 12 |

    13 | {count_factor.toLocaleString()} 14 |

    15 |

    16 | { 17 | factor !== '' ? factor.replace(/\//g, ' / ') : 'None Recorded' 18 | } 19 |

    20 |
  • ); 21 | }); 22 | 23 | return ( 24 |
    25 | 28 |
    29 | ); 30 | }; 31 | 32 | ContributingFactorsList.defaultProps = { 33 | factors: [] 34 | }; 35 | 36 | ContributingFactorsList.propTypes = { 37 | factors: PropTypes.arrayOf(PropTypes.shape({ 38 | count_factor: PropTypes.number, 39 | factor: PropTypes.string 40 | })) 41 | }; 42 | 43 | export default ContributingFactorsList; 44 | -------------------------------------------------------------------------------- /src/actions/filter_by_vehicle_actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | FILTER_BY_VEHICLE_CAR, 3 | FILTER_BY_VEHICLE_TRUCK, 4 | FILTER_BY_VEHICLE_MOTORCYCLE, 5 | FILTER_BY_VEHICLE_BICYCLE, 6 | FILTER_BY_VEHICLE_SUV, 7 | FILTER_BY_VEHICLE_BUSVAN, 8 | FILTER_BY_VEHICLE_SCOOTER, 9 | FILTER_BY_VEHICLE_OTHER 10 | } from '../constants/action_types'; 11 | 12 | export const filterByVehicleCar = () => ({ 13 | type: FILTER_BY_VEHICLE_CAR, 14 | }); 15 | 16 | export const filterByVehicleTruck = () => ({ 17 | type: FILTER_BY_VEHICLE_TRUCK, 18 | }); 19 | 20 | export const filterByVehicleMotorcycle = () => ({ 21 | type: FILTER_BY_VEHICLE_MOTORCYCLE, 22 | }); 23 | 24 | export const filterByVehicleBicycle = () => ({ 25 | type: FILTER_BY_VEHICLE_BICYCLE, 26 | }); 27 | 28 | export const filterByVehicleSuv = () => ({ 29 | type: FILTER_BY_VEHICLE_SUV, 30 | }); 31 | 32 | export const filterByVehicleBusVan = () => ({ 33 | type: FILTER_BY_VEHICLE_BUSVAN, 34 | }); 35 | 36 | export const filterByVehicleScooter = () => ({ 37 | type: FILTER_BY_VEHICLE_SCOOTER, 38 | }); 39 | 40 | export const filterByVehicleOther = () => ({ 41 | type: FILTER_BY_VEHICLE_OTHER, 42 | }); 43 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/FilterButton.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | 4 | const FilterButton = (props) => { 5 | const { label, btnSize, id, handleClick, isActive, preventRetrigger } = props; 6 | 7 | const btnClasses = cx(btnSize, { 8 | 'filter-options-button': true, 9 | 'roboto-medium': true, 10 | active: isActive 11 | }); 12 | 13 | function a() { 14 | if (preventRetrigger) { 15 | if (!isActive) { 16 | handleClick(id); 17 | } 18 | } else { 19 | handleClick(id); 20 | } 21 | } 22 | 23 | return ( 24 | 30 | ); 31 | }; 32 | 33 | FilterButton.defaultProps = { 34 | btnSize: 'wide', 35 | isActive: false, 36 | preventRetrigger: false, 37 | }; 38 | 39 | FilterButton.propTypes = { 40 | btnSize: PropTypes.string, 41 | label: PropTypes.string.isRequired, 42 | handleClick: PropTypes.func.isRequired, 43 | id: PropTypes.string.isRequired, 44 | isActive: PropTypes.bool, 45 | preventRetrigger: PropTypes.bool, 46 | }; 47 | 48 | export default FilterButton; 49 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { Router, Route, IndexRoute, hashHistory } from 'react-router'; 5 | import { Provider } from 'react-redux'; 6 | import { calculateResponsiveState } from 'redux-responsive'; 7 | import { syncHistoryWithStore } from 'react-router-redux'; 8 | import debounce from 'lodash/debounce'; 9 | 10 | import makeStore from './store'; 11 | import { makeDefaultState } from './constants/api'; 12 | import AppConnected from './containers/AppConnected'; 13 | import '../scss/main.scss'; 14 | 15 | const state = makeDefaultState(); 16 | const store = makeStore(state); 17 | const history = syncHistoryWithStore(hashHistory, store); 18 | 19 | // fire redux-responsive on window resize event 20 | window.addEventListener('resize', debounce(() => 21 | store.dispatch(calculateResponsiveState(window)), 250)); 22 | 23 | render( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | , 32 | document.getElementById('root') 33 | ); 34 | -------------------------------------------------------------------------------- /src/components/LoadingIndicator.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Spinner from 'spin'; 3 | 4 | class LoadingIndicator extends Component { 5 | constructor() { 6 | super(); 7 | this.el = undefined; 8 | this.spinner = undefined; 9 | this.opts = { 10 | color: '#17838f', // scss/_variables.$marine-light 11 | lines: 11, 12 | length: 7, 13 | width: 3, 14 | radius: 6, 15 | scale: 1, 16 | shadow: false, 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | this.spinner = new Spinner(this.opts).spin(this.el); 22 | window.spinner = this.spinner; 23 | window.el = this.el; 24 | } 25 | 26 | componentWillReceiveProps(nextProps) { 27 | const { isLoading } = nextProps; 28 | 29 | if (isLoading) { 30 | this.spinner.spin(this.el); 31 | } else { 32 | this.spinner.stop(); 33 | } 34 | } 35 | 36 | shouldComponentUpdate() { 37 | return false; 38 | } 39 | 40 | render() { 41 | return ( 42 |
    { this.el = _; }} className="loading-indicator" /> 43 | ); 44 | } 45 | } 46 | 47 | LoadingIndicator.propTypes = { 48 | isLoading: PropTypes.bool.isRequired, 49 | }; 50 | 51 | export default LoadingIndicator; 52 | -------------------------------------------------------------------------------- /scss/skeleton/base/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | // Breakpoints 5 | $bp-larger-than-mobile : "min-width: 414px" !default; 6 | $bp-larger-than-phablet : "min-width: 550px" !default; 7 | $bp-larger-than-tablet : "min-width: 750px" !default; 8 | $bp-larger-than-desktop : "min-width: 1000px" !default; 9 | $bp-larger-than-desktophd : "min-width: 1200px" !default; 10 | 11 | // Colors 12 | $light-grey: #e1e1e1 !default; 13 | $dark-grey: #333 !default; 14 | $primary-color: #33c3f0 !default; 15 | $secondary-color: lighten($dark-grey, 13.5%) !default; 16 | $border-color: #bbb !default; 17 | $link-color: #1eaedb !default; 18 | $font-color: #222 !default; 19 | 20 | // Typography 21 | $font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default; 22 | 23 | //Grid Variables 24 | $container-width: 960px !default; 25 | $container-width-larger-than-mobile: 85% !default; 26 | $container-width-larger-than-phablet: 80% !default; 27 | $total-columns: 12 !default; 28 | $column-width: 100 / $total-columns !default; // calculates individual column width based off of # of columns 29 | $column-margin: 4% !default; // space between columns 30 | 31 | // Misc 32 | $global-radius: 4px !default; 33 | -------------------------------------------------------------------------------- /src/components/Modal/ModalContent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | // Higher Order Component (HOC) that wraps modal content's children 4 | // see https://facebook.github.io/react/docs/higher-order-components.html 5 | const ModalContent = (WrappedComponent, config) => { 6 | const { modalType, closeModal } = config; 7 | 8 | return class MC extends Component { 9 | render() { 10 | const title = { 11 | about: 'About NYC Crash Mapper', 12 | help: 'How to Use Crash Mapper', 13 | copyright: 'Copyright', 14 | disclaimer: 'Disclaimer', 15 | 'download-data': 'Download Data', 16 | 'share-fb': 'Share on Facebook', 17 | 'share-tw': 'Share on Twitter', 18 | 'share-url': 'Share URL', 19 | }; 20 | 21 | // wraps the child component with a title and close button 22 | // passes closeModal action creator in case child needs to call it, 23 | // e.g. DownloadData buttons 24 | return ( 25 |
    26 |
    30 | ); 31 | } 32 | }; 33 | }; 34 | 35 | export default ModalContent; 36 | -------------------------------------------------------------------------------- /src/reducers/filter_by_type_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FILTER_BY_TYPE_INJURY, 3 | FILTER_BY_TYPE_FATALITY, 4 | FILTER_BY_NO_INJURY_FATALITY, 5 | } from '../constants/action_types'; 6 | 7 | const defaultState = { 8 | injury: { 9 | cyclist: false, 10 | motorist: false, 11 | pedestrian: false, 12 | }, 13 | fatality: { 14 | cyclist: false, 15 | motorist: false, 16 | pedestrian: false, 17 | }, 18 | noInjuryFatality: false 19 | }; 20 | 21 | export default (state = defaultState, action) => { 22 | const { personType } = action; 23 | const { injury, fatality } = state; 24 | 25 | switch (action.type) { 26 | case FILTER_BY_TYPE_INJURY: 27 | return { 28 | ...state, 29 | injury: { 30 | ...injury, 31 | [personType]: !injury[personType] 32 | }, 33 | noInjuryFatality: false 34 | }; 35 | case FILTER_BY_TYPE_FATALITY: 36 | return { 37 | ...state, 38 | fatality: { 39 | ...fatality, 40 | [personType]: !fatality[personType] 41 | }, 42 | noInjuryFatality: false 43 | }; 44 | case FILTER_BY_NO_INJURY_FATALITY: 45 | return { 46 | ...defaultState, 47 | noInjuryFatality: !state.noInjuryFatality 48 | }; 49 | default: 50 | return state; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /scss/components/_app-header.scss: -------------------------------------------------------------------------------- 1 | .app-header { 2 | width: $app-header-width; 3 | height: $app-header-height; 4 | display: flex; 5 | align-items: center; 6 | position: absolute; 7 | justify-content: flex-start; 8 | top: -5px; 9 | left: $margin-25; 10 | z-index: 1; 11 | color: rgba($marine-light, 0.7); 12 | 13 | .header-logo-title { 14 | display: inline-flex; 15 | align-items: center; 16 | } 17 | 18 | .logo-checkpeds, 19 | .header-title, 20 | .header-about { 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | .logo-chekpeds { 26 | display: inline-block; 27 | height: 33px; 28 | 29 | a, a:hover { 30 | background-color: none; 31 | border: none; 32 | } 33 | 34 | img { 35 | height: 33px; 36 | } 37 | } 38 | 39 | .header-title { 40 | display: inline-block; 41 | font-size: 2.6rem; 42 | text-transform: uppercase; 43 | margin-left: $margin-10; 44 | 45 | @media screen and (#{$bp-narrow-width}) { 46 | font-size: 4rem; 47 | } 48 | } 49 | 50 | .header-about { 51 | @extend .roboto-bold; 52 | display: inline-block; 53 | position: relative; 54 | bottom: 10px; 55 | 56 | a { 57 | cursor: pointer; 58 | color: rgba($marine-light, 0.7); 59 | 60 | &:hover { 61 | color: $marine; 62 | } 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /scss/components/_app-menu.scss: -------------------------------------------------------------------------------- 1 | .Menu { 2 | width: 350px; 3 | display: inline-flex; 4 | justify-content: flex-start; 5 | align-items: baseline; 6 | margin: 0 0 0 10px; 7 | padding: 0; 8 | list-style: none; 9 | 10 | li { 11 | position: relative; 12 | padding: 0; 13 | margin: 0; 14 | margin-left: 10px; 15 | font-weight: normal; 16 | cursor: pointer; 17 | transition: all .3s; 18 | 19 | &::before { 20 | content: ''; 21 | height: 1px; 22 | width: 0; 23 | background-color: rgba($marine-light, 0.7); 24 | display: block; 25 | position: absolute; 26 | bottom: 0; 27 | left: 50%; 28 | transition: all .3s; 29 | } 30 | 31 | &:hover::before { 32 | left: 0; 33 | width: 100%; 34 | } 35 | } 36 | 37 | button, 38 | a { 39 | height: auto; 40 | margin: 0; 41 | padding: 0 10px; 42 | font-size: 1.4rem; 43 | font-weight: normal; 44 | line-height: 1.4rem; 45 | color: rgba($marine-light, 0.7); 46 | text-transform: none; 47 | 48 | &.active { 49 | font-weight: bold; 50 | } 51 | 52 | &:hover { 53 | // color: $marine; 54 | } 55 | } 56 | 57 | button { 58 | background-color: transparent; 59 | border-radius: 0; 60 | border: none; 61 | } 62 | 63 | a { 64 | text-decoration: none; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /scss/main.scss: -------------------------------------------------------------------------------- 1 | // normalize browser defaults 2 | @import '~normalize-scss/sass/normalize'; 3 | @include normalize(); 4 | 5 | @import '~react-select/dist/react-select.css'; 6 | @import '~leaflet-draw/dist/leaflet.draw.css'; 7 | 8 | // skeleton.css base styles 9 | @import './skeleton/skeleton'; 10 | 11 | // skeleton overrides 12 | @import 'variables'; 13 | 14 | // app globals 15 | @import 'app'; 16 | @import 'helpers'; 17 | 18 | // individual component styles 19 | @import 'components/app-header'; 20 | @import 'components/app-menu'; 21 | @import 'components/zoom-controls'; 22 | @import 'components/loading-indicator'; 23 | @import 'components/leaflet-map'; 24 | @import 'components/app-options-filters'; 25 | @import 'components/app-stats-legend'; 26 | @import 'components/options-container'; 27 | @import 'components/filter-button'; 28 | @import 'components/filter-by-boundary'; 29 | @import 'components/filter-by-type'; 30 | @import 'components/filter-by-date'; 31 | @import 'components/download-data'; 32 | @import 'components/share-options'; 33 | @import 'components/options-filter-footer'; 34 | @import 'components/stats-counter'; 35 | @import 'components/stats-contributing-factors-list'; 36 | @import 'components/stats-legend-container'; 37 | @import 'components/modal'; 38 | @import 'components/small-device-message'; 39 | @import 'components/about-copy'; 40 | @import 'components/help-copy'; 41 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true 5 | }, 6 | "globals": { 7 | "L": false, 8 | "cartodb": false, 9 | "cdb": false 10 | }, 11 | "parser": "babel-eslint", 12 | "parserOptions": { 13 | "ecmaVersion": 6, 14 | "ecmaFeatures": { 15 | "experimentalObjectRestSpread": true, 16 | "spread": true, 17 | } 18 | }, 19 | "plugins": [ 20 | "babel" 21 | ], 22 | "rules": { 23 | "arrow-body-style": ["error", "as-needed"], 24 | "camelcase": 0, 25 | "comma-dangle": 0, 26 | "indent": [1, 2, { "SwitchCase": 1 }], 27 | "jsx-a11y/no-static-element-interactions": 0, 28 | "import/no-extraneous-dependencies": ["error", { 29 | "devDependencies": true, 30 | "optionalDependencies": false, 31 | "peerDependencies": false 32 | }], 33 | "no-param-reassign": 0, 34 | "no-underscore-dangle": 0, 35 | "react/jsx-filename-extension": 0, 36 | "react/no-array-index-key": 0, 37 | "react/no-unescaped-entities": 0, 38 | "react/no-unused-prop-types": 0, 39 | "react/prefer-stateless-function": 0, 40 | "semi": [2, "always"], 41 | "space-before-function-paren": 0 42 | }, 43 | "settings": { 44 | "import/extensions": [".js"], 45 | "import/resolver": { 46 | "webpack": { 47 | "config": "webpack.config.js" 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/StatsLegend/StatsCounter.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const StatsCounter = (props) => { 4 | const { cyclist, ped, driver, other, total, title } = props; 5 | 6 | return ( 7 |
    8 |
    {title}
    9 |
    10 |

    {cyclist.toLocaleString()}

    11 |

    Cyclist

    12 |
    13 |
    14 |

    {ped.toLocaleString()}

    15 |

    Ped

    16 |
    17 |
    18 |

    {driver.toLocaleString()}

    19 |

    Motorist

    20 |
    21 |
    22 |

    {other.toLocaleString()}

    23 |

    Unknown

    24 |
    25 |
    26 |

    {total.toLocaleString()}

    27 |

    Total

    28 |
    29 |
    30 | ); 31 | }; 32 | 33 | StatsCounter.defaultProps = { 34 | cyclist: 0, 35 | driver: 0, 36 | ped: 0, 37 | other: 0, 38 | total: 0, 39 | title: '' 40 | }; 41 | 42 | StatsCounter.propTypes = { 43 | cyclist: PropTypes.number, 44 | driver: PropTypes.number, 45 | ped: PropTypes.number, 46 | other: PropTypes.number, 47 | total: PropTypes.number, 48 | title: PropTypes.string 49 | }; 50 | 51 | export default StatsCounter; 52 | -------------------------------------------------------------------------------- /src/containers/MenuConnected.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import { dateStringFormatModel } from '../constants/api'; 4 | import { openModal } from '../actions'; 5 | import Menu from '../components/Menu'; 6 | 7 | const mapStateToProps = ({ filterDate, filterArea, filterType, filterVehicle }) => { 8 | const { startDate, endDate } = filterDate; 9 | const { geo, identifier, lngLats } = filterArea; 10 | const { injury, fatality } = filterType; 11 | 12 | // these props are used to create the query params when linking to the chart view app 13 | return { 14 | p1start: startDate.format(dateStringFormatModel), 15 | p1end: endDate.format(dateStringFormatModel), 16 | geo, 17 | lngLats: encodeURIComponent(JSON.stringify(lngLats)), 18 | primary: identifier, 19 | pinj: injury.pedestrian, 20 | pfat: fatality.pedestrian, 21 | cinj: injury.cyclist, 22 | cfat: fatality.cyclist, 23 | minj: injury.motorist, 24 | mfat: fatality.motorist, 25 | vcar: filterVehicle.vehicle.car, 26 | vtruck: filterVehicle.vehicle.truck, 27 | vmotorcycle: filterVehicle.vehicle.motorcycle, 28 | vbicycle: filterVehicle.vehicle.bicycle, 29 | vsuv: filterVehicle.vehicle.suv, 30 | vbusvan: filterVehicle.vehicle.busvan, 31 | vscooter: filterVehicle.vehicle.scooter, 32 | vother: filterVehicle.vehicle.other, 33 | }; 34 | }; 35 | 36 | export default connect(mapStateToProps, { 37 | openModal, 38 | })(Menu); 39 | -------------------------------------------------------------------------------- /scss/components/_stats-legend-container.scss: -------------------------------------------------------------------------------- 1 | .legend-container { 2 | position: relative; 3 | padding-left: $padding-10; 4 | width: calc(100% - #{$padding-10}); 5 | height: 100%; 6 | 7 | .legend-crash-types, 8 | .legend-crash-count { 9 | display: inline-block; 10 | float: left; 11 | height: 100%; 12 | width: 50%; 13 | } 14 | 15 | .legend-crash-count { 16 | width: 40%; 17 | position: absolute; 18 | top: -$stats-header-height - 5px; 19 | right: 10px; 20 | } 21 | 22 | ul, li { 23 | padding: 0; 24 | margin: 0; 25 | list-style-type: none; 26 | } 27 | 28 | li { 29 | width: 100%; 30 | margin-left: 5px; 31 | } 32 | 33 | p { 34 | display: inline; 35 | font-size: 1.1rem; 36 | margin-left: 5px; 37 | } 38 | 39 | span { 40 | width: 7px; 41 | height: 7px; 42 | display: inline-block; 43 | border: 0.7px solid $off-white; 44 | border-radius: 50%; 45 | background: $transparent; 46 | 47 | &.type-fatality { 48 | background: $orange-red; 49 | } 50 | 51 | &.type-injury { 52 | background: $orange; 53 | } 54 | 55 | &.type-no-inj-fat { 56 | background: $yellow-orange; 57 | } 58 | } 59 | 60 | svg, 61 | svg:not(:root) { 62 | overflow: auto; 63 | } 64 | 65 | circle { 66 | fill: rgba(0,0,0,0); 67 | stroke: $off-white; 68 | stroke-width: 1px; 69 | } 70 | 71 | text { 72 | @extend %roboto-regular; 73 | fill: $off-white; 74 | font-size: 1.1rem; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { createResponsiveStateReducer } from 'redux-responsive'; 3 | import { routerReducer } from 'react-router-redux'; 4 | 5 | import filterDate from './filter_by_date'; 6 | import filterArea from './filter_by_area_reducer'; 7 | import filterType from './filter_by_type_reducer'; 8 | import filterVehicle from './filter_by_vehicle_reducer'; 9 | import filterContributingFactor from './filter_contributing_factor_reducer'; 10 | import contributingFactors from './stats_contributing_factors_reducer'; 11 | import yearRange from './year_range_reducer'; 12 | import crashesDateRange from './crashes_date_range_reducer'; 13 | import crashesMaxDate from './crashes_max_date_reducer'; 14 | import crashStats from './crash_stats_reducer'; 15 | import modal from './modal_reducer'; 16 | 17 | // breakpoints for redux-responsive store 18 | // taken from scss/skeleton/base/variables 19 | const browser = createResponsiveStateReducer({ 20 | extraSmall: 400, 21 | small: 550, 22 | medium: 750, 23 | large: 1000, 24 | extraLarge: 1200 25 | }, 26 | { 27 | extraFields: () => ({ 28 | width: window.innerWidth, 29 | height: window.innerHeight 30 | }) 31 | }); 32 | 33 | const rootReducer = combineReducers({ 34 | browser, 35 | contributingFactors, 36 | crashesDateRange, 37 | crashesMaxDate, 38 | crashStats, 39 | filterDate, 40 | filterArea, 41 | filterContributingFactor, 42 | filterType, 43 | filterVehicle, 44 | modal, 45 | routing: routerReducer, 46 | yearRange, 47 | }); 48 | 49 | export default rootReducer; 50 | -------------------------------------------------------------------------------- /src/components/LeafletMap/customFilter.js: -------------------------------------------------------------------------------- 1 | class CustomFilter { 2 | constructor() { 3 | this.map = undefined; 4 | this.drawnItems = L.featureGroup(); 5 | this.customLayer = undefined; 6 | this.poly = undefined; 7 | this.polyStyle = {}; 8 | this.onLayerCreatedCB = () => {}; 9 | } 10 | 11 | set mapInstance(obj) { 12 | this.map = obj; 13 | } 14 | 15 | set layerCreatedCallback(func) { 16 | if (func && typeof func === 'function') { 17 | this.onLayerCreatedCB = func; 18 | } 19 | } 20 | 21 | set polyOverlayStyle(obj) { 22 | this.polyStyle = obj; 23 | } 24 | 25 | get drawLayer() { 26 | return this.drawnItems; 27 | } 28 | 29 | initCustomFilterLayer() { 30 | const self = this; 31 | this.customLayer = this.map.addLayer(self.drawnItems); 32 | } 33 | 34 | initDrawPolygon() { 35 | const self = this; 36 | this.poly = new L.Draw.Polygon(self.map, { 37 | shapeOptions: self.polyStyle, 38 | }); 39 | } 40 | 41 | onLayerCreated() { 42 | const self = this; 43 | 44 | this.map.on(L.Draw.Event.CREATED, (e) => { 45 | const type = e.layerType; 46 | const layer = e.layer; 47 | 48 | if (type === 'polygon') { 49 | const geoJson = layer.toGeoJSON(); 50 | const coordinates = geoJson.geometry.coordinates[0]; 51 | self.onLayerCreatedCB(coordinates); 52 | } 53 | 54 | self.drawnItems.addLayer(layer); 55 | }); 56 | } 57 | 58 | startDraw() { 59 | this.poly.enable(); 60 | } 61 | 62 | cancelDraw() { 63 | this.poly.disable(); 64 | } 65 | 66 | } 67 | 68 | export default CustomFilter; 69 | -------------------------------------------------------------------------------- /src/constants/cartocss.js: -------------------------------------------------------------------------------- 1 | import sls from 'single-line-string'; 2 | 3 | // for some reason importing this val from './app_config' isn't working... 4 | const nyc_crashes = 'export2016_07'; 5 | 6 | const cartocss = sls` 7 | /* color values match those ./scss/_variables.scss */ 8 | @fatality: #f03b20; 9 | @injury: #fd8d3c; 10 | @nofatinj: #fecc5c; 11 | 12 | #${nyc_crashes} { 13 | marker-fill-opacity: 0.9; 14 | marker-line-color: #FFFAD5; 15 | marker-line-width: 0.7; 16 | marker-line-opacity: 1; 17 | marker-placement: point; 18 | marker-type: ellipse; 19 | marker-width: 8; 20 | marker-fill: @nofatinj; 21 | marker-allow-overlap: true; 22 | 23 | /* sometimes the data will have pedestrain marked as injured but 24 | also have persons_injured = 0 (WTF data!!!) */ 25 | [persons_injured > 0], 26 | [cyclist_injured > 0], 27 | [pedestrian_injured > 0], 28 | [motorist_injured > 0] { 29 | marker-fill: @injury; 30 | } 31 | 32 | /* same goes for number_of_x_killed */ 33 | [persons_killed > 0], 34 | [cyclist_killed > 0], 35 | [pedestrian_killed > 0], 36 | [motorist_killed > 0] { 37 | marker-fill: @fatality; 38 | } 39 | 40 | #${nyc_crashes} [ total_crashes > 8] { 41 | marker-width: 24; 42 | } 43 | #${nyc_crashes} [ total_crashes <= 5] { 44 | marker-width: 20; 45 | } 46 | #${nyc_crashes} [ total_crashes <= 3] { 47 | marker-width: 16; 48 | } 49 | #${nyc_crashes} [ total_crashes <= 2] { 50 | marker-width: 12; 51 | } 52 | #${nyc_crashes} [ total_crashes <= 1] { 53 | marker-width: 8; 54 | } 55 | } 56 | `; 57 | 58 | export default cartocss; 59 | -------------------------------------------------------------------------------- /scss/skeleton/modules/_forms.scss: -------------------------------------------------------------------------------- 1 | // Forms 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | textarea, 5 | select { 6 | height: 38px; 7 | padding: 6px 10px; // The 6px vertically centers text on FF, ignored by Webkit 8 | background-color: #fff; 9 | border: 1px solid lighten($border-color, 8.8%); 10 | border-radius: $global-radius; 11 | box-shadow: none; 12 | box-sizing: border-box; 13 | } 14 | 15 | // Removes awkward default styles on some inputs for iOS 16 | input { 17 | &[type="email"], 18 | &[type="number"], 19 | &[type="search"], 20 | &[type="text"], 21 | &[type="tel"], 22 | &[type="url"], 23 | &[type="password"] { 24 | -webkit-appearance: none; 25 | -moz-appearance: none; 26 | appearance: none; 27 | } 28 | } 29 | 30 | textarea { 31 | -webkit-appearance: none; 32 | -moz-appearance: none; 33 | appearance: none; 34 | min-height: 65px; 35 | padding-top: 6px; 36 | padding-bottom: 6px; 37 | } 38 | 39 | input { 40 | &[type="email"]:focus, 41 | &[type="number"]:focus, 42 | &[type="search"]:focus, 43 | &[type="text"]:focus, 44 | &[type="tel"]:focus, 45 | &[type="url"]:focus, 46 | &[type="password"]:focus { 47 | border: 1px solid $primary-color; 48 | outline: 0; 49 | } 50 | } 51 | 52 | textarea:focus, 53 | select:focus { 54 | border: 1px solid $primary-color; 55 | outline: 0; 56 | } 57 | 58 | label, 59 | legend { 60 | display: block; 61 | margin-bottom: .5rem; 62 | font-weight: 600; 63 | } 64 | 65 | fieldset { 66 | padding: 0; 67 | border-width: 0; 68 | } 69 | 70 | input { 71 | &[type="checkbox"], 72 | &[type="radio"] { 73 | display: inline; 74 | } 75 | } 76 | 77 | label > .label-body { 78 | display: inline-block; 79 | margin-left: .5rem; 80 | font-weight: normal; 81 | } 82 | -------------------------------------------------------------------------------- /src/reducers/filter_by_area_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FILTER_BY_AREA_TYPE, 3 | FILTER_BY_AREA_IDENTIFIER, 4 | FILTER_BY_AREA_CUSTOM, 5 | TOGGLE_CUSTOM_AREA_DRAW, 6 | GEO_POLYGONS_REQUEST, 7 | GEO_POLYGONS_SUCCESS, 8 | GEO_POLYGONS_ERROR, 9 | } from '../constants/action_types'; 10 | 11 | export const defaultState = { 12 | _isFetching: false, 13 | geo: 'citywide', 14 | geojson: { 15 | type: '', 16 | features: [], 17 | geoName: '', 18 | }, 19 | identifier: undefined, 20 | lngLats: [], 21 | drawEnabeled: false, 22 | }; 23 | 24 | export default (state = defaultState, action) => { 25 | switch (action.type) { 26 | case FILTER_BY_AREA_TYPE: 27 | return { 28 | ...state, 29 | geo: action.geo, 30 | identifier: undefined, 31 | lngLats: [], 32 | drawEnabeled: action.geo === 'custom', 33 | }; 34 | 35 | case FILTER_BY_AREA_IDENTIFIER: 36 | return { 37 | ...state, 38 | identifier: action.identifier, 39 | }; 40 | 41 | case FILTER_BY_AREA_CUSTOM: 42 | return { 43 | ...state, 44 | lngLats: action.lngLats, 45 | drawEnabeled: false, 46 | }; 47 | 48 | case TOGGLE_CUSTOM_AREA_DRAW: 49 | return { 50 | ...state, 51 | drawEnabeled: !state.drawEnabeled, 52 | }; 53 | 54 | case GEO_POLYGONS_REQUEST: 55 | return { 56 | ...state, 57 | _isFetching: true, 58 | }; 59 | 60 | case GEO_POLYGONS_SUCCESS: 61 | return { 62 | ...state, 63 | _isFetching: false, 64 | geojson: action.geojson, 65 | }; 66 | 67 | case GEO_POLYGONS_ERROR: 68 | return { 69 | ...state, 70 | _isFetching: false, 71 | error: action.error, 72 | }; 73 | 74 | default: 75 | return state; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/constants/app_config.js: -------------------------------------------------------------------------------- 1 | import cartocss from './cartocss'; 2 | 3 | // CARTO account name 4 | export const cartoUser = 'chekpeds'; 5 | 6 | // CARTO SQL API endpoint 7 | export const cartoSQLQuery = (query, format) => 8 | `https://${cartoUser}.carto.com/api/v2/sql?${format === 'geojson' ? 'format=GeoJSON&' : ''}q=${query}`; 9 | 10 | // CARTO table names lookup 11 | export const cartoTables = { 12 | nyc_borough: 'nyc_borough', 13 | nyc_city_council: 'nyc_city_council', 14 | nyc_community_board: 'nyc_community_board', 15 | nyc_neighborhood: 'nyc_neighborhood', 16 | nyc_nypd_precinct: 'nyc_nypd_precinct', 17 | nyc_assembly: 'nyc_assembly', 18 | nyc_senate: 'nyc_senate', 19 | nyc_intersections: 'nyc_highcrash_intersections', 20 | nyc_crashes: 'crashes_all_prod' 21 | }; 22 | 23 | export const crashDataFieldNames = [ 24 | 'total_crashes', 25 | 'cyclist_injured', 26 | 'cyclist_killed', 27 | 'motorist_injured', 28 | 'motorist_killed', 29 | 'pedestrian_injured', 30 | 'pedestrian_killed', 31 | 'persons_killed', 32 | 'persons_injured', 33 | 'on_street_name', 34 | 'cross_street_name' 35 | ]; 36 | 37 | export const cartoLayerSource = { 38 | user_name: cartoUser, 39 | type: 'cartodb', 40 | sublayers: [{ 41 | sql: '', 42 | cartocss, 43 | interactivity: crashDataFieldNames.join(','), 44 | }] 45 | }; 46 | 47 | // basemap for Leaflet 48 | export const basemapURL = 49 | 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png'; 50 | 51 | // mapping of selectable area types onto a format string, for labels and tooltips 52 | // "NYPD Precinct 107" is nicer than "107" 53 | // use {} as the placeholder for the one value to be passed: the identifier 54 | export const labelFormats = { 55 | borough: '{}', 56 | city_council: 'City Council District {}', 57 | community_board: 'Community Board {}', 58 | assembly: 'Assembly District {}', 59 | senate: 'Senate District {}', 60 | neighborhood: '{}', 61 | nypd_precinct: 'NYPD Precinct {}', 62 | intersection: '{}', 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/Modal/DownloadData.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import momentPropTypes from 'react-moment-proptypes'; 3 | 4 | import { configureDownloadDataSQL } from '../../constants/sql_queries'; 5 | import { cartoUser } from '../../constants/app_config'; 6 | 7 | // TO DO: fix props validation for start & end dates 8 | const DownloadData = (props) => { 9 | const { startDate, endDate, closeModal } = props; 10 | const sql = configureDownloadDataSQL({ 11 | startDate, 12 | endDate, 13 | ...props, 14 | }); 15 | const sqlEncoded = window.encodeURIComponent(sql); 16 | const urlPartial = `https://${cartoUser}.carto.com/api/v2/sql?q=${sqlEncoded}&format=`; 17 | const dataTypes = ['CSV', 'GeoJSON', 'Shapefile', 'KML']; 18 | const renderButton = name => ( 19 | closeModal()} 26 | > 27 | {name} 28 | 29 | ); 30 | 31 | return ( 32 |
    33 |

    Any active filters will be applied to your download.

    34 |

    Choose from one of the following data formats:

    35 |
    36 | { dataTypes.map(d => renderButton(d)) } 37 |
    38 |
    39 | ); 40 | }; 41 | 42 | export default DownloadData; 43 | 44 | DownloadData.propTypes = { 45 | closeModal: PropTypes.func.isRequired, 46 | startDate: momentPropTypes.momentObj.isRequired, 47 | endDate: momentPropTypes.momentObj.isRequired, 48 | filterVehicle: PropTypes.shape({ 49 | vehicle: PropTypes.shape({ 50 | car: PropTypes.bool.isRequired, 51 | truck: PropTypes.bool.isRequired, 52 | motorcycle: PropTypes.bool.isRequired, 53 | bicycle: PropTypes.bool.isRequired, 54 | suv: PropTypes.bool.isRequired, 55 | busvan: PropTypes.bool.isRequired, 56 | scooter: PropTypes.bool.isRequired, 57 | other: PropTypes.bool.isRequired, 58 | }).isRequired, 59 | }).isRequired, 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import qs from 'query-string'; 3 | 4 | // Renders the nav menu items in the header 5 | const Menu = (props) => { 6 | const { openModal, ...rest } = props; 7 | const queryString = qs.stringify(rest); 8 | 9 | const hostname = process.env.NODE_ENV === 'production' ? 'vis.crashmapper.org' : 'localhost:8889'; 10 | 11 | const items = [ 12 | { type: null, value: 'map', label: 'Map' }, 13 | { type: 'link', value: 'trend', label: 'Trend' }, 14 | { type: 'link', value: 'vehicle', label: 'Vehicle' }, 15 | { type: 'link', value: 'compare', label: 'Compare' }, 16 | { type: 'link', value: 'rank', label: 'Rank' }, 17 | { type: 'modal', value: 'about', label: 'About' }, 18 | { type: 'modal', value: 'help', label: 'Help' }, 19 | ]; 20 | 21 | const mapTypeToElement = (item) => { 22 | const { label, type, value } = item; 23 | 24 | switch (type) { 25 | case 'link': 26 | return {label}; 27 | 28 | case 'modal': 29 | return ( 30 | 33 | ); 34 | 35 | default: 36 | return ( 37 | 43 | ); 44 | } 45 | }; 46 | 47 | return ( 48 | 51 | ); 52 | }; 53 | 54 | Menu.propTypes = { 55 | openModal: PropTypes.func.isRequired, 56 | p1start: PropTypes.string.isRequired, 57 | p1end: PropTypes.string.isRequired, 58 | geo: PropTypes.string.isRequired, 59 | primary: PropTypes.oneOfType([ 60 | PropTypes.string, 61 | PropTypes.number, 62 | ]), 63 | pinj: PropTypes.bool.isRequired, 64 | pfat: PropTypes.bool.isRequired, 65 | cinj: PropTypes.bool.isRequired, 66 | cfat: PropTypes.bool.isRequired, 67 | minj: PropTypes.bool.isRequired, 68 | mfat: PropTypes.bool.isRequired, 69 | }; 70 | 71 | Menu.defaultProps = { 72 | primary: null, 73 | }; 74 | 75 | export default Menu; 76 | -------------------------------------------------------------------------------- /src/reducers/filter_by_vehicle_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FILTER_BY_VEHICLE_CAR, 3 | FILTER_BY_VEHICLE_TRUCK, 4 | FILTER_BY_VEHICLE_MOTORCYCLE, 5 | FILTER_BY_VEHICLE_BICYCLE, 6 | FILTER_BY_VEHICLE_SUV, 7 | FILTER_BY_VEHICLE_BUSVAN, 8 | FILTER_BY_VEHICLE_SCOOTER, 9 | FILTER_BY_VEHICLE_OTHER, 10 | } from '../constants/action_types'; 11 | 12 | const defaultState = { 13 | vehicle: { 14 | car: false, 15 | truck: false, 16 | motorcycle: false, 17 | bicycle: false, 18 | suv: false, 19 | busvan: false, 20 | scooter: false, 21 | other: false, 22 | }, 23 | }; 24 | 25 | export default (state = defaultState, action) => { 26 | const { vehicle } = state; 27 | 28 | switch (action.type) { 29 | case FILTER_BY_VEHICLE_CAR: 30 | return { 31 | ...state, 32 | vehicle: { 33 | ...vehicle, 34 | car: !vehicle.car, 35 | }, 36 | }; 37 | case FILTER_BY_VEHICLE_TRUCK: 38 | return { 39 | ...state, 40 | vehicle: { 41 | ...vehicle, 42 | truck: !vehicle.truck, 43 | }, 44 | }; 45 | case FILTER_BY_VEHICLE_MOTORCYCLE: 46 | return { 47 | ...state, 48 | vehicle: { 49 | ...vehicle, 50 | motorcycle: !vehicle.motorcycle, 51 | }, 52 | }; 53 | case FILTER_BY_VEHICLE_BICYCLE: 54 | return { 55 | ...state, 56 | vehicle: { 57 | ...vehicle, 58 | bicycle: !vehicle.bicycle, 59 | }, 60 | }; 61 | case FILTER_BY_VEHICLE_SUV: 62 | return { 63 | ...state, 64 | vehicle: { 65 | ...vehicle, 66 | suv: !vehicle.suv, 67 | }, 68 | }; 69 | case FILTER_BY_VEHICLE_BUSVAN: 70 | return { 71 | ...state, 72 | vehicle: { 73 | ...vehicle, 74 | busvan: !vehicle.busvan, 75 | }, 76 | }; 77 | case FILTER_BY_VEHICLE_SCOOTER: 78 | return { 79 | ...state, 80 | vehicle: { 81 | ...vehicle, 82 | scooter: !vehicle.scooter, 83 | }, 84 | }; 85 | case FILTER_BY_VEHICLE_OTHER: 86 | return { 87 | ...state, 88 | vehicle: { 89 | ...vehicle, 90 | other: !vehicle.other, 91 | }, 92 | }; 93 | default: 94 | return state; 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /src/components/StatsLegend/LegendContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
    5 |
    6 |
      7 |
    • 8 | 9 |

      Fatality

      10 |
    • 11 |
    • 12 | 13 |

      Injury

      14 |
    • 15 |
    • 16 | 17 |

      None

      18 |
    • 19 |
    20 |
    21 |
    22 | {/* 23 | generated using https://github.com/susielu/d3-legend 24 | https://bl.ocks.org/clhenrick/ba0e4795ec2c273ef366a78911e1e2d7 25 | */} 26 | 27 | 28 | 29 | 30 | 31 | 8 or more crashes 32 | 33 | {'> 8'} 34 | 35 | 36 | 37 | 5 or fewer crashes 38 | 39 | 5 40 | 41 | 42 | 43 | 3 or fewer crashes 44 | 45 | 3 46 | 47 | 48 | 49 | 2 or fewer crashes 50 | 51 | {'<=2'} 52 | 53 | 54 | 55 | 56 |
    57 |
    58 | ); 59 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/MonthYearSelector.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import moment from 'moment'; 3 | import momentPropTypes from 'react-moment-proptypes'; 4 | import Select from 'react-select'; 5 | 6 | const MonthYearSelector = (props) => { 7 | const { crashesDateRange, handleChange, years, curYear, curMonth, prefix } = props; 8 | const { minDate, maxDate } = crashesDateRange; 9 | 10 | // years are the distinct year values in the dataset 11 | const yearOptions = years.map(year => ({ 12 | value: year, 13 | label: year 14 | })); 15 | 16 | const monthOptions = moment.months().map((month, i) => ({ 17 | value: i + 1, 18 | label: month 19 | })); 20 | 21 | // minDate and maxDate are loaded async 22 | if (moment.isMoment(minDate) && moment.isMoment(maxDate)) { 23 | return ( 24 |
    25 |
    26 | 29 | handleChange(curYear, m.value)} 48 | matchPos="start" 49 | ignoreCase 50 | clearable={false} 51 | value={curMonth} 52 | /> 53 |
    54 |
    55 | ); 56 | } 57 | 58 | return null; 59 | }; 60 | 61 | MonthYearSelector.propTypes = { 62 | crashesDateRange: PropTypes.shape({ 63 | min: momentPropTypes.momentObj, 64 | max: momentPropTypes.momentObj, 65 | }).isRequired, 66 | handleChange: PropTypes.func.isRequired, 67 | years: PropTypes.arrayOf( 68 | PropTypes.number 69 | ).isRequired, 70 | curYear: PropTypes.number.isRequired, 71 | curMonth: PropTypes.number.isRequired, 72 | prefix: PropTypes.string.isRequired, 73 | }; 74 | 75 | export default MonthYearSelector; 76 | -------------------------------------------------------------------------------- /scss/components/_filter-by-date.scss: -------------------------------------------------------------------------------- 1 | .filter-by-date { 2 | 3 | .filter-list li:nth-of-type(2) { 4 | padding: 10px 0; 5 | } 6 | 7 | .label-select-group { 8 | display: inline-block; 9 | 10 | &:nth-of-type(2) { 11 | margin-left: 10px; 12 | } 13 | 14 | label { 15 | font-size: 1.2rem; 16 | font-weight: 500; 17 | } 18 | } 19 | 20 | .Select-control, 21 | .Select-menu-outer { 22 | width: $select-width; 23 | border: none; 24 | font-size: 1.1rem; 25 | font-weight: 500; 26 | color: $marine-light; 27 | background-color: $off-white; 28 | } 29 | 30 | .Select-control { 31 | height: $select-height; 32 | } 33 | 34 | .Select-menu-outer { 35 | z-index: 5; 36 | } 37 | 38 | .Select-menu { 39 | background-color: $off-white; 40 | } 41 | 42 | .Select-option { 43 | color: $marine-light; 44 | background-color: $off-white; 45 | 46 | &.is-focused { 47 | background-color: $btn-hover; 48 | } 49 | 50 | &.is-selected { 51 | background-color: $btn-active; 52 | } 53 | } 54 | 55 | .has-value.Select--single > .Select-control .Select-value .Select-value-label, 56 | .has-value.is-pseudo-focused.Select--single > .Select-control .Select-value .Select-value-label { 57 | color: $marine-light; 58 | } 59 | 60 | .Select-placeholder, 61 | .Select--single > .Select-control .Select-value { 62 | line-height: 25px; 63 | } 64 | 65 | .Select-input { 66 | height: $select-height; 67 | } 68 | 69 | .Select-value-label { 70 | color: $marine-light; 71 | } 72 | 73 | .Select-arrow { 74 | border-color: $marine-light transparent transparent; 75 | } 76 | 77 | .is-open .Select-arrow, 78 | .Select-arrow-zone:hover > .Select-arrow, { 79 | border-top-color: $marine; 80 | } 81 | 82 | .is-open > .Select-control .Select-arrow { 83 | border-color: transparent transparent $marine-light; 84 | } 85 | 86 | .filter-date-label { 87 | width: 60px; 88 | display: inline-block; 89 | font-size: 1.2rem; 90 | padding-right: $padding-10; 91 | } 92 | 93 | // react-datepicker overrides 94 | .react-datepicker__input-container input { 95 | font-size: 1.2rem; 96 | padding: 0 10px; 97 | height: $button-height; 98 | background-color: $btn-bg; 99 | color: $marine-light; 100 | 101 | &:focus { 102 | 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Colors (in addition to colors in skeleton/base/variables) 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | // color values for crash point data 5 | $yellow-orange: #FECC5C !default; 6 | $orange: #FD8D3C !default; 7 | $orange-red: #F03B20 !default; 8 | $red: #BD0026 !default; 9 | 10 | // possible UI color values 11 | $marine: #105b63 !default; 12 | $marine-light: lighten($marine, 10) !default; 13 | 14 | $off-white: #FFFAD5 !default; 15 | $grey: #3d3d3d !default; 16 | $medium-grey: lighten($dark-grey, 50) !default; 17 | $ui-grey: rgba($grey, 0.9) !default; 18 | $transparent: rgba(0,0,0,0) !default; 19 | 20 | $btn-hover: lighten($yellow-orange, 10) !default; 21 | $btn-active: $yellow-orange !default; 22 | $btn-bg: $off-white !default; 23 | 24 | $hr-border: $off-white !default; 25 | $border-radius: 4px !default; 26 | 27 | // Additional Breakpoints 28 | $bp-narrow-width: "max-width: 1000px" !default; 29 | 30 | // Typography 31 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 32 | $roboto: "Roboto", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif !default; 33 | 34 | %roboto-regular { 35 | font-family: $roboto; 36 | font-weight: 400; 37 | } 38 | 39 | %roboto-medium { 40 | font-family: $roboto; 41 | font-weight: 500; 42 | } 43 | 44 | %roboto-bold { 45 | font-family: $roboto; 46 | font-weight: 700; 47 | } 48 | 49 | // Main Component Dimensions 50 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 51 | $app-header-height: 60px !default; 52 | $app-header-width: auto !default; 53 | 54 | $zoom-controls-height: 70px !default; 55 | $zoom-controls-width: 30px !default; 56 | 57 | $app-stats-legend-height: 120px !default; 58 | $app-stats-legend-width: calc(100vw - 300px) !default; 59 | 60 | $app-options-filters-height: 100% !default; 61 | $app-options-filters-width: 300px !default; 62 | 63 | $button-height: 25px !default; 64 | 65 | $options-footer-height: 27px !default; 66 | 67 | // react select overrides 68 | $select-height: 25px !default; 69 | $select-width: 125px !default; 70 | 71 | $stats-header-height: 30px !default; 72 | $stats-content-height: calc(100% - 30px) !default; 73 | 74 | $infowindowWidth: 105px !default; 75 | $infowindowWidthOuter: 155px !default; 76 | $infowindowWidthInner: 155px !default; 77 | 78 | // margins 79 | $margin-25: 25px !default; 80 | $margin-10: 10px !default; 81 | $margin-5: 5px !default; 82 | 83 | // padding 84 | $padding-25: 25px !default; 85 | $padding-15: 15px !default; 86 | $padding-10: 10px !default; 87 | $padding-5: 5px !default; 88 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | // vendor libs get bundled separately to take advantage of browser caching 7 | const VENDOR_LIBS = [ 8 | 'classnames', 9 | 'es6-promise', 10 | 'isomorphic-fetch', 11 | 'leaflet-draw', 12 | 'moment', 13 | 'normalize-scss', 14 | 'query-string', 15 | 'react', 16 | 'react-collapse', 17 | 'react-copy-to-clipboard', 18 | 'react-dom', 19 | 'react-height', 20 | 'react-moment-proptypes', 21 | 'react-motion', 22 | 'react-redux', 23 | 'react-router', 24 | 'react-router-redux', 25 | 'react-select', 26 | 'redux', 27 | 'redux-logger', 28 | 'redux-responsive', 29 | 'redux-thunk', 30 | 'single-line-string', 31 | ]; 32 | 33 | module.exports = { 34 | entry: { 35 | bundle: './src/index.js', 36 | vendor: VENDOR_LIBS 37 | }, 38 | output: { 39 | path: path.join(__dirname, 'dist'), 40 | filename: '[name].[chunkhash].js' 41 | }, 42 | devtool: 'source-map', 43 | module: { 44 | rules: [ 45 | { 46 | test: /\.js$/, 47 | enforce: 'pre', 48 | use: [ 49 | { 50 | loader: 'babel-loader' 51 | }, 52 | { 53 | loader: 'eslint-loader' 54 | } 55 | ], 56 | include: path.resolve(process.cwd(), 'src'), 57 | exclude: /node_modules/ 58 | }, 59 | { 60 | loader: ExtractTextPlugin.extract({ 61 | loader: "css-loader?sourceMap!sass-loader?sourceMap", 62 | fallbackLoader: 'style-loader', 63 | }), 64 | test: /\.scss$/ 65 | }, 66 | { 67 | test: /\.(jpe?g|png|gif|svg)$/, 68 | use: [ 69 | { 70 | loader: 'url-loader', 71 | options: { limit: 4000 } 72 | }, 73 | 'image-webpack-loader' 74 | ] 75 | } 76 | ] 77 | }, 78 | devServer: { 79 | watchContentBase: true, 80 | lazy: false, 81 | watchOptions: { 82 | poll: true 83 | } 84 | }, 85 | plugins: [ 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | names: ['vendor', 'manifest'] 88 | }), 89 | new HTMLWebpackPlugin({ 90 | template: 'src/index.html' 91 | }), 92 | new ExtractTextPlugin('[name].[chunkhash].css'), 93 | new webpack.DefinePlugin({ 94 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 95 | }) 96 | ] 97 | }; 98 | -------------------------------------------------------------------------------- /src/constants/action_types.js: -------------------------------------------------------------------------------- 1 | export const START_DATE_CHANGE = 'START_DATE_CHANGE'; 2 | export const END_DATE_CHANGE = 'END_DATE_CHANGE'; 3 | 4 | export const FILTER_BY_AREA_TYPE = 'FILTER_BY_AREA_TYPE'; 5 | export const FILTER_BY_AREA_IDENTIFIER = 'FILTER_BY_AREA_IDENTIFIER'; 6 | export const FILTER_BY_AREA_CUSTOM = 'FILTER_BY_AREA_CUSTOM'; 7 | 8 | export const TOGGLE_CUSTOM_AREA_DRAW = 'TOGGLE_CUSTOM_AREA_DRAW'; 9 | 10 | export const FILTER_BY_TYPE_INJURY = 'FILTER_BY_TYPE_INJURY'; 11 | export const FILTER_BY_TYPE_FATALITY = 'FILTER_BY_TYPE_FATALITY'; 12 | export const FILTER_BY_NO_INJURY_FATALITY = 'FILTER_BY_NO_INJURY_FATALITY'; 13 | 14 | export const FILTER_BY_VEHICLE_CAR = 'FILTER_BY_VEHICLE_CAR'; 15 | export const FILTER_BY_VEHICLE_TRUCK = 'FILTER_BY_VEHICLE_TRUCK'; 16 | export const FILTER_BY_VEHICLE_MOTORCYCLE = 'FILTER_BY_VEHICLE_MOTORCYCLE'; 17 | export const FILTER_BY_VEHICLE_BICYCLE = 'FILTER_BY_VEHICLE_BICYCLE'; 18 | export const FILTER_BY_VEHICLE_SUV = 'FILTER_BY_VEHICLE_SUV'; 19 | export const FILTER_BY_VEHICLE_BUSVAN = 'FILTER_BY_VEHICLE_BUSVAN'; 20 | export const FILTER_BY_VEHICLE_SCOOTER = 'FILTER_BY_VEHICLE_SCOOTER'; 21 | export const FILTER_BY_VEHICLE_OTHER = 'FILTER_BY_VEHICLE_OTHER'; 22 | 23 | export const FILTER_BY_CONTRIBUTING_FACTOR = 'FILTER_BY_CONTRIBUTING_FACTOR'; 24 | 25 | export const CRASHES_ALL_REQUEST = 'CRASHES_ALL_REQUEST'; 26 | export const CRASHES_ALL_SUCCESS = 'CRASHES_ALL_SUCCESS'; 27 | export const CRASHES_ALL_ERROR = 'CRASHES_ALL_ERROR'; 28 | 29 | export const CONTRIBUTING_FACTORS_REQUEST = 'CONTRIBUTING_FACTORS_REQUEST'; 30 | export const CONTRIBUTING_FACTORS_SUCCESS = 'CONTRIBUTING_FACTORS_SUCCESS'; 31 | export const CONTRIBUTING_FACTORS_ERROR = 'CONTRIBUTING_FACTORS_ERROR'; 32 | 33 | export const CRASHES_YEAR_RANGE_REQUEST = 'CRASHES_YEAR_RANGE_REQUEST'; 34 | export const CRASHES_YEAR_RANGE_SUCCESS = 'CRASHES_YEAR_RANGE_SUCCESS'; 35 | export const CRASHES_YEAR_RANGE_ERROR = 'CRASHES_YEAR_RANGE_ERROR'; 36 | 37 | export const CRASHES_DATE_RANGE_REQUEST = 'CRASHES_DATE_RANGE_REQUEST'; 38 | export const CRASHES_DATE_RANGE_SUCCESS = 'CRASHES_DATE_RANGE_SUCCESS'; 39 | export const CRASHES_DATE_RANGE_ERROR = 'CRASHES_DATE_RANGE_ERROR'; 40 | 41 | export const CRASHES_MAX_DATE_REQUEST = 'CRASHES_MAX_DATE_REQUEST'; 42 | export const CRASHES_MAX_DATE_RESPONSE = 'CRASHES_MAX_DATE_RESPONSE'; 43 | export const CRASHES_MAX_DATE_ERROR = 'CRASHES_MAX_DATE_ERROR'; 44 | 45 | export const GEO_POLYGONS_REQUEST = 'GEO_POLYGONS_REQUEST'; 46 | export const GEO_POLYGONS_SUCCESS = 'GEO_POLYGONS_SUCCESS'; 47 | export const GEO_POLYGONS_ERROR = 'GEO_POLYGONS_ERROR'; 48 | 49 | export const MODAL_OPENED = 'MODAL_OPENED'; 50 | export const MODAL_CLOSED = 'MODAL_CLOSED'; 51 | -------------------------------------------------------------------------------- /scss/components/_modal.scss: -------------------------------------------------------------------------------- 1 | // For use with node_modules/ReactModal Component 2 | .ReactModalPortal { 3 | position: relative; 4 | 5 | .Modal { 6 | position: absolute; 7 | top: 25%; 8 | left: 25%; 9 | transform: translate(-25%, -25%); 10 | background-color: #fff; 11 | border: 1px solid $medium-grey; 12 | border-radius: $border-radius; 13 | padding: $padding-25; 14 | outline: none; 15 | } 16 | 17 | .Modal { 18 | h1, h2, h3, h4, h5, h6, p { 19 | color: $marine; 20 | } 21 | 22 | a { 23 | text-decoration: none; 24 | } 25 | } 26 | 27 | .Overlay { 28 | position: fixed; 29 | top: 0; 30 | left: 0; 31 | right: 0; 32 | bottom: 0; 33 | z-index: 5; 34 | background-color: rgba(255, 255, 255, 0.75); 35 | } 36 | 37 | .btn-modal-close { 38 | height: 25px; 39 | width: 25px; 40 | float: right; 41 | margin-top: -38px; 42 | margin-right: -38px; 43 | color: #fff; 44 | border: 1px solid #AEAEAE; 45 | border-radius: 50%; 46 | background: $medium-grey; 47 | font-size: 2rem; 48 | font-weight: bold; 49 | display: inline-block; 50 | line-height: 0px; 51 | padding: 0; 52 | z-index: 6; 53 | 54 | &:before { 55 | content: "×"; 56 | } 57 | } 58 | 59 | .modal-about, 60 | .modal-copyright, 61 | .modal-disclaimer, 62 | .modal-download-data, 63 | .modal-share-fb, 64 | .modal-share-twitter, 65 | .modal-share-url { 66 | max-width: 700px; 67 | } 68 | 69 | .modal-about { 70 | height: 80vh; 71 | max-height: 80vh; 72 | overflow-y: auto; 73 | 74 | p { 75 | padding-right: $padding-25; 76 | } 77 | } 78 | 79 | .modal-download-data { 80 | 81 | } 82 | 83 | .download-data-btns { 84 | margin-top: $margin-25; 85 | } 86 | 87 | .dl-data-btn { 88 | @extend .filter-options-button; 89 | background-color: #fff; 90 | border: 1px solid $medium-grey; 91 | color: $marine-light; 92 | 93 | &:hover { 94 | color: $marine; 95 | background-color: #eee; 96 | border: 1px solid #333; 97 | } 98 | 99 | &:not(:first-of-type) { 100 | margin-left: $margin-10; 101 | } 102 | } 103 | 104 | .modal-share-url { 105 | input { 106 | width: 100%; 107 | padding: 0 5px; 108 | font-size: 12px; 109 | color: hsl(0, 0%, 40%); 110 | height: 22px; 111 | margin-top: 10px; 112 | } 113 | 114 | button { 115 | @extend .dl-data-btn; 116 | } 117 | 118 | .copied { 119 | margin-left: $margin-25; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import OptionsContainer from './OptionsContainer'; 4 | import FilterByArea from '../../containers/FilterByAreaConnected'; 5 | import FilterByAreaMessage from './FilterByAreaMessage'; 6 | import FilterByType from '../../containers/FilterByTypeConnected'; 7 | import FilterByVehicle from '../../containers/FilterByVehicleConnected'; 8 | import FilterByDate from '../../containers/FilterByDateConnected'; 9 | import DownloadData from './DownloadData'; 10 | import ShareOptions from './ShareOptions'; 11 | import FooterOptions from './FooterOptions'; 12 | 13 | class OptionsFilters extends Component { 14 | render() { 15 | const { height, maxDate, openModal, geo } = this.props; 16 | 17 | return ( 18 |
    19 | 26 | 27 | 28 | 29 |
    30 | 31 | 32 | 33 | 34 |
    35 | 36 | 37 | 38 |
    39 | 40 | 41 | 42 |
    43 | 44 | 48 | 49 |
    50 |
    51 | 57 | 58 | 59 | 60 |
    61 | ); 62 | } 63 | } 64 | 65 | OptionsFilters.defaultProps = { 66 | maxDate: '', 67 | height: 120, 68 | }; 69 | 70 | OptionsFilters.propTypes = { 71 | maxDate: PropTypes.string, 72 | openModal: PropTypes.func.isRequired, 73 | height: PropTypes.number, 74 | geo: PropTypes.string.isRequired, 75 | }; 76 | 77 | export default OptionsFilters; 78 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/OptionsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Collapse from 'react-collapse'; 3 | import cx from 'classnames'; 4 | 5 | class OptionsContainer extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | opened: props.isOpened 10 | }; 11 | this.handleOpenClose = this.handleOpenClose.bind(this); 12 | } 13 | 14 | handleOpenClose() { 15 | if (this.props.collapsable) { 16 | this.setState(prevState => ({ opened: !prevState.opened })); 17 | } 18 | } 19 | 20 | render() { 21 | const { children, collapsable, collapseHeight, optionsContainerHeight, 22 | ruledLine, scroll, title, className } = this.props; 23 | const { opened } = this.state; 24 | const fixedHeight = collapseHeight > 0 ? collapseHeight : undefined; 25 | const optionsContainerCX = cx(className, { 26 | 'options-container': true, 27 | collapsable, 28 | opened 29 | }); 30 | const collapseCX = cx({ 31 | 'options-container-collapsable': true, 32 | scroll 33 | }); 34 | 35 | return ( 36 |
    37 |
    this.handleOpenClose()}> 38 |
    {title}
    39 | { 40 | collapsable && 41 | {opened ? '–' : '+'} 42 | } 43 | { ruledLine ?
    : null } 44 |
    45 | 50 | { children } 51 | 52 |
    53 | ); 54 | } 55 | } 56 | 57 | OptionsContainer.defaultProps = { 58 | className: '', 59 | collapsable: true, 60 | collapseHeight: 0, 61 | isOpened: true, 62 | optionsContainerHeight: null, 63 | ruledLine: false, 64 | scroll: false 65 | }; 66 | 67 | OptionsContainer.propTypes = { 68 | children: PropTypes.oneOfType([ 69 | PropTypes.element, 70 | PropTypes.array 71 | ]).isRequired, 72 | className: PropTypes.string, // additional classname(s) to tack on to section el 73 | collapsable: PropTypes.bool, // should the content be collapsable? 74 | collapseHeight: PropTypes.number, // should the collapsable content have a fixed height? 75 | isOpened: PropTypes.bool, 76 | optionsContainerHeight: PropTypes.number, // height for the options container 77 | ruledLine: PropTypes.bool, // add a ruled line under the header? 78 | scroll: PropTypes.bool, // should the content in Collapse be scrollable? 79 | title: PropTypes.string.isRequired, // title in the header 80 | }; 81 | 82 | export default OptionsContainer; 83 | -------------------------------------------------------------------------------- /src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import momentPropTypes from 'react-moment-proptypes'; 3 | import ReactModal from 'react-modal'; 4 | 5 | import ModalContent from './ModalContent'; 6 | import About from './About'; 7 | import Help from './Help'; 8 | import DownloadData from './DownloadData'; 9 | import ShareURL from './ShareURL'; 10 | import ShareFB from './ShareFB'; 11 | import ShareTwitter from './ShareTwitter'; 12 | import Disclaimer from './Disclaimer'; 13 | import Copyright from './Copyright'; 14 | 15 | class ModalWrapper extends Component { 16 | constructor() { 17 | super(); 18 | this.handleModalType = this.handleModalType.bind(this); 19 | } 20 | 21 | handleModalType() { 22 | const { modalType, closeModal, } = this.props; 23 | const { startDate, endDate } = this.props; 24 | const { filterType, filterArea, filterVehicle } = this.props; 25 | const modalTypes = { 26 | about: About, 27 | help: Help, 28 | copyright: Copyright, 29 | disclaimer: Disclaimer, 30 | 'download-data': DownloadData, 31 | 'share-fb': ShareFB, 32 | 'share-tw': ShareTwitter, 33 | 'share-url': ShareURL, 34 | }; 35 | const hocConfig = { modalType, closeModal }; 36 | const HOC = ModalContent(modalTypes[modalType], hocConfig); 37 | 38 | return ( 39 | 40 | ); 41 | } 42 | 43 | render() { 44 | const { closeModal, showModal, modalType } = this.props; 45 | const contentLabel = `${modalType} modal`; 46 | 47 | return ( 48 | closeModal()} 51 | contentLabel={contentLabel} 52 | className="Modal" 53 | overlayClassName="Overlay" 54 | > 55 | { this.handleModalType() } 56 | 57 | ); 58 | } 59 | } 60 | 61 | ModalWrapper.defaultProps = { 62 | modalType: '', 63 | }; 64 | 65 | ModalWrapper.propTypes = { 66 | closeModal: PropTypes.func.isRequired, 67 | showModal: PropTypes.bool.isRequired, 68 | modalType: PropTypes.string, 69 | filterType: PropTypes.shape({}).isRequired, 70 | filterArea: PropTypes.shape({}).isRequired, 71 | filterVehicle: PropTypes.shape({ 72 | vehicle: PropTypes.shape({ 73 | car: PropTypes.bool.isRequired, 74 | truck: PropTypes.bool.isRequired, 75 | motorcycle: PropTypes.bool.isRequired, 76 | bicycle: PropTypes.bool.isRequired, 77 | suv: PropTypes.bool.isRequired, 78 | busvan: PropTypes.bool.isRequired, 79 | scooter: PropTypes.bool.isRequired, 80 | other: PropTypes.bool.isRequired, 81 | }).isRequired, 82 | }).isRequired, 83 | startDate: momentPropTypes.momentObj.isRequired, 84 | endDate: momentPropTypes.momentObj.isRequired, 85 | }; 86 | 87 | export default ModalWrapper; 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nyc-crash-mapper", 3 | "version": "1.0.0", 4 | "description": "Web app that geographically maps, filters, aggregates, & views trends of nyc automobile collision data.", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "clean": "rimraf dist", 8 | "build": "export NODE_ENV=production npm run clean && webpack -p", 9 | "serve": "webpack-dev-server", 10 | "deploy:gh-pages": "npm run build && . ./deploy_gh_pages.sh", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/clhenrick/nyc-crash-mapper.git" 16 | }, 17 | "keywords": [ 18 | "nyc", 19 | "chekpeds", 20 | "vision zero", 21 | "automobile crashes" 22 | ], 23 | "author": "Chris Henrick (http://chrishenrick.com)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/clhenrick/nyc-crash-mapper/issues" 27 | }, 28 | "homepage": "https://github.com/clhenrick/nyc-crash-mapper#readme", 29 | "dependencies": { 30 | "classnames": "^2.2.5", 31 | "es6-promise": "^4.0.5", 32 | "isomorphic-fetch": "^2.2.1", 33 | "leaflet-draw": "^0.4.9", 34 | "lodash": "^4.17.4", 35 | "moment": "^2.17.1", 36 | "normalize-scss": "^6.0.0", 37 | "query-string": "^5.0.1", 38 | "react": "^15.4.2", 39 | "react-collapse": "^2.3.3", 40 | "react-copy-to-clipboard": "^4.2.3", 41 | "react-dom": "^15.4.2", 42 | "react-height": "^2.1.1", 43 | "react-modal": "^1.6.5", 44 | "react-moment-proptypes": "^1.2.1", 45 | "react-motion": "^0.4.7", 46 | "react-redux": "^5.0.2", 47 | "react-router": "^3.0.1", 48 | "react-router-redux": "^4.0.7", 49 | "react-select": "^1.0.0-rc.3", 50 | "redux": "^3.6.0", 51 | "redux-logger": "^2.7.4", 52 | "redux-responsive": "^4.1.1", 53 | "redux-thunk": "^2.1.0", 54 | "single-line-string": "0.0.1", 55 | "spin": "0.0.1" 56 | }, 57 | "devDependencies": { 58 | "babel-core": "^6.21.0", 59 | "babel-eslint": "^6.1.2", 60 | "babel-loader": "^6.2.10", 61 | "babel-polyfill": "^6.22.0", 62 | "babel-preset-env": "^1.1.8", 63 | "babel-preset-react": "^6.16.0", 64 | "babel-preset-stage-0": "^6.22.0", 65 | "css-loader": "^0.26.1", 66 | "eslint": "^3.14.0", 67 | "eslint-config-airbnb": "^14.0.0", 68 | "eslint-loader": "^1.6.1", 69 | "eslint-plugin-babel": "^4.0.1", 70 | "eslint-plugin-import": "^2.2.0", 71 | "eslint-plugin-jsx-a11y": "^3.0.2", 72 | "eslint-plugin-react": "^6.9.0", 73 | "extract-text-webpack-plugin": "^2.0.0-beta.4", 74 | "html-webpack-plugin": "^2.26.0", 75 | "image-webpack-loader": "^3.2.0", 76 | "install": "^0.8.4", 77 | "node-sass": "^4.3.0", 78 | "rimraf": "^2.5.4", 79 | "sass-loader": "^4.1.1", 80 | "style-loader": "^0.13.1", 81 | "url-loader": "^0.5.7", 82 | "webpack": "^2.2.0", 83 | "webpack-dev-server": "^2.2.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /scss/_app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: $roboto; 3 | } 4 | 5 | html, 6 | body, 7 | #root, 8 | .app { 9 | height: 100%; 10 | width: 100%; 11 | overflow: hidden; 12 | background-color: #fff; 13 | } 14 | 15 | .app { 16 | position: relative; 17 | } 18 | 19 | // UI elements 20 | .ui { 21 | position: absolute; 22 | z-index: 1; 23 | box-sizing: border-box; 24 | background-color: $ui-grey; 25 | color: $off-white; 26 | padding: $padding-10 0 $padding-5 $padding-10; 27 | } 28 | 29 | // positioning for UI elements 30 | .bottom { bottom: 0; } 31 | .left { left: 0; } 32 | .right { right: 0; } 33 | 34 | // remove margins on h1 - h6 35 | h1, h2, h3, h4, h5, h6 { 36 | margin: 0; 37 | padding-bottom: 1rem; 38 | } 39 | 40 | h6 { 41 | font-size: 1.4rem; 42 | } 43 | 44 | p { 45 | margin: 0; 46 | } 47 | 48 | // map options ul .filter-lists 49 | .filter-list { 50 | display: inline-block; 51 | height: 100%; 52 | width: calc(100% - 20px); 53 | padding: 0; 54 | margin: 0; 55 | list-style-type: none; 56 | padding-left: $padding-10; 57 | 58 | li { 59 | padding: 0; 60 | margin: 0; 61 | } 62 | } 63 | 64 | // ruled lines 65 | hr { 66 | border-top-color: $hr-border; 67 | margin: 0; 68 | padding: 0; 69 | } 70 | 71 | // Skeleton Grid Overrides 72 | .container { 73 | @media screen and (#{$bp-larger-than-phablet}) { 74 | height: 100%; 75 | width: 100%; 76 | max-width: 100%; 77 | } 78 | } 79 | 80 | .row { 81 | &.stats-header { 82 | height: $stats-header-height; 83 | overflow-y: hidden; 84 | } 85 | 86 | &.stats-content { 87 | height: $stats-content-height; 88 | } 89 | } 90 | 91 | .column, 92 | .columns { 93 | height: 100%; 94 | // border: 1px solid $medium-grey; 95 | 96 | @media screen and (#{$bp-larger-than-phablet}) { 97 | margin-left: 0; 98 | } 99 | } 100 | 101 | .two.columns { 102 | @media screen and (#{$bp-larger-than-phablet}) { 103 | width: 16.666666666666667%; 104 | } 105 | 106 | @media screen and (#{$bp-narrow-width}) { 107 | display: none; 108 | } 109 | } 110 | 111 | .three.columns { 112 | @media screen and (#{$bp-larger-than-phablet}) { 113 | width: 25%; 114 | } 115 | 116 | @media screen and (#{$bp-narrow-width}) { 117 | display: none; 118 | } 119 | } 120 | 121 | .four.columns { 122 | @media screen and (#{$bp-larger-than-phablet}) { 123 | width: 33.333333333333%; 124 | } 125 | 126 | @media screen and (#{$bp-narrow-width}) { 127 | display: none; 128 | } 129 | } 130 | 131 | .six.columns { 132 | @media screen and (#{$bp-larger-than-phablet}) { 133 | width: 50%; 134 | } 135 | 136 | @media screen and (#{$bp-narrow-width}) { 137 | display: none; 138 | } 139 | } 140 | 141 | .seven.columns { 142 | @media screen and (#{$bp-larger-than-phablet}) { 143 | width: 58.3333333333% 144 | } 145 | 146 | @media screen and (#{$bp-narrow-width}) { 147 | width: 100%; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /sql/2016_data_update.sql: -------------------------------------------------------------------------------- 1 | --- Updating 2016 crashes data in CARTO 2 | --- The NYPD deleted and updated rows in Socrata for 2016 sometime in early 2017, 3 | --- requiring a db update in CARTO 4 | --- This may happen for future years as well, so documenting the process here 5 | 6 | --- When imported into CARTO, a value for the "contributing_factor" field may look like: 7 | --- {"Driver Inattention/Distraction",Unspecified} 8 | --- Getting CARTO to recognize "contributing_factor" as an array type requires 9 | --- doing a find and replace, then converting the column type to text array 10 | 11 | --- Note that SQL queries in CARTO do not require a trailing semicolon except 12 | --- in the case of running multiple queries in a row. 13 | 14 | --- Remove curly braces 15 | UPDATE crashes_2016_to_2017 16 | SET contributing_factor = replace(contributing_factor, '{', ''); 17 | 18 | UPDATE crashes_2016_to_2017 19 | SET contributing_factor = replace(contributing_factor, '}', ''); 20 | 21 | UPDATE crashes_2016_to_2017 22 | SET vehicle_type = replace(vehicle_type, '{', ''); 23 | 24 | UPDATE crashes_2016_to_2017 25 | SET vehicle_type = replace(vehicle_type, '}', ''); 26 | 27 | -- Remove double quotes 28 | UPDATE crashes_2016_to_2017 29 | SET contributing_factor = replace(contributing_factor, '"', ''); 30 | 31 | UPDATE crashes_2016_to_2017 32 | SET vehicle_type = replace(vehicle_type, '"', ''); 33 | 34 | --- Convert data type from text to text array 35 | ALTER TABLE crashes_2016_to_2017 36 | ALTER contributing_factor type text[] using array[string_to_array(contributing_factor, ',', '')]; 37 | 38 | ALTER TABLE crashes_2016_to_2017 39 | ALTER vehicle_type type text[] using array[string_to_array(vehicle_type, ',', '')]; 40 | 41 | --- Drop rows in master crashes table for 2016 & early 2017 42 | DELETE FROM crashes_all_prod 43 | WHERE date_val >= date '2016-01-01' AND date_val <= '2017-02-26'; 44 | 45 | --- Insert updated data into the master crashes table 46 | INSERT INTO crashes_all_prod 47 | (the_geom, borough, contributing_factor, crash_count, cross_street_name, date_val, latitude, longitude, 48 | month, number_of_cyclist_injured, number_of_cyclist_killed, number_of_motorist_injured, 49 | number_of_motorist_killed, number_of_pedestrian_injured, number_of_pedestrian_killed, 50 | number_of_persons_injured, number_of_persons_killed, off_street_name, on_street_name, 51 | socrata_id, unique_key, vehicle_type, year, zip_code) 52 | SELECT 53 | the_geom, borough, contributing_factor, crash_count, cross_street_name, date_val, latitude, longitude, 54 | month, number_of_cyclist_injured, number_of_cyclist_killed, number_of_motorist_injured, 55 | number_of_motorist_killed, number_of_pedestrian_injured, number_of_pedestrian_killed, 56 | number_of_persons_injured, number_of_persons_killed, off_street_name, on_street_name, 57 | socrata_id, unique_key, vehicle_type, year, zip_code 58 | FROM crashes_2016_to_2017; 59 | 60 | --- Double check it worked! 61 | SELECT count(*) FROM crashes_all_prod WHERE date_val >= date '2016-01-01' AND date_val <= '2017-02-26'; 62 | --- 258082 63 | 64 | --- After doing this create a copy of the production table as a backup and lock it in the CARTO dashboard 65 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/FilterByType.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | 3 | import FilterButton from './FilterButton'; 4 | 5 | class FilterByType extends Component { 6 | render() { 7 | const { filterByTypeFatality, filterByTypeInjury, filterByNoInjFat, injury, 8 | fatality, noInjuryFatality } = this.props; 9 | 10 | return ( 11 |
    12 |
      13 |
    • 14 | 21 |
    • 22 |
    • 23 | 30 |
    • 31 |
    • 32 | 39 |
    • 40 |
    • 41 | 48 |
    • 49 |
    50 |
      51 |
    • 52 | 59 |
    • 60 |
    • 61 | 68 |
    • 69 |
    • 70 | 77 |
    • 78 |
    79 |
    80 | ); 81 | } 82 | } 83 | 84 | FilterByType.propTypes = { 85 | filterByTypeInjury: PropTypes.func.isRequired, 86 | filterByTypeFatality: PropTypes.func.isRequired, 87 | filterByNoInjFat: PropTypes.func.isRequired, 88 | fatality: PropTypes.shape({ 89 | cyclist: PropTypes.bool.isRequired, 90 | motorist: PropTypes.bool.isRequired, 91 | pedestrian: PropTypes.bool.isRequired, 92 | }).isRequired, 93 | injury: PropTypes.shape({ 94 | cyclist: PropTypes.bool.isRequired, 95 | motorist: PropTypes.bool.isRequired, 96 | pedestrian: PropTypes.bool.isRequired, 97 | }).isRequired, 98 | noInjuryFatality: PropTypes.bool.isRequired 99 | }; 100 | 101 | export default FilterByType; 102 | -------------------------------------------------------------------------------- /scss/components/_leaflet-map.scss: -------------------------------------------------------------------------------- 1 | .leaflet-map { 2 | position: absolute; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | z-index: 0; 8 | 9 | #map { 10 | height: 100%; 11 | } 12 | 13 | // attribution stuff 14 | .leaflet-bottom.leaflet-right { 15 | left: 0; 16 | right: initial; 17 | bottom: 120px; 18 | 19 | * { 20 | color: $marine; 21 | background: none; 22 | background: transparent; 23 | } 24 | 25 | a { 26 | color: $marine-light; 27 | } 28 | } 29 | 30 | .cartodb-tooltip { 31 | width: auto; 32 | padding: $padding-15 0 0 $padding-15; 33 | } 34 | 35 | .cartodb-tooltip-content-wrapper { 36 | @extend %roboto-regular; 37 | width: auto; 38 | padding: $padding-5; 39 | color: $off-white; 40 | background: $ui-grey; 41 | 42 | span { 43 | @extend %roboto-bold; 44 | } 45 | } 46 | 47 | // infowindow custom styles 48 | div.cartodb-popup.customized { 49 | width: $infowindowWidthOuter; 50 | max-width: $infowindowWidthOuter; 51 | } 52 | 53 | div.cartodb-popup.customized .cartodb-popup-content-wrapper, 54 | div.jspContainer, 55 | div.jspPane { 56 | width: $infowindowWidthInner !important; 57 | max-width: $infowindowWidthInner !important; 58 | &:before, 59 | &:after { 60 | background: none; 61 | } 62 | } 63 | 64 | div.cartodb-popup.customized { 65 | @extend %roboto-regular; 66 | background: none; 67 | padding: $padding-10; 68 | 69 | h1, 70 | h2, 71 | h3, 72 | h4, 73 | h5, 74 | h6, 75 | p { 76 | color: $off-white; 77 | letter-spacing: 0.01rem; 78 | } 79 | 80 | p { 81 | @extend %roboto-regular; 82 | font-size: 1.1rem; 83 | } 84 | 85 | h4 { 86 | @extend %roboto-bold; 87 | font-size: 1.2rem; 88 | } 89 | 90 | .cartodb-popup-close-button.close { 91 | @extend %roboto-bold; 92 | right: -17px; 93 | background: lighten($grey, 10); 94 | border-radius: 50%; 95 | font-size: 2rem; 96 | text-indent: 7.5px; 97 | line-height: 26px; 98 | text-decoration: none; 99 | color: $btn-bg; 100 | 101 | &:hover { 102 | color: $btn-hover; 103 | } 104 | } 105 | 106 | div.cartodb-popup-content-wrapper { 107 | width: $infowindowWidthInner; 108 | max-width: $infowindowWidthInner; 109 | background: $ui-grey; 110 | padding: $padding-10 $padding-5 $padding-5 $padding-10; 111 | border-radius: $border-radius; 112 | } 113 | 114 | .jspContainer { 115 | &:before, 116 | &:after { 117 | background: none; 118 | } 119 | } 120 | 121 | div.cartodb-popup-tip-container { 122 | position: relative; 123 | left: 18px; 124 | background: none; 125 | width: 0; 126 | height: 0; 127 | border-style: solid; 128 | border-width: 25px 25px 0 0; 129 | border-color: $ui-grey transparent transparent transparent; 130 | } 131 | } 132 | 133 | .map-stats-disclaimer { 134 | position: absolute; 135 | bottom: 119px; 136 | left: 198px; 137 | z-index: 5; 138 | font-size: 1.1rem; 139 | color: $marine-light; 140 | } 141 | 142 | .filter-area-tooltip { 143 | display: none; 144 | position: absolute; 145 | left: 0; 146 | top: 0; 147 | z-index: 6; 148 | padding: 0 3px; 149 | color: $marine-light; 150 | background-color: $off-white; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /scss/skeleton/modules/_buttons.scss: -------------------------------------------------------------------------------- 1 | // Buttons 2 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 3 | 4 | .button, 5 | button { 6 | display: inline-block; 7 | height: 38px; 8 | padding: 0 30px; 9 | color: $secondary-color; 10 | text-align: center; 11 | font-size: 11px; 12 | font-weight: 600; 13 | line-height: 38px; 14 | letter-spacing: .1rem; 15 | text-transform: uppercase; 16 | text-decoration: none; 17 | white-space: nowrap; 18 | background-color: transparent; 19 | border-radius: $global-radius; 20 | border: 1px solid $border-color; 21 | cursor: pointer; 22 | box-sizing: border-box; 23 | } 24 | 25 | input { 26 | &[type="submit"], 27 | &[type="reset"], 28 | &[type="button"] { 29 | display: inline-block; 30 | height: 38px; 31 | padding: 0 30px; 32 | color: $secondary-color; 33 | text-align: center; 34 | font-size: 11px; 35 | font-weight: 600; 36 | line-height: 38px; 37 | letter-spacing: .1rem; 38 | text-transform: uppercase; 39 | text-decoration: none; 40 | white-space: nowrap; 41 | background-color: transparent; 42 | border-radius: $global-radius; 43 | border: 1px solid $border-color; 44 | cursor: pointer; 45 | box-sizing: border-box; 46 | } 47 | } 48 | 49 | .button:hover, 50 | button:hover { 51 | color: $dark-grey; 52 | border-color: lighten($dark-grey, 33.3%); 53 | outline: 0; 54 | } 55 | 56 | input { 57 | &[type="submit"]:hover, 58 | &[type="reset"]:hover, 59 | &[type="button"]:hover { 60 | color: $dark-grey; 61 | border-color: lighten($dark-grey, 33.3%); 62 | outline: 0; 63 | } 64 | } 65 | 66 | .button:focus, 67 | button:focus { 68 | color: $dark-grey; 69 | border-color: lighten($dark-grey, 33.3%); 70 | outline: 0; 71 | } 72 | 73 | input { 74 | &[type="submit"]:focus, 75 | &[type="reset"]:focus, 76 | &[type="button"]:focus { 77 | color: $dark-grey; 78 | border-color: lighten($dark-grey, 33.3%); 79 | outline: 0; 80 | } 81 | } 82 | 83 | .button.button-primary, 84 | button.button-primary { 85 | color: #fff; 86 | background-color: $primary-color; 87 | border-color: $primary-color; 88 | } 89 | 90 | input { 91 | &[type="submit"].button-primary, 92 | &[type="reset"].button-primary, 93 | &[type="button"].button-primary { 94 | color: #fff; 95 | background-color: $primary-color; 96 | border-color: $primary-color; 97 | } 98 | } 99 | 100 | .button.button-primary:hover, 101 | button.button-primary:hover { 102 | color: #fff; 103 | background-color: $link-color; 104 | border-color: $link-color; 105 | } 106 | 107 | input { 108 | &[type="submit"].button-primary:hover, 109 | &[type="reset"].button-primary:hover, 110 | &[type="button"].button-primary:hover { 111 | color: #fff; 112 | background-color: $link-color; 113 | border-color: $link-color; 114 | } 115 | } 116 | 117 | .button.button-primary:focus, 118 | button.button-primary:focus { 119 | color: #fff; 120 | background-color: $link-color; 121 | border-color: $link-color; 122 | } 123 | 124 | input { 125 | &[type="submit"].button-primary:focus, 126 | &[type="reset"].button-primary:focus, 127 | &[type="button"].button-primary:focus { 128 | color: #fff; 129 | background-color: $link-color; 130 | border-color: $link-color; 131 | } 132 | &[type="email"], 133 | &[type="number"], 134 | &[type="search"], 135 | &[type="text"], 136 | &[type="tel"], 137 | &[type="url"], 138 | &[type="password"] { 139 | height: 38px; 140 | padding: 6px 10px; // The 6px vertically centers text on FF, ignored by Webkit 141 | background-color: #fff; 142 | border: 1px solid lighten($border-color, 8.8%); 143 | border-radius: $global-radius; 144 | box-shadow: none; 145 | box-sizing: border-box; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /scss/skeleton/modules/_grid.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/9/2014 8 | * Sass Version by Seth Coelen https://github.com/whatsnewsaes 9 | */ 10 | 11 | .container { 12 | position: relative; 13 | width: 100%; 14 | max-width: $container-width; 15 | margin: 0 auto; 16 | padding: 0 20px; 17 | box-sizing: border-box; 18 | } 19 | 20 | .column, 21 | .columns { 22 | width: 100%; 23 | float: left; 24 | box-sizing: border-box; 25 | } 26 | 27 | // For devices larger than 400px 28 | @media (#{$bp-larger-than-mobile}) { 29 | .container { 30 | width: $container-width-larger-than-mobile; 31 | padding: 0; 32 | } 33 | } 34 | 35 | // For devices larger than 550px 36 | @media (#{$bp-larger-than-phablet}) { 37 | .container { 38 | width: $container-width-larger-than-phablet; 39 | } 40 | .column, 41 | .columns { 42 | margin-left: $column-margin; 43 | } 44 | .column:first-child, 45 | .columns:first-child { 46 | margin-left: 0; 47 | } 48 | 49 | .one.column, 50 | .one.columns { width: grid-column-width(1); } 51 | .two.columns { width: grid-column-width(2); } 52 | .three.columns { width: grid-column-width(3); } 53 | .four.columns { width: grid-column-width(4); } 54 | .five.columns { width: grid-column-width(5); } 55 | .six.columns { width: grid-column-width(6); } 56 | .seven.columns { width: grid-column-width(7); } 57 | .eight.columns { width: grid-column-width(8); } 58 | .nine.columns { width: grid-column-width(9); } 59 | .ten.columns { width: grid-column-width(10); } 60 | .eleven.columns { width: grid-column-width(11); } 61 | .twelve.columns { width: 100%; margin-left: 0; } 62 | 63 | .one-third.column { width: grid-column-width(4); } 64 | .two-thirds.column { width: grid-column-width(8); } 65 | 66 | .one-half.column { width: grid-column-width(6); } 67 | 68 | 69 | // Offsets 70 | .offset-by-one.column, 71 | .offset-by-one.columns { margin-left: grid-offset-length(1); } 72 | .offset-by-two.column, 73 | .offset-by-two.columns { margin-left: grid-offset-length(2); } 74 | .offset-by-three.column, 75 | .offset-by-three.columns { margin-left: grid-offset-length(3); } 76 | .offset-by-four.column, 77 | .offset-by-four.columns { margin-left: grid-offset-length(4); } 78 | .offset-by-five.column, 79 | .offset-by-five.columns { margin-left: grid-offset-length(5); } 80 | .offset-by-six.column, 81 | .offset-by-six.columns { margin-left: grid-offset-length(6); } 82 | .offset-by-seven.column, 83 | .offset-by-seven.columns { margin-left: grid-offset-length(7); } 84 | .offset-by-eight.column, 85 | .offset-by-eight.columns { margin-left: grid-offset-length(8); } 86 | .offset-by-nine.column, 87 | .offset-by-nine.columns { margin-left: grid-offset-length(9); } 88 | .offset-by-ten.column, 89 | .offset-by-ten.columns { margin-left: grid-offset-length(10); } 90 | .offset-by-eleven.column, 91 | .offset-by-eleven.columns { margin-left: grid-offset-length(11); } 92 | 93 | 94 | .offset-by-one-third.column, 95 | .offset-by-one-third.columns { margin-left: grid-offset-length(4); } 96 | .offset-by-two-thirds.column, 97 | .offset-by-two-thirds.columns { margin-left: grid-offset-length(8); } 98 | 99 | .offset-by-one-half.column, 100 | .offset-by-one-half.column { margin-left: grid-offset-length(6); } 101 | 102 | 103 | } 104 | 105 | // Clearing 106 | //–––––––––––––––––––––––––––––––––––––––––––––––––– 107 | 108 | // Self Clearing Goodness 109 | 110 | .container:after, 111 | .row:after, 112 | .u-cf { 113 | content: ""; 114 | display: table; 115 | clear: both; 116 | } 117 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/FilterByDate.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import momentPropTypes from 'react-moment-proptypes'; 3 | 4 | import { momentize } from '../../constants/api'; 5 | import MonthYearSelector from './MonthYearSelector'; 6 | 7 | class FilterByDate extends Component { 8 | constructor() { 9 | super(); 10 | this.handleStartDateChange = this.handleStartDateChange.bind(this); 11 | this.handleEndDateChange = this.handleEndDateChange.bind(this); 12 | } 13 | 14 | handleStartDateChange(year, month) { 15 | // validates the new start date before updating the redux store & selecting new data 16 | const { endDate, crashesDateRange: { minDate } } = this.props; 17 | 18 | // don't allow a user to select a month before the begining of the dataset 19 | if (year === minDate.year() && month < minDate.month() + 1) { 20 | month = minDate.month() + 1; 21 | } 22 | 23 | const mm = month < 10 ? `0${month}` : month; 24 | const startMoment = momentize(`${year}-${mm}`); 25 | 26 | if (startMoment.isSameOrBefore(endDate)) { 27 | this.props.startDateChange(startMoment); 28 | } else { 29 | // if the user attempts to select a startDate greater than the current endDate 30 | // set the new startDate to the current endDate 31 | this.props.startDateChange(endDate); 32 | } 33 | } 34 | 35 | handleEndDateChange(year, month) { 36 | // validates the new end date before updating the redux store & selecting new data 37 | const { startDate, crashesDateRange: { maxDate } } = this.props; 38 | 39 | // don't allow a user to select a month after the end of the dataset 40 | if (year === maxDate.year() && month > maxDate.month() + 1) { 41 | month = maxDate.month() + 1; 42 | } 43 | 44 | const mm = month < 10 ? `0${month}` : month; 45 | const endMoment = momentize(`${year}-${mm}`); 46 | 47 | if (endMoment.isSameOrAfter(startDate)) { 48 | this.props.endDateChange(endMoment); 49 | } else { 50 | // if the user attempts to select an endDate earlier than the current startDate 51 | // set the new endDate to the current startDate 52 | this.props.endDateChange(startDate); 53 | } 54 | } 55 | 56 | render() { 57 | const { crashesDateRange, startDate, endDate, years } = this.props; 58 | // months in moment.js are zero based 59 | const startMonth = startDate.month() + 1; 60 | const startYear = startDate.year(); 61 | const endMonth = endDate.month() + 1; 62 | const endYear = endDate.year(); 63 | 64 | return ( 65 |
    66 |
      67 |
    • 68 | 76 |
    • 77 |
    • 78 | 86 |
    • 87 |
    88 |
    89 | ); 90 | } 91 | } 92 | 93 | // react-datepicker requires dates to be moment objects 94 | FilterByDate.propTypes = { 95 | crashesDateRange: PropTypes.shape({ 96 | minDate: momentPropTypes.momentObj, 97 | maxDate: momentPropTypes.momentObj, 98 | }).isRequired, 99 | startDateChange: PropTypes.func.isRequired, 100 | endDateChange: PropTypes.func.isRequired, 101 | startDate: momentPropTypes.momentObj.isRequired, 102 | endDate: momentPropTypes.momentObj.isRequired, 103 | years: PropTypes.arrayOf(PropTypes.number).isRequired, 104 | }; 105 | 106 | export default FilterByDate; 107 | -------------------------------------------------------------------------------- /src/components/OptionsFilters/FilterByVehicle.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | 3 | import FilterButton from './FilterButton'; 4 | 5 | class FilterByVehicle extends Component { 6 | render() { 7 | const { vehicle } = this.props; 8 | const { filterByVehicleCar, filterByVehicleTruck, filterByVehicleMotorcycle, 9 | filterByVehicleBicycle, filterByVehicleSuv, filterByVehicleBusVan, 10 | filterByVehicleScooter, filterByVehicleOther } = this.props; 11 | 12 | return ( 13 |
    14 |
      15 |
    • 16 | 23 |
    • 24 |
    • 25 | 32 |
    • 33 |
    • 34 | 41 |
    • 42 |
    • 43 | 50 |
    • 51 |
    52 |
      53 |
    • 54 | 61 |
    • 62 |
    • 63 | 70 |
    • 71 |
    • 72 | 79 |
    • 80 |
    • 81 | 88 |
    • 89 |
    90 |
    91 | ); 92 | } 93 | } 94 | 95 | FilterByVehicle.propTypes = { 96 | filterByVehicleCar: PropTypes.func.isRequired, 97 | filterByVehicleTruck: PropTypes.func.isRequired, 98 | filterByVehicleMotorcycle: PropTypes.func.isRequired, 99 | filterByVehicleBicycle: PropTypes.func.isRequired, 100 | filterByVehicleSuv: PropTypes.func.isRequired, 101 | filterByVehicleBusVan: PropTypes.func.isRequired, 102 | filterByVehicleScooter: PropTypes.func.isRequired, 103 | filterByVehicleOther: PropTypes.func.isRequired, 104 | vehicle: PropTypes.shape({ 105 | car: PropTypes.bool.isRequired, 106 | truck: PropTypes.bool.isRequired, 107 | motorcycle: PropTypes.bool.isRequired, 108 | bicycle: PropTypes.bool.isRequired, 109 | suv: PropTypes.bool.isRequired, 110 | busvan: PropTypes.bool.isRequired, 111 | scooter: PropTypes.bool.isRequired, 112 | other: PropTypes.bool.isRequired, 113 | }).isRequired, 114 | }; 115 | 116 | export default FilterByVehicle; 117 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NYC Crash Mapper 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
    31 | 32 | 33 | 62 | 71 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/components/HelpCopy.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HelpCopy = () => ( 4 |
    5 | 6 | 7 | 8 |
    9 |