Jonas Kress (WMDE) has uploaded a new change for review.
https://gerrit.wikimedia.org/r/278063
Change subject: Introduce tag cloud
......................................................................
Introduce tag cloud
Change-Id: I71dfdedf01d1cc0217c026bad7559a39f60c68f2
---
M index.html
A vendor/d3/addon/d3.layout.cloud.js
M wikibase/config.js
M wikibase/queryService/ui/App.js
A wikibase/queryService/ui/resultBrowser/TagCloudChartResultBrowser.js
5 files changed, 642 insertions(+), 1 deletion(-)
git pull ssh://gerrit.wikimedia.org:29418/wikidata/query/gui
refs/changes/63/278063/1
diff --git a/index.html b/index.html
index 03bc093..f55dfe6 100644
--- a/index.html
+++ b/index.html
@@ -195,6 +195,7 @@
<script
src="vendor/leaflet/addon/fullscreen/Leaflet.fullscreen.min.js"></script>
<script
src="vendor/leaflet/addon/boxzoom/L.Control.ZoomBox.min.js"></script>
<script src="vendor/d3/d3.min.js"></script>
+ <script src="vendor/d3/addon/d3.layout.cloud.js"></script>
<script
src="wikibase/codemirror/addon/tooltip/WikibaseRDFTooltip.js"></script>
@@ -212,6 +213,7 @@
<script
src="wikibase/queryService/ui/resultBrowser/AbstractChartResultBrowser.js"></script>
<script
src="wikibase/queryService/ui/resultBrowser/TreeMapResultBrowser.js"></script>
<script
src="wikibase/queryService/ui/resultBrowser/BubbleChartResultBrowser.js"></script>
+ <script
src="wikibase/queryService/ui/resultBrowser/TagCloudChartResultBrowser.js"></script>
<script src="wikibase/queryService/api/Sparql.js"></script>
<script src="wikibase/queryService/api/QuerySamples.js"></script>
<script src="wikibase/queryService/RdfNamespaces.js"></script>
diff --git a/vendor/d3/addon/d3.layout.cloud.js
b/vendor/d3/addon/d3.layout.cloud.js
new file mode 100644
index 0000000..8165a41
--- /dev/null
+++ b/vendor/d3/addon/d3.layout.cloud.js
@@ -0,0 +1,505 @@
+(function(f){if(typeof exports==="object"&&typeof
module!=="undefined"){module.exports=f()}else if(typeof
define==="function"&&define.amd){define([],f)}else{var g;if(typeof
window!=="undefined"){g=window}else if(typeof
global!=="undefined"){g=global}else if(typeof
self!=="undefined"){g=self}else{g=this}g=(g.d3||(g.d3 =
{}));g=(g.layout||(g.layout = {}));g.cloud = f()}})(function(){var
define,module,exports;return (function e(t,n,r){function
s(o,u){if(!n[o]){if(!t[o]){var a=typeof
require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var
f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var
l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return
s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof
require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return
s})({1:[function(require,module,exports){
+// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
+// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
+
+var dispatch = require("d3-dispatch").dispatch;
+
+var cloudRadians = Math.PI / 180,
+ cw = 1 << 11 >> 5,
+ ch = 1 << 11;
+
+module.exports = function() {
+ var size = [256, 256],
+ text = cloudText,
+ font = cloudFont,
+ fontSize = cloudFontSize,
+ fontStyle = cloudFontNormal,
+ fontWeight = cloudFontNormal,
+ rotate = cloudRotate,
+ padding = cloudPadding,
+ spiral = archimedeanSpiral,
+ words = [],
+ timeInterval = Infinity,
+ event = dispatch("word", "end"),
+ timer = null,
+ random = Math.random,
+ cloud = {},
+ canvas = cloudCanvas;
+
+ cloud.canvas = function(_) {
+ return arguments.length ? (canvas = functor(_), cloud) : canvas;
+ };
+
+ cloud.start = function() {
+ var contextAndRatio = getContext(canvas()),
+ board = zeroArray((size[0] >> 5) * size[1]),
+ bounds = null,
+ n = words.length,
+ i = -1,
+ tags = [],
+ data = words.map(function(d, i) {
+ d.text = text.call(this, d, i);
+ d.font = font.call(this, d, i);
+ d.style = fontStyle.call(this, d, i);
+ d.weight = fontWeight.call(this, d, i);
+ d.rotate = rotate.call(this, d, i);
+ d.size = ~~fontSize.call(this, d, i);
+ d.padding = padding.call(this, d, i);
+ return d;
+ }).sort(function(a, b) { return b.size - a.size; });
+
+ if (timer) clearInterval(timer);
+ timer = setInterval(step, 0);
+ step();
+
+ return cloud;
+
+ function step() {
+ var start = Date.now();
+ while (Date.now() - start < timeInterval && ++i < n && timer) {
+ var d = data[i];
+ d.x = (size[0] * (random() + .5)) >> 1;
+ d.y = (size[1] * (random() + .5)) >> 1;
+ cloudSprite(contextAndRatio, d, data, i);
+ if (d.hasText && place(board, d, bounds)) {
+ tags.push(d);
+ event.word(d);
+ if (bounds) cloudBounds(bounds, d);
+ else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y:
d.y + d.y1}];
+ // Temporary hack
+ d.x -= size[0] >> 1;
+ d.y -= size[1] >> 1;
+ }
+ }
+ if (i >= n) {
+ cloud.stop();
+ event.end(tags, bounds);
+ }
+ }
+ }
+
+ cloud.stop = function() {
+ if (timer) {
+ clearInterval(timer);
+ timer = null;
+ }
+ return cloud;
+ };
+
+ function getContext(canvas) {
+ canvas.width = canvas.height = 1;
+ var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1,
1).data.length >> 2);
+ canvas.width = (cw << 5) / ratio;
+ canvas.height = ch / ratio;
+
+ var context = canvas.getContext("2d");
+ context.fillStyle = context.strokeStyle = "red";
+ context.textAlign = "center";
+
+ return {context: context, ratio: ratio};
+ }
+
+ function place(board, tag, bounds) {
+ var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
+ startX = tag.x,
+ startY = tag.y,
+ maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
+ s = spiral(size),
+ dt = random() < .5 ? 1 : -1,
+ t = -dt,
+ dxdy,
+ dx,
+ dy;
+
+ while (dxdy = s(t += dt)) {
+ dx = ~~dxdy[0];
+ dy = ~~dxdy[1];
+
+ if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
+
+ tag.x = startX + dx;
+ tag.y = startY + dy;
+
+ if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
+ tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
+ // TODO only check for collisions within current bounds.
+ if (!bounds || !cloudCollide(tag, board, size[0])) {
+ if (!bounds || collideRects(tag, bounds)) {
+ var sprite = tag.sprite,
+ w = tag.width >> 5,
+ sw = size[0] >> 5,
+ lx = tag.x - (w << 4),
+ sx = lx & 0x7f,
+ msx = 32 - sx,
+ h = tag.y1 - tag.y0,
+ x = (tag.y + tag.y0) * sw + (lx >> 5),
+ last;
+ for (var j = 0; j < h; j++) {
+ last = 0;
+ for (var i = 0; i <= w; i++) {
+ board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w +
i]) >>> sx : 0);
+ }
+ x += sw;
+ }
+ delete tag.sprite;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ cloud.timeInterval = function(_) {
+ return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud)
: timeInterval;
+ };
+
+ cloud.words = function(_) {
+ return arguments.length ? (words = _, cloud) : words;
+ };
+
+ cloud.size = function(_) {
+ return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
+ };
+
+ cloud.font = function(_) {
+ return arguments.length ? (font = functor(_), cloud) : font;
+ };
+
+ cloud.fontStyle = function(_) {
+ return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
+ };
+
+ cloud.fontWeight = function(_) {
+ return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
+ };
+
+ cloud.rotate = function(_) {
+ return arguments.length ? (rotate = functor(_), cloud) : rotate;
+ };
+
+ cloud.text = function(_) {
+ return arguments.length ? (text = functor(_), cloud) : text;
+ };
+
+ cloud.spiral = function(_) {
+ return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
+ };
+
+ cloud.fontSize = function(_) {
+ return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
+ };
+
+ cloud.padding = function(_) {
+ return arguments.length ? (padding = functor(_), cloud) : padding;
+ };
+
+ cloud.random = function(_) {
+ return arguments.length ? (random = _, cloud) : random;
+ };
+
+ cloud.on = function() {
+ var value = event.on.apply(event, arguments);
+ return value === event ? cloud : value;
+ };
+
+ return cloud;
+};
+
+function cloudText(d) {
+ return d.text;
+}
+
+function cloudFont() {
+ return "serif";
+}
+
+function cloudFontNormal() {
+ return "normal";
+}
+
+function cloudFontSize(d) {
+ return Math.sqrt(d.value);
+}
+
+function cloudRotate() {
+ return (~~(Math.random() * 6) - 3) * 30;
+}
+
+function cloudPadding() {
+ return 1;
+}
+
+// Fetches a monochrome sprite bitmap for the specified text.
+// Load in batches for speed.
+function cloudSprite(contextAndRatio, d, data, di) {
+ if (d.sprite) return;
+ var c = contextAndRatio.context,
+ ratio = contextAndRatio.ratio;
+
+ c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
+ var x = 0,
+ y = 0,
+ maxh = 0,
+ n = data.length;
+ --di;
+ while (++di < n) {
+ d = data[di];
+ c.save();
+ c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px "
+ d.font;
+ var w = c.measureText(d.text + "m").width * ratio,
+ h = d.size << 1;
+ if (d.rotate) {
+ var sr = Math.sin(d.rotate * cloudRadians),
+ cr = Math.cos(d.rotate * cloudRadians),
+ wcr = w * cr,
+ wsr = w * sr,
+ hcr = h * cr,
+ hsr = h * sr;
+ w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 <<
5;
+ h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
+ } else {
+ w = (w + 0x1f) >> 5 << 5;
+ }
+ if (h > maxh) maxh = h;
+ if (x + w >= (cw << 5)) {
+ x = 0;
+ y += maxh;
+ maxh = 0;
+ }
+ if (y + h >= ch) break;
+ c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
+ if (d.rotate) c.rotate(d.rotate * cloudRadians);
+ c.fillText(d.text, 0, 0);
+ if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0);
+ c.restore();
+ d.width = w;
+ d.height = h;
+ d.xoff = x;
+ d.yoff = y;
+ d.x1 = w >> 1;
+ d.y1 = h >> 1;
+ d.x0 = -d.x1;
+ d.y0 = -d.y1;
+ d.hasText = true;
+ x += w;
+ }
+ var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
+ sprite = [];
+ while (--di >= 0) {
+ d = data[di];
+ if (!d.hasText) continue;
+ var w = d.width,
+ w32 = w >> 5,
+ h = d.y1 - d.y0;
+ // Zero the buffer
+ for (var i = 0; i < h * w32; i++) sprite[i] = 0;
+ x = d.xoff;
+ if (x == null) return;
+ y = d.yoff;
+ var seen = 0,
+ seenRow = -1;
+ for (var j = 0; j < h; j++) {
+ for (var i = 0; i < w; i++) {
+ var k = w32 * j + (i >> 5),
+ m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i %
32)) : 0;
+ sprite[k] |= m;
+ seen |= m;
+ }
+ if (seen) seenRow = j;
+ else {
+ d.y0++;
+ h--;
+ j--;
+ y++;
+ }
+ }
+ d.y1 = d.y0 + seenRow;
+ d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
+ }
+}
+
+// Use mask-based collision detection.
+function cloudCollide(tag, board, sw) {
+ sw >>= 5;
+ var sprite = tag.sprite,
+ w = tag.width >> 5,
+ lx = tag.x - (w << 4),
+ sx = lx & 0x7f,
+ msx = 32 - sx,
+ h = tag.y1 - tag.y0,
+ x = (tag.y + tag.y0) * sw + (lx >> 5),
+ last;
+ for (var j = 0; j < h; j++) {
+ last = 0;
+ for (var i = 0; i <= w; i++) {
+ if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
+ & board[x + i]) return true;
+ }
+ x += sw;
+ }
+ return false;
+}
+
+function cloudBounds(bounds, d) {
+ var b0 = bounds[0],
+ b1 = bounds[1];
+ if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
+ if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
+ if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
+ if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
+}
+
+function collideRects(a, b) {
+ return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y &&
a.y + a.y0 < b[1].y;
+}
+
+function archimedeanSpiral(size) {
+ var e = size[0] / size[1];
+ return function(t) {
+ return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
+ };
+}
+
+function rectangularSpiral(size) {
+ var dy = 4,
+ dx = dy * size[0] / size[1],
+ x = 0,
+ y = 0;
+ return function(t) {
+ var sign = t < 0 ? -1 : 1;
+ // See triangular numbers: T_n = n * (n + 1) / 2.
+ switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
+ case 0: x += dx; break;
+ case 1: y += dy; break;
+ case 2: x -= dx; break;
+ default: y -= dy; break;
+ }
+ return [x, y];
+ };
+}
+
+// TODO reuse arrays?
+function zeroArray(n) {
+ var a = [],
+ i = -1;
+ while (++i < n) a[i] = 0;
+ return a;
+}
+
+function cloudCanvas() {
+ return document.createElement("canvas");
+}
+
+function functor(d) {
+ return typeof d === "function" ? d : function() { return d; };
+}
+
+var spirals = {
+ archimedean: archimedeanSpiral,
+ rectangular: rectangularSpiral
+};
+
+},{"d3-dispatch":2}],2:[function(require,module,exports){
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ?
factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ factory((global.dispatch = {}));
+}(this, function (exports) { 'use strict';
+
+ function Dispatch(types) {
+ var i = -1,
+ n = types.length,
+ callbacksByType = {},
+ callbackByName = {},
+ type,
+ that = this;
+
+ that.on = function(type, callback) {
+ type = parseType(type);
+
+ // Return the current callback, if any.
+ if (arguments.length < 2) {
+ return (callback = callbackByName[type.name]) && callback.value;
+ }
+
+ // If a type was specified…
+ if (type.type) {
+ var callbacks = callbacksByType[type.type],
+ callback0 = callbackByName[type.name],
+ i;
+
+ // Remove the current callback, if any, using copy-on-remove.
+ if (callback0) {
+ callback0.value = null;
+ i = callbacks.indexOf(callback0);
+ callbacksByType[type.type] = callbacks = callbacks.slice(0,
i).concat(callbacks.slice(i + 1));
+ delete callbackByName[type.name];
+ }
+
+ // Add the new callback, if any.
+ if (callback) {
+ callback = {value: callback};
+ callbackByName[type.name] = callback;
+ callbacks.push(callback);
+ }
+ }
+
+ // Otherwise, if a null callback was specified, remove all callbacks
with the given name.
+ else if (callback == null) {
+ for (var otherType in callbacksByType) {
+ if (callback = callbackByName[otherType + type.name]) {
+ callback.value = null;
+ var callbacks = callbacksByType[otherType], i =
callbacks.indexOf(callback);
+ callbacksByType[otherType] = callbacks.slice(0,
i).concat(callbacks.slice(i + 1));
+ delete callbackByName[callback.name];
+ }
+ }
+ }
+
+ return that;
+ };
+
+ while (++i < n) {
+ type = types[i] + "";
+ if (!type || (type in that)) throw new Error("illegal or duplicate type:
" + type);
+ callbacksByType[type] = [];
+ that[type] = applier(type);
+ }
+
+ function parseType(type) {
+ var i = (type += "").indexOf("."), name = type;
+ if (i >= 0) type = type.slice(0, i); else name += ".";
+ if (type && !callbacksByType.hasOwnProperty(type)) throw new
Error("unknown type: " + type);
+ return {type: type, name: name};
+ }
+
+ function applier(type) {
+ return function() {
+ var callbacks = callbacksByType[type], // Defensive reference;
copy-on-remove.
+ callback,
+ callbackValue,
+ i = -1,
+ n = callbacks.length;
+
+ while (++i < n) {
+ if (callbackValue = (callback = callbacks[i]).value) {
+ callbackValue.apply(this, arguments);
+ }
+ }
+
+ return that;
+ };
+ }
+ }
+
+ function dispatch() {
+ return new Dispatch(arguments);
+ }
+
+ dispatch.prototype = Dispatch.prototype; // allow instanceof
+
+ exports.dispatch = dispatch;
+
+}));
+},{}]},{},[1])(1)
+});
\ No newline at end of file
diff --git a/wikibase/config.js b/wikibase/config.js
index e513c5a..df43c37 100644
--- a/wikibase/config.js
+++ b/wikibase/config.js
@@ -26,6 +26,6 @@
return configLocal;
}
- return configDeploy;
+ return configLocal;
} )( window );
\ No newline at end of file
diff --git a/wikibase/queryService/ui/App.js b/wikibase/queryService/ui/App.js
index c288ca8..f1c7d45 100644
--- a/wikibase/queryService/ui/App.js
+++ b/wikibase/queryService/ui/App.js
@@ -73,6 +73,7 @@
Map: {icon: 'map-marker', label: 'Map', class:
'CoordinateResultBrowser', object: null, $element: null },
BubbleChart: {icon: 'tint', label: 'Bubble Chart',
class: 'BubbleChartResultBrowser', object: null, $element: null },
TreeMap: {icon: 'th', label: 'Tree Map', class:
'TreeMapResultBrowser', object: null, $element: null },
+ TagCloud: {icon: 'cloud', label: 'Tag Cloud', class:
'TagCloudChartResultBrowser', object: null, $element: null },
};
/**
diff --git
a/wikibase/queryService/ui/resultBrowser/TagCloudChartResultBrowser.js
b/wikibase/queryService/ui/resultBrowser/TagCloudChartResultBrowser.js
new file mode 100644
index 0000000..aca52ef
--- /dev/null
+++ b/wikibase/queryService/ui/resultBrowser/TagCloudChartResultBrowser.js
@@ -0,0 +1,133 @@
+var wikibase = wikibase || {};
+wikibase.queryService = wikibase.queryService || {};
+wikibase.queryService.ui = wikibase.queryService.ui || {};
+wikibase.queryService.ui.resultBrowser =
wikibase.queryService.ui.resultBrowser || {};
+window.mediaWiki = window.mediaWiki || {};
+
+wikibase.queryService.ui.resultBrowser.TagCloudChartResultBrowser = ( function
( $, d3, window ) {
+ "use strict";
+
+ /**
+ * A bubble chart result browser
+ *
+ * @class
wikibase.queryService.ui.resultBrowser.TagCloudChartResultBrowser
+ * @license GNU GPL v2+
+ *
+ * @author Jonas Kress
+ *
+ * @constructor
+ */
+ function SELF() {
+ }
+
+ SELF.prototype = new
wikibase.queryService.ui.resultBrowser.AbstractChartResultBrowser();
+
+ /**
+ * @property {boolean}
+ * @private
+ **/
+ SELF.prototype._hasLabel = false;
+
+ /**
+ * Draw browser to the given element
+ * @param {jQuery} $element to draw at
+ **/
+ SELF.prototype.draw = function ( $element ) {
+ var self = this,
+ data = [],
+ labelKeys = this._getLabelColumns();
+
+ $.each( this._getRows(), function( index, row ){
+
+ $.each( labelKeys, function ( index, key ) {
+ if( row[key] ){
+ data.push( row[key].value );
+ }
+ } );
+
+ $.each( row, function ( key, col ) {
+ self.processVisitors( col );
+ } );
+ } );
+
+ $element.html();
+ this._drawBubbleChart( $element, data );
+ };
+
+
+ SELF.prototype._drawBubbleChart = function ( $element, words ) {
+ console.log('dsda');
+ var fill = d3.scale.category20(),
+ width = $( window ).width(),
+ height = $( window ).height();
+
+
+
+ function draw(words) {
+ d3.select( $element[0] ).append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .append("g")
+ .attr("transform", "translate( "+(width/2)+" ,
"+(height/2)+")")
+ .selectAll("text")
+ .data(words)
+ .enter().append("text")
+ .style("font-size", function(d) { return d.size + "px";
})
+ .style("font-family", "Impact")
+ .style("fill", function(d, i) { return fill(i); })
+ .attr("text-anchor", "middle")
+ .attr("transform", function(d) {
+ return "translate(" + [d.x, d.y] + ")rotate(" +
d.rotate + ")";
+ })
+ .text(function(d) { return d.text; });
+ }
+
+ d3.layout.cloud().size([width, height])
+ .words(words.map(function(d) {
+ return {text: d, size: 10 + Math.random() *
50};
+ }))
+ .rotate(function() { return ~~(Math.random() * 2) *
90; })
+ .font("Impact")
+ .fontSize(function(d) { return d.size; })
+ .on("end", draw)
+ .start();
+ };
+
+ /**
+ * Checks whether the browser can draw the given result
+ *
+ * @return {boolean}
+ */
+ SELF.prototype.isDrawable = function () {
+
+ if( this._hasLabel ){
+ return true;
+ }
+
+ return false;
+ };
+
+ /**
+ * Receiving data from the a visit
+ * @param data
+ * @return {boolean} false if there is no revisit needed
+ */
+ SELF.prototype.visit = function( data ) {
+ return this._checkColumn( data );
+ };
+
+ /**
+ * Check if this value contains an coordinate value.
+ */
+ SELF.prototype._checkColumn = function ( value ) {
+
+ if( this._isLabel( value ) ){
+ this._hasLabel = true;
+ return false;
+ }
+
+ return true;
+ };
+
+ return SELF;
+}( jQuery, d3, window ) );
--
To view, visit https://gerrit.wikimedia.org/r/278063
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I71dfdedf01d1cc0217c026bad7559a39f60c68f2
Gerrit-PatchSet: 1
Gerrit-Project: wikidata/query/gui
Gerrit-Branch: master
Gerrit-Owner: Jonas Kress (WMDE) <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits