├── .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 |
{ 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 | closeModal()} />
27 | {title[modalType]}
28 |
29 |
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 |
openModal(value)}>
31 | {label}
32 |
33 | );
34 |
35 | default:
36 | return (
37 |
{}}
40 | >
41 | {item.label}
42 |
43 | );
44 | }
45 | };
46 |
47 | return (
48 |
49 | {items.map(item => {mapTypeToElement(item)} )}
50 |
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 |
27 | {`${prefix} Year`}
28 |
29 | handleChange(y.value, curMonth)}
34 | matchPos="start"
35 | clearable={false}
36 | value={curYear}
37 | />
38 |
39 |
40 |
41 | {`${prefix} Month`}
42 |
43 | 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 |
17 |
18 |
19 | Crashmapper Navigation
20 |
21 | Analyze crash data like a pro. With a few clicks, master the information contained in
22 | the NYC Open Data Collision dataset, collected by NYPD. Choose a data view, filter by
23 | dates, geographies and crash type and get maps, charts and information summaries.
24 | Share with others.
25 |
26 |
27 |
28 |
29 |
30 |
38 |
39 |
40 | Crashmapper Custom Map
41 |
42 | How is my area of focus (corridor, school zone, business improvement district) faring
43 | in terms of traffic safety?
44 |
45 |
46 |
47 |
48 |
49 |
57 |
58 |
59 | Crashmapper Trend
60 |
61 | How does my neighborhood compare to others and to the borough in terms of traffic
62 | safety?
63 |
64 |
65 |
66 |
67 |
68 |
76 |
77 |
78 | Crashmapper Compare
79 |
80 | How has traffic safety changed in my neighborhood since a past period? Are the traffic
81 | calming measures working?
82 |
83 |
84 |
85 |
86 |
87 |
95 |
96 |
97 | Crashmapper Rank
98 |
99 | How is this dangerous intersection ranked in the district, borough or city overall?
100 | Are we concentrating our efforts on the most dangerous intersections?
101 |
102 |
103 |
104 |
105 |
106 |
114 |
115 |
116 | Crashmapper Vehicle Filter
117 |
118 | Do many crashes involve certain type of vehicles?
119 | Is there a category of vehicles more dangerous
120 | to vulnerable users as they cause more fatalities and injuries?
121 | Is it specific to an area when enforcement should be increased?
122 |
123 |
124 |
125 |
126 |
127 |
128 | );
129 |
130 | export default HelpCopy;
131 |
--------------------------------------------------------------------------------
/src/components/StatsLegend/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import momentPropTypes from 'react-moment-proptypes';
3 |
4 | import { dateStringFormatView, } from '../../constants/api';
5 | import DateRange from './DateRange';
6 | import TotalCrashCounter from './TotalCrashCounter';
7 | import StatsCounter from './StatsCounter';
8 | import ContributingFactorsList from './ContributingFactorsList';
9 | import LegendContainer from './LegendContainer';
10 |
11 | class StatsLegend extends Component {
12 |
13 | render() {
14 | const {
15 | startDate,
16 | endDate,
17 | contributingFactors,
18 | cyclist_injured,
19 | cyclist_killed,
20 | motorist_injured,
21 | motorist_killed,
22 | pedestrian_injured,
23 | pedestrian_killed,
24 | other_injured,
25 | other_killed,
26 | persons_injured,
27 | persons_killed,
28 | total_crashes,
29 | } = this.props;
30 |
31 | return (
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
Contributing Factors
44 |
45 |
46 |
Legend
47 |
48 |
49 |
50 |
51 |
59 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 | }
80 |
81 | StatsLegend.defaultProps = {
82 | contributingFactors: [],
83 | cyclist_injured: 0,
84 | cyclist_killed: 0,
85 | motorist_injured: 0,
86 | motorist_killed: 0,
87 | pedestrian_injured: 0,
88 | pedestrian_killed: 0,
89 | other_injured: 0,
90 | other_killed: 0,
91 | persons_injured: 0,
92 | persons_killed: 0,
93 | total_crashes: 0,
94 | identifier: '',
95 | lngLats: [],
96 | };
97 |
98 | StatsLegend.propTypes = {
99 | startDate: momentPropTypes.momentObj.isRequired,
100 | endDate: momentPropTypes.momentObj.isRequired,
101 | contributingFactors: PropTypes.arrayOf(PropTypes.shape({
102 | count_factor: PropTypes.number,
103 | factor: PropTypes.string
104 | })),
105 | cyclist_injured: PropTypes.number,
106 | cyclist_killed: PropTypes.number,
107 | motorist_injured: PropTypes.number,
108 | motorist_killed: PropTypes.number,
109 | pedestrian_injured: PropTypes.number,
110 | pedestrian_killed: PropTypes.number,
111 | other_injured: PropTypes.number,
112 | other_killed: PropTypes.number,
113 | persons_injured: PropTypes.number,
114 | persons_killed: PropTypes.number,
115 | total_crashes: PropTypes.number,
116 | filterType: PropTypes.shape({
117 | fatality: PropTypes.shape({
118 | cyclist: PropTypes.bool.isRequired,
119 | motorist: PropTypes.bool.isRequired,
120 | pedestrian: PropTypes.bool.isRequired,
121 | }).isRequired,
122 | injury: PropTypes.shape({
123 | cyclist: PropTypes.bool.isRequired,
124 | motorist: PropTypes.bool.isRequired,
125 | pedestrian: PropTypes.bool.isRequired,
126 | }).isRequired,
127 | noInjuryFatality: PropTypes.bool.isRequired
128 | }).isRequired,
129 | geo: PropTypes.string.isRequired,
130 | identifier: PropTypes.oneOfType([
131 | PropTypes.string,
132 | PropTypes.number
133 | ]),
134 | lngLats: PropTypes.arrayOf(
135 | PropTypes.arrayOf(PropTypes.number)
136 | ),
137 | };
138 |
139 | export default StatsLegend;
140 |
--------------------------------------------------------------------------------
/src/components/OptionsFilters/FilterByArea.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | import FilterButton from './FilterButton';
4 |
5 | import { labelFormats } from './../../constants/app_config';
6 |
7 | const FilterByBoundary = (props) => {
8 | const { filterByAreaType, filterByAreaIdentifier, toggleCustomAreaDraw, drawEnabeled, geo,
9 | identifier } = props;
10 |
11 | const showIdentifier = (name) => {
12 | if (geo === name && identifier) {
13 | let label = identifier;
14 | if (geo === 'intersection') label = label.split('|')[0].split(', ')[1];
15 |
16 | const tooltip = labelFormats[geo].replace('{}', label);
17 |
18 | if (label.length > 12) {
19 | label = `${label.substring(0, 12)}...`;
20 | }
21 |
22 | return (
23 |
24 |
25 | {label}
26 |
27 | filterByAreaIdentifier(undefined)}
30 | >
31 | {'✕'}
32 |
33 |
34 | );
35 | }
36 | return null;
37 | };
38 |
39 | return (
40 |
41 |
42 |
49 |
50 |
51 |
58 | { showIdentifier('borough') }
59 |
60 |
61 |
68 | { showIdentifier('community_board') }
69 |
70 |
71 |
78 | { showIdentifier('city_council') }
79 |
80 |
81 |
88 | { showIdentifier('neighborhood') }
89 |
90 |
91 |
98 | { showIdentifier('nypd_precinct') }
99 |
100 |
101 |
108 | { showIdentifier('assembly') }
109 |
110 |
111 |
118 | { showIdentifier('senate') }
119 |
120 |
121 |
128 | { showIdentifier('intersection') }
129 |
130 |
131 |
138 | { geo === 'custom' && !drawEnabeled ?
139 | toggleCustomAreaDraw()}>
140 | Draw Again
141 | : null
142 | }
143 | { geo === 'custom' && drawEnabeled ?
144 | toggleCustomAreaDraw()}>
145 | Cancel Draw
146 | : null
147 | }
148 |
149 |
150 | );
151 | };
152 |
153 | FilterByBoundary.defaultProps = {
154 | identifier: '',
155 | lngLats: [],
156 | };
157 |
158 | FilterByBoundary.propTypes = {
159 | filterByAreaType: PropTypes.func.isRequired,
160 | filterByAreaIdentifier: PropTypes.func.isRequired,
161 | toggleCustomAreaDraw: PropTypes.func.isRequired,
162 | drawEnabeled: PropTypes.bool.isRequired,
163 | geo: PropTypes.string.isRequired,
164 | identifier: PropTypes.oneOfType([
165 | PropTypes.number,
166 | PropTypes.string
167 | ]),
168 | };
169 |
170 | export default FilterByBoundary;
171 |
--------------------------------------------------------------------------------
/src/actions/async_actions.js:
--------------------------------------------------------------------------------
1 | import { polyfill } from 'es6-promise';
2 | import fetch from 'isomorphic-fetch';
3 | import { cartoSQLQuery } from '../constants/app_config';
4 | import * as actions from '../constants/action_types';
5 | import {
6 | configureStatsSQL,
7 | configureFactorsSQL,
8 | crashesYearRangeSQL,
9 | minMaxDateRange,
10 | crashesMaxDate,
11 | filterByAreaSQL } from '../constants/sql_queries';
12 |
13 | polyfill();
14 |
15 | const requestCrashStatsData = () => ({
16 | type: actions.CRASHES_ALL_REQUEST
17 | });
18 |
19 | const receiveCrashStatsData = json => ({
20 | type: actions.CRASHES_ALL_SUCCESS,
21 | json
22 | });
23 |
24 | const receiveCrashStatsError = error => ({
25 | type: actions.CRASHES_ALL_ERROR,
26 | error
27 | });
28 |
29 | export const fetchCrashStatsData = (params) => {
30 | const query = encodeURIComponent(configureStatsSQL(params));
31 | const url = cartoSQLQuery(query);
32 | return (dispatch) => {
33 | dispatch(requestCrashStatsData());
34 | return fetch(url)
35 | .then(res => res.json())
36 | .then(json => dispatch(receiveCrashStatsData(json.rows[0])))
37 | .catch(error => dispatch(receiveCrashStatsError(error)));
38 | };
39 | };
40 |
41 | const requestContributingFactors = () => ({
42 | type: actions.CONTRIBUTING_FACTORS_REQUEST
43 | });
44 |
45 | const receiveContributingFactors = json => ({
46 | type: actions.CONTRIBUTING_FACTORS_SUCCESS,
47 | json
48 | });
49 |
50 | const receiveContributingFactorsError = error => ({
51 | type: actions.CONTRIBUTING_FACTORS_ERROR,
52 | error
53 | });
54 |
55 | export const fetchContributingFactors = (params) => {
56 | const query = encodeURIComponent(configureFactorsSQL(params));
57 | const url = cartoSQLQuery(query);
58 | return (dispatch) => {
59 | dispatch(requestContributingFactors());
60 | return fetch(url)
61 | .then(res => res.json())
62 | .then(json => dispatch(receiveContributingFactors(json.rows)))
63 | .catch(error => dispatch(receiveContributingFactorsError(error)));
64 | };
65 | };
66 |
67 | const requestCrashesYearRange = () => ({
68 | type: actions.CRASHES_YEAR_RANGE_REQUEST
69 | });
70 |
71 | const receiveCrashesYearRange = json => ({
72 | type: actions.CRASHES_YEAR_RANGE_SUCCESS,
73 | json
74 | });
75 |
76 | const receiveCrashesYearRangeError = error => ({
77 | type: actions.CRASHES_YEAR_RANGE_ERROR,
78 | error
79 | });
80 |
81 | export const fetchCrashesYearRange = () => {
82 | const query = encodeURIComponent(crashesYearRangeSQL());
83 | const url = cartoSQLQuery(query);
84 | return (dispatch) => {
85 | dispatch(requestCrashesYearRange());
86 | return fetch(url)
87 | .then(res => res.json())
88 | .then(json => dispatch(receiveCrashesYearRange(json.rows)))
89 | .catch(error => dispatch(receiveCrashesYearRangeError(error)));
90 | };
91 | };
92 |
93 | const requestCrashesDateRange = () => ({
94 | type: actions.CRASHES_DATE_RANGE_REQUEST
95 | });
96 |
97 | const receiveCrashesDateRange = json => ({
98 | type: actions.CRASHES_DATE_RANGE_SUCCESS,
99 | json
100 | });
101 |
102 | const receiveCrashesDateRangeError = error => ({
103 | type: actions.CRASHES_DATE_RANGE_ERROR,
104 | error
105 | });
106 |
107 | export const fetchCrashesDateRange = () => {
108 | const query = encodeURIComponent(minMaxDateRange());
109 | const url = cartoSQLQuery(query);
110 | return (dispatch) => {
111 | dispatch(requestCrashesDateRange());
112 | return fetch(url)
113 | .then(res => res.json())
114 | .then(json => dispatch(receiveCrashesDateRange(json.rows)))
115 | .catch(error => dispatch(receiveCrashesDateRangeError(error)));
116 | };
117 | };
118 |
119 | const requestCrashesMaxDate = () => ({
120 | type: actions.CRASHES_MAX_DATE_REQUEST
121 | });
122 |
123 | const receiveCrashesMaxDate = json => ({
124 | type: actions.CRASHES_MAX_DATE_RESPONSE,
125 | json
126 | });
127 |
128 | const receiveCrashesMaxDateError = error => ({
129 | type: actions.CRASHES_MAX_DATE_ERROR,
130 | error
131 | });
132 |
133 | export const fetchCrashesMaxDate = () => {
134 | const query = encodeURIComponent(crashesMaxDate());
135 | const url = cartoSQLQuery(query);
136 | return (dispatch) => {
137 | dispatch(requestCrashesMaxDate());
138 | return fetch(url)
139 | .then(res => res.json())
140 | .then(json => dispatch(receiveCrashesMaxDate(json.rows)))
141 | .catch(error => dispatch(receiveCrashesMaxDateError(error)));
142 | };
143 | };
144 |
145 | const requestGeoPolygons = () => ({
146 | type: actions.GEO_POLYGONS_REQUEST,
147 | });
148 |
149 | const receiveGeoPolygons = geojson => ({
150 | type: actions.GEO_POLYGONS_SUCCESS,
151 | geojson,
152 | });
153 |
154 | const receiveGeoPolygonsError = error => ({
155 | type: actions.GEO_POLYGONS_ERROR,
156 | error,
157 | });
158 |
159 | export const fetchGeoPolygons = (geo) => {
160 | const query = encodeURIComponent(filterByAreaSQL[geo]);
161 | const url = cartoSQLQuery(query, 'geojson');
162 | return (dispatch) => {
163 | dispatch(requestGeoPolygons());
164 | return fetch(url)
165 | .then(res => res.json())
166 | .then((json) => {
167 | // tack on the geography name so that it may be diff'd in LeafletMap propTypes
168 | json.geoName = geo;
169 | return dispatch(receiveGeoPolygons(json));
170 | })
171 | .catch(error => dispatch(receiveGeoPolygonsError(error)));
172 | };
173 | };
174 |
--------------------------------------------------------------------------------
/src/constants/api.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import queryString from 'query-string';
3 | import isEqual from 'lodash/isEqual';
4 |
5 | import { cartoLayerSource } from './app_config';
6 | import { defaultState } from '../reducers/filter_by_area_reducer';
7 |
8 | export const dateStringFormatModel = 'YYYY-MM';
9 | export const dateStringFormatView = 'MMM, YYYY';
10 |
11 | export const momentize = dateString => moment(dateString, dateStringFormatModel, true);
12 |
13 | // Names for Filter by Boundary
14 | export const geos = ['citywide', 'borough', 'community_board', 'city_council',
15 | 'neighborhood', 'assembly', 'senate', 'nypd_precinct', 'intersection', 'custom'];
16 |
17 | // Borough Names mapped to array index position
18 | export const boroughs = [undefined, 'Manhattan', 'Bronx', 'Brooklyn', 'Queens', 'Staten Island'];
19 |
20 | // creates default app state using any available params from window.location.hash
21 | export const makeDefaultState = () => {
22 | const hash = window.location.hash;
23 | const qString = hash.substring(3, hash.length);
24 | const q = queryString.parse(qString);
25 | const p = {};
26 |
27 | const isJsonString = (str) => {
28 | try {
29 | JSON.parse(str);
30 | } catch (e) {
31 | return false;
32 | }
33 | return true;
34 | };
35 |
36 | const isBool = val => typeof val === 'boolean';
37 |
38 | const isValidMomentObj = (dateString) => {
39 | const m = dateString ? momentize(dateString) : undefined;
40 | if (m && m.isValid()) {
41 | return m;
42 | }
43 | // fallback to last month, which is more likely to have data then the current month
44 | // e.g. if the current date is the first week of the month,
45 | // then data may not exist for that month yet
46 | const m2 = moment().subtract(1, 'month');
47 | const defaultYearMonth = `${m2.year()}-${m2.format('MM')}`;
48 | return momentize(defaultYearMonth);
49 | };
50 |
51 | const isValidGeo = (geo) => {
52 | if (geos.indexOf(geo) !== -1) {
53 | return geo;
54 | }
55 | return geos[0];
56 | };
57 |
58 | // parse query params, with compatibility hack issue 97 to accept Chart View identifier
59 | // which is really the full string name with a borough prefix
60 | Object.keys(q).forEach((key) => {
61 | const decoded = decodeURIComponent(q[key]);
62 | if (isJsonString(decoded)) {
63 | p[key] = JSON.parse(decoded);
64 | } else {
65 | p[key] = decoded;
66 | }
67 | });
68 | if (p.identifier && typeof p.identifier === 'string') {
69 | if (p.identifier.indexOf(',') !== -1) {
70 | p.identifier = p.identifier.split(',')[1];
71 | }
72 | p.identifier = p.identifier.trim();
73 |
74 | const identifierEndsInNumber = p.identifier.match(/(\d+)$/);
75 | if (identifierEndsInNumber) {
76 | p.identifier = identifierEndsInNumber[1];
77 | }
78 | }
79 |
80 | return {
81 | filterDate: {
82 | startDate: isValidMomentObj(p.startDate),
83 | endDate: isValidMomentObj(p.endDate),
84 | },
85 | filterArea: {
86 | ...defaultState,
87 | geo: isValidGeo(p.geo),
88 | identifier: p.identifier || undefined,
89 | lngLats: p.lngLats || [],
90 | },
91 | // filterTypes default to true for all injury & fatality
92 | // except for noInjuryFatality which defaults to false
93 | filterType: {
94 | injury: {
95 | cyclist: isBool(p.cinj) ? p.cinj : true,
96 | motorist: isBool(p.minj) ? p.minj : true,
97 | pedestrian: isBool(p.pinj) ? p.pinj : true,
98 | },
99 | fatality: {
100 | cyclist: isBool(p.cfat) ? p.cfat : true,
101 | motorist: isBool(p.mfat) ? p.mfat : true,
102 | pedestrian: isBool(p.pfat) ? p.pfat : true,
103 | },
104 | noInjuryFatality: isBool(p.noInjFat) ? p.noInjFat : false,
105 | },
106 | filterVehicle: {
107 | vehicle: {
108 | car: isBool(p.vcar) ? p.vcar : true,
109 | truck: isBool(p.vtruck) ? p.vtruck : true,
110 | motorcycle: isBool(p.vmotorcycle) ? p.vmotorcycle : true,
111 | bicycle: isBool(p.vbicycle) ? p.vbicycle : true,
112 | suv: isBool(p.vsuv) ? p.vsuv : true,
113 | busvan: isBool(p.vbusvan) ? p.vbusvan : true,
114 | scooter: isBool(p.vscooter) ? p.vscooter : true,
115 | other: isBool(p.vother) ? p.vother : true,
116 | },
117 | },
118 | filterContributingFactor: p.contrFactor || 'ALL',
119 | modal: {
120 | showModal: false,
121 | modalType: '',
122 | }
123 | };
124 | };
125 |
126 | // configures Carto crashes map layer's SQL
127 | export const configureLayerSource = (sql) => {
128 | cartoLayerSource.sublayers[0].sql = sql;
129 | return cartoLayerSource;
130 | };
131 |
132 | // Should the component fetch new crash data?
133 | // also used in App.js to determine whether to update URL params
134 | // @param {object} curProps; the component's this.props
135 | // @param {object} nextProps; the component's nextProps in componentWillReceiveProps
136 | export const crashDataChanged = (curProps, nextProps) => {
137 | const { endDate, startDate, filterType, identifier, geo, lngLats } = nextProps;
138 | const { injury, fatality, noInjuryFatality } = filterType;
139 | const { filterVehicle } = nextProps;
140 |
141 | if (!startDate.isSame(curProps.startDate) ||
142 | !endDate.isSame(curProps.endDate) ||
143 | !isEqual(injury, curProps.filterType.injury) ||
144 | !isEqual(fatality, curProps.filterType.fatality) ||
145 | noInjuryFatality !== curProps.filterType.noInjuryFatality ||
146 | !isEqual(filterVehicle.vehicle, curProps.filterVehicle.vehicle) ||
147 | (geo === 'citywide' && curProps.geo !== 'citywide') ||
148 | (identifier && identifier !== curProps.identifier) ||
149 | (lngLats && lngLats.length && !isEqual(lngLats, curProps.lngLats))) {
150 | return true;
151 | }
152 | return false;
153 | };
154 |
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import momentPropTypes from 'react-moment-proptypes';
3 | import queryString from 'query-string';
4 |
5 | import { dateStringFormatModel, crashDataChanged } from '../constants/api';
6 | import AppHeader from './AppHeader';
7 | import LeafletMapConnected from '../containers/LeafletMapConnected';
8 | import StatsLegendConnected from '../containers/StatsLegendConnected';
9 | import OptionsFiltersConnected from '../containers/OptionsFiltersConnected';
10 | import ModalConnected from '../containers/ModalConnected';
11 | import SmallDeviceMessage from './SmallDeviceMessage';
12 | import LoadingIndicator from './LoadingIndicator';
13 |
14 | class App extends Component {
15 | constructor() {
16 | super();
17 | this.state = {
18 | isLoading: false
19 | };
20 | this.dataLoading = this.dataLoading.bind(this);
21 | this.onMapMoved = this.onMapMoved.bind(this);
22 | }
23 |
24 | componentWillMount() {
25 | // async calls for UI dependent on data
26 | this.props.fetchCrashesYearRange()
27 | .then(this.props.fetchCrashesDateRange())
28 | .then(this.props.fetchCrashesMaxDate())
29 | .then(this.props.fetchCrashStatsData(this.props))
30 | .then(this.props.fetchContributingFactors(this.props))
31 | .catch((error) => { throw error; });
32 | }
33 |
34 | componentWillReceiveProps(nextProps) {
35 | if (crashDataChanged(this.props, nextProps)) {
36 | const { query } = this.props.location;
37 | this.updateQueryParams({ ...query, ...nextProps });
38 |
39 | this.props.fetchCrashStatsData(nextProps)
40 | .then(this.props.fetchContributingFactors(nextProps))
41 | .catch((error) => { throw error; });
42 | }
43 | }
44 |
45 | onMapMoved(event) {
46 | // update the url query params with map center & zoom
47 | if (event && event.target) {
48 | const query = {};
49 | query.zoom = event.target.getZoom();
50 | query.lat = event.target.getCenter().lat;
51 | query.lng = event.target.getCenter().lng;
52 | this.updateQueryParams({ ...query, ...this.props });
53 | }
54 | }
55 |
56 | updateQueryParams(params) {
57 | // updates the URL location query with app state filters & map zoom & center
58 | // this allows for "stateful URLs" so that when app loads, it will load
59 | // with the same filters & map zoom & center last viewed, enabling sharing
60 | // of unique views of the data via the URL between users
61 | const { lat, lng, zoom, startDate, endDate, identifier, geo, lngLats } = params;
62 | const { filterType, filterVehicle } = params;
63 | const { injury, fatality, noInjuryFatality } = filterType;
64 | const { vehicle } = filterVehicle;
65 |
66 | const newQueryParams = {
67 | lat,
68 | lng,
69 | zoom,
70 | startDate: startDate.format(dateStringFormatModel),
71 | endDate: endDate.format(dateStringFormatModel),
72 | identifier,
73 | geo,
74 | lngLats: encodeURIComponent(JSON.stringify(lngLats)),
75 | noInjFat: noInjuryFatality,
76 | cinj: injury.cyclist,
77 | minj: injury.motorist,
78 | pinj: injury.pedestrian,
79 | cfat: fatality.cyclist,
80 | mfat: fatality.motorist,
81 | pfat: fatality.pedestrian,
82 | vcar: vehicle.car,
83 | vtruck: vehicle.truck,
84 | vmotorcycle: vehicle.motorcycle,
85 | vbicycle: vehicle.bicycle,
86 | vsuv: vehicle.suv,
87 | vbusvan: vehicle.busvan,
88 | vscooter: vehicle.scooter,
89 | vother: vehicle.other,
90 | };
91 |
92 | this.context.router.push(`?${queryString.stringify(newQueryParams)}`);
93 | }
94 |
95 | dataLoading(bool) {
96 | this.setState({ isLoading: bool });
97 | }
98 |
99 | render() {
100 | const { isLoading } = this.state;
101 | const { location, openModal, height, width } = this.props;
102 |
103 | return (
104 |
105 | { /* hide app to mobile users for now */
106 | (width < 768 || height < 416) ?
107 |
:
108 | [
109 |
,
110 |
,
117 |
,
118 |
,
119 |
,
120 |
121 | ]
122 | }
123 |
124 | );
125 | }
126 | }
127 |
128 | App.defaultProps = {
129 | identifier: '',
130 | geo: '',
131 | lngLats: [],
132 | };
133 |
134 | App.propTypes = {
135 | fetchContributingFactors: PropTypes.func.isRequired,
136 | fetchCrashesDateRange: PropTypes.func.isRequired,
137 | fetchCrashStatsData: PropTypes.func.isRequired,
138 | fetchCrashesMaxDate: PropTypes.func.isRequired,
139 | fetchCrashesYearRange: PropTypes.func.isRequired,
140 | openModal: PropTypes.func.isRequired,
141 | location: PropTypes.shape({
142 | query: PropTypes.object.isRequired
143 | }).isRequired,
144 | startDate: momentPropTypes.momentObj.isRequired,
145 | endDate: momentPropTypes.momentObj.isRequired,
146 | filterType: PropTypes.shape({
147 | noInjuryFatality: PropTypes.bool,
148 | injury: PropTypes.shape({
149 | cyclist: PropTypes.bool,
150 | motorist: PropTypes.bool,
151 | pedestrian: PropTypes.bool,
152 | }),
153 | fatality: PropTypes.shape({
154 | cyclist: PropTypes.bool,
155 | motorist: PropTypes.bool,
156 | pedestrian: PropTypes.bool,
157 | }),
158 | }).isRequired,
159 | filterVehicle: PropTypes.shape({
160 | vehicle: PropTypes.shape({
161 | car: PropTypes.bool.isRequired,
162 | truck: PropTypes.bool.isRequired,
163 | motorcycle: PropTypes.bool.isRequired,
164 | bicycle: PropTypes.bool.isRequired,
165 | suv: PropTypes.bool.isRequired,
166 | busvan: PropTypes.bool.isRequired,
167 | scooter: PropTypes.bool.isRequired,
168 | other: PropTypes.bool.isRequired,
169 | }).isRequired,
170 | }).isRequired,
171 | identifier: PropTypes.oneOfType([
172 | PropTypes.number,
173 | PropTypes.string,
174 | ]),
175 | geo: PropTypes.string,
176 | lngLats: PropTypes.arrayOf(
177 | PropTypes.arrayOf(PropTypes.number)
178 | ),
179 | height: PropTypes.number.isRequired,
180 | width: PropTypes.number.isRequired,
181 | };
182 |
183 | App.contextTypes = {
184 | router: React.PropTypes.object.isRequired
185 | };
186 |
187 | export default App;
188 |
--------------------------------------------------------------------------------
/src/components/AboutCopy.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const AboutCopy = () => (
4 |
5 |
TAKE ACTION
6 |
7 |
8 |
9 | Start a{' '}
10 |
11 | PETITION
12 | {' '}
13 | to raise the profile of your traffic problems
14 |
15 |
16 |
17 |
18 | Start a{' '}
19 |
20 | CAMPAIGN
21 | {' '}
22 | to obtain the installation of traffic safety features in your area
23 |
24 |
25 |
26 |
27 | Get traffic safety{' '}
28 |
29 | EDUCATION
30 | {' '}
31 | for your school
32 |
33 |
34 |
35 |
36 |
37 | JOIN
38 | {' '}
39 | families of other victims to seek traffic justice
40 |
41 |
42 |
43 |
44 | Report a crash to the{' '}
45 |
46 | PRESS
47 | .
48 |
49 |
50 |
51 |
GET MORE INFORMATION
52 |
95 |
ABOUT CRASH MAPPER
96 |
97 | MAPPING WITH CUSTOMIZED FILTERING : The map allows a user to create a
98 | customized filter of any area and track it over time. This is particularly helpful to see the
99 | impact of Vision Zero improvements on a corridor, a slow zone, a school zone, etc. This is in
100 | addition to other more traditional filters like Borough, Community Boards, NYPD Precincts and
101 | more. Any active filters may easily be shared with others using the website's URL.
102 |
103 |
104 | ANALYZING: TREND allows you to compare two selected areas’ performance
105 | against each other and to a citywide or borough-wide reference, while filtering by date range
106 | or type of crash. COMPARE lets you compare two time periods for a given area. RANK shows
107 | where one area or intersection ranks in terms of safety in the context of all similar areas
108 | over the most recent two-year period.
109 |
110 |
111 | ENHANCED DATA : Collision data is updated from the NYC Open Data Portal daily
112 | or as soon as new data becomes available, with a sweep back one month to catch recently
113 | updated data. NYC Crash Mapper uses vehicle collision data for New York City from August 2011
114 | to the present. The data was aggregated and normalized from the NYC Open Data Portal{' '}
115 |
116 | NYPD Motor Vehicle Collisions
117 | {' '}
118 | and{' '}
119 |
120 | John Krauss's NYPD Crash Data Bandaid
121 | .
122 |
123 |
124 | As a result of some of the source data not being geocoded, the statistics (total crashes,
125 | fatalities, and injuries) displayed in the bottom of the map view may differ from what
126 | is shown on the map when Filter By Boundary is set to "Citywide." When a Filter By
127 | Boundary other than "Citywide" is used, such as a "Community Board", the stats will
128 | only display counts for crashes that have been geocoded and are located within a
129 | selected boundary.
130 |
131 |
132 | In the NYPD motor vehicles collisions dataset, some injuries and fatalities do not have a
133 | role/vehicle ascribed to them. “Other” includes crashes in the source data that are not
134 | labeled by category (pedestrian, cyclists or motorists). In our experience they include
135 | mostly crashes where the victims are e-bike users.
136 |
137 |
138 | NYPD captures up to five (5) vehicles included in a crash. As long as any one of these
139 | vehicles matches the filter selected, the related injuries and fatalities will be counted
140 | – even if the vehicle is potentially not directly responsible for the fatalities/injuries.
141 |
142 |
143 | NYC CRASH MAPPER was made possible by{' '}
144 |
145 | CARTO
146 | {' '}
147 | for hosting the data and making the creation of this tool possible through their Grants For
148 | Good program; by The Lily Auchincloss and PEPSI foundations, the Manhattan Borough President
149 | and the New York City Council. The{' '}
150 |
151 | LILY AUCHINCLOSS FOUNDATION
152 | {' '}
153 | generous grant helped to write the vehicular analysis modules.
154 |
155 |
156 | This application is the result of four iterations of a mapping initiative started by
157 | Transportation Alternatives in 2006 and continued by John Kraus.The current version of the
158 | application with charts was built in 2017 by Chris Henrick and {' '}
159 |
160 | GreenInfo Network
161 | .
162 |
163 |
164 |
165 | CHECKPEDS {' '}
166 | is a 501(c)(3) non-profit organization founded in 2005 that advocates for pedestrian
167 | safety, primarily focusing on the west side of Manhattan. Data has played a critical role in
168 | the coalition's success in highlighting the concentration of fatalities and injuries to the
169 | authorities. This resulted in the DOT installing traffic calming features in the area,
170 | including bike lanes, protected pedestrian crossings and traffic calming measures. It is
171 | CHEKPEDS’ hope that this easy-to-use tool will empower many other activists and individuals in
172 | New York City to gain an understanding of how dangerous their streets are and obtain the
173 | installation of safety features.
174 |
175 |
176 | You may{' '}
177 |
178 | email CHEKPEDS
179 | {' '}
180 | regarding any questions or concerns about NYC Crash Mapper.
181 |
182 |
183 | );
184 |
185 | export default AboutCopy;
186 |
--------------------------------------------------------------------------------
/scss/skeleton/base/_normalize.scss:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
2 |
3 | //
4 | // 1. Set default font family to sans-serif.
5 | // 2. Prevent iOS and IE text size adjust after device orientation change,
6 | // without disabling user zoom.
7 | //
8 |
9 | html {
10 | font-family: sans-serif; // 1
11 | -ms-text-size-adjust: 100%; // 2
12 | -webkit-text-size-adjust: 100%; // 2
13 | }
14 |
15 | //
16 | // Remove default margin.
17 | //
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | // HTML5 display definitions
24 | // ==========================================================================
25 |
26 | //
27 | // Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | // Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | // and Firefox.
30 | // Correct `block` display not defined for `main` in IE 11.
31 | //
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | //
50 | // 1. Correct `inline-block` display not defined in IE 8/9.
51 | // 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | //
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; // 1
59 | vertical-align: baseline; // 2
60 | }
61 |
62 | //
63 | // Prevent modern browsers from displaying `audio` without controls.
64 | // Remove excess height in iOS 5 devices.
65 | //
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | //
73 | // Address `[hidden]` styling not present in IE 8/9/10.
74 | // Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
75 | //
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | // Links
83 | // ==========================================================================
84 |
85 | //
86 | // Remove the gray background color from active links in IE 10.
87 | //
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | //
94 | // Improve readability of focused elements when they are also in an
95 | // active/hover state.
96 | //
97 |
98 | a {
99 | &:active {
100 | outline: 0;
101 | }
102 | &:hover {
103 | outline: 0;
104 | }
105 | }
106 |
107 | // Text-level semantics
108 | // ==========================================================================
109 |
110 | //
111 | // Address styling not present in IE 8/9/10/11, Safari, and Chrome.
112 | //
113 |
114 | abbr[title] {
115 | border-bottom: 1px dotted;
116 | }
117 |
118 | //
119 | // Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
120 | //
121 |
122 | b,
123 | strong {
124 | font-weight: bold;
125 | }
126 |
127 | //
128 | // Address styling not present in Safari and Chrome.
129 | //
130 |
131 | dfn {
132 | font-style: italic;
133 | }
134 |
135 | //
136 | // Address variable `h1` font-size and margin within `section` and `article`
137 | // contexts in Firefox 4+, Safari, and Chrome.
138 | //
139 |
140 | h1 {
141 | font-size: 2em;
142 | margin: 0.67em 0;
143 | }
144 |
145 | //
146 | // Address styling not present in IE 8/9.
147 | //
148 |
149 | mark {
150 | background: #ff0;
151 | color: #000;
152 | }
153 |
154 | //
155 | // Address inconsistent and variable font size in all browsers.
156 | //
157 |
158 | small {
159 | font-size: 80%;
160 | }
161 |
162 | //
163 | // Prevent `sub` and `sup` affecting `line-height` in all browsers.
164 | //
165 |
166 | sub,
167 | sup {
168 | font-size: 75%;
169 | line-height: 0;
170 | position: relative;
171 | vertical-align: baseline;
172 | }
173 |
174 | sup {
175 | top: -0.5em;
176 | }
177 |
178 | sub {
179 | bottom: -0.25em;
180 | }
181 |
182 | // Embedded content
183 | // ==========================================================================
184 |
185 | //
186 | // Remove border when inside `a` element in IE 8/9/10.
187 | //
188 |
189 | img {
190 | border: 0;
191 | }
192 |
193 | //
194 | // Correct overflow not hidden in IE 9/10/11.
195 | //
196 |
197 | svg:not(:root) {
198 | overflow: hidden;
199 | }
200 |
201 | // Grouping content
202 | // ==========================================================================
203 |
204 | //
205 | // Address margin not present in IE 8/9 and Safari.
206 | //
207 |
208 | figure {
209 | margin: 1em 40px;
210 | }
211 |
212 | //
213 | // Address differences between Firefox and other browsers.
214 | //
215 |
216 | hr {
217 | box-sizing: content-box;
218 | height: 0;
219 | }
220 |
221 | //
222 | // Contain overflow in all browsers.
223 | //
224 |
225 | pre {
226 | overflow: auto;
227 | }
228 |
229 | //
230 | // Address odd `em`-unit font size rendering in all browsers.
231 | //
232 |
233 | code,
234 | kbd,
235 | pre,
236 | samp {
237 | font-family: monospace, monospace;
238 | font-size: 1em;
239 | }
240 |
241 | // Forms
242 | // ==========================================================================
243 |
244 | //
245 | // Known limitation: by default, Chrome and Safari on OS X allow very limited
246 | // styling of `select`, unless a `border` property is set.
247 | //
248 |
249 | //
250 | // 1. Correct color not being inherited.
251 | // Known issue: affects color of disabled elements.
252 | // 2. Correct font properties not being inherited.
253 | // 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
254 | //
255 |
256 | button,
257 | input,
258 | optgroup,
259 | select,
260 | textarea {
261 | color: inherit; // 1
262 | font: inherit; // 2
263 | margin: 0; // 3
264 | }
265 |
266 | //
267 | // Address `overflow` set to `hidden` in IE 8/9/10/11.
268 | //
269 |
270 | button {
271 | overflow: visible;
272 | }
273 |
274 | //
275 | // Address inconsistent `text-transform` inheritance for `button` and `select`.
276 | // All other form control elements do not inherit `text-transform` values.
277 | // Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
278 | // Correct `select` style inheritance in Firefox.
279 | //
280 |
281 | button,
282 | select {
283 | text-transform: none;
284 | }
285 |
286 | //
287 | // 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
288 | // and `video` controls.
289 | // 2. Correct inability to style clickable `input` types in iOS.
290 | // 3. Improve usability and consistency of cursor style between image-type
291 | // `input` and others.
292 | //
293 |
294 | button,
295 | html input[type="button"], // 1
296 | input[type="reset"],
297 | input[type="submit"] {
298 | -webkit-appearance: button; // 2
299 | cursor: pointer; // 3
300 | }
301 |
302 | //
303 | // Re-set default cursor for disabled elements.
304 | //
305 |
306 | button[disabled],
307 | html input[disabled] {
308 | cursor: default;
309 | }
310 |
311 | //
312 | // Remove inner padding and border in Firefox 4+.
313 | //
314 |
315 | button::-moz-focus-inner,
316 | input::-moz-focus-inner {
317 | border: 0;
318 | padding: 0;
319 | }
320 |
321 | //
322 | // Address Firefox 4+ setting `line-height` on `input` using `!important` in
323 | // the UA stylesheet.
324 | //
325 |
326 | input {
327 | line-height: normal;
328 | }
329 |
330 | //
331 | // It's recommended that you don't attempt to style these elements.
332 | // Firefox's implementation doesn't respect box-sizing, padding, or width.
333 | //
334 | // 1. Address box sizing set to `content-box` in IE 8/9/10.
335 | // 2. Remove excess padding in IE 8/9/10.
336 | //
337 |
338 | input[type="checkbox"],
339 | input[type="radio"] {
340 | box-sizing: border-box; // 1
341 | padding: 0; // 2
342 | }
343 |
344 | //
345 | // Fix the cursor style for Chrome's increment/decrement buttons. For certain
346 | // `font-size` values of the `input`, it causes the cursor style of the
347 | // decrement button to change from `default` to `text`.
348 | //
349 |
350 | input[type="number"]::-webkit-inner-spin-button,
351 | input[type="number"]::-webkit-outer-spin-button {
352 | height: auto;
353 | }
354 |
355 | //
356 | // 1. Address `appearance` set to `searchfield` in Safari and Chrome.
357 | // 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
358 | //
359 |
360 | input[type="search"] {
361 | -webkit-appearance: textfield; // 1
362 | box-sizing: content-box; //2
363 | }
364 |
365 | //
366 | // Remove inner padding and search cancel button in Safari and Chrome on OS X.
367 | // Safari (but not Chrome) clips the cancel button when the search input has
368 | // padding (and `textfield` appearance).
369 | //
370 |
371 | input[type="search"]::-webkit-search-cancel-button,
372 | input[type="search"]::-webkit-search-decoration {
373 | -webkit-appearance: none;
374 | }
375 |
376 | //
377 | // Define consistent border, margin, and padding.
378 | //
379 |
380 | fieldset {
381 | border: 1px solid #c0c0c0;
382 | margin: 0 2px;
383 | padding: 0.35em 0.625em 0.75em;
384 | }
385 |
386 | //
387 | // 1. Correct `color` not being inherited in IE 8/9/10/11.
388 | // 2. Remove padding so people aren't caught out if they zero out fieldsets.
389 | //
390 |
391 | legend {
392 | border: 0; // 1
393 | padding: 0; // 2
394 | }
395 |
396 | //
397 | // Remove default vertical scrollbar in IE 8/9/10/11.
398 | //
399 |
400 | textarea {
401 | overflow: auto;
402 | }
403 |
404 | //
405 | // Don't inherit the `font-weight` (applied by a rule above).
406 | // NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
407 | //
408 |
409 | optgroup {
410 | font-weight: bold;
411 | }
412 |
413 | // Tables
414 | // ==========================================================================
415 |
416 | //
417 | // Remove most spacing between table cells.
418 | //
419 |
420 | table {
421 | border-collapse: collapse;
422 | border-spacing: 0;
423 | }
424 |
425 | td,
426 | th {
427 | padding: 0;
428 | }
429 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NYC CRASH MAPPER
2 | 
3 |
4 | Web application that geographically maps, filters, and aggregates NYC automobile collision data. Built with CARTO, ES6, React, Redux, Leaflet, and Webpack2.
5 |
6 | This work was funded by [CHEKPEDS](http://chekpeds.com), a 501(c)(3) non-profit organization that was founded in 2005 and advocates for pedestrian safety, primarily focusing on the west side of Manhattan.
7 |
8 | **NYC Crash Mapper** was designed and built by [Chris Henrick](http://clhenrick.io).
9 |
10 | ### Install
11 | Installation of this app requires knowledge of the command line interface and a shell application such as Terminal on Mac OS.
12 |
13 | App was developed using `NodeJS @ ^6.7`, `NPM @ ^3.10`, and `Yarn @ ^0.21.3`.
14 |
15 | It is recommended to use Node Version Manager ([`nvm`](https://github.com/creationix/nvm)) and [Yarn](https://yarnpkg.com) to ensure compatibility with NodeJS and dependencies in `project.json`.
16 |
17 | Assuming `nvm` is available globally, do:
18 |
19 | ```
20 | nvm use
21 | ```
22 |
23 | If you get an error such as
24 |
25 | ```
26 | N/A: version v6.7.0 is not yet installed
27 | ```
28 |
29 | Then do:
30 |
31 | ```
32 | nvm install 6.7
33 | ```
34 |
35 | To install the app's dependencies do:
36 |
37 | ```
38 | yarn -i
39 | ```
40 |
41 | ### Develop
42 | To start the webpack dev server and run the app locally do:
43 |
44 | ```
45 | npm run serve
46 | ```
47 |
48 | And open your browser to `localhost:8080`. Any changes made to the app's code base will cause Webpack to recompile the source code and refresh the browser. ES Lint will report any javascript errors as well as complain about broken style rules.
49 |
50 | **NOTE:** Running this app locally assumes that the companion app, [nyc-crash-mapper-chart-view](https://github.com/GreenInfo-Network/nyc-crash-mapper-chart-view) is also running locally on a separate port to allow debugging between both apps. Thus, the navigation list items for `trend`, `compare`, and `rank` will link to `localhost:8889`. When the app is deployed, these navigation list items will link to `vis.crashmapper.org`.
51 |
52 | ### Build
53 | To compile the source code do:
54 |
55 | ```
56 | npm run build
57 | ```
58 |
59 | The compiled code will be available in the `dist/` directory.
60 |
61 | ### Deploy
62 | Currently the app is being hosted using Github Pages.
63 |
64 | #### To Github Pages
65 | Be sure this folder is under version control using `git` and is pointing to a remote repository on Github.
66 |
67 | Enable permissions to read/execute the `deploy_gh_pages.sh` bash script:
68 |
69 | ```
70 | chmod u+rx deploy_gh_pages.sh
71 | ```
72 |
73 | Then do:
74 |
75 | ```
76 | npm run deploy:gh-pages
77 | ```
78 |
79 | That will execute the `build` script then the bash script, creating a Github Pages site with the content of the `dist` directory.
80 |
81 | **NOTE:** Doing this will remove the custom domain name (`crashmapper.org`) from the repo's settings. _**You will need to manually add it back after redeploying.**_
82 |
83 | ### About the Data
84 | NYC Crash Mapper uses vehicle collision data for New York City from August, 2011 - present. The data was aggregated and normalized from the following sources:
85 |
86 | - NYC Open Data Portal (Socrata) [NYPD Motor Vehicle Collisions](https://data.cityofnewyork.us/Public-Safety/NYPD-Motor-Vehicle-Collisions/h9gi-nx95)
87 |
88 | - Civic hacker [John Krauss](https://github.com/talos)'s [NYPD Crash Data Bandaid](https://github.com/talos/nypd-crash-data-bandaid) (PDF scraped data)
89 |
90 | The complete dataset may be downloaded via the [Chekpeds CARTO account](https://chekpeds.carto.com/crashes_all_prod).
91 |
92 | At the time of launch, the combined data contained 1,075,468 rows, of which 157,554 rows were not geocoded due to the source data lacking values for latitude and longitude or adequate address attributes. The majority of non-geocoded rows come from the NYC Open Data.
93 |
94 | Prior to importing the data into CARTO, rows lacking lat lon with potentially valid address information were attempted to be geocoded using the [NYC GeoClient API](https://developer.cityofnewyork.us/api/geoclient-api). For example, if a row lacked values for lat lon, but contained values for cross streets and borough or zip code, the NYC GeoClient [intersection endpoint](https://api.cityofnewyork.us/geoclient/v1/doc#section-1.2.5) was used to geocode the crash location.
95 |
96 | Stats displayed in the bottom UI for number of total crashes, injuries, and fatalities will differ from what is shown on the map when the app's "Filter By Boundary" is set to "Citywide." Stats display counts for all collision data, whether geocoded or not, while the map only displays data that has valid lat lon coordinates and thus can be geographically mapped. When a "Filter By Boundary" setting other than "Citywide" is used, such as a "Community Board", the stats UI will only display counts for data that has been geocoded and falls within a selected boundary.
97 |
98 | _**We include non-geocoded data in the app's "citywide" query to advocate for the NYPD getting its act together to improve the quality of NYC's vehicle collision data. This means: geocoding all crash locations (not just 75% of them), providing values for contributing factors, and providing values for vehicle type for all crashes.**_
99 |
100 | Because the portion of the data which comes from the **NYPD Crash Data Bandaid** is summarized by month, each row for the dataset contains a value for `crash_count`. The only rows that have a value higher than 1 for this field are for rows that correspond to the NYPD Crash Data Bandaid, which are from July 2011 to June 2012. Unfortunately this limits the app to filtering data by month and year, rather than day and time.
101 |
102 | #### Data Updates
103 |
104 | Collision data is updated using an [ETL Script](https://github.com/clhenrick/nyc-crash-mapper-etl-script) that runs daily, consuming data from the NYC Open Data Portal, formatting it for the NYC Crash Mapper crash data table, and inserting it using the [CARTO SQL API](https://carto.com/docs/carto-engine/sql-api) if the data does not currently exist in CARTO.
105 |
106 | #### Notes on importing data into CARTO
107 | Because the app's database folds values from multiple columns for the **contributing factor** and **vehicle type** categories into single Postgres text array columns for each category, exporting the data from a local PostgreSQL db and importing it into CARTO requires special attention. CARTO _will not_ recognize array columns when importing data so you must do some data processing to get them to be recognized as such. See [2016_data_update.sql](./sql/2016_data_update.sql) for how this was accomplished in March 2017.
108 |
109 | ### App Structure
110 | This app is written in ES6, and compiles to ES5 JavaScript via [Babel](https://babeljs.io) using [Webpack 2](https://webpack.js.org/). The application's point of entry is [`./src/index.jsx`](./src/main.jsx).
111 |
112 | #### Redux
113 | Redux uses the concept of keeping all application state within an immutable [store](http://redux.js.org/docs/basics/Store.html) and returning new application state via [action creators](http://redux.js.org/docs/basics/Actions.html). [Reducers](http://redux.js.org/docs/basics/Reducers.html) trigger changes in the Store after receiving Actions from Action Creators. When the app loads its store is hydrated from query params in the URL hash if they are present. This easily allows for the sharing of application state between users via the app's URL.
114 |
115 | - [`action creators`](./src/actions/index.js)
116 | - [`action types`](./src/constants/action_types.js)
117 | - [`reducers`](./src/reducers/index.js)
118 | - [`store configuration`](./src/store.js)
119 | - [`default store`](./src/constants/api.js)
120 |
121 | #### React
122 | All UI components are built using [React](https://facebook.github.io/react/), which allow for transforming application data into UI views.
123 |
124 | This project follows the React Redux concept of using "Containers" which may be connected to the Redux store and/or action creators via [React-Redux](https://github.com/reactjs/react-redux). Regular components receive data from Containers or parent components as props, and only use Component level state for trivial UI changes, eg: tracking whether or not a UI panel is collapsed or opened.
125 |
126 | - [`components`](./src/components/)
127 | - [`containers`](./src/containers/)
128 |
129 | The main scaffolding of the app resides within `src/components/App.jsx`.
130 | The app is connected to Redux via `Provider` and rendered to the DOM within `./src/index.jsx`.
131 |
132 | ##### React Component Tree
133 | The following describes how React Components are nested within the app:
134 |
135 | - AppConnected\*
136 |
137 | - App
138 |
139 | - AppHeader
140 |
141 | - MenuConnected\*
142 |
143 | - Menu
144 |
145 | - LeafletMapConnected\*
146 |
147 | - LeafletMap/index
148 |
149 | - customFilter (_es6 class, but not a react component_)
150 |
151 | - ZoomControls
152 |
153 | - StatsLegendConnected\*
154 |
155 | - StatsLegend/index
156 |
157 | - ContributingFactorsList
158 |
159 | - LegendContainer
160 |
161 | - StatsCounter
162 |
163 | - TotalCrashesCounter
164 |
165 | - OptionsFiltersConnected\*
166 |
167 | - OptionsFilters/index
168 |
169 | - OptionsContainer
170 |
171 | - FilterByAreaConnected\*
172 |
173 | - FilterByArea
174 |
175 | - FilterByTypeConnected\*
176 |
177 | - FilterByType
178 |
179 | - FilterByDateConnected\*
180 |
181 | - FilterByDate
182 |
183 | - DownloadData
184 |
185 | - ShareOptions
186 |
187 | - FooterOptions
188 |
189 | - ModalConnected\*
190 |
191 | - Modal/index
192 |
193 | - About
194 |
195 | - AboutCopy
196 |
197 | - ModalContent
198 |
199 | - Copyright
200 |
201 | - Disclaimer
202 |
203 | - DownloadData
204 |
205 | - ShareURL
206 |
207 | - ShareFB
208 |
209 | - ShareTwitter
210 |
211 | - SmallDeviceMessage
212 |
213 | - LoadingIndicator
214 |
215 | \* means Component is a **Container Component** connected to the Redux Store.
216 |
217 | #### Configuration
218 | This app uses [CARTO](https://carto.com) as a backend datastore and web map tile generator. The CARTO user name, data tables, and other app configuration parameters are specified in `./src/constants/app_config.js`.
219 |
220 | Note that any tables used by the app must be set to either `"Public"` or `"With Link"` via the CARTO dashboard.
221 |
222 | #### SQL
223 | PostgreSQL and PostGIS power the app via CARTO.
224 |
225 | The app uses ES6 Template Literals to generate SQL queries based on values from the Redux Store. These are available in [./src/constants/sql_queries.js](./src/constants/sql_queries.js).
226 |
227 | Sample SQL queries are stored in `./sql` are for reference and are not used by the app during runtime.
228 |
229 | #### Sass
230 | This app uses [SASS]() which compiles down to regular CSS. The Sass is organized into partials that map to specific React components in `./scss/components/`.
231 |
232 | #### Skeleton
233 | The app uses a slightly modified [Sass version](https://github.com/WhatsNewSaes/Skeleton-Sass) of the [Skeleton](http://getskeleton.com/) CSS framework.
234 |
--------------------------------------------------------------------------------