UNPKG

transitive-js

Version:

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

251 lines (212 loc) 6.92 kB
import LinearScale from '../util/linear-scale.js' export default class Display { constructor(transitive) { this.transitive = transitive this.zoomFactors = transitive.options.zoomFactors || this.getDefaultZoomFactors() this.updateActiveZoomFactors(1) } setDimensions(width, height) { this.width = width this.height = height } setXDomain(domain) { // [minX , maxX] this.xDomain = domain this.xScale = new LinearScale(domain, [0, this.width]) if (!this.initialXDomain) { this.initialXDomain = domain this.initialXRes = (domain[1] - domain[0]) / this.width } } setYDomain(domain) { // [minY , maxY] this.yDomain = domain this.yScale = new LinearScale(domain, [this.height, 0]) if (!this.initialYDomain) this.initialYDomain = domain } fitToWorldBounds(bounds) { const domains = this.computeDomainsFromBounds(bounds) this.setXDomain(domains[0]) this.setYDomain(domains[1]) this.computeScale() } reset() { this.initialXDomain = null this.initialYDomain = null this.scaleSet = false this.lastScale = undefined } /** * Apply a transformation {x, y, k} to the *initial* state of the map, where * (x, y) is the pixel offset and k is a scale factor relative to an initial * zoom level of 1.0. Intended primarily to support D3-style panning/zooming. */ applyTransform(transform) { const { k, x, y } = transform let xMin = this.initialXDomain[0] let xMax = this.initialXDomain[1] let yMin = this.initialYDomain[0] let yMax = this.initialYDomain[1] // Apply the scale factor xMax = xMin + (xMax - xMin) / k yMin = yMax - (yMax - yMin) / k // Apply the translation const xOffset = (-x * (xMax - xMin)) / this.width xMin += xOffset xMax += xOffset const yOffset = (y * (yMax - yMin)) / this.height yMin += yOffset yMax += yOffset // Update the scale functions and recompute the internal scale factor this.setXDomain([xMin, xMax]) this.setYDomain([yMin, yMax]) this.computeScale() } getDefaultZoomFactors(data) { return [ { angleConstraint: 45, gridCellSize: 25, internalVertexFactor: 1000000, mergeVertexThreshold: 200, minScale: 0 }, { angleConstraint: 5, gridCellSize: 0, internalVertexFactor: 0, mergeVertexThreshold: 0, minScale: 1.5 } ] } updateActiveZoomFactors(scale) { let updated = false for (let i = 0; i < this.zoomFactors.length; i++) { const min = this.zoomFactors[i].minScale const max = i < this.zoomFactors.length - 1 ? this.zoomFactors[i + 1].minScale : Number.MAX_VALUE // check if we've crossed into a new zoomFactor partition if ( (!this.lastScale || this.lastScale < min || this.lastScale >= max) && scale >= min && scale < max ) { this.activeZoomFactors = this.zoomFactors[i] updated = true } } return updated } computeScale() { this.lastScale = this.scale this.scaleSet = true const newXRes = (this.xDomain[1] - this.xDomain[0]) / this.width this.scale = this.initialXRes / newXRes if (this.lastScale !== this.scale) this.scaleChanged() } scaleChanged() { const zoomFactorsChanged = this.updateActiveZoomFactors(this.scale) if (zoomFactorsChanged) { this.transitive.network = null this.transitive.render() } } /** * Compute the x/y coordinate space domains to fit the graph. */ computeDomainsFromBounds(bounds) { const xmin = bounds[0][0] const xmax = bounds[1][0] const ymin = bounds[0][1] const ymax = bounds[1][1] const xRange = xmax - xmin const yRange = ymax - ymin const { options } = this.transitive const paddingFactor = options && options.paddingFactor ? options.paddingFactor : 0.1 const margins = this.getMargins() const usableHeight = this.height - margins.top - margins.bottom const usableWidth = this.width - margins.left - margins.right const displayAspect = this.width / this.height const usableDisplayAspect = usableWidth / usableHeight const graphAspect = xRange / (yRange === 0 ? -Infinity : yRange) let padding let dispX1, dispX2, dispY1, dispY2 let dispXRange, dispYRange if (usableDisplayAspect > graphAspect) { // y-axis is limiting padding = paddingFactor * yRange dispY1 = ymin - padding dispY2 = ymax + padding dispYRange = yRange + 2 * padding const addedYRange = (this.height / usableHeight) * dispYRange - dispYRange if (margins.top > 0 || margins.bottom > 0) { dispY1 -= (margins.bottom / (margins.bottom + margins.top)) * addedYRange dispY2 += (margins.top / (margins.bottom + margins.top)) * addedYRange } dispXRange = (dispY2 - dispY1) * displayAspect const xOffset = (margins.left - margins.right) / this.width const xMidpoint = (xmax + xmin - dispXRange * xOffset) / 2 dispX1 = xMidpoint - dispXRange / 2 dispX2 = xMidpoint + dispXRange / 2 } else { // x-axis limiting padding = paddingFactor * xRange dispX1 = xmin - padding dispX2 = xmax + padding dispXRange = xRange + 2 * padding const addedXRange = (this.width / usableWidth) * dispXRange - dispXRange if (margins.left > 0 || margins.right > 0) { dispX1 -= (margins.left / (margins.left + margins.right)) * addedXRange dispX2 += (margins.right / (margins.left + margins.right)) * addedXRange } dispYRange = (dispX2 - dispX1) / displayAspect const yOffset = (margins.bottom - margins.top) / this.height const yMidpoint = (ymax + ymin - dispYRange * yOffset) / 2 dispY1 = yMidpoint - dispYRange / 2 dispY2 = yMidpoint + dispYRange / 2 } return [ [dispX1, dispX2], [dispY1, dispY2] ] } getMargins() { return Object.assign( { bottom: 0, left: 0, right: 0, top: 0 }, this.transitive.options.displayMargins ) } isInRange(x, y) { return x >= 0 && x <= this.width && y >= 0 && y <= this.height } /** Methods to be defined by subclasses **/ clear() { throw new Error('method not implemented by subclass!') } drawCircle(coord, attrs) { throw new Error('method not implemented by subclass!') } drawEllipse(coord, attrs) { throw new Error('method not implemented by subclass!') } drawRect(upperLeft, attrs) { throw new Error('method not implemented by subclass!') } drawText(text, anchor, attrs) { throw new Error('method not implemented by subclass!') } drawPath(renderData, attrs) { throw new Error('method not implemented by subclass!') } }