/** * Transform the container view center on (x,y) with a scale. * @param x coordinate * @param y coordinate * @param scale */ function transform(x, y, scale) { var translate = [width / 2 - scale * x, height / 2 - scale * y]; container.transition(transition().duration(750)) .attr("transform", "translate(" + translate + ")scale(" + scale + ")") .selectAll(".label") .style("font-size", (15 / scale) + "px") .on("start", function (d) { return d.parent !== focus || d.parent === null ? this.style.display = "none" : undefined; }) .on("end", function (d) { return d.parent === focus ? this.style.display = "inline" : undefined; }); }
// Animation on update // This method is not called for the initial render // It also seems that this method is called several times // during an animation, so you should always test your props // before doing something with this one componentWillReceiveProps(nextProps) { if(!_.isEqual(this.props, nextProps)){ select(findDOMNode(this)) .transition(transition().duration(750)) .attr('x', nextProps.i * 32) .style('fill', 'blue') .on('end', () => { this.setState({ x: nextProps.i * 32, fill: 'blue' }); }); } }
// Animation on enter: // called at the same time as componentDidMount, // it means your element is already on the dom componentWillEnter(callback) { this.setState({ x: this.props.i * 32 }); select(findDOMNode(this)) .transition(transition().duration(750)) .attr('y', 0) .style('fill-opacity', 1) .on('end', () => { this.setState({ y: 0, fillOpacity: 1 }); callback(); }); }
it('should use implicit rather than explicit transition', (done) => { join.transition(transition().duration(timeout * 10).ease(easeLinear)); container = container.selection(); const update = join(container, data); const node = update.enter().node(); expect(node.style.opacity).toBeCloseTo(0.000001, 6); expect(node.parentNode).not.toBe(null); setTimeout(() => { expect(node.style.opacity).not.toBe('1'); expect(node.parentNode).not.toBe(null); done(); }, timeout); });
// Animation on exit componentWillLeave(callback) { this.setState({ fill: 'red' }); select(findDOMNode(this)) .transition(transition().duration(750)) .attr('y', 100) .style('fill-opacity', 0) .style('fill', 'red') .on('end', () => { this.setState({ y: 100, fillOpacity: .1, }); callback(); }); }
constructor() { // You have to call the super() method to initialize the base class. super(); // Stores the currently displayed data so view can be reflown during transitions this._data = null; this._sizeClass = null; this._pie = null; this._stack = null; this.currencySymbols = { BRL: 'R$', EUR: '€', GBP: '£', USD: '$' }; this.fill = { uber: { color: presentation10.standard[presentation10.names.blue], pattern: this._getPattern(presentation10.names.blue) }, hailo: { color: presentation10.standard[presentation10.names.yellow], pattern: this._getPattern(presentation10.names.yellow) }, addisonlee: { color: presentation10.standard[presentation10.names.grey], pattern: this._getPattern(presentation10.names.grey) } }; // TODO: remove once the build pipeline is fixed transition(); // We subscribe to 'storageupdate' updates from the Controller this.controller.subscribe('storageupdate', this.onStorageUpdate.bind(this)); // The SiftView provides lifecycle methods for when the Sift is fully loaded into the DOM (onLoad) // NOTE: the registration of these methods will be handled in the SiftView base class and do not have to be // called by the developer manually in the next version. this.registerOnLoadHandler(this.onLoad.bind(this)); // FIXXME: expose as 'onLoad' }
didReceiveAttrs() { // Schedule a call to our `drawCircles` method on Ember's "render" queue, which will // happen after the component has been placed in the DOM, and subsequently // each time data is changed. run.scheduleOnce('render', this, this.drawCircles) }, drawCircles() { let plot = select(this.element) let data = get(this, 'data') let width = get(this, 'width') let height = get(this, 'height') // Create a transition to use later let t = transition() .duration(250) .ease(easeCubicInOut) // X scale to scale position on x axis let xScale = scaleLinear() .domain(extent(data.map(d => d.timestamp))) .range([0, width]) // Y scale to scale radius of circles proportional to size of plot let yScale = scaleLinear() .domain( // `extent()` requires that data is sorted ascending extent(data.map(d => d.value).sort(ascending)) ) .range([0, height])
function render(element, state) { if (!defined(state.data)) { return; } const margin = defaultValue(state.margin, defaultMargin); const allUnits = getUniqueValues(state.data.map(line => line.units)); const size = Size.calculate(element, margin, state, state.mini ? 1 : allUnits.length); const scales = Scales.calculate(size, state.domain, state.data); const data = state.data; const transitionDuration = defaultValue(state.transitionDuration, defaultTransitionDuration); // The last condition in hasData checks that at least one y-value of one chart is defined. const hasData = (data.length > 0 && data[0].points.length > 0 && data.some(d=>d.points.some(p=>defined(p.y)))); const d3Element = d3Select(element); const svg = d3Element.select('svg') .attr('width', state.width) .attr('height', state.height); const g = d3Select(element).selectAll('.line-chart'); const t = d3Transition().duration(transitionDuration); // Some axis animations need to be a little different if this is the first time we are showing the graph. // Assume it's the first time if we have no lines yet. const lines = g.selectAll('.line').data(data, d => d.id).attr('class', 'line'); const isFirstLine = (!defined(lines.nodes()[0])); const gTransform = 'translate(' + (margin.left + size.yAxesWidth) + ',' + (margin.top + Title.getHeight(state.titleSettings)) + ')'; if (isFirstLine) { g.attr('transform', gTransform); } else { g.transition(t) .attr('transform', gTransform); } const plotArea = d3Element.select('rect.plot-area'); plotArea.attr('width', size.width) .attr('height', size.plotHeight); // Returns a path function which can be called with an array of points. function getPathForUnits(units) { return d3Line() // .curve(d3Shape.curveBasis) .x(d => scales.x(d.x)) .y(d => scales.y[units](d.y)); // NOTE: it was originally 'basic', which is not a interpolation } // Enter. // https://github.com/d3/d3/blob/master/CHANGES.md#selections-d3-selection // If there are undefined or null y-values, just ignore them. This works well for initial and final undefined values, // and simply interpolates over intermediate ones. This may not be what we want. lines.enter().append('path') .attr('class', 'line') .attr('d', line => getPathForUnits(line.units || Scales.unknownUnits)(line.points.filter(point => defined(point.y)))) .style('fill', 'none') .style('opacity', 1e-6) .transition(t) .style('opacity', 1) .style('stroke', d => defined(d.color) ? d.color : ''); // Mouse event. lines .on('mouseover', fade(g, 0.33)) .on('mouseout', fade(g, 1)); // Update. // Same approach to undefined or null y-values as enter. lines .transition(t) .attr('d', line => getPathForUnits(line.units || Scales.unknownUnits)(line.points.filter(point => defined(point.y)))) .style('opacity', 1) .style('stroke', d => defined(d.color) ? d.color : ''); // Exit. lines.exit() .transition(t) .style('opacity', 1e-6) .remove(); // Title. Title.enterUpdateAndExit(d3Element, state.titleSettings, margin, data, transitionDuration); // Hilighted data and tooltips. if (defined(state.tooltipSettings)) { const tooltip = Tooltip.select(state.tooltipSettings); // Whenever the chart updates, remove the hilighted points and tooltips. const boundHilightDataAndShowTooltip = highlightDataAndShowTooltip.bind(null, hasData, data, state, scales, g, tooltip); unhilightDataAndHideTooltip(g, tooltip); svg.on('mouseover', boundHilightDataAndShowTooltip) .on('mousemove', boundHilightDataAndShowTooltip) .on('click', boundHilightDataAndShowTooltip) .on('mouseout', unhilightDataAndHideTooltip.bind(null, g, tooltip)); } if (defined(state.highlightX)) { const selectedData = findSelectedData(data, state.highlightX); hilightData(selectedData, scales, g); } // Create the y-axes as needed. const yAxisElements = g.select('.y.axes').selectAll('.y.axis').data(allUnits, d => d); const newYAxisElements = yAxisElements.enter().append('g') .attr('class', 'y axis') .style('opacity', 1e-6); newYAxisElements.append('g') .attr('class', 'ticks'); newYAxisElements.append('g') .attr('class', 'colors'); newYAxisElements.append('g') .attr('class', 'units-label-shadow-group') // This doesn't yet do what we want, because it comes before the ticks. .attr('transform', 'translate(' + -(Size.yAxisWidth - yAxisLabelWidth) + ',6)rotate(270)') .append('text') .attr('class', 'units-label-shadow') .style('text-anchor', 'end'); newYAxisElements.append('g') .attr('class', 'units-label-group') .attr('transform', 'translate(' + -(Size.yAxisWidth - yAxisLabelWidth) + ',6)rotate(270)') .append('text') .attr('class', 'units-label') .style('text-anchor', 'end'); yAxisElements.exit().remove(); // No data. Show message if no data to show. var noData = g.select('.no-data') .style('opacity', hasData ? 1e-6 : 1); noData.select('text') .text(defaultNoDataText) .style('text-anchor', 'middle') .attr('x', element.offsetWidth / 2 - margin.left - size.yAxesWidth) .attr('y', (size.height - 24) / 2); // Axes. if (!defined(scales)) { return; } // An extra calculation to decide whether we want the last automatically-generated tick value. const xTickValues = Scales.truncatedTickValues(scales.x, Math.min(12, Math.floor(size.width / 150) + 1)); const xAxis = d3AxisBottom() .tickValues(xTickValues); if (defined(state.grid) && state.grid.x) { // Note this only extends up; if the axis is not at the bottom, we need to translate the ticks down too. xAxis.tickSizeInner(-size.plotHeight); } const yAxis = d3AxisLeft() .tickSizeOuter((allUnits.length > 1) ? 0 : 3); let y0 = 0; let mainYScale; if (defined(scales)) { mainYScale = scales.y[allUnits[0] || Scales.unknownUnits]; xAxis.scale(scales.x); y0 = Math.min(Math.max(mainYScale(0), 0), size.plotHeight); } // Mini charts have the x-axis label at the bottom, regardless of where the x-axis would actually be. if (state.mini) { y0 = size.plotHeight; } // If this is the first line, start the x-axis in the right place straight away. if (isFirstLine) { g.select('.x.axis').attr('transform', 'translate(0,' + y0 + ')'); } g.select('.x.axis') .transition(t) .attr('transform', 'translate(0,' + y0 + ')') .style('opacity', 1) .call(xAxis); if (defined(state.grid) && state.grid.x) { // Recall the x-axis-grid lines only extended up; we need to translate the ticks down to the bottom of the plot. g.selectAll('.x.axis line').attr('transform', 'translate(0,' + (size.plotHeight - y0) + ')'); } // If mini with label, or no data: hide the ticks, but not the axis, so the x-axis label can still be shown. var hasXLabel = defined(state.axisLabel) && defined(state.axisLabel.x); g.select('.x.axis') .selectAll('.tick') .transition(t) .style('opacity', ((state.mini && hasXLabel) || !hasData) ? 1e-6 : 1); if (hasXLabel) { g.select('.x.axis .label') .style('text-anchor', 'middle') .text(state.axisLabel.x) .style('opacity', hasData ? 1 : 1e-6) // Translate the x-axis-label to the bottom of the plot, even if the x-axis itself is in the middle or the top. .attr('transform', 'translate(' + (size.width / 2) + ', ' + (size.height - y0) + ')'); } if (state.mini) { yAxis.tickSize(0, 0); } const yAxisElementsElements = g.select('.y.axes').selectAll('.y.axis'); yAxisElementsElements .transition(t) .attr('transform', (d, i) => ('translate(' + (- i * Size.yAxisWidth) + ', 0)')); yAxisElementsElements.nodes().forEach(yAxisElement => { const yAxisD3 = d3Select(yAxisElement); const theseUnits = yAxisElement.__data__; const thisYScale = scales.y[theseUnits || Scales.unknownUnits]; // Only show the horizontal grid lines for the main y-axis, or it gets too confusing. const tickSizeInner = (thisYScale === mainYScale && defined(state.grid) && state.grid.y) ? -size.width : 3; let yTickValues; if (state.mini) { yTickValues = thisYScale.domain(); } else { const numYTicks = state.mini ? 2 : Math.min(6, Math.floor(size.plotHeight / 30) + 1); yTickValues = Scales.truncatedTickValues(thisYScale, numYTicks); } yAxis .tickValues(yTickValues) .tickSizeInner(tickSizeInner) .scale(thisYScale); yAxisD3 .transition(t) .style('opacity', hasData ? 1 : 1e-6) .select('.ticks') .call(yAxis); let colorKeyHtml = ''; if (allUnits.length > 1) { const unitColors = data.filter(line=>(line.units === theseUnits)).map(line=>line.color); unitColors.forEach((color, index)=>{ const y = -1 - index * 4; colorKeyHtml += '<path d="M-30 ' + y + ' h 30" style="stroke:' + color + '"/>'; }); } yAxisD3.select('.colors').html(colorKeyHtml); yAxisD3.select('.units-label').text(theseUnits || ''); yAxisD3.select('.units-label-shadow').text(theseUnits || ''); }); }
static render(container, state) { if (!defined(state.data)) { return; } initializeChartTypes(state); const data = state.data; const margin = defaultValue(state.margin, defaultMargin); const units = uniq( state.data.filter(data => data.type !== "moment").map(data => data.units) ); const size = Size.calculate( container, margin, state, state.mini ? 1 : units.length ); const scales = Scales.calculate( size, state.domain, state.data, this.getXpadding(state) ); const transitionDuration = defaultValue( state.transitionDuration, defaultTransitionDuration ); // The last condition in hasData checks that at least one y-value of one chart is defined. const hasData = data.length > 0 && data[0].points.length > 0 && data.some(d => d.points.some(p => defined(p.y))); const d3Container = d3Select(container); // Update SVG const chartSVGContainer = d3Container .select("svg") .attr("width", state.width) .attr("height", state.height) .style("user-select", "none") .style("cursor", "default"); // Update Plot Area const plotArea = d3Container.select("rect.plot-area"); plotArea.attr("width", size.width).attr("height", size.plotHeight); // Update chart area size.yChartOffset = margin.top + Title.getHeight(state.titleSettings); const yOffsets = computeYOffset(state, units, scales, size); // Adjust x offset let totalOffset; if (yOffsets.length > 0) { totalOffset = yOffsets.reduce((a, b) => a + b); } else { const remainingGap = container.clientWidth - size.width; totalOffset = Math.min(Size.yAxisWidth, remainingGap / 2); } size.yAxesWidth = totalOffset; const chartTransform = "translate(" + (margin.left + size.yAxesWidth) + "," + size.yChartOffset + ")"; const chartTransition = d3Transition().duration(transitionDuration); const chart = d3Select(container) .selectAll(".base-chart") .attr("transform", chartTransform); const chartPlotContainer = chart.select(".content"); Title.enterUpdateAndExit( d3Container, state.titleSettings, margin, state, transitionDuration ); const renderContext = { // helpful baseline input container, state, // helpful constraints size, yOffsets, margin, scales, units, // helpful chart specifics chart, chartPlotContainer, chartTransform, chartTransition, chartSVGContainer }; // Axes. if (defined(scales)) { this.renderAxis(renderContext, hasData); } this.renderChart(renderContext); this.renderToolTip(renderContext, hasData); this.renderHighlight(renderContext); // No data. Show message if no data to show. this.renderNoData(renderContext, hasData); }
export default function tip(id) { let d3_tip_functor = v => (typeof v === "function" ? v : () => v); let d3_tip_direction = () => 'n'; let d3_tip_offset = () => [0, 0]; let d3_tip_html = () => ' '; let IsDOMElement = o => o instanceof Node; dummy(); // dummy injection for transition let direction = d3_tip_direction, offset = d3_tip_offset, html = d3_tip_html, classed = 'd3-tip', node = initNode(), point = null, target = null, parent = null, theme = 'light', transition= false, style = undefined, zIndex = 16777271; function initNode() { let node = select('div' + (id ? '#' + id : '.' + classed)); if (node.empty()) node = select(document.createElement('div')) node .attr('id', id) .attr('class', classed) .style('position','absolute') .style('top', 0) .style('left', 0) .style('opacity', 0) .style('pointer-events', 'none') .style('box-sizing', 'border-box'); return node.node() } function getSVGNode(el) { el = el.node() if(!el) return; return el.tagName.toLowerCase() === 'svg' ? el : el.ownerSVGElement; } function getNodeEl() { //TODO: this check might not be valid any more if(node === null) { node = initNode(); // re-add node to DOM parent.appendChild(node); } return select(node); } function _impl(vis) { if(!parent) { document.body.appendChild(node); } let svg = getSVGNode(vis) if (!svg) return; if (svg.createSVGPoint != null) { point = svg.createSVGPoint(); } svg = select(svg); let defsEl = svg.select('defs'); if (defsEl.empty()) { defsEl = svg.append('defs'); } let _style = style; if (_style === undefined) { _style = _impl.defaultStyle(theme, DEFAULT_WIDTH); } let styleEl = defsEl.selectAll('style' + (id ? '#style-tip-' + id : '.style-' + classed)).data(_style ? [ _style ] : []); styleEl.exit().remove(); styleEl = styleEl.enter() .append('style') .attr('type', 'text/css') .attr('id', (id ? 'style-tip-' + id : null)) .attr('class', (id ? null : 'style-' + classed)) .merge(styleEl); styleEl.text(s => s); } _impl.self = function() { return 'div' + (id ? '#' + id : '.' + classed); } _impl.id = function() { return id; }; _impl.classed = function(_) { return arguments.length ? (classed = _, _impl) : classed; }; // Public - show the tooltip on the screen // // Returns a tip _impl.show = function() { if(!parent) _impl.parent(document.body); let args = [].slice.call(arguments); target = this; let standalone = false; if(args.length === 1 && IsDOMElement(args[0])){ target = args[0]; args[0] = target.__data__; standalone = true; } let content = html.apply(target, args); if (content === null) return _impl; let poffset = offset.apply(target, args), dir = direction ? direction.apply(target, args) : null, nodel = getNodeEl(), i = directions.length, parentCoords = node.offsetParent.getBoundingClientRect(); while(i--) nodel.classed(directions[i], false); if (content != null) { nodel.html(content) } if (dir) { nodel.classed(dir, true); } else { dir = 'n'; } let coords = direction_callbacks[dir].apply(target); nodel .style('top', (coords.top + poffset[0]) - parentCoords.top + 'px') .style('left', (coords.left + poffset[1]) - parentCoords.left + 'px') .style('z-index', zIndex); if(standalone){ window.addEventListener('load', function() { // for testing // console.log('offsets',node.offsetHeight, node.offsetWidth) coords = direction_callbacks[dir].apply(target); nodel .style('top', (coords.top + poffset[0]) - parentCoords.top + 'px') .style('left', (coords.left + poffset[1]) - parentCoords.left + 'px') }); } if (transition != null && transition !== false) { nodel = nodel.transition(); if (typeof transition === 'number') { nodel = nodel.duration(transition); } } nodel.style('opacity', 1.0); return _impl; } // Public - hide the tooltip // // Returns a tip _impl.hide = function() { let nodel = getNodeEl(); if (nodel.interrupt) { nodel.interrupt(); // stop the fade in if happening } nodel.style('opacity', 0.0); return _impl; } // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value. // // n - name of the attribute // v - value of the attribute // // Returns tip or attribute value _impl.attr = function(n) { if (arguments.length < 2 && typeof n === 'string') { return getNodeEl().attr(n) } else { let args = [].slice.call(arguments) selection.prototype.attr.apply(getNodeEl(), args) } return _impl; } // Public: Set or get the direction of the tooltip // // v - One of n(north), s(south), e(east), or w(west), nw(northwest), // sw(southwest), ne(northeast) or se(southeast) // // Returns tip or direction _impl.direction = function(v) { if (!arguments.length) return direction direction = v == null ? v : d3_tip_functor(v) return _impl; } // Public: Sets or gets the offset of the tip // // v - Array of [x, y] offset // // Returns offset or _impl.offset = function(v) { if (!arguments.length) return offset offset = v == null ? v : d3_tip_functor(v) return _impl; } // Public: sets or gets the html value of the tooltip // // v - String value of the tip // // Returns html value or tip _impl.html = function(v) { if (!arguments.length) return html html = v == null ? v : d3_tip_functor(v) return _impl; } // Public: destroys the tooltip and removes it from the DOM // // Returns a tip _impl.destroy = function() { if(node) { getNodeEl().remove(); node = null; } return _impl; } _impl.style = function(_) { return arguments.length ? (style = _, _impl) : style; } _impl.transition = function(_) { return arguments.length ? (transition = _, _impl) : transition; } _impl.theme = function(_) { return arguments.length ? (theme = _, _impl) : theme; } _impl.zIndex = function(_) { return arguments.length ? (zIndex = _, _impl) : zIndex; } _impl.parent = function(v) { if (!arguments.length) return parent; parent = v || document.body; parent.appendChild(node); // Make sure offsetParent has a position so the tip can be // based from it. Mainly a concern with <body>. let offsetParent = select(node.offsetParent) if (offsetParent.style('position') === 'static') { offsetParent.style('position', 'relative') } return _impl; } _impl.defaultStyle = (_theme, _width) => ` ${fonts.fixed.cssImport} ${_impl.self()} { line-height: 1; font-family: ${fonts.fixed.family}; color: ${display[_theme].negative.text}; font-weight: ${fonts.fixed.weightMonochrome}; font-size: ${fonts.fixed.sizeForWidth(_width)}; padding: 8px; background: ${display[_theme].negative.background}; border-radius: 2px; pointer-events: none; } /* Creates a small triangle extender for the tooltip */ ${_impl.self()}:after { box-sizing: border-box; display: inline; width: 100%; line-height: 1; color: ${display[_theme].negative.background}; font-size: ${fonts.fixed.sizeForWidth(1)}; position: absolute; pointer-events: none; } /* Northward tooltips */ ${_impl.self()}.n:after { content: "\\25bc"; margin: -3px 0 0 0; top: 100%; left: 0; text-align: center; } /* Eastward tooltips */ ${_impl.self()}.e:after { content: "\\25C0"; margin: -7px 0 0 0; top: 50%; left: -7px; } /* Southward tooltips */ ${_impl.self()}.s:after { content: "\\25B2"; margin: 0 0 1px 0; top: -10px; left: 0; text-align: center; } /* Westward tooltips */ ${_impl.self()}.w:after { content: "\\25B6"; margin: -7px 0 0 0; top: 50%; left: 100%; } `; function direction_n() { let bbox = getScreenBBox() return { top: (bbox.n.y - node.offsetHeight), left: (bbox.n.x - node.offsetWidth / 2) } } function direction_s() { let bbox = getScreenBBox() return { top: (bbox.s.y), left: (bbox.s.x - node.offsetWidth / 2) } } function direction_e() { let bbox = getScreenBBox() return { top: (bbox.e.y - node.offsetHeight / 2), left: (bbox.e.x) } } function direction_w() { let bbox = getScreenBBox() return { top: (bbox.w.y - node.offsetHeight / 2), left: (bbox.w.x - node.offsetWidth) } } function direction_nw() { let bbox = getScreenBBox() return { top: (bbox.nw.y - node.offsetHeight), left: (bbox.nw.x - node.offsetWidth) } } function direction_ne() { let bbox = getScreenBBox() return { top: (bbox.ne.y - node.offsetHeight), left: (bbox.ne.x) } } function direction_sw() { let bbox = getScreenBBox() return { top: (bbox.sw.y), left: (bbox.sw.x - node.offsetWidth) } } function direction_se() { let bbox = getScreenBBox() return { top: (bbox.se.y), left: (bbox.se.x) } } // Private - gets the screen coordinates of a shape // // Given a shape on the screen, will return an SVGPoint for the directions // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest), // sw(southwest). // // +-+-+ // | | // + + // | | // +-+-+ // // Returns an Object {n, s, e, w, nw, sw, ne, se} function getScreenBBox() { let targetel = target || event.target; while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) { targetel = targetel.parentNode; } let bbox = {}, matrix = targetel.getScreenCTM(), tbbox = targetel.getBBox(), width = tbbox.width, height = tbbox.height, x = tbbox.x, y = tbbox.y point.x = x point.y = y bbox.nw = point.matrixTransform(matrix) point.x += width bbox.ne = point.matrixTransform(matrix) point.y += height bbox.se = point.matrixTransform(matrix) point.x -= width bbox.sw = point.matrixTransform(matrix) point.y -= height / 2 bbox.w = point.matrixTransform(matrix) point.x += width bbox.e = point.matrixTransform(matrix) point.x -= width / 2 point.y -= height / 2 bbox.n = point.matrixTransform(matrix) point.y += height bbox.s = point.matrixTransform(matrix) return bbox; } let direction_callbacks = { n: direction_n, s: direction_s, e: direction_e, w: direction_w, nw: direction_nw, ne: direction_ne, sw: direction_sw, se: direction_se }, directions = Object.keys(direction_callbacks); return _impl; }