Skip to content

Commit 1bebca6

Browse files
authored
Merge pull request #4069 from plotly/mapbox-attributions-inject-css
mapbox: add attributions
2 parents 56dd829 + 4db3809 commit 1bebca6

31 files changed

+470
-25
lines changed

src/plots/mapbox/constants.js

+167-19
Large diffs are not rendered by default.

src/plots/mapbox/index.js

+88-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ var mapboxgl = require('mapbox-gl');
1313
var Lib = require('../../lib');
1414
var getSubplotCalcData = require('../../plots/get_data').getSubplotCalcData;
1515
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
16+
var d3 = require('d3');
17+
var Drawing = require('../../components/drawing');
18+
var svgTextUtils = require('../../lib/svg_text_utils');
1619

1720
var Mapbox = require('./mapbox');
1821

@@ -118,7 +121,91 @@ exports.toSVG = function(gd) {
118121
preserveAspectRatio: 'none'
119122
});
120123

121-
mapbox.destroy();
124+
var subplotDiv = d3.select(opts._subplot.div);
125+
126+
// Append logo if visible
127+
var hidden = subplotDiv.select('.mapboxgl-ctrl-logo').node().offsetParent === null;
128+
if(!hidden) {
129+
var logo = fullLayout._glimages.append('g');
130+
logo.attr('transform', 'translate(' + (size.l + size.w * domain.x[0] + 10) + ', ' + (size.t + size.h * (1 - domain.y[0]) - 31) + ')');
131+
logo.append('path')
132+
.attr('d', constants.mapboxLogo.path0)
133+
.style({
134+
opacity: 0.9,
135+
fill: '#ffffff',
136+
'enable-background': 'new'
137+
});
138+
139+
logo.append('path')
140+
.attr('d', constants.mapboxLogo.path1)
141+
.style('opacity', 0.35)
142+
.style('enable-background', 'new');
143+
144+
logo.append('path')
145+
.attr('d', constants.mapboxLogo.path2)
146+
.style('opacity', 0.35)
147+
.style('enable-background', 'new');
148+
149+
logo.append('polygon')
150+
.attr('points', constants.mapboxLogo.polygon)
151+
.style({
152+
opacity: 0.9,
153+
fill: '#ffffff',
154+
'enable-background': 'new'
155+
});
156+
}
157+
158+
// Add attributions
159+
var attributions = subplotDiv
160+
.select('.mapboxgl-ctrl-attrib').text()
161+
.replace('Improve this map', '');
162+
163+
var attributionGroup = fullLayout._glimages.append('g');
164+
165+
var attributionText = attributionGroup.append('text');
166+
attributionText
167+
.text(attributions)
168+
.classed('static-attribution', true)
169+
.attr({
170+
'font-size': 12,
171+
'font-family': 'Arial',
172+
'color': 'rgba(0, 0, 0, 0.75)',
173+
'text-anchor': 'end',
174+
'data-unformatted': attributions
175+
});
176+
177+
var bBox = Drawing.bBox(attributionText.node());
178+
179+
// Break into multiple lines twice larger than domain
180+
var maxWidth = size.w * (domain.x[1] - domain.x[0]);
181+
if((bBox.width > maxWidth / 2)) {
182+
var multilineAttributions = attributions.split('|').join('<br>');
183+
attributionText
184+
.text(multilineAttributions)
185+
.attr('data-unformatted', multilineAttributions)
186+
.call(svgTextUtils.convertToTspans, gd);
187+
188+
bBox = Drawing.bBox(attributionText.node());
189+
}
190+
attributionText.attr('transform', 'translate(-3, ' + (-bBox.height + 8) + ')');
191+
192+
// Draw white rectangle behind text
193+
attributionGroup
194+
.insert('rect', '.static-attribution')
195+
.attr({
196+
x: -bBox.width - 6,
197+
y: -bBox.height - 3,
198+
width: bBox.width + 6,
199+
height: bBox.height + 3,
200+
fill: 'rgba(255, 255, 255, 0.75)'
201+
});
202+
203+
// Scale down if larger than domain
204+
var scaleRatio = 1;
205+
if((bBox.width + 6) > maxWidth) scaleRatio = maxWidth / (bBox.width + 6);
206+
207+
var offset = [(size.l + size.w * domain.x[1]), (size.t + size.h * (1 - domain.y[0]))];
208+
attributionGroup.attr('transform', 'translate(' + offset[0] + ',' + offset[1] + ') scale(' + scaleRatio + ')');
122209
}
123210
};
124211

src/plots/mapbox/layers.js

+2
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ function convertSourceOpts(opts) {
242242

243243
sourceOpts[field] = source;
244244

245+
if(opts.sourceattribution) sourceOpts.attribution = opts.sourceattribution;
246+
245247
return sourceOpts;
246248
}
247249

src/plots/mapbox/layout_attributes.js

+8
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ var attrs = module.exports = overrideAll({
131131
].join(' ')
132132
},
133133

