Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format attribute for widgets, popups and legends [ch60845] #1626

Merged
merged 17 commits into from
May 12, 2020
Merged
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 .jshintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cartoframes/assets/src/bundle.js
3 changes: 2 additions & 1 deletion .jshintrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"esversion": 6
"esversion": 6,
"laxbreak" : true
}
330 changes: 321 additions & 9 deletions cartoframes/assets/src/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,18 +135,330 @@ var init = (function () {
stacktrace$.innerHTML = list.join('\n');
}

function format(value) {
// Computes the decimal coefficient and exponent of the specified number x with
// significant digits p, where x is positive and p is in [1, 21] or undefined.
// For example, formatDecimal(1.23) returns ["123", 0].
function formatDecimal(x, p) {
if ((i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf("e")) < 0) return null; // NaN, ±Infinity
var i, coefficient = x.slice(0, i);

// The string returned by toExponential either has the form \d\.\d+e[-+]\d+
// (e.g., 1.2e+3) or the form \de[-+]\d+ (e.g., 1e+3).
return [
coefficient.length > 1 ? coefficient[0] + coefficient.slice(2) : coefficient,
+x.slice(i + 1)
];
}

function exponent(x) {
return x = formatDecimal(Math.abs(x)), x ? x[1] : NaN;
}

function formatGroup(grouping, thousands) {
return function(value, width) {
var i = value.length,
t = [],
j = 0,
g = grouping[0],
length = 0;

while (i > 0 && g > 0) {
if (length + g + 1 > width) g = Math.max(1, width - length);
t.push(value.substring(i -= g, i + g));
if ((length += g + 1) > width) break;
g = grouping[j = (j + 1) % grouping.length];
}

return t.reverse().join(thousands);
};
}

function formatNumerals(numerals) {
return function(value) {
return value.replace(/[0-9]/g, function(i) {
return numerals[+i];
});
};
}

// [[fill]align][sign][symbol][0][width][,][.precision][~][type]
var re = /^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;

function formatSpecifier(specifier) {
if (!(match = re.exec(specifier))) throw new Error("invalid format: " + specifier);
var match;
return new FormatSpecifier({
fill: match[1],
align: match[2],
sign: match[3],
symbol: match[4],
zero: match[5],
width: match[6],
comma: match[7],
precision: match[8] && match[8].slice(1),
trim: match[9],
type: match[10]
});
}

formatSpecifier.prototype = FormatSpecifier.prototype; // instanceof

function FormatSpecifier(specifier) {
this.fill = specifier.fill === undefined ? " " : specifier.fill + "";
this.align = specifier.align === undefined ? ">" : specifier.align + "";
this.sign = specifier.sign === undefined ? "-" : specifier.sign + "";
this.symbol = specifier.symbol === undefined ? "" : specifier.symbol + "";
this.zero = !!specifier.zero;
this.width = specifier.width === undefined ? undefined : +specifier.width;
this.comma = !!specifier.comma;
this.precision = specifier.precision === undefined ? undefined : +specifier.precision;
this.trim = !!specifier.trim;
this.type = specifier.type === undefined ? "" : specifier.type + "";
}

FormatSpecifier.prototype.toString = function() {
return this.fill
+ this.align
+ this.sign

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '+'; readers may interpret this as an expression boundary.

+ this.symbol

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '+'; readers may interpret this as an expression boundary.

+ (this.zero ? "0" : "")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '+'; readers may interpret this as an expression boundary.

+ (this.width === undefined ? "" : Math.max(1, this.width | 0))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '+'; readers may interpret this as an expression boundary.

+ (this.comma ? "," : "")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '+'; readers may interpret this as an expression boundary.

+ (this.precision === undefined ? "" : "." + Math.max(0, this.precision | 0))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '+'; readers may interpret this as an expression boundary.

+ (this.trim ? "~" : "")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '+'; readers may interpret this as an expression boundary.

+ this.type;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '+'; readers may interpret this as an expression boundary.

};

// Trims insignificant zeros, e.g., replaces 1.2000k with 1.2k.
function formatTrim(s) {
out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) {
switch (s[i]) {
case ".": i0 = i1 = i; break;
case "0": if (i0 === 0) i0 = i; i1 = i; break;
default: if (!+s[i]) break out; if (i0 > 0) i0 = 0; break;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confusing use of '!'.

}
}
return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'i0' used out of scope.
'i1' used out of scope.

}

var prefixExponent;

function formatPrefixAuto(x, p) {
var d = formatDecimal(x, p);
if (!d) return x + "";
var coefficient = d[0],
exponent = d[1],
i = exponent - (prefixExponent = Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + 1,
n = coefficient.length;
return i === n ? coefficient
: i > n ? coefficient + new Array(i - n + 1).join("0")
: i > 0 ? coefficient.slice(0, i) + "." + coefficient.slice(i)
: "0." + new Array(1 - i).join("0") + formatDecimal(x, Math.max(0, p + i - 1))[0]; // less than 1y!
}

function formatRounded(x, p) {
var d = formatDecimal(x, p);
if (!d) return x + "";
var coefficient = d[0],
exponent = d[1];
return exponent < 0 ? "0." + new Array(-exponent).join("0") + coefficient
: coefficient.length > exponent + 1 ? coefficient.slice(0, exponent + 1) + "." + coefficient.slice(exponent + 1)
: coefficient + new Array(exponent - coefficient.length + 2).join("0");
}

var formatTypes = {
"%": function(x, p) { return (x * 100).toFixed(p); },
"b": function(x) { return Math.round(x).toString(2); },
"c": function(x) { return x + ""; },
"d": function(x) { return Math.round(x).toString(10); },
"e": function(x, p) { return x.toExponential(p); },
"f": function(x, p) { return x.toFixed(p); },
"g": function(x, p) { return x.toPrecision(p); },
"o": function(x) { return Math.round(x).toString(8); },
"p": function(x, p) { return formatRounded(x * 100, p); },
"r": formatRounded,
"s": formatPrefixAuto,
"X": function(x) { return Math.round(x).toString(16).toUpperCase(); },
"x": function(x) { return Math.round(x).toString(16); }
};

function identity(x) {
return x;
}

var map = Array.prototype.map,
prefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];

function formatLocale(locale) {
var group = locale.grouping === undefined || locale.thousands === undefined ? identity : formatGroup(map.call(locale.grouping, Number), locale.thousands + ""),
currencyPrefix = locale.currency === undefined ? "" : locale.currency[0] + "",
currencySuffix = locale.currency === undefined ? "" : locale.currency[1] + "",
decimal = locale.decimal === undefined ? "." : locale.decimal + "",
numerals = locale.numerals === undefined ? identity : formatNumerals(map.call(locale.numerals, String)),
percent = locale.percent === undefined ? "%" : locale.percent + "",
minus = locale.minus === undefined ? "-" : locale.minus + "",
nan = locale.nan === undefined ? "NaN" : locale.nan + "";

function newFormat(specifier) {
specifier = formatSpecifier(specifier);

var fill = specifier.fill,
align = specifier.align,
sign = specifier.sign,
symbol = specifier.symbol,
zero = specifier.zero,
width = specifier.width,
comma = specifier.comma,
precision = specifier.precision,
trim = specifier.trim,
type = specifier.type;

// The "n" type is an alias for ",g".
if (type === "n") comma = true, type = "g";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected an assignment or function call and instead saw an expression.


// The "" type, and any invalid type, is an alias for ".12~g".
else if (!formatTypes[type]) precision === undefined && (precision = 12), trim = true, type = "g";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected an assignment or function call and instead saw an expression.


// If zero fill is specified, padding goes after sign and before digits.
if (zero || (fill === "0" && align === "=")) zero = true, fill = "0", align = "=";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected an assignment or function call and instead saw an expression.


// Compute the prefix and suffix.
// For SI-prefix, the suffix is lazily computed.
var prefix = symbol === "$" ? currencyPrefix : symbol === "#" && /[boxX]/.test(type) ? "0" + type.toLowerCase() : "",
suffix = symbol === "$" ? currencySuffix : /[%p]/.test(type) ? percent : "";

// What format function should we use?
// Is this an integer type?
// Can this type generate exponential notation?
var formatType = formatTypes[type],
maybeSuffix = /[defgprs%]/.test(type);

// Set the default precision if not specified,
// or clamp the specified precision to the supported range.
// For significant precision, it must be in [1, 21].
// For fixed precision, it must be in [0, 20].
precision = precision === undefined ? 6
: /[gprs]/.test(type) ? Math.max(1, Math.min(21, precision))
: Math.max(0, Math.min(20, precision));

function format(value) {
var valuePrefix = prefix,
valueSuffix = suffix,
i, n, c;

if (type === "c") {
valueSuffix = formatType(value) + valueSuffix;
value = "";
} else {
value = +value;

// Determine the sign. -0 is not less than 0, but 1 / -0 is!
var valueNegative = value < 0 || 1 / value < 0;

// Perform the initial formatting.
value = isNaN(value) ? nan : formatType(Math.abs(value), precision);

// Trim insignificant zeros.
if (trim) value = formatTrim(value);

// If a negative value rounds to zero after formatting, and no explicit positive sign is requested, hide the sign.
if (valueNegative && +value === 0 && sign !== "+") valueNegative = false;

// Compute the prefix and suffix.
valuePrefix = (valueNegative ? (sign === "(" ? sign : minus) : sign === "-" || sign === "(" ? "" : sign) + valuePrefix;
valueSuffix = (type === "s" ? prefixes[8 + prefixExponent / 3] : "") + valueSuffix + (valueNegative && sign === "(" ? ")" : "");

// Break the formatted value into the integer “value” part that can be
// grouped, and fractional or exponential “suffix” part that is not.
if (maybeSuffix) {
i = -1, n = value.length;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected an assignment or function call and instead saw an expression.

while (++i < n) {
if (c = value.charCodeAt(i), 48 > c || c > 57) {
valueSuffix = (c === 46 ? decimal + value.slice(i + 1) : value.slice(i)) + valueSuffix;
value = value.slice(0, i);
break;
}
}
}
}

// If the fill character is not "0", grouping is applied before padding.
if (comma && !zero) value = group(value, Infinity);

// Compute the padding.
var length = valuePrefix.length + value.length + valueSuffix.length,
padding = length < width ? new Array(width - length + 1).join(fill) : "";

// If the fill character is "0", grouping is applied after padding.
if (comma && zero) value = group(padding + value, padding.length ? width - valueSuffix.length : Infinity), padding = "";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected an assignment or function call and instead saw an expression.


// Reconstruct the final output based on the desired alignment.
switch (align) {
case "<": value = valuePrefix + value + valueSuffix + padding; break;
case "=": value = valuePrefix + padding + value + valueSuffix; break;
case "^": value = padding.slice(0, length = padding.length >> 1) + valuePrefix + value + valueSuffix + padding.slice(length); break;
default: value = padding + valuePrefix + value + valueSuffix; break;
}

return numerals(value);
}

format.toString = function() {
return specifier + "";
};

return format;
}

function formatPrefix(specifier, value) {
var f = newFormat((specifier = formatSpecifier(specifier), specifier.type = "f", specifier)),
e = Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3,
k = Math.pow(10, -e),
prefix = prefixes[8 + e / 3];
return function(value) {
return f(k * value) + prefix;
};
}

return {
format: newFormat,
formatPrefix: formatPrefix
};
}

var locale;
var format;
var formatPrefix;

defaultLocale({
decimal: ".",
thousands: ",",
grouping: [3],
currency: ["$", ""],
minus: "-"
});

function defaultLocale(definition) {
locale = formatLocale(definition);
format = locale.format;
formatPrefix = locale.formatPrefix;
return locale;
}

function formatter(value, specifier) {
const formatFunc = specifier ? format(specifier) : formatValue;

if (Array.isArray(value)) {
const [first, second] = value;
if (first === -Infinity) {
return `< ${formatValue(second)}`;
return `< ${formatFunc(second)}`;
}
if (second === Infinity) {
return `> ${formatValue(first)}`;
return `> ${formatFunc(first)}`;
}
return `${formatValue(first)} - ${formatValue(second)}`;
return `${formatFunc(first)} - ${formatFunc(second)}`;
}
return formatValue(value);
return formatFunc(value);
}

function formatValue(value) {
Expand Down Expand Up @@ -259,7 +571,7 @@ var init = (function () {
const variable = feature.variables[item.name];
if (variable) {
let value = variable.value;
value = formatValue(value);
value = formatter(value, item.format);

popupHTML = `
<span class="popup-name">${item.title}</span>
Expand Down Expand Up @@ -329,7 +641,7 @@ var init = (function () {
widget.element = widget.element || document.querySelector(`#${widget.id}-value`);

if (value && widget.element) {
widget.element.innerText = typeof value === 'number' ? format(value) : value;
widget.element.innerText = typeof value === 'number' ? formatter(value, widget.options.format) : value;
}
}

Expand All @@ -341,7 +653,6 @@ var init = (function () {
const type = _getWidgetType(mapLayer, widget.value, widget.prop);
const histogram = type === 'category' ? 'categoricalHistogram' : 'numericalHistogram';
bridge[histogram](widget.element, widget.value, widget.options);

break;
case 'category':
bridge.category(widget.element, widget.value, widget.options);
Expand Down Expand Up @@ -402,7 +713,8 @@ var init = (function () {
const order = legend.ascending ? 'ASC' : 'DESC';
const variable = legend.variable;
const config = { othersLabel, variable, order };
const options = { format, config, dynamic };
const formatFunc = (value) => formatter(value, legend.format);
const options = { format: formatFunc, config, dynamic };

if (legend.type.startsWith('size-continuous')) {
config.samples = 4;
Expand Down
5 changes: 3 additions & 2 deletions cartoframes/assets/src/legends.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { format } from './utils';
import { formatter } from './utils';

export function createLegends(layer, legends, layerIndex, mapIndex=0) {
if (legends.length) {
Expand All @@ -18,7 +18,8 @@ function _createLegend(layer, legend, layerIndex, legendIndex, mapIndex=0) {
const order = legend.ascending ? 'ASC' : 'DESC';
const variable = legend.variable;
const config = { othersLabel, variable, order };
const options = { format, config, dynamic };
const formatFunc = (value) => formatter(value, legend.format);
const options = { format: formatFunc, config, dynamic };

if (legend.type.startsWith('size-continuous')) {
config.samples = 4;
Expand Down
Loading