Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/8041_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add spikelines to ternary plots [[#8041](https://github.com/plotly/plotly.js/pull/8041)]
56 changes: 55 additions & 1 deletion src/components/fx/hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
var plots = fullLayout._plots || [];
var plotinfo = plots[subplot];
var hasCartesian = fullLayout._has('cartesian');
var hasTernary = fullLayout._has('ternary');

var hovermode = evt.hovermode || fullLayout.hovermode;
var hovermodeHasX = (hovermode || '').charAt(0) === 'x';
Expand Down Expand Up @@ -376,12 +377,15 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {

var itemnum, curvenum, cd, trace, subplotId, subploti, _mode, xval, yval, pointData, closedataPreviousLength;

// spikePoints: the set of candidate points we've found to draw spikes to
// cartesian candidate points for drawing spikes
var spikePoints = {
hLinePoint: null,
vLinePoint: null
};

// candidate spike points for ternary subplots
const ternarySpikePoints = {};

// does subplot have one (or more) horizontal traces?
// This is used to determine whether we rotate the labels or not
var hasOneHorizontalTrace = false;
Expand Down Expand Up @@ -658,6 +662,36 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
distance = hoverData[0].distance;
}

// TODO need to support 'scatterternarygl' in future
if (trace.type === 'scatterternary' && spikedistance !== 0) {
const ternary = pointData.subplot;

// as in Cartesian, 'hovered data' relies only on the normal hover result, while
// 'data' and 'cursor' need a closest data point
const needsClosestPoint = ['aaxis', 'baxis', 'caxis'].some((name) =>
ternary[name].showspikes && ternary[name].spikesnap !== 'hovered data');

if (needsClosestPoint) {
let spikePoint = pointData;

// reuse the normal hover candidate when available
// otherwise search again using spikedistance
if (!Number.isFinite(spikePoint.spikeDistance)) {
const spikeData = Lib.extendFlat({}, pointData, {distance: spikedistance, index: false});
const closestPoints = trace._module.hoverPoints(spikeData, xval, yval, 'closest');
spikePoint = closestPoints && closestPoints[0];
}

const previousSpikePoint = ternarySpikePoints[subplotId];

if (spikePoint && spikePoint.index !== undefined && spikePoint.index !== false &&
spikePoint.spikeDistance <= spikedistance &&
(!previousSpikePoint || spikePoint.spikeDistance < previousSpikePoint.spikeDistance)) {
ternarySpikePoints[subplotId] = spikePoint;
}
}
}

// Now if there is range to look in, find the points to draw the spikelines
// Do it only if there is no hoverData
if (hasCartesian && spikedistance !== 0) {
Expand Down Expand Up @@ -715,6 +749,21 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {

findHoverPoints();

function updateTernarySpikelines() {
fullLayout._hoverlayer.selectAll('.ternary-spikes').remove();

if (spikedistance === 0) return;
for (let i = 0; i < subplots.length; i++) {
const id = subplots[i];
const ternary = fullLayout[id] && fullLayout[id]._subplot;
if (ternary && ternary.drawSpikelines) {
const point = hoverData.find((d) => d.trace.subplot === id &&
d.index !== undefined && d.index !== false && d.spikeDistance <= spikedistance);
ternary.drawSpikelines(point, ternarySpikePoints[id], xvalArray && xvalArray[i], yvalArray && yvalArray[i]);
}
}
}

function selectClosestPoint(pointsData, spikedistance, spikeOnWinning) {
var resultPoint = null;
var minDistance = Infinity;
Expand Down Expand Up @@ -829,6 +878,9 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
// See dragelement/unhover.js.
gd._hoverAnywhereActive = true;
}

if (hasTernary) updateTernarySpikelines();

return result;
}

Expand All @@ -838,6 +890,8 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) {
}
}

if (hasTernary) updateTernarySpikelines();

if (
helpers.isXYhover(_mode) &&
hoverData[0].length !== 0 &&
Expand Down
7 changes: 7 additions & 0 deletions src/plots/ternary/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ var ternaryAxesAttrs = {
'all the minima set to zero.'
].join(' ')
},
// spikelines
showspikes: axesAttrs.showspikes,
spikecolor: axesAttrs.spikecolor,
spikethickness: axesAttrs.spikethickness,
spikedash: axesAttrs.spikedash,
spikemode: axesAttrs.spikemode,
spikesnap: axesAttrs.spikesnap,
};

