From 17f6fb8fd074f5975b1f0607cfa1f6faa4f0d95a Mon Sep 17 00:00:00 2001 From: Andrei Palchys Date: Wed, 8 Nov 2017 17:52:28 +0300 Subject: [PATCH 1/4] add crossline functionality --- src/components/fx/hover.js | 133 ++++++++++++++- src/components/fx/index.js | 1 + src/plot_api/plot_api.js | 3 + src/plots/cartesian/axes.js | 26 ++- src/plots/cartesian/layout_attributes.js | 22 +++ src/plots/cartesian/layout_defaults.js | 7 + test/jasmine/tests/hover_crossline_test.js | 179 +++++++++++++++++++++ 7 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 test/jasmine/tests/hover_crossline_test.js diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 37d80fbc126..3315f1ff626 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -232,7 +232,13 @@ function _hover(gd, evt, subplot, noHoverEvent) { xval, yval, pointData, - closedataPreviousLength; + closedataPreviousLength, + + // crosslinePoints: the set of candidate points we've found to draw crosslines to + crosslinePoints = { + hLinePoint: null, + vLinePoint: null + }; // Figure out what we're hovering on: // mouse location or user-supplied data @@ -402,10 +408,60 @@ function _hover(gd, evt, subplot, noHoverEvent) { hoverData.splice(0, closedataPreviousLength); distance = hoverData[0].distance; } + + var showSpikes = fullLayout.xaxis && fullLayout.xaxis.showspikes && fullLayout.yaxis && fullLayout.yaxis.showspikes; + var showCrosslines = fullLayout.xaxis && fullLayout.xaxis.showcrossline || fullLayout.yaxis && fullLayout.yaxis.showcrossline; + + if(fullLayout._has('cartesian') && showCrosslines && !(showSpikes && hovermode === 'closest')) { + // Now find the points for the crosslines. + if(fullLayout.yaxis.showcrossline) { + crosslinePoints.hLinePoint = findCrosslinePoint(pointData, xval, yval, 'y', crosslinePoints.hLinePoint); + } + if(fullLayout.xaxis.showcrossline) { + crosslinePoints.vLinePoint = findCrosslinePoint(pointData, xval, yval, 'x', crosslinePoints.vLinePoint); + } + } } - // nothing left: remove all labels and quit - if(hoverData.length === 0) return dragElement.unhoverRaw(gd, evt); + function findCrosslinePoint(pointData, xval, yval, mode, endPoint) { + var resultPoint = endPoint; + pointData.distance = Infinity; + pointData.index = false; + var closestPoints = trace._module.hoverPoints(pointData, xval, yval, mode); + if(closestPoints) { + var closestPt = closestPoints[0]; + if(isNumeric(closestPt.x0) && isNumeric(closestPt.y0)) { + var tmpPoint = { + xa: closestPt.xa, + ya: closestPt.ya, + x0: closestPt.x0, + x1: closestPt.x1, + y0: closestPt.y0, + y1: closestPt.y1, + distance: closestPt.distance, + curveNumber: closestPt.trace.index, + pointNumber: closestPt.index + }; + if(!resultPoint || (resultPoint.distance > tmpPoint.distance)) { + resultPoint = tmpPoint; + } + } + } + return resultPoint; + } + + // if hoverData is empty check for the crosslines to draw and quit if there are none + if(hoverData.length === 0) { + var result = dragElement.unhoverRaw(gd, evt); + if(fullLayout._has('cartesian') && ((crosslinePoints.hLinePoint !== null) || (crosslinePoints.vLinePoint !== null))) { + createCrosslines(crosslinePoints, fullLayout); + } + return result; + } + + if(fullLayout._has('cartesian')) { + createCrosslines(crosslinePoints, fullLayout); + } hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); @@ -1108,6 +1164,77 @@ function cleanPoint(d, hovermode) { return d; } +function createCrosslines(hoverData, fullLayout) { + var showXSpikeline = fullLayout.xaxis && fullLayout.xaxis.showspikes; + var showYSpikeline = fullLayout.yaxis && fullLayout.yaxis.showspikes; + var showH = fullLayout.yaxis && fullLayout.yaxis.showcrossline; + var showV = fullLayout.xaxis && fullLayout.xaxis.showcrossline; + var container = fullLayout._hoverlayer; + var hovermode = fullLayout.hovermode; + if(!(showV || showH) || (showXSpikeline && showYSpikeline && hovermode === 'closest')) return; + var hLinePoint, + vLinePoint, + xa, + ya, + hLinePointY, + vLinePointX; + + // Remove old crossline items + container.selectAll('.crossline').remove(); + + var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor); + var dfltCrosslineColor = Color.contrast(contrastColor); + + // do not draw a crossline if there is a spikeline + if(showV && !(showXSpikeline && hovermode === 'closest')) { + vLinePoint = hoverData.vLinePoint; + xa = vLinePoint.xa; + vLinePointX = xa._offset + (vLinePoint.x0 + vLinePoint.x1) / 2; + + var xThickness = xa.crosslinethickness; + var xDash = xa.crosslinedash; + var xColor = xa.crosslinecolor || dfltCrosslineColor; + + // Foreground vertical line (to x-axis) + container.insert('line', ':first-child') + .attr({ + 'x1': vLinePointX, + 'x2': vLinePointX, + 'y1': xa._counterSpan[0], + 'y2': xa._counterSpan[1], + 'stroke-width': xThickness, + 'stroke': xColor, + 'stroke-dasharray': Drawing.dashStyle(xDash, xThickness) + }) + .classed('crossline', true) + .classed('crisp', true); + } + + if(showH && !(showYSpikeline && hovermode === 'closest')) { + hLinePoint = hoverData.hLinePoint; + ya = hLinePoint.ya; + hLinePointY = ya._offset + (hLinePoint.y0 + hLinePoint.y1) / 2; + + var yThickness = ya.crosslinethickness; + var yDash = ya.crosslinedash; + var yColor = ya.crosslinecolor || dfltCrosslineColor; + + // Foreground horizontal line (to y-axis) + container.insert('line', ':first-child') + .attr({ + 'x1': ya._counterSpan[0], + 'x2': ya._counterSpan[1], + 'y1': hLinePointY, + 'y2': hLinePointY, + 'stroke-width': yThickness, + 'stroke': yColor, + 'stroke-dasharray': Drawing.dashStyle(yDash, yThickness) + }) + .classed('crossline', true) + .classed('crisp', true); + } +} + function createSpikelines(hoverData, opts) { var hovermode = opts.hovermode; var container = opts.container; diff --git a/src/components/fx/index.js b/src/components/fx/index.js index dd69c0315e3..db19b820499 100644 --- a/src/components/fx/index.js +++ b/src/components/fx/index.js @@ -59,6 +59,7 @@ function loneUnhover(containerOrSelection) { selection.selectAll('g.hovertext').remove(); selection.selectAll('.spikeline').remove(); + selection.selectAll('.crossline').remove(); } // helpers for traces that use Fx.loneHover diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index e4a1e83d9bb..ca734401ef2 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -166,6 +166,9 @@ Plotly.plot = function(gd, data, layout, config) { // save initial show spikes once per graph if(graphWasEmpty) Plotly.Axes.saveShowSpikeInitial(gd); + // save initial show crosslines once per graph + if(graphWasEmpty) Plotly.Axes.saveShowCrosslineInitial(gd); + // prepare the data and find the autorange // generate calcdata, if we need to diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 3b24d00cc41..da6bf1a8bdd 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -426,6 +426,30 @@ axes.saveShowSpikeInitial = function(gd, overwrite) { return hasOneAxisChanged; }; +// save a copy of the initial crossline visibility +axes.saveShowCrosslineInitial = function(gd, overwrite) { + var axList = axes.list(gd, '', true), + hasOneAxisChanged = false; + + for(var i = 0; i < axList.length; i++) { + var ax = axList[i]; + + var isNew = (ax._showCrosslineInitial === undefined); + var hasChanged = ( + isNew || !( + ax.showcrossline === ax._showcrossline + ) + ); + + if((isNew) || (overwrite && hasChanged)) { + ax._showCrosslineInitial = ax.showcrossline; + hasOneAxisChanged = true; + } + + } + return hasOneAxisChanged; +}; + // axes.expand: if autoranging, include new data in the outer limits // for this axis // data is an array of numbers (ie already run through ax.d2c) @@ -2121,7 +2145,7 @@ axes.doTicks = function(gd, axid, skipTitle) { top: pos, bottom: pos, left: ax._offset, - rigth: ax._offset + ax._length, + right: ax._offset + ax._length, width: ax._length, height: 0 }; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index aae0ac26b43..796a8ecd3ee 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -387,6 +387,28 @@ module.exports = { 'plotted on' ].join(' ') }, + showcrossline: { + valType: 'boolean', + dflt: false, + role: 'style', + editType: 'none', + description: 'Determines whether or not crossline are drawn for this axis.' + }, + crosslinecolor: { + valType: 'color', + dflt: null, + role: 'style', + editType: 'none', + description: 'Sets the crossline color. If undefined, will use the contrast to background color' + }, + crosslinethickness: { + valType: 'number', + dflt: 2, + role: 'style', + editType: 'none', + description: 'Sets the width (in px) of the zero line.' + }, + crosslinedash: extendFlat({}, dash, {dflt: 'solid', editType: 'none'}), tickfont: fontAttrs({ editType: 'ticks', description: 'Sets the tick font.' diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 6765fdcdec7..7061388ec3a 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -191,6 +191,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut); + var showCrossline = coerce('showcrossline'); + if(showCrossline) { + coerce('crosslinecolor'); + coerce('crosslinethickness'); + coerce('crosslinedash'); + } + var showSpikes = coerce('showspikes'); if(showSpikes) { coerce('spikecolor'); diff --git a/test/jasmine/tests/hover_crossline_test.js b/test/jasmine/tests/hover_crossline_test.js new file mode 100644 index 00000000000..3150959af79 --- /dev/null +++ b/test/jasmine/tests/hover_crossline_test.js @@ -0,0 +1,179 @@ +var d3 = require('d3'); + +var Plotly = require('@lib/index'); +var Fx = require('@src/components/fx'); +var Lib = require('@src/lib'); + +var fail = require('../assets/fail_test'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); + +describe('crossline', function() { + 'use strict'; + + afterEach(destroyGraphDiv); + + describe('hover', function() { + var gd; + + function makeMock() { + var _mock = Lib.extendDeep({}, require('@mocks/19.json')); + _mock.layout.xaxis.showcrossline = true; + _mock.layout.yaxis.showcrossline = true; + _mock.layout.xaxis2.showcrossline = true; + _mock.layout.hovermode = 'closest'; + return _mock; + } + + function _hover(evt, subplot) { + Fx.hover(gd, evt, subplot); + Lib.clearThrottle(); + } + + function _assert(lineExpect) { + var TOL = 5; + var lines = d3.selectAll('line.crossline'); + + expect(lines.size()).toBe(lineExpect.length, '# of line nodes'); + + lines.each(function(_, i) { + var sel = d3.select(this); + ['x1', 'y1', 'x2', 'y2'].forEach(function(d, j) { + expect(sel.attr(d)) + .toBeWithin(lineExpect[i][j], TOL, 'line ' + i + ' attr ' + d); + }); + }); + } + + it('draws lines and markers on enabled axes in the closest hovermode', function(done) { + gd = createGraphDiv(); + var _mock = makeMock(); + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}, 'xy'); + _assert( + [[80, 250, 1033, 250], [557, 100, 557, 401]] + ); + }) + .then(function() { + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[651, 167, 985, 167], [820, 115, 820, 220]] + ); + }) + .catch(fail) + .then(done); + }); + + it('draws lines and markers on enabled axes in the x hovermode', function(done) { + gd = createGraphDiv(); + var _mock = makeMock(); + + _mock.layout.hovermode = 'x'; + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}, 'xy'); + _assert( + [[80, 250, 1033, 250], [557, 100, 557, 401]] + ); + }) + .then(function() { + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[651, 167, 985, 167], [820, 115, 820, 220]] + ); + }) + .catch(fail) + .then(done); + }); + + it('draws lines and markers on enabled axes in the y hovermode', function(done) { + gd = createGraphDiv(); + var _mock = makeMock(); + + _mock.layout.hovermode = 'y'; + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}, 'xy'); + _assert( + [[80, 250, 1033, 250], [557, 100, 557, 401]] + ); + }) + .then(function() { + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[652, 167, 985, 167], [820, 115, 820, 220]] + ); + }) + .catch(fail) + .then(done); + }); + + it('does not draw lines and markers on enabled axes if spikes are enabled on the same axes', function(done) { + gd = createGraphDiv(); + var _mock = makeMock(); + + _mock.layout.xaxis.showspikes = true; + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}, 'xy'); + _assert( + [[80, 250, 1033, 250]] + ); + }) + .then(function() { + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[652, 167, 985, 167]] + ); + }) + .catch(fail) + .then(done); + }); + + it('does not draw lines and markers on enabled axes if spikes are enabled on the same axes', function(done) { + gd = createGraphDiv(); + var _mock = makeMock(); + + _mock.layout.yaxis.showspikes = true; + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}, 'xy'); + _assert( + [[557, 100, 557, 401]] + ); + }) + .then(function() { + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[818, 115, 818, 220]] + ); + }) + .catch(fail) + .then(done); + }); + + it('draws lines and markers on enabled axes w/o tick labels', function(done) { + gd = createGraphDiv(); + var _mock = makeMock(); + + _mock.layout.xaxis.showticklabels = false; + _mock.layout.yaxis.showticklabels = false; + + Plotly.plot(gd, _mock).then(function() { + _hover({xval: 2, yval: 3}, 'xy'); + _assert( + [[80, 250, 1033, 250], [557, 100, 557, 401]] + ); + }) + .then(function() { + _hover({xval: 30, yval: 40}, 'x2y2'); + _assert( + [[652, 167, 985, 167], [820, 115, 820, 220]] + ); + }) + .catch(fail) + .then(done); + }); + }); +}); From 771107e9223770fb29ee9be147a5ffada5de0201 Mon Sep 17 00:00:00 2001 From: Andrei Palchys Date: Thu, 9 Nov 2017 12:24:13 +0300 Subject: [PATCH 2/4] fix crossline tests --- test/jasmine/tests/hover_crossline_test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/jasmine/tests/hover_crossline_test.js b/test/jasmine/tests/hover_crossline_test.js index 3150959af79..6ae0350b4d1 100644 --- a/test/jasmine/tests/hover_crossline_test.js +++ b/test/jasmine/tests/hover_crossline_test.js @@ -52,13 +52,13 @@ describe('crossline', function() { Plotly.plot(gd, _mock).then(function() { _hover({xval: 2, yval: 3}, 'xy'); _assert( - [[80, 250, 1033, 250], [557, 100, 557, 401]] + [[80, 250, 1036, 250], [557, 100, 557, 401]] ); }) .then(function() { _hover({xval: 30, yval: 40}, 'x2y2'); _assert( - [[651, 167, 985, 167], [820, 115, 820, 220]] + [[651, 167, 988, 167], [820, 115, 820, 220]] ); }) .catch(fail) @@ -74,13 +74,13 @@ describe('crossline', function() { Plotly.plot(gd, _mock).then(function() { _hover({xval: 2, yval: 3}, 'xy'); _assert( - [[80, 250, 1033, 250], [557, 100, 557, 401]] + [[80, 250, 1036, 250], [557, 100, 557, 401]] ); }) .then(function() { _hover({xval: 30, yval: 40}, 'x2y2'); _assert( - [[651, 167, 985, 167], [820, 115, 820, 220]] + [[651, 167, 988, 167], [820, 115, 820, 220]] ); }) .catch(fail) @@ -96,13 +96,13 @@ describe('crossline', function() { Plotly.plot(gd, _mock).then(function() { _hover({xval: 2, yval: 3}, 'xy'); _assert( - [[80, 250, 1033, 250], [557, 100, 557, 401]] + [[80, 250, 1036, 250], [557, 100, 557, 401]] ); }) .then(function() { _hover({xval: 30, yval: 40}, 'x2y2'); _assert( - [[652, 167, 985, 167], [820, 115, 820, 220]] + [[652, 167, 988, 167], [820, 115, 820, 220]] ); }) .catch(fail) @@ -118,13 +118,13 @@ describe('crossline', function() { Plotly.plot(gd, _mock).then(function() { _hover({xval: 2, yval: 3}, 'xy'); _assert( - [[80, 250, 1033, 250]] + [[80, 250, 1036, 250]] ); }) .then(function() { _hover({xval: 30, yval: 40}, 'x2y2'); _assert( - [[652, 167, 985, 167]] + [[652, 167, 988, 167]] ); }) .catch(fail) @@ -146,7 +146,7 @@ describe('crossline', function() { .then(function() { _hover({xval: 30, yval: 40}, 'x2y2'); _assert( - [[818, 115, 818, 220]] + [[820, 115, 820, 220]] ); }) .catch(fail) From 71bcb2ce9241eb2ea85702ca6959c917e8131ee5 Mon Sep 17 00:00:00 2001 From: Andrei Palchys Date: Thu, 9 Nov 2017 12:48:55 +0300 Subject: [PATCH 3/4] fix crossline tests --- test/jasmine/tests/hover_crossline_test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/hover_crossline_test.js b/test/jasmine/tests/hover_crossline_test.js index 6ae0350b4d1..6e054f3d61c 100644 --- a/test/jasmine/tests/hover_crossline_test.js +++ b/test/jasmine/tests/hover_crossline_test.js @@ -163,13 +163,13 @@ describe('crossline', function() { Plotly.plot(gd, _mock).then(function() { _hover({xval: 2, yval: 3}, 'xy'); _assert( - [[80, 250, 1033, 250], [557, 100, 557, 401]] + [[80, 250, 1036, 250], [557, 100, 557, 401]] ); }) .then(function() { _hover({xval: 30, yval: 40}, 'x2y2'); _assert( - [[652, 167, 985, 167], [820, 115, 820, 220]] + [[652, 167, 988, 167], [820, 115, 820, 220]] ); }) .catch(fail) From f3ed835728ad7624626ed1b69cbb1196e0ae73d2 Mon Sep 17 00:00:00 2001 From: Andrei Palchys Date: Thu, 9 Nov 2017 15:26:12 +0300 Subject: [PATCH 4/4] move the search for crosslinePoints to ensure that the hoverLabels data will not be overwritten --- src/components/fx/hover.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 3315f1ff626..ce06b829326 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -385,6 +385,19 @@ function _hover(gd, evt, subplot, noHoverEvent) { yval = yvalArray[subploti]; } + var showSpikes = fullLayout.xaxis && fullLayout.xaxis.showspikes && fullLayout.yaxis && fullLayout.yaxis.showspikes; + var showCrosslines = fullLayout.xaxis && fullLayout.xaxis.showcrossline || fullLayout.yaxis && fullLayout.yaxis.showcrossline; + + // Find the points for the crosslines first to avoid overwriting the hoverLabels data. + if(fullLayout._has('cartesian') && showCrosslines && !(showSpikes && hovermode === 'closest')) { + if(fullLayout.yaxis.showcrossline) { + crosslinePoints.hLinePoint = findCrosslinePoint(pointData, xval, yval, 'y', crosslinePoints.hLinePoint); + } + if(fullLayout.xaxis.showcrossline) { + crosslinePoints.vLinePoint = findCrosslinePoint(pointData, xval, yval, 'x', crosslinePoints.vLinePoint); + } + } + // Now find the points. if(trace._module && trace._module.hoverPoints) { var newPoints = trace._module.hoverPoints(pointData, xval, yval, mode, fullLayout._hoverlayer); @@ -408,22 +421,11 @@ function _hover(gd, evt, subplot, noHoverEvent) { hoverData.splice(0, closedataPreviousLength); distance = hoverData[0].distance; } - - var showSpikes = fullLayout.xaxis && fullLayout.xaxis.showspikes && fullLayout.yaxis && fullLayout.yaxis.showspikes; - var showCrosslines = fullLayout.xaxis && fullLayout.xaxis.showcrossline || fullLayout.yaxis && fullLayout.yaxis.showcrossline; - - if(fullLayout._has('cartesian') && showCrosslines && !(showSpikes && hovermode === 'closest')) { - // Now find the points for the crosslines. - if(fullLayout.yaxis.showcrossline) { - crosslinePoints.hLinePoint = findCrosslinePoint(pointData, xval, yval, 'y', crosslinePoints.hLinePoint); - } - if(fullLayout.xaxis.showcrossline) { - crosslinePoints.vLinePoint = findCrosslinePoint(pointData, xval, yval, 'x', crosslinePoints.vLinePoint); - } - } } function findCrosslinePoint(pointData, xval, yval, mode, endPoint) { + var tmpDistance = pointData.distance; + var tmpIndex = pointData.index; var resultPoint = endPoint; pointData.distance = Infinity; pointData.index = false; @@ -447,6 +449,8 @@ function _hover(gd, evt, subplot, noHoverEvent) { } } } + pointData.index = tmpIndex; + pointData.distance = tmpDistance; return resultPoint; } @@ -1164,7 +1168,7 @@ function cleanPoint(d, hovermode) { return d; } -function createCrosslines(hoverData, fullLayout) { +function createCrosslines(closestPoints, fullLayout) { var showXSpikeline = fullLayout.xaxis && fullLayout.xaxis.showspikes; var showYSpikeline = fullLayout.yaxis && fullLayout.yaxis.showspikes; var showH = fullLayout.yaxis && fullLayout.yaxis.showcrossline; @@ -1187,7 +1191,7 @@ function createCrosslines(hoverData, fullLayout) { // do not draw a crossline if there is a spikeline if(showV && !(showXSpikeline && hovermode === 'closest')) { - vLinePoint = hoverData.vLinePoint; + vLinePoint = closestPoints.vLinePoint; xa = vLinePoint.xa; vLinePointX = xa._offset + (vLinePoint.x0 + vLinePoint.x1) / 2; @@ -1211,7 +1215,7 @@ function createCrosslines(hoverData, fullLayout) { } if(showH && !(showYSpikeline && hovermode === 'closest')) { - hLinePoint = hoverData.hLinePoint; + hLinePoint = closestPoints.hLinePoint; ya = hLinePoint.ya; hLinePointY = ya._offset + (hLinePoint.y0 + hLinePoint.y1) / 2;