Skip to content
Closed
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
6 changes: 4 additions & 2 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ module.exports = {
// '-' means we haven't yet run autotype or couldn't find any data
// it gets turned into linear in gd._fullLayout but not copied back
// to gd.data like the others are.
values: ['-', 'linear', 'log', 'date', 'category', 'multicategory'],
values: ['-', 'linear', 'log', 'symlog', 'date', 'category', 'multicategory'],
dflt: '-',
editType: 'calc',
// we forget when an axis has been autotyped, just writing the auto
Expand All @@ -247,7 +247,9 @@ module.exports = {
'Sets the axis type.',
'By default, plotly attempts to determined the axis type',
'by looking into the data of the traces that referenced',
'the axis in question.'
'the axis in question.',
'With *symlog*, the axis is log-scaled with a linear range',
'around zero.'
].join(' ')
},
autotypenumbers: {
Expand Down
53 changes: 45 additions & 8 deletions src/plots/cartesian/set_convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ module.exports = function setConvert(ax, fullLayout) {
} else return BADNUM;
}

function toSymlog(v) {
// Using asinh as a smooth approximation to symlog
// TODO: Make the linear threshold configurable? Currently implicit 1.
return Math.asinh(v) / Math.LN10;
}

function fromSymlog(v) {
return Math.sinh(v * Math.LN10);
}