var attrs = module.exports = overrideAll({
Expand Down
8 changes: 8 additions & 0 deletions src/plots/ternary/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,12 @@ function handleAxisDefaults(containerIn, containerOut, options, ternaryLayoutOut

coerce('hoverformat');
coerce('layer');

if(coerce('showspikes')) {
coerce('spikecolor');
coerce('spikethickness');
coerce('spikedash');
coerce('spikemode');
coerce('spikesnap');
}
}
130 changes: 130 additions & 0 deletions src/plots/ternary/ternary.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var strTranslate = Lib.strTranslate;
var _ = Lib._;
var Color = require('../../components/color');
var Drawing = require('../../components/drawing');
var svgTextUtils = require('../../lib/svg_text_utils');
var setConvert = require('../cartesian/set_convert');
var extendFlat = require('../../lib/extend').extendFlat;
var Plots = require('../plots');
Expand Down Expand Up @@ -170,6 +171,135 @@ proto.updateLayers = function(ternaryLayout) {

var whRatio = Math.sqrt(4 / 3);

proto.drawSpikelines = function (hoverPoint, spikePoint, cursorXVal, cursorYVal) {
const fullLayout = this.graphDiv._fullLayout;
const axes = [this.aaxis, this.baxis, this.caxis];

if(!axes.some((axis) => axis.showspikes)) return;

const span = this.sum - axes[0].min - axes[1].min - axes[2].min;

let layer;

for(let i = 0; i < axes.length; i++) {
const axis = axes[i];
if(!axis.showspikes) continue;

const snap = axis.spikesnap;

// 'hovered data' uses the current hover point
// 'data' and 'cursor' use the closest point within spikedistance
// 'data' draws at that point, while 'cursor' draws at the cursor position
const selectedPoint = snap === 'hovered data' ? hoverPoint : spikePoint || hoverPoint;

if(!selectedPoint) continue;

const snapToCursor = snap === 'cursor';

if(snapToCursor && (!Number.isFinite(cursorXVal) || !Number.isFinite(cursorYVal))) {
continue;
}

const calcPoint = selectedPoint.cd[selectedPoint.index];

// ternary plots use synthetic x/y axes (x = c - b, y = a), see src/traces/scatterternary/calc.js
// cursor positions are expressed in these coordinates,
// so we need to convert them to a/b/c for the ternary geometry below
const a = snapToCursor ? cursorYVal : calcPoint.a;
const b = snapToCursor ? (this.sum - cursorYVal - cursorXVal) / 2 : calcPoint.b;
const c = snapToCursor ? (this.sum - cursorYVal + cursorXVal) / 2 : calcPoint.c;

if(!this.xaxis.isPtWithinRange({a, b, c})) continue;

if(!layer) {
layer = fullLayout._hoverlayer
.insert('g', ':first-child')
.attr('class', 'ternary-spikes')
.attr('transform', strTranslate(this.x0, this.y0))
.style('pointer-events', 'none');
}

// normalize to the visible ternary range so the spike
// geometry also works when plot is zoomed, and constrain fraction at [0, 1] boundaries
const fa = Lib.constrain((a - axes[0].min) / span, 0, 1);
const fb = Lib.constrain((b - axes[1].min) / span, 0, 1);
const fc = Lib.constrain((c - axes[2].min) / span, 0, 1);

const x = this.w * (fc + fa / 2);
const y = this.h * (1 - fa);

// intersections of the line with the target axis and the opposite triangle edge
const [axisX, axisY, acrossX, acrossY] = [
[this.w * fa / 2, y, this.w * (1 - fa / 2), y], // aaxis
[this.w * (1 - fb), this.h, this.w * (1 - fb) / 2, this.h * fb], // baxis
[this.w * (1 + fc) / 2, this.h * fc, this.w * fc, this.h] // caxis
][i];

const color = axis.spikecolor || selectedPoint.color || axis.color;
const thickness = axis.spikethickness;
const mode = axis.spikemode;

if(mode.indexOf('toaxis') !== -1 || mode.indexOf('across') !== -1) {
const across = mode.indexOf('across') !== -1;
layer.append('line').attr({
class: 'spikeline',
x1: across ? acrossX : x,
y1: across ? acrossY : y,
x2: axisX,
y2: axisY,
'stroke-width': thickness,
'stroke-dasharray': Drawing.dashStyle(axis.spikedash, thickness)
}).call(Color.stroke, color);
}

if(mode.indexOf('marker') !== -1) {
layer.append('circle').attr({
class: 'spikeline',
cx: axisX,
cy: axisY,
r: thickness
}).call(Color.fill, color);
}

// draw the axis value label (similar to commonlabel for Cartesian) at the spike intersection
// note that strictly speaking, spikelines feature shouldn't include this label,
// in Carteisan, the label behavior is controlled by hovermode
// but in ternary plot, the current hovermode options are not fit and it is very intuitive
// that we want the labels to show together with spikelines
const label = layer.append('g')
.attr('class', 'ternary-spikelabel')
.attr('transform', strTranslate(axisX, axisY));

const text = label.append('text')
.attr('text-anchor', 'middle')
.call(Drawing.font, axis.tickfont || fullLayout.font)
.text(Axes.tickText(axis, [a, b, c][i], true).text)
.call(svgTextUtils.convertToTspans, this.graphDiv);

const box = Drawing.bBox(text.node());
const width = box.width + 6;
const height = box.height + 4;
const arrow = Math.min(6, height / 2, width / 2);
const left = i === 0 ? -arrow - width : i === 1 ? -width / 2 : 0;
const top = i === 0 ? -height / 2 : i === 1 ? arrow : -height;

let outline;
if(i === 0) {
outline = `M0,0L${-arrow},${-arrow}V${top}H${left}v${height}H${-arrow}V${arrow}Z`;
} else if(i === 1) {
outline = `M0,0L${arrow},${arrow}H${width / 2}v${height}H${left}V${arrow}H${-arrow}Z`;
} else {
outline = `M0,0V${top}h${width}V0Z`;
}

label.insert('path', 'text').attr('d', outline).call(Color.fill, color);
text.call(svgTextUtils.positionText,
left + width / 2 - box.left - box.width / 2,
top + height / 2 - box.top - box.height / 2);
text.call(Color.fill, Color.contrast(color));
}
};

proto.adjustLayout = function(ternaryLayout, graphSize) {
var _this = this;
var domain = ternaryLayout.domain;
Expand Down
81 changes: 81 additions & 0 deletions src/types/generated/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14751,6 +14751,11 @@ export interface TernaryLayout {
* @default true
*/
showline?: boolean;
/**
* Determines whether or not spikes (aka droplines) are drawn for this axis. Note that spikes will never be drawn when `hovermode` is *false*.
* @default false
*/
showspikes?: boolean;
/**
* Determines whether or not the tick labels are drawn.
* @default true
Expand All @@ -14766,6 +14771,28 @@ export interface TernaryLayout {
* @default 'all'
*/
showticksuffix?: 'all' | 'first' | 'last' | 'none';
/** Sets the spike color. If undefined, will use the series color */
spikecolor?: Color;
/**
* Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).
* @default 'dash'
*/
spikedash?: Dash;
/**
* Determines the drawing mode for the spike line If *toaxis*, the line is drawn from the data point to the axis the series is plotted on. If *across*, the line is drawn across the entire plot area, and supercedes *toaxis*. If *marker*, then a marker dot is drawn on the axis the series is plotted on
* @default 'toaxis'
*/
spikemode?: 'toaxis' | 'across' | 'marker' | (string & {});
/**
* Determines whether spikelines are stuck to the cursor or to the closest datapoints.
* @default 'hovered data'
*/
spikesnap?: 'data' | 'cursor' | 'hovered data';
/**
* Sets the spike line width in pixels.
* @default 3
*/
spikethickness?: number;
/**
* Sets the placement of the first tick on this axis. Use with `dtick`. If the axis `type` is *log*, then you must take the log of your starting tick (e.g. to set the starting tick to 100, set the `tick0` to 2) except when `dtick`=*L<f>* (see `dtick` for more info). If the axis `type` is *date*, it should be a date string, like date data. If the axis `type` is *category*, it should be a number, using the scale where each category is assigned a serial number from zero in the order it appears.
* Setting this also sets: tickmode = "linear"
Expand Down Expand Up @@ -14913,6 +14940,11 @@ export interface TernaryLayout {
* @default true
*/
showline?: boolean;
/**
* Determines whether or not spikes (aka droplines) are drawn for this axis. Note that spikes will never be drawn when `hovermode` is *false*.
* @default false
*/
showspikes?: boolean;
/**
* Determines whether or not the tick labels are drawn.
* @default true
Expand All @@ -14928,6 +14960,28 @@ export interface TernaryLayout {
* @default 'all'
*/
showticksuffix?: 'all' | 'first' | 'last' | 'none';
/** Sets the spike color. If undefined, will use the series color */
spikecolor?: Color;
/**
* Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).
* @default 'dash'
*/
spikedash?: Dash;
/**
* Determines the drawing mode for the spike line If *toaxis*, the line is drawn from the data point to the axis the series is plotted on. If *across*, the line is drawn across the entire plot area, and supercedes *toaxis*. If *marker*, then a marker dot is drawn on the axis the series is plotted on
* @default 'toaxis'
*/
spikemode?: 'toaxis' | 'across' | 'marker' | (string & {});
/**
* Determines whether spikelines are stuck to the cursor or to the closest datapoints.
* @default 'hovered data'
*/
spikesnap?: 'data' | 'cursor' | 'hovered data';
/**
* Sets the spike line width in pixels.
* @default 3
*/
spikethickness?: number;
/**
* Sets the placement of the first tick on this axis. Use with `dtick`. If the axis `type` is *log*, then you must take the log of your starting tick (e.g. to set the starting tick to 100, set the `tick0` to 2) except when `dtick`=*L<f>* (see `dtick` for more info). If the axis `type` is *date*, it should be a date string, like date data. If the axis `type` is *category*, it should be a number, using the scale where each category is assigned a serial number from zero in the order it appears.
* Setting this also sets: tickmode = "linear"
Expand Down Expand Up @@ -15080,6 +15134,11 @@ export interface TernaryLayout {
* @default true
*/
showline?: boolean;
/**
* Determines whether or not spikes (aka droplines) are drawn for this axis. Note that spikes will never be drawn when `hovermode` is *false*.
* @default false
*/
showspikes?: boolean;
/**
* Determines whether or not the tick labels are drawn.
* @default true
Expand All @@ -15095,6 +15154,28 @@ export interface TernaryLayout {
* @default 'all'
*/
showticksuffix?: 'all' | 'first' | 'last' | 'none';
/** Sets the spike color. If undefined, will use the series color */
spikecolor?: Color;
/**
* Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).
* @default 'dash'
*/
spikedash?: Dash;
/**
* Determines the drawing mode for the spike line If *toaxis*, the line is drawn from the data point to the axis the series is plotted on. If *across*, the line is drawn across the entire plot area, and supercedes *toaxis*. If *marker*, then a marker dot is drawn on the axis the series is plotted on
* @default 'toaxis'
*/
spikemode?: 'toaxis' | 'across' | 'marker' | (string & {});
/**
* Determines whether spikelines are stuck to the cursor or to the closest datapoints.
* @default 'hovered data'
*/
spikesnap?: 'data' | 'cursor' | 'hovered data';
/**
* Sets the spike line width in pixels.
* @default 3
*/
spikethickness?: number;
/**
* Sets the placement of the first tick on this axis. Use with `dtick`. If the axis `type` is *log*, then you must take the log of your starting tick (e.g. to set the starting tick to 100, set the `tick0` to 2) except when `dtick`=*L<f>* (see `dtick` for more info). If the axis `type` is *date*, it should be a date string, like date data. If the axis `type` is *category*, it should be a number, using the scale where each category is assigned a serial number from zero in the order it appears.
* Setting this also sets: tickmode = "linear"
Expand Down
Loading
Loading