gtfs-to-chart 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +28 -0
- package/.husky/pre-commit +4 -0
- package/CHANGELOG.md +20 -2
- package/README.md +1 -2
- package/app/routes.js +1 -1
- package/lib/gtfs-to-chart.js +4 -4
- package/lib/log-utils.js +7 -9
- package/lib/utils.js +25 -28
- package/package.json +25 -23
- package/public/js/chart.js +30 -31
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"env": {
|
|
3
|
+
"node": true,
|
|
4
|
+
"es2021": true
|
|
5
|
+
},
|
|
6
|
+
"extends": ["xo", "prettier"],
|
|
7
|
+
"parserOptions": {
|
|
8
|
+
"ecmaVersion": 12,
|
|
9
|
+
"sourceType": "module"
|
|
10
|
+
},
|
|
11
|
+
"rules": {
|
|
12
|
+
"arrow-parens": ["error", "always"],
|
|
13
|
+
"camelcase": [
|
|
14
|
+
"error",
|
|
15
|
+
{
|
|
16
|
+
"properties": "never"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"indent": "off",
|
|
20
|
+
"object-curly-spacing": ["error", "always"],
|
|
21
|
+
"no-unused-vars": [
|
|
22
|
+
"error",
|
|
23
|
+
{
|
|
24
|
+
"varsIgnorePattern": "^[A-Z]"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,31 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [2.0.5] - 2022-12-31
|
|
8
|
+
### Updated
|
|
9
|
+
- Update to node-gtfs v4
|
|
10
|
+
- Dependency updates
|
|
11
|
+
- Add Husky
|
|
12
|
+
|
|
13
|
+
## [2.0.4] - 2022-11-09
|
|
14
|
+
### Updated
|
|
15
|
+
- Dependency updates
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fixed charts path in generated charts HTML
|
|
19
|
+
|
|
20
|
+
## [2.0.3] - 2022-06-25
|
|
8
21
|
### Updated
|
|
9
22
|
- Dependency updates
|
|
10
23
|
|
|
11
24
|
### Fixed
|
|
12
25
|
- Fixed charts path in generated index.html
|
|
13
26
|
|
|
27
|
+
## [2.0.2] - 2022-06-25
|
|
28
|
+
### Updated
|
|
29
|
+
- Dependency updates
|
|
30
|
+
- Readme udpates
|
|
31
|
+
|
|
14
32
|
## [2.0.1] - 2021-08-17
|
|
15
33
|
### Updated
|
|
16
34
|
- Dependency updates
|
|
@@ -20,4 +38,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
20
38
|
|
|
21
39
|
## [2.0.0] - 2021-05-14
|
|
22
40
|
### Breaking Changes
|
|
23
|
-
- Converted to ES6 Module
|
|
41
|
+
- Converted to ES6 Module
|
package/README.md
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
<br /><br />
|
|
10
10
|
<a href="https://www.npmjs.com/package/gtfs-to-chart" rel="nofollow"><img src="https://img.shields.io/npm/v/gtfs-to-chart.svg?style=flat" style="max-width: 100%;"></a>
|
|
11
11
|
<a href="https://www.npmjs.com/package/gtfs-to-chart" rel="nofollow"><img src="https://img.shields.io/npm/dm/gtfs-to-chart.svg?style=flat" style="max-width: 100%;"></a>
|
|
12
|
-
<a href="https://github.com/BlinkTagInc/gtfs-to-chart/actions?query=workflow%3A%22Node+CI%22"><img src="https://img.shields.io/github/workflow/status/BlinkTagInc/gtfs-to-chart/Node%20CI.svg" alt="CircleCI" style="max-width: 100%;"></a>
|
|
13
12
|
<img src="https://img.shields.io/badge/License-MIT-yellow.svg">
|
|
14
13
|
<br /><br />
|
|
15
14
|
Generate stringline charts from GTFS transit data.
|
|
@@ -100,7 +99,7 @@ All files starting with `config*.json` are .gitignored - so you can create multi
|
|
|
100
99
|
| ------ | ---- | ----------- |
|
|
101
100
|
| [`agencies`](#agencies) | array | An array of GTFS files to be imported. |
|
|
102
101
|
| [`beautify`](#beautify) | boolean | Whether or not to beautify the HTML output. |
|
|
103
|
-
| [`chartDate`](#
|
|
102
|
+
| [`chartDate`](#chartdate) | string | The date to use for generating the stringline chart. |
|
|
104
103
|
| [`templatePath`](#templatepath) | string | Path to custom pug template for rendering chart html. |
|
|
105
104
|
|
|
106
105
|
### agencies
|
package/app/routes.js
CHANGED
package/lib/gtfs-to-chart.js
CHANGED
|
@@ -14,7 +14,7 @@ import { formatRouteName } from './formatters.js';
|
|
|
14
14
|
/*
|
|
15
15
|
* Generate HTML charts from GTFS
|
|
16
16
|
*/
|
|
17
|
-
const gtfsToChart = async initialConfig => {
|
|
17
|
+
const gtfsToChart = async (initialConfig) => {
|
|
18
18
|
const config = setDefaultConfig(initialConfig);
|
|
19
19
|
config.log = log(config);
|
|
20
20
|
config.logWarning = logWarning(config);
|
|
@@ -25,7 +25,7 @@ const gtfsToChart = async initialConfig => {
|
|
|
25
25
|
throw new Error('No agencies defined in `config.json`');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
return Promise.all(config.agencies.map(async agency => {
|
|
28
|
+
return Promise.all(config.agencies.map(async (agency) => {
|
|
29
29
|
const timer = new Timer();
|
|
30
30
|
const agencyKey = agency.agency_key;
|
|
31
31
|
const exportPath = path.join(process.cwd(), 'charts', sanitize(agencyKey));
|
|
@@ -45,12 +45,12 @@ const gtfsToChart = async initialConfig => {
|
|
|
45
45
|
|
|
46
46
|
await prepDirectory(exportPath);
|
|
47
47
|
|
|
48
|
-
const routes =
|
|
48
|
+
const routes = getRoutes();
|
|
49
49
|
const bar = progressBar(`${agencyKey}: Generating charts [:bar] :current/:total`, { total: routes.length }, config);
|
|
50
50
|
|
|
51
51
|
// Make directory if it doesn't exist
|
|
52
52
|
await mkdir(exportPath, { recursive: true });
|
|
53
|
-
config.assetPath = '';
|
|
53
|
+
config.assetPath = '../';
|
|
54
54
|
|
|
55
55
|
/* eslint-disable no-await-in-loop */
|
|
56
56
|
for (const route of routes) {
|
package/lib/log-utils.js
CHANGED
|
@@ -36,7 +36,7 @@ export function logWarning(config) {
|
|
|
36
36
|
return config.logFunction;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
return text => {
|
|
39
|
+
return (text) => {
|
|
40
40
|
process.stdout.write(`\n${formatWarning(text)}\n`);
|
|
41
41
|
};
|
|
42
42
|
}
|
|
@@ -49,7 +49,7 @@ export function logError(config) {
|
|
|
49
49
|
return config.logFunction;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
return text => {
|
|
52
|
+
return (text) => {
|
|
53
53
|
process.stdout.write(`\n${formatError(text)}\n`);
|
|
54
54
|
};
|
|
55
55
|
}
|
|
@@ -79,20 +79,18 @@ export function progressBar(formatString, barOptions, config) {
|
|
|
79
79
|
|
|
80
80
|
if (config.logFunction) {
|
|
81
81
|
let barProgress = 0;
|
|
82
|
-
const renderProgressString = () =>
|
|
83
|
-
return formatString
|
|
82
|
+
const renderProgressString = () => formatString
|
|
84
83
|
.replace(':current', barProgress)
|
|
85
84
|
.replace(':total', barOptions.total)
|
|
86
85
|
.replace('[:bar] ', '');
|
|
87
|
-
};
|
|
88
86
|
|
|
89
87
|
config.log(renderProgressString());
|
|
90
88
|
|
|
91
89
|
return {
|
|
92
|
-
interrupt
|
|
90
|
+
interrupt(text) {
|
|
93
91
|
config.logWarning(text);
|
|
94
92
|
},
|
|
95
|
-
tick
|
|
93
|
+
tick() {
|
|
96
94
|
barProgress += 1;
|
|
97
95
|
config.log(renderProgressString());
|
|
98
96
|
}
|
|
@@ -103,10 +101,10 @@ export function progressBar(formatString, barOptions, config) {
|
|
|
103
101
|
bar.render();
|
|
104
102
|
|
|
105
103
|
return {
|
|
106
|
-
interrupt
|
|
104
|
+
interrupt(text) {
|
|
107
105
|
bar.interrupt(text);
|
|
108
106
|
},
|
|
109
|
-
tick
|
|
107
|
+
tick() {
|
|
110
108
|
bar.tick();
|
|
111
109
|
}
|
|
112
110
|
};
|
package/lib/utils.js
CHANGED
|
@@ -49,12 +49,12 @@ const reverseStationDistances = (stations, oppositeDirectionDistance) => {
|
|
|
49
49
|
/*
|
|
50
50
|
* Find longest trip
|
|
51
51
|
*/
|
|
52
|
-
const findLongestTrip = trips => maxBy(trips, trip => size(trip.stoptimes));
|
|
52
|
+
const findLongestTrip = (trips) => maxBy(trips, (trip) => size(trip.stoptimes));
|
|
53
53
|
|
|
54
54
|
/*
|
|
55
55
|
* Determine if a stoptime is a timepoint.
|
|
56
56
|
*/
|
|
57
|
-
const isTimepoint = stoptime => {
|
|
57
|
+
const isTimepoint = (stoptime) => {
|
|
58
58
|
if (stoptime.timepoint === null) {
|
|
59
59
|
return stoptime.arrival_time !== '' && stoptime.departure_time !== '';
|
|
60
60
|
}
|
|
@@ -62,9 +62,9 @@ const isTimepoint = stoptime => {
|
|
|
62
62
|
return stoptime.timepoint === 1;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
const getStationsFromTrip =
|
|
66
|
-
const stops =
|
|
67
|
-
const stops =
|
|
65
|
+
const getStationsFromTrip = (trip) => {
|
|
66
|
+
const stops = trip.stoptimes.map((stoptime) => {
|
|
67
|
+
const stops = getStops({
|
|
68
68
|
stop_id: stoptime.stop_id
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -73,12 +73,12 @@ const getStationsFromTrip = async trip => {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return stops[0];
|
|
76
|
-
})
|
|
76
|
+
});
|
|
77
77
|
|
|
78
78
|
let previousStationCoordinates;
|
|
79
79
|
return trip.stoptimes.map((stoptime, index) => {
|
|
80
80
|
const stop = stops[index];
|
|
81
|
-
const hasShapeDistance = every(trip.stoptimes, stoptime => stoptime.shape_dist_traveled !== null);
|
|
81
|
+
const hasShapeDistance = every(trip.stoptimes, (stoptime) => stoptime.shape_dist_traveled !== null);
|
|
82
82
|
|
|
83
83
|
if (!hasShapeDistance) {
|
|
84
84
|
if (index === 0) {
|
|
@@ -107,24 +107,20 @@ const getStationsFromTrip = async trip => {
|
|
|
107
107
|
/*
|
|
108
108
|
* Get all trips and stoptimes for a given route
|
|
109
109
|
*/
|
|
110
|
-
const getDataforChart =
|
|
111
|
-
const db =
|
|
110
|
+
const getDataforChart = (config, routeId) => {
|
|
111
|
+
const db = openDb(config);
|
|
112
112
|
const notes = [];
|
|
113
113
|
const dayOfWeek = moment(config.chartDate, 'YYYYMMDD').format('dddd').toLowerCase();
|
|
114
|
-
const calendars =
|
|
115
|
-
config.chartDate,
|
|
116
|
-
config.chartDate
|
|
117
|
-
]);
|
|
114
|
+
const calendars = db.prepare(`SELECT DISTINCT service_id FROM calendar WHERE start_date <= $date AND end_date >= $date AND ${sqlString.escapeId(dayOfWeek)} = 1`).all({ date: config.chartDate });
|
|
118
115
|
|
|
119
116
|
if (calendars.length === 0) {
|
|
120
117
|
throw new Error(`No calendars found for route ${routeId} on ${moment(config.chartDate, 'YYYYMMDD').format('MMM D, YYYY')}`);
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
const serviceIds = calendars.map(calendar => calendar.service_id);
|
|
124
|
-
const trips =
|
|
120
|
+
const serviceIds = calendars.map((calendar) => calendar.service_id);
|
|
121
|
+
const trips = db.prepare(`SELECT service_id, trip_id, trip_headsign, direction_id, shape_id FROM trips where route_id = ? AND service_id IN (${serviceIds.map(() => '?').join(', ')})`).all(
|
|
125
122
|
routeId,
|
|
126
|
-
...serviceIds
|
|
127
|
-
]);
|
|
123
|
+
...serviceIds);
|
|
128
124
|
|
|
129
125
|
if (trips.length === 0) {
|
|
130
126
|
throw new Error(`No trips found for route ${routeId} on ${moment(config.chartDate, 'YYYYMMDD').format('MMM D, YYYY')}`);
|
|
@@ -136,8 +132,8 @@ const getDataforChart = async (config, routeId) => {
|
|
|
136
132
|
throw new Error('Route has no shapes.');
|
|
137
133
|
}
|
|
138
134
|
|
|
139
|
-
|
|
140
|
-
const stoptimes =
|
|
135
|
+
for (const trip of trips) {
|
|
136
|
+
const stoptimes = getStoptimes(
|
|
141
137
|
{
|
|
142
138
|
trip_id: trip.trip_id
|
|
143
139
|
},
|
|
@@ -153,11 +149,11 @@ const getDataforChart = async (config, routeId) => {
|
|
|
153
149
|
]
|
|
154
150
|
);
|
|
155
151
|
|
|
156
|
-
trip.stoptimes = stoptimes.filter(stoptime => isTimepoint(stoptime));
|
|
157
|
-
}
|
|
152
|
+
trip.stoptimes = stoptimes.filter((stoptime) => isTimepoint(stoptime));
|
|
153
|
+
}
|
|
158
154
|
|
|
159
155
|
const longestTrip = findLongestTrip(trips);
|
|
160
|
-
let stations =
|
|
156
|
+
let stations = getStationsFromTrip(longestTrip);
|
|
161
157
|
const tripDistance = max(map(stations, 'distance'));
|
|
162
158
|
const directionGroups = groupBy(trips, 'direction_id');
|
|
163
159
|
|
|
@@ -165,13 +161,14 @@ const getDataforChart = async (config, routeId) => {
|
|
|
165
161
|
if (size(directionGroups) > 1) {
|
|
166
162
|
const oppositeDirection = longestTrip.direction_id === 1 ? '0' : '1';
|
|
167
163
|
const longestTripOppositeDirection = findLongestTrip(directionGroups[oppositeDirection]);
|
|
168
|
-
const stationsOppositeDirection =
|
|
164
|
+
const stationsOppositeDirection = getStationsFromTrip(longestTripOppositeDirection);
|
|
169
165
|
|
|
170
166
|
reverseStationDistances(stationsOppositeDirection, tripDistance);
|
|
171
167
|
|
|
172
168
|
stations = [...stations, ...stationsOppositeDirection];
|
|
173
169
|
}
|
|
174
|
-
|
|
170
|
+
|
|
171
|
+
const hasShapeDistance = every(longestTrip.stoptimes, (stoptime) => stoptime.shape_dist_traveled !== null);
|
|
175
172
|
if (!hasShapeDistance) {
|
|
176
173
|
notes.push('Distance between stops calculated assuming a straight line.');
|
|
177
174
|
}
|
|
@@ -201,7 +198,7 @@ export function setDefaultConfig(initialConfig) {
|
|
|
201
198
|
* Generate the HTML for the agency overview page.
|
|
202
199
|
*/
|
|
203
200
|
export async function generateOverviewHTML(config, routes) {
|
|
204
|
-
const agencies =
|
|
201
|
+
const agencies = getAgencies();
|
|
205
202
|
if (agencies.length === 0) {
|
|
206
203
|
throw new Error('No agencies found');
|
|
207
204
|
}
|
|
@@ -215,7 +212,7 @@ export async function generateOverviewHTML(config, routes) {
|
|
|
215
212
|
const templateVars = {
|
|
216
213
|
agency,
|
|
217
214
|
config,
|
|
218
|
-
routes: sortBy(routes, r => Number.parseInt(r.route_short_name, 10))
|
|
215
|
+
routes: sortBy(routes, (r) => Number.parseInt(r.route_short_name, 10))
|
|
219
216
|
};
|
|
220
217
|
return renderTemplate('overview_page', templateVars, config);
|
|
221
218
|
}
|
|
@@ -224,7 +221,7 @@ export async function generateOverviewHTML(config, routes) {
|
|
|
224
221
|
* Generate the HTML for a chart.
|
|
225
222
|
*/
|
|
226
223
|
export async function generateChartHTML(config, routeId) {
|
|
227
|
-
const routes =
|
|
224
|
+
const routes = getRoutes({
|
|
228
225
|
route_id: routeId
|
|
229
226
|
});
|
|
230
227
|
|
|
@@ -232,7 +229,7 @@ export async function generateChartHTML(config, routeId) {
|
|
|
232
229
|
throw new Error('Invalid route id provided');
|
|
233
230
|
}
|
|
234
231
|
|
|
235
|
-
const chartData =
|
|
232
|
+
const chartData = getDataforChart(config, routeId);
|
|
236
233
|
|
|
237
234
|
return renderTemplate('chart_page', {
|
|
238
235
|
route: routes[0],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtfs-to-chart",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Generate stringline charts of a transit routes from GTFS",
|
|
6
6
|
"keywords": [
|
|
@@ -24,30 +24,15 @@
|
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"start": "node ./app",
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"xo": {
|
|
30
|
-
"esnext": true,
|
|
31
|
-
"rules": {
|
|
32
|
-
"camelcase": [
|
|
33
|
-
"error",
|
|
34
|
-
{
|
|
35
|
-
"properties": "never"
|
|
36
|
-
}
|
|
37
|
-
],
|
|
38
|
-
"object-curly-spacing": [
|
|
39
|
-
"error",
|
|
40
|
-
"always"
|
|
41
|
-
]
|
|
42
|
-
},
|
|
43
|
-
"space": true
|
|
27
|
+
"lint": "eslint app/**/*.js lib/**/*.js public/**/*.js --fix",
|
|
28
|
+
"prepare": "husky install"
|
|
44
29
|
},
|
|
45
30
|
"dependencies": {
|
|
46
31
|
"better-copy": "^1.0.4",
|
|
47
|
-
"chalk": "^5.
|
|
32
|
+
"chalk": "^5.2.0",
|
|
48
33
|
"connect-slashes": "^1.4.0",
|
|
49
34
|
"express": "^4.18.2",
|
|
50
|
-
"gtfs": "^
|
|
35
|
+
"gtfs": "^4.0.1",
|
|
51
36
|
"js-beautify": "^1.14.7",
|
|
52
37
|
"lodash-es": "^4.17.21",
|
|
53
38
|
"moment": "^2.29.4",
|
|
@@ -59,13 +44,30 @@
|
|
|
59
44
|
"sqlstring": "^2.3.3",
|
|
60
45
|
"timer-machine": "^1.1.0",
|
|
61
46
|
"untildify": "^4.0.0",
|
|
62
|
-
"yargs": "^17.6.
|
|
47
|
+
"yargs": "^17.6.2"
|
|
63
48
|
},
|
|
64
49
|
"devDependencies": {
|
|
65
|
-
"
|
|
50
|
+
"eslint": "^8.31.0",
|
|
51
|
+
"eslint-config-prettier": "^8.5.0",
|
|
52
|
+
"eslint-config-xo": "^0.43.1",
|
|
53
|
+
"husky": "^7.0.0",
|
|
54
|
+
"prettier": "^2.8.1",
|
|
55
|
+
"pretty-quick": "^3.1.3"
|
|
66
56
|
},
|
|
67
57
|
"engines": {
|
|
68
58
|
"node": ">= 14.x"
|
|
69
59
|
},
|
|
70
|
-
"
|
|
60
|
+
"release-it": {
|
|
61
|
+
"github": {
|
|
62
|
+
"release": true
|
|
63
|
+
},
|
|
64
|
+
"plugins": {
|
|
65
|
+
"@release-it/keep-a-changelog": {
|
|
66
|
+
"filename": "CHANGELOG.md"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"prettier": {
|
|
71
|
+
"singleQuote": true
|
|
72
|
+
}
|
|
71
73
|
}
|
package/public/js/chart.js
CHANGED
|
@@ -15,9 +15,8 @@ function padTimeRange(range) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function geStopsFromStoptimes(stoptimes, stations) {
|
|
18
|
-
/* eslint-disable-next-line unicorn/no-array-reduce */
|
|
19
18
|
return stoptimes.reduce((memo, stoptime) => {
|
|
20
|
-
const station = stations.find(station => station.stop_id === stoptime.stop_id);
|
|
19
|
+
const station = stations.find((station) => station.stop_id === stoptime.stop_id);
|
|
21
20
|
if (stoptime.arrival_time === stoptime.departure_time) {
|
|
22
21
|
memo.push({
|
|
23
22
|
station,
|
|
@@ -58,7 +57,7 @@ function formatStopTime(stop) {
|
|
|
58
57
|
|
|
59
58
|
function getPrimaryDirectionId(stations) {
|
|
60
59
|
const directionGroups = _.groupBy(stations, 'direction_id');
|
|
61
|
-
const largestDirectionGroup = _.maxBy(Object.values(directionGroups), group => group.length);
|
|
60
|
+
const largestDirectionGroup = _.maxBy(Object.values(directionGroups), (group) => group.length);
|
|
62
61
|
return largestDirectionGroup[0].direction_id;
|
|
63
62
|
}
|
|
64
63
|
|
|
@@ -68,85 +67,85 @@ function renderChart(data) {
|
|
|
68
67
|
stations
|
|
69
68
|
} = data;
|
|
70
69
|
|
|
71
|
-
const formattedTrips = trips.map(trip => ({
|
|
70
|
+
const formattedTrips = trips.map((trip) => ({
|
|
72
71
|
number: trip.trip_id,
|
|
73
72
|
direction: trip.direction_id,
|
|
74
73
|
trip_headsign: trip.trip_headsign,
|
|
75
74
|
stops: geStopsFromStoptimes(trip.stoptimes, stations)
|
|
76
75
|
}));
|
|
77
76
|
|
|
78
|
-
const stops = formattedTrips.flatMap(trip => trip.stops.map(stop => ({
|
|
77
|
+
const stops = formattedTrips.flatMap((trip) => trip.stops.map((stop) => ({
|
|
79
78
|
trip,
|
|
80
79
|
stop
|
|
81
80
|
})));
|
|
82
81
|
|
|
83
82
|
const height = 2400;
|
|
84
83
|
const width = 800;
|
|
85
|
-
const topMargin = 20 + (_.max(_.map(stations, station => station.name.length)) * 4.6);
|
|
84
|
+
const topMargin = 20 + (_.max(_.map(stations, (station) => station.name.length)) * 4.6);
|
|
86
85
|
const margin = ({ top: topMargin, right: 30, bottom: topMargin, left: 50 });
|
|
87
86
|
|
|
88
87
|
const primaryDirectionId = getPrimaryDirectionId(stations);
|
|
89
88
|
|
|
90
89
|
const line = d3.line()
|
|
91
|
-
.x(d => x(d.station.distance))
|
|
92
|
-
.y(d => y(d.time));
|
|
90
|
+
.x((d) => x(d.station.distance))
|
|
91
|
+
.y((d) => y(d.time));
|
|
93
92
|
|
|
94
93
|
const x = d3.scaleLinear()
|
|
95
|
-
.domain(d3.extent(stations, d => d.distance))
|
|
94
|
+
.domain(d3.extent(stations, (d) => d.distance))
|
|
96
95
|
.range([margin.left + 10, width - margin.right]);
|
|
97
96
|
|
|
98
97
|
const y = d3.scaleUtc()
|
|
99
|
-
.domain(padTimeRange(d3.extent(stops, s => s.stop.time)))
|
|
98
|
+
.domain(padTimeRange(d3.extent(stops, (s) => s.stop.time)))
|
|
100
99
|
.range([margin.top, height - margin.bottom]);
|
|
101
100
|
|
|
102
|
-
const xAxis = g => g
|
|
101
|
+
const xAxis = (g) => g
|
|
103
102
|
.style('font', '10px sans-serif')
|
|
104
103
|
.selectAll('g')
|
|
105
104
|
.data(stations)
|
|
106
105
|
.join('g')
|
|
107
|
-
.attr('transform', d => `translate(${x(d.distance)},0)`)
|
|
108
|
-
.call(g => g.append('line')
|
|
106
|
+
.attr('transform', (d) => `translate(${x(d.distance)},0)`)
|
|
107
|
+
.call((g) => g.append('line')
|
|
109
108
|
.attr('y1', margin.top - 6)
|
|
110
109
|
.attr('y2', margin.top)
|
|
111
110
|
.attr('stroke', 'currentColor'))
|
|
112
|
-
.call(g => g.append('line')
|
|
111
|
+
.call((g) => g.append('line')
|
|
113
112
|
.attr('y1', height - margin.bottom + 6)
|
|
114
113
|
.attr('y2', height - margin.bottom)
|
|
115
114
|
.attr('stroke', 'currentColor'))
|
|
116
|
-
.call(g => g.append('line')
|
|
115
|
+
.call((g) => g.append('line')
|
|
117
116
|
.attr('y1', margin.top)
|
|
118
117
|
.attr('y2', height - margin.bottom)
|
|
119
118
|
.attr('stroke-opacity', 0.2)
|
|
120
119
|
.attr('stroke-dasharray', '1.5,2')
|
|
121
120
|
.attr('stroke', 'currentColor'))
|
|
122
|
-
.call(g => g.append('text')
|
|
121
|
+
.call((g) => g.append('text')
|
|
123
122
|
.attr('transform', `translate(0,${margin.top}) rotate(-90)`)
|
|
124
123
|
.attr('x', 12)
|
|
125
124
|
.attr('dy', '0.35em')
|
|
126
|
-
.text(d => d.name))
|
|
127
|
-
.style('display', d => d.direction_id === primaryDirectionId ? 'block' : 'none')
|
|
128
|
-
.call(g => g.append('text')
|
|
125
|
+
.text((d) => d.name))
|
|
126
|
+
.style('display', (d) => d.direction_id === primaryDirectionId ? 'block' : 'none')
|
|
127
|
+
.call((g) => g.append('text')
|
|
129
128
|
.attr('text-anchor', 'end')
|
|
130
129
|
.attr('transform', `translate(0,${height - margin.top}) rotate(-90)`)
|
|
131
130
|
.attr('x', -12)
|
|
132
131
|
.attr('dy', '0.35em')
|
|
133
|
-
.text(d => d.name));
|
|
132
|
+
.text((d) => d.name));
|
|
134
133
|
|
|
135
|
-
const yAxis = g => g
|
|
134
|
+
const yAxis = (g) => g
|
|
136
135
|
.attr('transform', `translate(${margin.left},0)`)
|
|
137
136
|
.call(d3.axisLeft(y)
|
|
138
137
|
.ticks(d3.utcHour)
|
|
139
138
|
.tickFormat(d3.utcFormat('%-I %p')))
|
|
140
|
-
.call(g => g.select('.domain').remove())
|
|
141
|
-
.call(g => g.selectAll('.tick line').clone().lower()
|
|
139
|
+
.call((g) => g.select('.domain').remove())
|
|
140
|
+
.call((g) => g.selectAll('.tick line').clone().lower()
|
|
142
141
|
.attr('stroke-opacity', 0.2)
|
|
143
142
|
.attr('x2', width));
|
|
144
143
|
|
|
145
144
|
const voronoi = d3.Delaunay
|
|
146
|
-
.from(stops, d => x(d.stop.station.distance), d => y(d.stop.time))
|
|
145
|
+
.from(stops, (d) => x(d.stop.station.distance), (d) => y(d.stop.time))
|
|
147
146
|
.voronoi([0, 0, width, height]);
|
|
148
147
|
|
|
149
|
-
const tooltip = g => {
|
|
148
|
+
const tooltip = (g) => {
|
|
150
149
|
const tooltip = g.append('g')
|
|
151
150
|
.style('font', '10px sans-serif');
|
|
152
151
|
|
|
@@ -176,7 +175,7 @@ function renderChart(data) {
|
|
|
176
175
|
.join('path')
|
|
177
176
|
.attr('d', (d, i) => voronoi.renderCell(i))
|
|
178
177
|
.on('mouseout', () => tooltip.style('display', 'none'))
|
|
179
|
-
.on('mouseover', d => {
|
|
178
|
+
.on('mouseover', (d) => {
|
|
180
179
|
tooltip.style('display', null);
|
|
181
180
|
line1.text(`Trip ${d.trip.number} to ${d.trip.trip_headsign}`);
|
|
182
181
|
line2.text(d.stop.station.name);
|
|
@@ -216,16 +215,16 @@ function renderChart(data) {
|
|
|
216
215
|
|
|
217
216
|
vehicle.append('path')
|
|
218
217
|
.attr('fill', 'none')
|
|
219
|
-
.attr('stroke', d => 'rgb(34, 34, 34)')
|
|
220
|
-
.attr('d', d => line(d.stops));
|
|
218
|
+
.attr('stroke', (d) => 'rgb(34, 34, 34)')
|
|
219
|
+
.attr('d', (d) => line(d.stops));
|
|
221
220
|
|
|
222
221
|
vehicle.append('g')
|
|
223
222
|
.attr('stroke', 'white')
|
|
224
|
-
.attr('fill', d => 'rgb(34, 34, 34)')
|
|
223
|
+
.attr('fill', (d) => 'rgb(34, 34, 34)')
|
|
225
224
|
.selectAll('circle')
|
|
226
|
-
.data(d => d.stops)
|
|
225
|
+
.data((d) => d.stops)
|
|
227
226
|
.join('circle')
|
|
228
|
-
.attr('transform', d => `translate(${x(d.station.distance)},${y(d.time)})`)
|
|
227
|
+
.attr('transform', (d) => `translate(${x(d.station.distance)},${y(d.time)})`)
|
|
229
228
|
.attr('r', 2.5);
|
|
230
229
|
|
|
231
230
|
svg.append('g')
|