UNPKG

transitive-js

Version:

A tool for generating dynamic stylized transit maps that are easy to understand.

400 lines (358 loc) 9.94 kB
import d3 from 'd3' /** * Scales for utility functions to use */ const zoomScale = d3.scale.linear().domain([0.25, 1, 4]) const strokeScale = d3.scale.linear().domain([0.25, 1, 4]).range([5, 12, 19]) const fontScale = d3.scale.linear().domain([0.25, 1, 4]).range([10, 14, 18]) const notFocusedColor = '#e0e0e0' /** * Expose `utils` for the style functions to use */ const utils = { defineSegmentCircleMarker: function (display, segment, radius, fillColor) { const markerId = 'circleMarker-' + segment.getId() display.svg .append('defs') .append('svg:marker') .attr('id', markerId) .attr('refX', radius) .attr('refY', radius) .attr('markerWidth', radius * 2) .attr('markerHeight', radius * 2) .attr('markerUnits', 'userSpaceOnUse') .append('svg:circle') .attr('cx', radius) .attr('cy', radius) .attr('r', radius) .attr('fill', segment.focused ? fillColor : notFocusedColor) return 'url(#' + markerId + ')' }, fontSize: function (display) { return Math.floor(fontScale(display.scale)) }, pixels: function (zoom, min, normal, max) { return zoomScale.range([min, normal, max])(zoom) }, strokeWidth: function (display) { return strokeScale(display.scale) } } /** * Default Wireframe Edge/Vertex Rules */ const wireframeVertices = { cx: 0, cy: 0, fill: '#000', r: 3 } const wireframeEdges = { fill: 'none', stroke: '#444', 'stroke-dasharray': '3px 2px', 'stroke-width': 2 } /** * Default Merged Stops Rules */ const stopsMerged = { fill: function (display, point, index, utils) { return '#fff' }, /** * Transitive-specific attribute specifying any additional padding, in pixels, * to apply to main stop marker. A value of zero (default) results in a that * marker is flush to the edges of the pattern segment(s) the point is set against. * A value greater than zero creates a marker that is larger than the width of * the segments(s). */ 'marker-padding': 3, /** * Transitive-specific attribute specifying the shape of the main stop marker. * Can be 'roundedrect', 'rectangle' or 'circle' */ 'marker-type': [ 'circle', function (display, data, index, utils) { const point = data.owner if ( (point.containsBoardPoint() || point.containsAlightPoint()) && !point.containsTransferPoint() ) return 'circle' } ], r: function (display, point, index, utils) { return utils.pixels(display.scale, 8, 12, 16) }, stroke: function (display, point, index, utils) { if (!point.isFocused()) return notFocusedColor return '#000' }, 'stroke-width': function (display, point, index, utils) { return 2 }, visibility: (display, data) => { if (!data.owner.containsSegmentEndPoint()) return 'hidden' } } /** * Stops Along a Pattern */ const stopsPattern = { cx: 0, cy: 0, r: [ 4, (display, data, index, utils) => { return utils.pixels(display.scale, 1, 2, 4) }, (display, data, index, utils) => { const point = data.owner let busOnly = true point.getPatterns().forEach((pattern) => { if (pattern.route && pattern.route.route_type !== 3) busOnly = false }) if (busOnly && !point.containsSegmentEndPoint()) { return 0.5 * utils.pixels(display.scale, 2, 4, 6.5) } } ], stroke: 'none', visibility: (display, data) => { if (display.scale < 1.5) return 'hidden' if (data.owner.containsSegmentEndPoint()) return 'hidden' } } /** * Default place rules */ const places = { cx: 0, cy: 0, fill: '#fff', r: 14, stroke: '#000', 'stroke-width': '2px' } /** * Default MultiPoint rules -- based on Stop rules */ const multipointsMerged = Object.assign({}, stopsMerged) multipointsMerged.visibility = true /** * Default Multipoint Stops along a pattern */ const multipointsPattern = Object.assign({}, stopsPattern) /** * Default label rules */ const labels = { 'font-family': 'sans-serif', 'font-size': 15, /* 'font-weight': function (display, data, index, utils) { var point = data.owner.parent if (point.containsBoardPoint() || point.containsAlightPoint()) return 'bold' }, */ /** * 'orientations' is a transitive-specific attribute used to specify allowable * label placement orientations expressed as one of eight compass directions * relative to the point being labeled: * * 'N' * 'NW' | 'NE' * \ | / * 'W' -- O -- 'E' * / | \ * 'SW' | 'SE' * 'S * * Labels oriented 'E' or 'W' are rendered horizontally, 'N' and 'S' vertically, * and all others at a 45-degree angle. * * Returns an array of allowed orientation codes in the order that they will be * tried by the labeler. */ orientations: [['E', 'W']] } const segmentLabels = { background: [ '#008', // Background color falls back on dark blue. function (display, segment) { if (segment.type === 'TRANSIT') { if (segment.patterns) { if (patternIsDcCirculatorBusRoute(segment.patterns[0])) return '#f00' return segment.patterns[0].route.getColor() } } } ], color: [ '#fff', // Text color falls back on white. function (display, segment) { if (segment.type === 'TRANSIT') { if (segment.patterns) { if (patternIsDcCirculatorBusRoute(segment.patterns[0])) return '#fff' return segment.patterns[0].route.getTextColor() } } } ], 'font-family': 'sans-serif', 'font-size': 15 } /** * All path segments * TODO: update old route-pattern-specific code below */ const segments = { envelope: [ function (display, data, index, utils) { const segment = data if (segment.type !== 'TRANSIT') { return '8px' } if (segment.mode === 3) { return utils.pixels(display.scale, 4, 6, 10) + 'px' } return utils.pixels(display.scale, 6, 10, 14) + 'px' } ], stroke: [ '#008', // Dark blue function (display, segment) { if (!segment.focused) return notFocusedColor if (segment.type === 'TRANSIT') { if (segment.patterns) { if (patternIsDcCirculatorBusRoute(segment.patterns[0])) return '#f00' return segment.patterns[0].route.getColor() } } else if (segment.type === 'CAR') { return '#888' } else if (segment.type.startsWith('BICYCLE')) { return '#f00' } else if ( segment.type.startsWith('MICROMOBILITY') || segment.type.startsWith('SCOOTER') ) { return '#f5a729' } else if (segment.type === 'WALK') { return '#86cdf9' } } ], 'stroke-dasharray': [ false, function (display, data) { const segment = data if (segment.type === 'WALK') return '0, 8' if ( segment.type.startsWith('BICYCLE') || segment.type.startsWith('CAR') || segment.type.startsWith('MICROMOBILITY') || segment.type.startsWith('SCOOTER') ) { return '8, 3' } if (segment.frequency && segment.frequency.average < 12) { if (segment.frequency.average > 6) return '12, 12' return '12, 2' } } ], 'stroke-linecap': [ 'butt', (display, segment) => { if (segment.type === 'WALK') return 'round' } ], 'stroke-width': [ 10, function (display, segment, index, utils) { if (segment.type === 'WALK') return 6 if (segment.type.startsWith('BICYCLE')) return 4 if (segment.type.startsWith('CAR')) return 4 if (segment.type.startsWith('MICROMOBILITY')) return 4 if (segment.type.startsWith('SCOOTER')) return 4 if (segment.mode === 3) return 6 // Buses } ] } /** * Segments Front */ const segmentsFront = { display: [ 'none', function (display, data, index, utils) { if (patternIsDcCirculatorBusRoute(data.pattern)) { return 'inline' } } ], fill: 'none', stroke: '#008', 'stroke-width': function (display, data, index, utils) { return utils.pixels(display.scale, 3, 6, 10) / 2 + 'px' } } /** * Segments Halo */ const segmentsHalo = { fill: 'none', stroke: '#fff', 'stroke-linecap': 'round', 'stroke-width': function (display, data, index, utils) { return data.computeLineWidth(display) + 8 } } /** * Label Containers */ const segmentLabelContainers = { fill: function (display, data) { if (!data.isFocused()) return notFocusedColor }, rx: 3, ry: 3, 'stroke-width': function (display, data) { if (patternIsDcCirculatorBusRoute(data.parent.pattern)) return 1 return 0 } } /** * Checks that a pattern runs a DC Bus Circulator route. This check dates back * to https://github.com/conveyal/transitive.js/commit/b1561dcb6d864fbe6b01de11aa13d06761e1cefd * and was likely added to support CarFreeAtoZ (Arlington, VA). However, this may * need to be removed because Arlington no longer runs this service. Also, it * appears to be hyper-specific to this one implementation. */ function patternIsDcCirculatorBusRoute(pattern) { if (!pattern) return false const { route } = pattern const hasShortName = route && route.route_short_name const isBus = route && route.route_type === 3 return ( hasShortName && isBus && route.route_short_name.toLowerCase().substring(0, 2) === 'dc' ) } export default { labels, multipoints_merged: multipointsMerged, multipoints_pattern: multipointsPattern, places, segment_label_containers: segmentLabelContainers, segment_labels: segmentLabels, segments, segments_front: segmentsFront, segments_halo: segmentsHalo, stops_merged: stopsMerged, stops_pattern: stopsPattern, utils, wireframe_edges: wireframeEdges, wireframe_vertices: wireframeVertices }