diff --git a/.eslintrc.js b/.eslintrc.js
index 2f93580a2..e053723a1 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -21,6 +21,15 @@ module.exports = {
ecmaVersion: 2018,
sourceType: 'module',
},
+ settings: {
+ "import/extensions": [
+ ".js",
+ ".jsx"
+ ],
+ 'import/resolver': {
+ webpack: "webpack.config.js",
+ },
+ },
plugins: [
'react',
'react-hooks'
@@ -28,6 +37,7 @@ module.exports = {
rules: {
'linebreak-style': 'off',
"react-hooks/rules-of-hooks": "error",
- "react-hooks/exhaustive-deps": "warn"
+ "react-hooks/exhaustive-deps": "warn",
+ "arrow-parens": ["error", "as-needed"]
},
};
diff --git a/.example.env b/.example.env
index 05872ca35..39d38b052 100644
--- a/.example.env
+++ b/.example.env
@@ -1 +1,2 @@
REACT_APP_MAPBOX_TOKEN=REDACTED
+DB_URL=REDACTED
\ No newline at end of file
diff --git a/package.json b/package.json
index f03f96322..8232ca517 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"dataframe-js": "^1.4.3",
"dotenv": "^8.2.0",
"dotenv-webpack": "^1.7.0",
+ "eslint-import-resolver-webpack": "^0.12.1",
"gh-pages": "^2.1.1",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0",
@@ -33,13 +34,15 @@
"redux": "^4.0.4",
"redux-devtools-extension": "^2.13.8",
"redux-logger": "^3.0.6",
+ "redux-saga": "^1.1.3",
+ "regenerator-runtime": "^0.13.3",
"webpack-merge": "^4.2.2"
},
"scripts": {
"start": "npm run check-env && npm run dev",
"dev": "webpack-dev-server --config webpack.dev.js --host 0.0.0.0",
"build": "webpack --config webpack.prod.js",
- "lint": "eslint src/**/*.js*",
+ "lint": "eslint 'src/**/*.js*'",
"test": "jest --passWithNoTests",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist",
diff --git a/public/index.html b/public/index.html
index 3997002d4..39a8ee63f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,5 +1,5 @@
-
+
diff --git a/server.js b/server.js
index c12bbd50c..b56eb49ad 100644
--- a/server.js
+++ b/server.js
@@ -1,15 +1,16 @@
const express = require('express');
const path = require('path');
+
const port = process.env.PORT || 3000;
const app = express();
// the __dirname is the current directory from where the script is running
-app.use(express.static(__dirname + '/dist'));
+app.use(express.static(path.join(__dirname, '/dist')));
// send the user to index html page inspite of the url
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'public/index.html'));
});
-console.log(`Listening on port ${port}`)
+console.log(`Listening on port ${port}`);
app.listen(port);
diff --git a/src/App.jsx b/src/App.jsx
index 06b378b8c..a56d1914d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -12,12 +12,12 @@ const App = () => {
}, []);
return (
-
+ <>
-
+ >
);
};
diff --git a/src/Util/DataService.js b/src/Util/DataService.js
index 5179c250e..bf95a1d84 100644
--- a/src/Util/DataService.js
+++ b/src/Util/DataService.js
@@ -51,12 +51,12 @@ export function getBroadCallVolume(year, startMonth = 0, endMonth = 13, onBroadD
const end = Math.max(startMonth, endMonth);
DataFrame.fromJSON(`https://data.lacity.org/resource/${dataResources[year]}.json?$select=count(*)+AS+CallVolume,NCName,RequestType&$where=date_extract_m(CreatedDate)+between+${start}+and+${end}&$group=NCName,RequestType&$order=CallVolume DESC`)
- .then((df) => {
+ .then(df => {
df.show();
- const totalCounts = df.groupBy('ncname').aggregate((group) => group.stat.sum('callvolume')).rename('aggregation', 'callvolume');
+ const totalCounts = df.groupBy('ncname').aggregate(group => group.stat.sum('callvolume')).rename('aggregation', 'callvolume');
const biggestProblems = {};
- df.toCollection().forEach((row) => {
+ df.toCollection().forEach(row => {
const rhs = parseInt(row.callvolume, 10);
const lhs = parseInt(biggestProblems[row.ncname], 10);
if (!lhs) {
@@ -68,7 +68,7 @@ export function getBroadCallVolume(year, startMonth = 0, endMonth = 13, onBroadD
}
});
const colorMap = getColorMap(false);
- totalCounts.toCollection().forEach((row) => {
+ totalCounts.toCollection().forEach(row => {
const biggestProblem = biggestProblems[`${row.ncname}_biggestproblem`];
const dataPoint = {
title: row.ncname,
@@ -92,9 +92,9 @@ export function getZoomedCallVolume(
const start = Math.min(startMonth, endMonth);
const end = Math.max(startMonth, endMonth);
- DataFrame.fromJSON(`https://data.lacity.org/resource/${dataResources[year]}.json?$select=count(*)+AS+CallVolume,NCName,RequestType&$where=NCName+=+'${ncName}'+and+date_extract_m(CreatedDate)+between+${start}+and+${end}&$group=NCName,RequestType&$order=CallVolume DESC`).then((df) => {
+ DataFrame.fromJSON(`https://data.lacity.org/resource/${dataResources[year]}.json?$select=count(*)+AS+CallVolume,NCName,RequestType&$where=NCName+=+'${ncName}'+and+date_extract_m(CreatedDate)+between+${start}+and+${end}&$group=NCName,RequestType&$order=CallVolume DESC`).then(df => {
const colorMap = getColorMap(false);
- df.toCollection().forEach((row) => {
+ df.toCollection().forEach(row => {
const dataPoint = {
title: row.requesttype,
color: colorMap[row.requesttype],
diff --git a/src/components/PinMap/PinMap.jsx b/src/components/PinMap/PinMap.jsx
index 7f1c1de8f..3b1f40607 100644
--- a/src/components/PinMap/PinMap.jsx
+++ b/src/components/PinMap/PinMap.jsx
@@ -1,6 +1,12 @@
import React, { Component } from 'react';
+import { connect } from 'react-redux';
import {
- Map, Marker, Popup, TileLayer, Rectangle, Tooltip,
+ Map,
+ Marker,
+ Popup,
+ TileLayer,
+ Rectangle,
+ Tooltip,
} from 'react-leaflet';
import Choropleth from 'react-leaflet-choropleth';
import PropTypes from 'proptypes';
@@ -10,27 +16,20 @@ import PropTypes from 'proptypes';
// import councilDistrictsOverlay from '../../data/la-city-council-districts-2012.json';
import ncOverlay from '../../data/nc-boundary-2019.json';
-const pinMapProps = {
- data: PropTypes.string,
- showMarkers: PropTypes.boolean,
-};
-
-
class PinMap extends Component {
constructor(props) {
super(props);
this.state = {
- position: [34.0173157, -118.2497254],
+ position: [34.0094213, -118.6008506],
zoom: 10,
mapUrl: `https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}.png?access_token=${process.env.REACT_APP_MAPBOX_TOKEN}`,
- // dataUrl: 'https://data.lacity.org/resource/h65r-yf5i.json?$select=location,zipcode,address,requesttype,status,ncname,streetname,housenumber&$where=date_extract_m(CreatedDate)+between+2+and+3',
geoJSON: ncOverlay,
bounds: null,
};
}
- highlightRegion = (e) => {
+ highlightRegion = e => {
const layer = e.target;
layer.setStyle({
@@ -43,7 +42,7 @@ class PinMap extends Component {
layer.bringToFront();
}
- resetRegionHighlight = (e) => {
+ resetRegionHighlight = e => {
const layer = e.target;
layer.setStyle({
@@ -56,7 +55,7 @@ class PinMap extends Component {
});
}
- zoomToRegion = (e) => {
+ zoomToRegion = e => {
const bounds = e.target.getBounds();
this.setState({ bounds });
}
@@ -96,7 +95,7 @@ class PinMap extends Component {
fillOpacity: 0.7,
}}
onEachFeature={this.onEachFeature}
- ref={(el) => {
+ ref={el => {
if (el) {
this.choropleth = el.leafletElement;
return this.choropleth;
@@ -115,13 +114,12 @@ class PinMap extends Component {
const { data, showMarkers } = this.props;
if (showMarkers && data) {
- return data.map((d) => {
- if (d.location) {
- const { location } = d;
- const position = [location.latitude, location.longitude];
+ return data.map(d => {
+ if (d.latitude && d.longitude) {
+ const position = [d.latitude, d.longitude];
return (
-
+
Type:
{d.requesttype}
@@ -141,7 +139,12 @@ class PinMap extends Component {
return (
-
+
No Data To Display
@@ -162,7 +165,7 @@ class PinMap extends Component {
center={position}
zoom={zoom}
bounds={bounds}
- style={{ height: '85vh' }}
+ style={{ height: '88.4vh' }}
>
({
+ data: state.data.data,
+});
+
+PinMap.propTypes = {
+ data: PropTypes.arrayOf(PropTypes.shape({})),
+ showMarkers: PropTypes.bool,
+};
+
PinMap.defaultProps = {
- data: '',
+ data: undefined,
showMarkers: true,
};
-PinMap.propTypes = pinMapProps;
-
-export default PinMap;
+export default connect(mapStateToProps, null)(PinMap);
diff --git a/src/components/Visualizations/Chart.js b/src/components/Visualizations/Chart.jsx
similarity index 79%
rename from src/components/Visualizations/Chart.js
rename to src/components/Visualizations/Chart.jsx
index efb528d39..0773025de 100644
--- a/src/components/Visualizations/Chart.js
+++ b/src/components/Visualizations/Chart.jsx
@@ -4,7 +4,7 @@ import Chart from 'chart.js';
import 'chartjs-chart-box-and-violin-plot';
import COLORS from '@styles/COLORS';
-/////////// CHARTJS DEFAULTS ///////////
+// ///////// CHARTJS DEFAULTS ///////////
Object.assign(Chart.defaults.global, {
defaultFontColor: COLORS.FONTS,
@@ -38,10 +38,9 @@ Object.assign(Chart.defaults.global.tooltips, {
cornerRadius: 4,
});
-////////////// COMPONENT //////////////
+// //////////// COMPONENT //////////////
class ReactChart extends React.Component {
-
canvasRef = React.createRef();
componentDidMount() {
@@ -56,18 +55,22 @@ class ReactChart extends React.Component {
}
componentDidUpdate(prevProps) {
- if (prevProps.data !== this.props.data) {
- this.chart.data = this.props.data;
+ const { data } = this.props;
+
+ if (prevProps.data !== data) {
+ this.chart.data = data;
this.chart.update();
this.setHeight();
}
}
setHeight = () => {
- if (this.props.height) {
+ const { height } = this.props;
+
+ if (height) {
const numLabels = this.chart.data.labels.length;
- const height = this.props.height(numLabels);
- this.canvasRef.current.parentNode.style.height = height + 'px';
+ const heightPx = height(numLabels);
+ this.canvasRef.current.parentNode.style.height = `${heightPx}px`;
}
}
@@ -84,8 +87,8 @@ export default ReactChart;
ReactChart.propTypes = {
type: PropTypes.string.isRequired,
- data: PropTypes.object.isRequired,
- options: PropTypes.object.isRequired,
+ data: PropTypes.shape.isRequired,
+ options: PropTypes.shape.isRequired,
height: PropTypes.func,
};
diff --git a/src/components/Visualizations/Criteria.jsx b/src/components/Visualizations/Criteria.jsx
index f940dfb8f..acd7e442d 100644
--- a/src/components/Visualizations/Criteria.jsx
+++ b/src/components/Visualizations/Criteria.jsx
@@ -47,7 +47,7 @@ export default connect(mapStateToProps)(Criteria);
Criteria.propTypes = {
startDate: PropTypes.string,
endDate: PropTypes.string,
- councils: PropTypes.array.isRequired,
+ councils: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
};
Criteria.defaultProps = {
diff --git a/src/components/Visualizations/Frequency.jsx b/src/components/Visualizations/Frequency.jsx
index 1ec043091..ab9f2cec3 100644
--- a/src/components/Visualizations/Frequency.jsx
+++ b/src/components/Visualizations/Frequency.jsx
@@ -1,43 +1,41 @@
import React from 'react';
import PropTypes from 'proptypes';
import { connect } from 'react-redux';
-import Chart from './Chart';
import { REQUEST_TYPES } from '@components/common/CONSTANTS';
import moment from 'moment';
+import Chart from './Chart';
const Frequency = ({
requestTypes,
}) => {
+ // // DATA ////
- //// DATA ////
-
- const randomPoints = (count, min, max) => {
- return Array.from({ length: count })
- .map((el, idx) => ({
- x: moment().add(idx, 'd').toDate(),
- y: Math.round(Math.random() * (max - min) + min)
- }));
- };
+ const randomPoints = (count, min, max) => Array.from({ length: count })
+ .map((el, idx) => ({
+ x: moment().add(idx, 'd').toDate(),
+ y: Math.round(Math.random() * (max - min) + min),
+ }));
const dummyData = REQUEST_TYPES.reduce((p, c) => {
- p[c.type] = randomPoints(10, 20, 200);
- return p;
+ const acc = p;
+ acc[c.type] = randomPoints(10, 20, 200);
+ return acc;
}, {});
const selectedTypes = REQUEST_TYPES.filter(el => requestTypes[el.type]);
const chartData = {
datasets: selectedTypes.map(t => ({
- label: t.abbrev + ' requests',
+ label: `${t.abbrev} requests`,
backgroundColor: t.color,
borderColor: t.color,
fill: false,
lineTension: 0,
data: dummyData[t.type],
- }))
+ })),
};
- //// OPTIONS ////
+ // // OPTIONS ////
const chartOptions = {
aspectRatio: 0.7,
@@ -67,7 +65,7 @@ const Frequency = ({
ticks: {
beginAtZero: true,
},
- }]
+ }],
},
tooltips: {
callbacks: {
@@ -76,8 +74,7 @@ const Frequency = ({
},
};
- if (chartData.datasets.length === 0)
- return null;
+ if (chartData.datasets.length === 0) return null;
return (
@@ -88,7 +85,7 @@ const Frequency = ({
/>
);
-}
+};
const mapStateToProps = state => ({
requestTypes: state.data.requestTypes,
@@ -97,5 +94,5 @@ const mapStateToProps = state => ({
export default connect(mapStateToProps)(Frequency);
Frequency.propTypes = {
- requestTypes: PropTypes.object.isRequired,
+ requestTypes: PropTypes.shape({}).isRequired,
};
diff --git a/src/components/Visualizations/Legend.jsx b/src/components/Visualizations/Legend.jsx
index a8cbc3449..451f4a613 100644
--- a/src/components/Visualizations/Legend.jsx
+++ b/src/components/Visualizations/Legend.jsx
@@ -14,21 +14,27 @@ const Legend = ({
Legend
{
- selectedTypes.length > 0 ?
- selectedTypes.map(({ type, color, abbrev }, idx) => (
-
-
- { type } [{abbrev}]
-
- )) :
-
- No request types selected.
-
+ selectedTypes.length > 0
+ ? selectedTypes.map(({ type, color, abbrev }) => (
+
+
+ { type }
+ {' '}
+ [
+ {abbrev}
+ ]
+
+ ))
+ : (
+
+ No request types selected.
+
+ )
}
@@ -42,5 +48,5 @@ const mapStateToProps = state => ({
export default connect(mapStateToProps)(Legend);
Legend.propTypes = {
- requestTypes: PropTypes.object.isRequired,
+ requestTypes: PropTypes.shape({}).isRequired,
};
diff --git a/src/components/Visualizations/NumberOfRequests.jsx b/src/components/Visualizations/NumberOfRequests.jsx
index b3fd66b8b..dd84e7c40 100644
--- a/src/components/Visualizations/NumberOfRequests.jsx
+++ b/src/components/Visualizations/NumberOfRequests.jsx
@@ -21,9 +21,9 @@ const NumberOfRequests = ({
export default NumberOfRequests;
NumberOfRequests.propTypes = {
- numRequests: PropTypes.number.isRequired,
+ numRequests: PropTypes.number,
};
NumberOfRequests.defaultProps = {
- numRequests: 1285203, // until we get data
+ numRequests: 1285203, // until we get data
};
diff --git a/src/components/Visualizations/TimeToClose.jsx b/src/components/Visualizations/TimeToClose.jsx
index 8a95d92fd..bd8f17b8f 100644
--- a/src/components/Visualizations/TimeToClose.jsx
+++ b/src/components/Visualizations/TimeToClose.jsx
@@ -1,23 +1,22 @@
import React from 'react';
import PropTypes from 'proptypes';
import { connect } from 'react-redux';
-import Chart from './Chart';
import { REQUEST_TYPES } from '@components/common/CONSTANTS';
+import Chart from './Chart';
+
const TimeToClose = ({
requestTypes,
}) => {
+ // // DATA ////
- //// DATA ////
-
- const randomSeries = (count, min, max) => {
- return Array.from({ length: count })
- .map(() => Math.random() * (max - min) + min);
- };
+ const randomSeries = (count, min, max) => Array.from({ length: count })
+ .map(() => Math.random() * (max - min) + min);
const dummyData = REQUEST_TYPES.reduce((p, c) => {
- p[c.type] = randomSeries(8, 0, 11);
- return p;
+ const acc = p;
+ acc[c.type] = randomSeries(8, 0, 11);
+ return acc;
}, {});
const selectedTypes = REQUEST_TYPES.filter(el => requestTypes[el.type]);
@@ -32,7 +31,7 @@ const TimeToClose = ({
}],
};
- //// OPTIONS ////
+ // // OPTIONS ////
const chartOptions = {
title: {
@@ -64,7 +63,7 @@ const TimeToClose = ({
tooltipDecimals: 1,
};
- //// HEIGHT ////
+ // // HEIGHT ////
const chartHeight = numLabels => (
numLabels > 0
@@ -82,7 +81,7 @@ const TimeToClose = ({
/>
);
-}
+};
const mapStateToProps = state => ({
requestTypes: state.data.requestTypes,
@@ -91,5 +90,5 @@ const mapStateToProps = state => ({
export default connect(mapStateToProps)(TimeToClose);
TimeToClose.propTypes = {
- requestTypes: PropTypes.object.isRequired,
+ requestTypes: PropTypes.shape({}).isRequired,
};
diff --git a/src/components/Visualizations/index.jsx b/src/components/Visualizations/index.jsx
index ec85f8ef8..029221dca 100644
--- a/src/components/Visualizations/index.jsx
+++ b/src/components/Visualizations/index.jsx
@@ -13,13 +13,13 @@ const Visualizations = ({
menuIsOpen,
menuActiveTab,
}) => {
- if (menuActiveTab !== MENU_TABS.VISUALIZATIONS)
- return null;
+ if (menuActiveTab !== MENU_TABS.VISUALIZATIONS) return null;
return (
+ 'full-width': !menuIsOpen,
+ })}
+ >
diff --git a/src/components/common/Dropdown.jsx b/src/components/common/Dropdown.jsx
index 6ff448565..f174f2307 100644
--- a/src/components/common/Dropdown.jsx
+++ b/src/components/common/Dropdown.jsx
@@ -29,7 +29,7 @@ const Dropdown = ({
}, className);
useEffect(() => {
- const handleClickOutside = (e) => {
+ const handleClickOutside = e => {
// Clicked inside dropdown
if (dropdownNode.current.contains(e.target) || !isOpen) {
return;
@@ -38,7 +38,7 @@ const Dropdown = ({
updateIsOpen(false);
};
- const handleEscapeKeydown = (e) => {
+ const handleEscapeKeydown = e => {
// Non-esc key pressed
if (e.keyCode !== 27 || !isOpen) {
return;
@@ -60,16 +60,16 @@ const Dropdown = ({
};
}, [isOpen, currentSelection]);
- const toggleOpen = () => updateIsOpen((prevIsOpen) => !prevIsOpen);
+ const toggleOpen = () => updateIsOpen(prevIsOpen => !prevIsOpen);
- const handleItemClick = (e) => {
+ const handleItemClick = e => {
e.preventDefault();
updateSelection(e.currentTarget.textContent);
updateIsOpen(false);
onClick(e.currentTarget.getAttribute('value'));
};
- const renderDropdownItems = (items) => items.map((item) => (
+ const renderDropdownItems = items => items.map(item => (
(
+ data-place={position}
+ >
{ children }
);
@@ -21,8 +22,12 @@ HoverOverInfo.propTypes = {
title: PropTypes.string,
text: PropTypes.string,
position: PropTypes.oneOf(['top', 'bottom', 'left', 'right']),
+ children: PropTypes.element,
};
HoverOverInfo.defaultProps = {
- position: 'right'
+ title: undefined,
+ text: undefined,
+ position: 'right',
+ children: (),
};
diff --git a/src/components/common/Icon.jsx b/src/components/common/Icon.jsx
index 9ee4ea8a9..52f2280c7 100644
--- a/src/components/common/Icon.jsx
+++ b/src/components/common/Icon.jsx
@@ -1,3 +1,5 @@
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
import React from 'react';
import PropTypes from 'proptypes';
import classNames from 'classnames';
@@ -12,6 +14,7 @@ const Icon = ({
*/
size,
iconSize,
+ iconStyle,
className,
fixedWidth,
spin,
@@ -44,7 +47,7 @@ const Icon = ({
className={containerClassName}
style={style}
>
-
+
{label}
);
@@ -86,6 +89,7 @@ Icon.propTypes = {
pulse: PropTypes.bool,
bordered: PropTypes.bool,
style: PropTypes.shape({}),
+ iconStyle: PropTypes.shape({}),
};
Icon.defaultProps = {
@@ -101,4 +105,5 @@ Icon.defaultProps = {
pulse: false,
bordered: false,
style: undefined,
+ iconStyle: undefined,
};
diff --git a/src/components/main/body/Body.jsx b/src/components/main/body/Body.jsx
index 20195ba3b..4893a9bff 100644
--- a/src/components/main/body/Body.jsx
+++ b/src/components/main/body/Body.jsx
@@ -1,14 +1,16 @@
import React from 'react';
+import Visualizations from '@components/Visualizations';
import Menu from '../menu/Menu';
import PinMap from '../../PinMap/PinMap';
-import Visualizations from '@components/Visualizations';
const Body = () => (
);
diff --git a/src/components/main/footer/Footer.jsx b/src/components/main/footer/Footer.jsx
index 6726e0e14..5ff72142b 100644
--- a/src/components/main/footer/Footer.jsx
+++ b/src/components/main/footer/Footer.jsx
@@ -1,4 +1,8 @@
import React from 'react';
+import { connect } from 'react-redux';
+import propTypes from 'proptypes';
+import moment from 'moment';
+
import COLORS from '../../../styles/COLORS';
const footerTextStyle = {
@@ -7,7 +11,9 @@ const footerTextStyle = {
width: '100vw',
};
-const Footer = () => (
+const Footer = ({
+ lastUpdated,
+}) => (
);
-export default Footer;
+const mapStateToProps = state => ({
+ lastUpdated: state.data.lastUpdated,
+});
+
+Footer.propTypes = {
+ lastUpdated: propTypes.string,
+};
+
+Footer.defaultProps = {
+ lastUpdated: undefined,
+};
+
+export default connect(mapStateToProps, null)(Footer);
diff --git a/src/components/main/header/Header.jsx b/src/components/main/header/Header.jsx
index d709ab035..6b70222a1 100644
--- a/src/components/main/header/Header.jsx
+++ b/src/components/main/header/Header.jsx
@@ -1,7 +1,11 @@
import React from 'react';
+import propTypes from 'proptypes';
+import { connect } from 'react-redux';
import COLORS from '../../../styles/COLORS';
-const Header = () => {
+const Header = ({
+ data,
+}) => {
const cta2Style = {
color: COLORS.BRAND.CTA2,
fontWeight: 'bold',
@@ -18,46 +22,86 @@ const Header = () => {
};
return (
-