134+
sourceattribution: {
135+
valType: 'string',
136+
role: 'info',
137+
description: [
138+
'Sets the attribution for this source.'
139+
].join(' ')
140+
},
141+
134142
type: {
135143
valType: 'enumerated',
136144
values: ['circle', 'line', 'fill', 'symbol', 'raster'],

src/plots/mapbox/layout_defaults.js

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ function handleLayerDefaults(layerIn, layerOut) {
5454
var mustBeRasterLayer = sourceType === 'raster' || sourceType === 'image';
5555

5656
coerce('source');
57+
coerce('sourceattribution');
5758

5859
if(sourceType === 'vector') {
5960
coerce('sourcelayer');

src/plots/mapbox/mapbox.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,14 @@ proto.createMap = function(calcData, fullLayout, resolve, reject) {
105105
preserveDrawingBuffer: self.isStatic,
106106

107107
doubleClickZoom: false,
108-
boxZoom: false
109-
});
108+
boxZoom: false,
109+
110+
attributionControl: false
111+
})
112+
.addControl(new mapboxgl.AttributionControl({
113+
compact: true
114+
}));
115+
110116

111117
// make sure canvas does not inherit left and top css
112118
map._canvas.style.left = '0px';
@@ -763,8 +769,8 @@ function getStyleObj(val) {
763769

764770
if(constants.styleValuesMapbox.indexOf(val) !== -1) {
765771
styleObj.style = convertStyleVal(val);
766-
} else if(val === constants.styleValueOSM) {
767-
styleObj.style = constants.styleOSM;
772+
} else if(constants.styles[val]) {
773+
styleObj.style = constants.styles[val];
768774
} else {
769775
styleObj.style = val;
770776
}

src/registry.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ function registerTraceModule(_module) {
276276
if(bpmName === 'mapbox') {
277277
var styleRules = basePlotModule.constants.styleRules;
278278
for(var k in styleRules) {
279-
addStyleRule('.mapboxgl-' + k, styleRules[k]);
279+
addStyleRule('.js-plotly-plot .plotly .mapboxgl-' + k, styleRules[k]);
280280
}
281281
}
282282

test/image/baselines/mapbox_0.png

23.8 KB
Loading
39.6 KB
Loading
19.6 KB
Loading
23.7 KB
Loading
71.3 KB
Loading
Loading
Loading
17.5 KB
Loading
40 KB
Loading
19.9 KB
Loading
8.17 KB
Loading
13.3 KB
Loading
Loading

test/image/baselines/mapbox_fill.png

20.5 KB
Loading
Loading
12 KB
Loading
19.6 KB
Loading
196 KB
Loading
42.8 KB
Loading
1.73 KB
Loading
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"data": [
3+
{
4+
"type": "scattermapbox",
5+
"name": "carto-positron",
6+
"lon": [ 10, 20 ],
7+
"lat": [ 20, 10 ]
8+
},
9+
{
10+
"type": "scattermapbox",
11+
"name": "carto-darkmatter",
12+
"lon": [ 10, 20 ],
13+
"lat": [ 20, 10 ],
14+
"subplot": "mapbox2"
15+
}
16+
],
17+
"layout": {
18+
"grid": {"rows": 1, "columns": 2},
19+
20+
"legend": {
21+
"x": 0,
22+
"y": 1, "yanchor": "bottom"
23+
},
24+
25+
"mapbox": {
26+
"domain": {"row": 0, "column": 0},
27+
"style": "carto-positron"
28+
},
29+
"mapbox2": {
30+
"domain": {"row": 0, "column": 1},
31+
"style": "carto-darkmatter"
32+
}
33+
}
34+
}
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"data": [
3+
{
4+
"type": "scattermapbox",
5+
"name": "stamen-terrain",
6+
"lon": [ 10, 20 ],
7+
"lat": [ 20, 10 ]
8+
},
9+
{
10+
"type": "scattermapbox",
11+
"name": "stamen-toner",
12+
"lon": [ 10, 20 ],
13+
"lat": [ 20, 10 ],
14+
"subplot": "mapbox2"
15+
},
16+
{
17+
"type": "scattermapbox",
18+
"name": "stamen-watercolor",
19+
"lon": [ 10, 20 ],
20+
"lat": [ 20, 10 ],
21+
"subplot": "mapbox3"
22+
}
23+
],
24+
"layout": {
25+
"grid": {"rows": 1, "columns": 3},
26+
27+
"legend": {
28+
"x": 0,
29+
"y": 1, "yanchor": "bottom"
30+
},
31+
"mapbox": {
32+
"domain": {"row": 0, "column": 0},
33+
"style": "stamen-terrain"
34+
},
35+
"mapbox2": {
36+
"domain": {"row": 0, "column": 1},
37+
"style": "stamen-toner"
38+
},
39+
"mapbox3": {
40+
"domain": {"row": 0, "column": 2},
41+
"style": "stamen-watercolor"
42+
}
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"data": [
3+
{
4+
"type": "scattermapbox",
5+
"name": "white-bg",
6+
"lon": [ 10, 20 ],
7+
"lat": [ 20, 10 ]
8+
}
9+
],
10+
"layout": {
11+
"width": 200,
12+
"height": 200,
13+
"margin": {"t": 0, "b": 0, "l": 0, "r": 0},
14+
"mapbox": {
15+
"style": "white-bg"
16+
}
17+
}
18+
}

test/jasmine/tests/mapbox_test.js

+97
Original file line numberDiff line numberDiff line change
@@ -1201,6 +1201,103 @@ describe('@noCI, mapbox plots', function() {
12011201
.then(done);
12021202
}, LONG_TIMEOUT_INTERVAL);
12031203

1204+
describe('attributions', function() {
1205+
it('@gl should be displayed for style "open-street-map"', function(done) {
1206+
Plotly.newPlot(gd, [{type: 'scattermapbox'}], {mapbox: {style: 'open-street-map'}})
1207+
.then(function() {
1208+
var s = Plotly.d3.selectAll('.mapboxgl-ctrl-attrib');
1209+
expect(s.size()).toBe(1);
1210+
expect(s.text()).toEqual('© OpenStreetMap');
1211+
})
1212+
.catch(failTest)
1213+
.then(done);
1214+
});
1215+
1216+
it('@gl should be displayed for style from Mapbox', function(done) {
1217+
Plotly.newPlot(gd, [{type: 'scattermapbox'}], {mapbox: {style: 'basic'}})
1218+
.then(function() {
1219+
var s = Plotly.d3.selectAll('.mapboxgl-ctrl-attrib');
1220+
expect(s.size()).toBe(1);
1221+
expect(s.text()).toEqual('© Mapbox © OpenStreetMap Improve this map');
1222+
})
1223+
.catch(failTest)
1224+
.then(done);
1225+
});
1226+
1227+
function mockLayoutCustomStyle() {
1228+
return {
1229+
'mapbox': {
1230+
'style': {
1231+
'id': 'osm',
1232+
'version': 8,
1233+
'sources': {
1234+
'simple-tiles': {
1235+
'type': 'raster',
1236+
'tiles': [
1237+
'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png',
1238+
'https://b.tile.openstreetmap.org/{z}/{x}/{y}.png'
1239+
],
1240+
'tileSize': 256
1241+
}
1242+
},
1243+
'layers': [
1244+
{
1245+
'id': 'simple-tiles',
1246+
'type': 'raster',
1247+
'source': 'simple-tiles',
1248+
'minzoom': 0,
1249+
'maxzoom': 22
1250+
}
1251+
]
1252+
}
1253+
}
1254+
};
1255+
}
1256+
1257+
it('@gl should not be displayed for custom style without attribution', function(done) {
1258+
Plotly.newPlot(gd, [{type: 'scattermapbox'}], mockLayoutCustomStyle())
1259+
.then(function() {
1260+
var s = Plotly.d3.selectAll('.mapboxgl-ctrl-attrib');
1261+
expect(s.size()).toBe(1);
1262+
expect(s.text()).toEqual('');
1263+
})
1264+
.catch(failTest)
1265+
.then(done);
1266+
});
1267+
1268+
it('@gl should be displayed for custom style with attribution', function(done) {
1269+
var attr = 'custom attribution';
1270+
var layout = mockLayoutCustomStyle();
1271+
layout.mapbox.style.sources['simple-tiles'].attribution = attr;
1272+
Plotly.newPlot(gd, [{type: 'scattermapbox'}], layout)
1273+
.then(function() {
1274+
var s = Plotly.d3.selectAll('.mapboxgl-ctrl-attrib');
1275+
expect(s.size()).toBe(1);
1276+
expect(s.text()).toEqual(attr);
1277+
})
1278+
.catch(failTest)
1279+
.then(done);
1280+
});
1281+
1282+
it('@gl should be displayed for attributions defined in layers\' sourceattribution', function(done) {
1283+
var mock = require('@mocks/mapbox_layers.json');
1284+
var customMock = Lib.extendDeep(mock);
1285+
1286+
var attr = 'super custom attribution';
1287+
customMock.data.pop();
1288+
customMock.layout.mapbox.layers[0].sourceattribution = attr;
1289+
1290+
Plotly.newPlot(gd, customMock)
1291+
.then(function() {
1292+
var s = Plotly.d3.selectAll('.mapboxgl-ctrl-attrib');
1293+
expect(s.size()).toBe(1);
1294+
expect(s.text()).toEqual([attr, '© Mapbox © OpenStreetMap Improve this map'].join(' | '));
1295+
})
1296+
.catch(failTest)
1297+
.then(done);
1298+
});
1299+
});
1300+
12041301
function getMapInfo(gd) {
12051302
var subplot = gd._fullLayout.mapbox._subplot;
12061303
var map = subplot.map;

0 commit comments

Comments
 (0)