/*
* wrapped dateTime2ms that:
* - accepts ms numbers for backward compatibility
Expand Down Expand Up @@ -241,14 +251,22 @@ module.exports = function setConvert(ax, fullLayout) {
}

// conversions among c/l/p are fairly simple - do them together for all axis types
ax.c2l = (ax.type === 'log') ? toLog : ensureNumber;
ax.l2c = (ax.type === 'log') ? fromLog : ensureNumber;

ax.l2p = l2p;
ax.p2l = p2l;

ax.c2p = (ax.type === 'log') ? function(v, clip) { return l2p(toLog(v, clip)); } : l2p;
ax.p2c = (ax.type === 'log') ? function(px) { return fromLog(p2l(px)); } : p2l;
if(ax.type === 'log') {
ax.c2l = toLog;
ax.l2c = fromLog;
ax.c2p = function(v, clip) { return l2p(toLog(v, clip)); };
ax.p2c = function(px) { return fromLog(p2l(px)); };
} else if(ax.type === 'symlog') {
ax.c2l = toSymlog;
ax.l2c = fromSymlog;
ax.c2p = function(v) { return l2p(toSymlog(v)); };
ax.p2c = function(px) { return fromSymlog(p2l(px)); };
} else {
ax.c2l = ensureNumber;
ax.l2c = ensureNumber;
ax.c2p = l2p;
ax.p2c = p2l;
}

/*
* now type-specific conversions for **ALL** other combinations
Expand Down Expand Up @@ -281,6 +299,25 @@ module.exports = function setConvert(ax, fullLayout) {
ax.r2p = function(v) { return ax.l2p(cleanNumber(v)); };
ax.p2r = p2l;

ax.cleanPos = ensureNumber;
} else if(ax.type === 'symlog') {
// Symlog implementation using arcsinh
// d and c are data vals, r and l are transformed (symlogged)
ax.d2r = ax.d2l = function(v) { return toSymlog(cleanNumber(v)); };
ax.r2d = ax.r2c = function(v) { return fromSymlog(cleanNumber(v)); };

ax.d2c = ax.r2l = cleanNumber;
ax.c2d = ax.l2r = ensureNumber;

ax.c2r = toSymlog;
ax.l2d = fromSymlog;

ax.d2p = function(v) { return ax.l2p(ax.d2r(v)); };
ax.p2d = function(px) { return fromSymlog(p2l(px)); };

ax.r2p = function(v) { return ax.l2p(cleanNumber(v)); };
ax.p2r = p2l;

ax.cleanPos = ensureNumber;
} else if(ax.type === 'date') {
// r and d are date strings, l and c are ms
Expand Down
120 changes: 118 additions & 2 deletions src/plots/map/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
type: 'map',
attributes: layoutAttributes,
handleDefaults: handleDefaults,
partition: 'y'
partition: 'y',
fullData: fullData
});
};

function handleDefaults(containerIn, containerOut, coerce) {
function handleDefaults(containerIn, containerOut, coerce, opts) {
coerce('style');
coerce('center.lon');
coerce('center.lat');
Expand All @@ -28,13 +29,128 @@ function handleDefaults(containerIn, containerOut, coerce) {
var east = coerce('bounds.east');
var south = coerce('bounds.south');
var north = coerce('bounds.north');

// Auto-calculate bounds from data if not provided
if(west === undefined && east === undefined && south === undefined && north === undefined) {
var traceBounds = {
west: Infinity,
east: -Infinity,
south: Infinity,
north: -Infinity
};
var hasTraceBounds = false;

for(var i = 0; i < fullData.length; i++) {
var trace = fullData[i];
// Check if trace belongs to this map subplot
if(trace.visible !== true || trace.subplot !== opts.id) continue;

// Handle scattermap traces
if(trace.type === 'scattermap') {
var lon = trace.lon || [];
var lat = trace.lat || [];
var len = Math.min(lon.length, lat.length);

for(var j = 0; j < len; j++) {
var l = lon[j];
var t = lat[j];
if(l !== undefined && t !== undefined) {
traceBounds.west = Math.min(traceBounds.west, l);
traceBounds.east = Math.max(traceBounds.east, l);
traceBounds.south = Math.min(traceBounds.south, t);
traceBounds.north = Math.max(traceBounds.north, t);
hasTraceBounds = true;
}
}
}
// Add other map trace types here if needed (choroplethmap, etc)
}

if(hasTraceBounds) {
var domain = containerOut.domain;
var width = layoutOut.width * (domain.x[1] - domain.x[0]);
var height = layoutOut.height * (domain.y[1] - domain.y[0]);

// Default zoom calculation
if(containerOut.zoom === undefined) {
var padding = 0.1; // 10% padding
var dLon = Math.abs(traceBounds.east - traceBounds.west);
if(dLon === 0) dLon = 0.01; // Avoid division by zero
if(dLon > 360) dLon = 360;

// Simple Mercator projection for lat
function mercatorY(lat) {
var rad = lat * Math.PI / 180;
return Math.log(Math.tan(Math.PI / 4 + rad / 2));
}

var yNorth = mercatorY(traceBounds.north);
var ySouth = mercatorY(traceBounds.south);
var dLat = Math.abs(yNorth - ySouth);
if(dLat === 0) dLat = 0.01;

// 360 degrees = 2*PI radians. Mapbox world width is 512 pixels at zoom 0?
// Wait, Mapbox tile size is 512px by default in GL JS.
// 360 degrees fits in 512px at z=0?
// Let's use 360 degrees fits in 256 * 2^z (standard web mercator)
// Mapbox GL usually matches this.
// But let's be conservative.

// Zoom for longitude fit:
// pixelWidth = (dLon / 360) * 512 * 2^z
// 2^z = (pixelWidth * 360) / (dLon * 512)
// z = log2(...)
var zLon = Math.log2((width * 360) / (dLon * 512));

// Zoom for latitude fit:
// pixelHeight = (dLat / (2 * Math.PI)) * 512 * 2^z
// 2^z = (pixelHeight * 2 * Math.PI) / (dLat * 512)
var zLat = Math.log2((height * 2 * Math.PI) / (dLat * 512));

var zoom = Math.min(zLon, zLat);
zoom = Math.max(0, Math.min(22, zoom)); // Clamp to valid zoom range
zoom -= padding; // Apply padding

containerOut.zoom = zoom;
}

// Default center calculation
if(containerOut.center.lon === undefined) {
containerOut.center.lon = (traceBounds.west + traceBounds.east) / 2;
}
if(containerOut.center.lat === undefined) {
// Center latitude in Mercator projection, then unproject
function mercatorY(lat) {
var rad = lat * Math.PI / 180;
return Math.log(Math.tan(Math.PI / 4 + rad / 2));
}
function unMercatorY(y) {
return (2 * Math.atan(Math.exp(y)) - Math.PI / 2) * 180 / Math.PI;
}

var yNorth = mercatorY(traceBounds.north);
var ySouth = mercatorY(traceBounds.south);
var yCenter = (yNorth + ySouth) / 2;
containerOut.center.lat = unMercatorY(yCenter);
}

// Do NOT set containerOut.bounds here as that restricts panning!
}
} // End of auto-calculate bounds block

if(
west === undefined ||
east === undefined ||
south === undefined ||
north === undefined
) {
delete containerOut.bounds;

if(fitBounds) {
delete containerOut.center.lon;
delete containerOut.center.lat;
delete containerOut.zoom;
}
}

handleArrayContainerDefaults(containerIn, containerOut, {
Expand Down
8 changes: 4 additions & 4 deletions src/traces/scattermap/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ module.exports = overrideAll(

line: {
color: lineAttrs.color,
width: lineAttrs.width

// TODO
// dash: dash
width: lineAttrs.width,
dash: extendFlat({}, scatterAttrs.line.dash, {
editType: 'plot'
})
},

connectgaps: scatterAttrs.connectgaps,
Expand Down
22 changes: 21 additions & 1 deletion src/traces/scattermap/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ module.exports = function convert(gd, calcTrace) {
'line-opacity': trace.opacity
});

// TODO convert line.dash into line-dasharray
if (trace.line.dash) {
line.paint['line-dasharray'] = convertDash(trace.line.dash, trace.line.width);
}
}

if (hasCircles) {
Expand Down Expand Up @@ -197,6 +199,24 @@ function makeCircleOpts(calcTrace) {
return s / 2;
}

function convertDash(dash, width) {
var dashList;
if (dash === 'solid') dashList = [];
else if (dash === 'dot') dashList = [1, 2];
else if (dash === 'dash') dashList = [4, 2];
else if (dash === 'longdash') dashList = [8, 2];
else if (dash === 'dashdot') dashList = [4, 2, 1, 2];
else if (dash === 'longdashdot') dashList = [8, 2, 1, 2];
else if (typeof dash === 'string') dashList = dash.split(/[\s,]+/).map(Number);
else dashList = dash;

if (dashList.length) {
return dashList.map(function (v) {
return (v * width) / 2;
});
}
}

var colorFn;
if (arrayColor) {
if (Colorscale.hasColorscale(trace, 'marker')) {
Expand Down
8 changes: 4 additions & 4 deletions src/traces/scattermapbox/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ module.exports = overrideAll(

line: {
color: lineAttrs.color,
width: lineAttrs.width

// TODO
// dash: dash
width: lineAttrs.width,
dash: extendFlat({}, scatterAttrs.line.dash, {
editType: 'plot'
})
},

connectgaps: scatterAttrs.connectgaps,
Expand Down
22 changes: 21 additions & 1 deletion src/traces/scattermapbox/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ module.exports = function convert(gd, calcTrace) {
'line-opacity': trace.opacity
});

// TODO convert line.dash into line-dasharray
if (trace.line.dash) {
line.paint['line-dasharray'] = convertDash(trace.line.dash, trace.line.width);
}
}

if (hasCircles) {
Expand Down Expand Up @@ -197,6 +199,24 @@ function makeCircleOpts(calcTrace) {
return s / 2;
}

function convertDash(dash, width) {
var dashList;
if (dash === 'solid') dashList = [];
else if (dash === 'dot') dashList = [1, 2];
else if (dash === 'dash') dashList = [4, 2];
else if (dash === 'longdash') dashList = [8, 2];
else if (dash === 'dashdot') dashList = [4, 2, 1, 2];
else if (dash === 'longdashdot') dashList = [8, 2, 1, 2];
else if (typeof dash === 'string') dashList = dash.split(/[\s,]+/).map(Number);
else dashList = dash;

if (dashList.length) {
return dashList.map(function (v) {
return (v * width) / 2;
});
}
}

var colorFn;
if (arrayColor) {
if (Colorscale.hasColorscale(trace, 'marker')) {
Expand Down