New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@flourish/pocket-knife

Package Overview
Dependencies
Maintainers
22
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@flourish/pocket-knife - npm Package Compare versions

Comparing version

to
1.3.2

6

.eslintrc.json
{
"extends": "../../.eslintrc.cjs"
"extends": "../../.eslintrc.cjs",
"ignorePatterns": [
"pocket_knife.js",
"pocket_knife.min.js"
]
}

13

package.json
{
"name": "@flourish/pocket-knife",
"type": "module",
"version": "1.3.0",
"version": "1.3.2",
"private": false,

@@ -24,3 +24,6 @@ "description": "Flourish module with handy tools",

"devDependencies": {
"@babel/core": "^7.23.6",
"@babel/preset-env": "^7.23.6",
"@flourish/eslint-plugin-flourish": "^0.7.2",
"@rollup/plugin-babel": "^6.0.4",
"@types/mocha": "^10.0.1",

@@ -32,4 +35,4 @@ "canvas": "^2.11.0",

"mocha": "^10.2.0",
"rollup": "^1.1.2",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup": "^4.6.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"uglify-js": "^3.4.9"

@@ -43,7 +46,7 @@ },

"d3-collection": "^1.0.7",
"d3-color": "^1.4.0"
"d3-color": "^3.1.0"
},
"scripts": {
"test": "t(){ mocha ${1:-src}; }; t",
"lint": "eslint src",
"lint": "eslint .",
"build": "rollup -c",

@@ -50,0 +53,0 @@ "minify": "uglifyjs -m -o pocket_knife.min.js pocket_knife.js",

(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.pocketKnife = {}));
}(this, (function (exports) { 'use strict';
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.pocketKnife = {}));
})(this, (function (exports) { 'use strict';

@@ -24,11 +24,11 @@ function define(constructor, factory, prototype) {

var reI = "\\s*([+-]?\\d+)\\s*",
reN = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*",
reP = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*",
reN = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",
reP = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",
reHex = /^#([0-9a-f]{3,8})$/,
reRgbInteger = new RegExp("^rgb\\(" + [reI, reI, reI] + "\\)$"),
reRgbPercent = new RegExp("^rgb\\(" + [reP, reP, reP] + "\\)$"),
reRgbaInteger = new RegExp("^rgba\\(" + [reI, reI, reI, reN] + "\\)$"),
reRgbaPercent = new RegExp("^rgba\\(" + [reP, reP, reP, reN] + "\\)$"),
reHslPercent = new RegExp("^hsl\\(" + [reN, reP, reP] + "\\)$"),
reHslaPercent = new RegExp("^hsla\\(" + [reN, reP, reP, reN] + "\\)$");
reRgbInteger = new RegExp(`^rgb\\(${reI},${reI},${reI}\\)$`),
reRgbPercent = new RegExp(`^rgb\\(${reP},${reP},${reP}\\)$`),
reRgbaInteger = new RegExp(`^rgba\\(${reI},${reI},${reI},${reN}\\)$`),
reRgbaPercent = new RegExp(`^rgba\\(${reP},${reP},${reP},${reN}\\)$`),
reHslPercent = new RegExp(`^hsl\\(${reN},${reP},${reP}\\)$`),
reHslaPercent = new RegExp(`^hsla\\(${reN},${reP},${reP},${reN}\\)$`);

@@ -187,6 +187,6 @@ var named = {

define(Color, color, {
copy: function(channels) {
copy(channels) {
return Object.assign(new this.constructor, this, channels);
},
displayable: function() {
displayable() {
return this.rgb().displayable();

@@ -196,2 +196,3 @@ },

formatHex: color_formatHex,
formatHex8: color_formatHex8,
formatHsl: color_formatHsl,

@@ -206,2 +207,6 @@ formatRgb: color_formatRgb,

function color_formatHex8() {
return this.rgb().formatHex8();
}
function color_formatHsl() {

@@ -262,14 +267,17 @@ return hslConvert(this).formatHsl();

define(Rgb, rgb, extend(Color, {
brighter: function(k) {
brighter(k) {
k = k == null ? brighter : Math.pow(brighter, k);
return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
},
darker: function(k) {
darker(k) {
k = k == null ? darker : Math.pow(darker, k);
return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);
},
rgb: function() {
rgb() {
return this;
},
displayable: function() {
clamp() {
return new Rgb(clampi(this.r), clampi(this.g), clampi(this.b), clampa(this.opacity));
},
displayable() {
return (-0.5 <= this.r && this.r < 255.5)

@@ -282,2 +290,3 @@ && (-0.5 <= this.g && this.g < 255.5)

formatHex: rgb_formatHex,
formatHex8: rgb_formatHex8,
formatRgb: rgb_formatRgb,

@@ -288,16 +297,24 @@ toString: rgb_formatRgb

function rgb_formatHex() {
return "#" + hex(this.r) + hex(this.g) + hex(this.b);
return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}`;
}
function rgb_formatHex8() {
return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}${hex((isNaN(this.opacity) ? 1 : this.opacity) * 255)}`;
}
function rgb_formatRgb() {
var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
return (a === 1 ? "rgb(" : "rgba(")
+ Math.max(0, Math.min(255, Math.round(this.r) || 0)) + ", "
+ Math.max(0, Math.min(255, Math.round(this.g) || 0)) + ", "
+ Math.max(0, Math.min(255, Math.round(this.b) || 0))
+ (a === 1 ? ")" : ", " + a + ")");
const a = clampa(this.opacity);
return `${a === 1 ? "rgb(" : "rgba("}${clampi(this.r)}, ${clampi(this.g)}, ${clampi(this.b)}${a === 1 ? ")" : `, ${a})`}`;
}
function clampa(opacity) {
return isNaN(opacity) ? 1 : Math.max(0, Math.min(1, opacity));
}
function clampi(value) {
return Math.max(0, Math.min(255, Math.round(value) || 0));
}
function hex(value) {
value = Math.max(0, Math.min(255, Math.round(value) || 0));
value = clampi(value);
return (value < 16 ? "0" : "") + value.toString(16);

@@ -351,11 +368,11 @@ }

define(Hsl, hsl, extend(Color, {
brighter: function(k) {
brighter(k) {
k = k == null ? brighter : Math.pow(brighter, k);
return new Hsl(this.h, this.s, this.l * k, this.opacity);
},
darker: function(k) {
darker(k) {
k = k == null ? darker : Math.pow(darker, k);
return new Hsl(this.h, this.s, this.l * k, this.opacity);
},
rgb: function() {
rgb() {
var h = this.h % 360 + (this.h < 0) * 360,

@@ -373,3 +390,6 @@ s = isNaN(h) || isNaN(this.s) ? 0 : this.s,

},
displayable: function() {
clamp() {
return new Hsl(clamph(this.h), clampt(this.s), clampt(this.l), clampa(this.opacity));
},
displayable() {
return (0 <= this.s && this.s <= 1 || isNaN(this.s))

@@ -379,12 +399,17 @@ && (0 <= this.l && this.l <= 1)

},
formatHsl: function() {
var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a));
return (a === 1 ? "hsl(" : "hsla(")
+ (this.h || 0) + ", "
+ (this.s || 0) * 100 + "%, "
+ (this.l || 0) * 100 + "%"
+ (a === 1 ? ")" : ", " + a + ")");
formatHsl() {
const a = clampa(this.opacity);
return `${a === 1 ? "hsl(" : "hsla("}${clamph(this.h)}, ${clampt(this.s) * 100}%, ${clampt(this.l) * 100}%${a === 1 ? ")" : `, ${a})`}`;
}
}));
function clamph(value) {
value = (value || 0) % 360;
return value < 0 ? value + 360 : value;
}
function clampt(value) {
return Math.max(0, Math.min(1, value || 0));
}
/* From FvD 13.37, CSS Color Module Level 3 */

@@ -398,175 +423,2 @@ function hsl2rgb(h, m1, m2) {

var deg2rad = Math.PI / 180;
var rad2deg = 180 / Math.PI;
// https://observablehq.com/@mbostock/lab-and-rgb
var K = 18,
Xn = 0.96422,
Yn = 1,
Zn = 0.82521,
t0 = 4 / 29,
t1 = 6 / 29,
t2 = 3 * t1 * t1,
t3 = t1 * t1 * t1;
function labConvert(o) {
if (o instanceof Lab) return new Lab(o.l, o.a, o.b, o.opacity);
if (o instanceof Hcl) return hcl2lab(o);
if (!(o instanceof Rgb)) o = rgbConvert(o);
var r = rgb2lrgb(o.r),
g = rgb2lrgb(o.g),
b = rgb2lrgb(o.b),
y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn), x, z;
if (r === g && g === b) x = z = y; else {
x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn);
z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn);
}
return new Lab(116 * y - 16, 500 * (x - y), 200 * (y - z), o.opacity);
}
function lab(l, a, b, opacity) {
return arguments.length === 1 ? labConvert(l) : new Lab(l, a, b, opacity == null ? 1 : opacity);
}
function Lab(l, a, b, opacity) {
this.l = +l;
this.a = +a;
this.b = +b;
this.opacity = +opacity;
}
define(Lab, lab, extend(Color, {
brighter: function(k) {
return new Lab(this.l + K * (k == null ? 1 : k), this.a, this.b, this.opacity);
},
darker: function(k) {
return new Lab(this.l - K * (k == null ? 1 : k), this.a, this.b, this.opacity);
},
rgb: function() {
var y = (this.l + 16) / 116,
x = isNaN(this.a) ? y : y + this.a / 500,
z = isNaN(this.b) ? y : y - this.b / 200;
x = Xn * lab2xyz(x);
y = Yn * lab2xyz(y);
z = Zn * lab2xyz(z);
return new Rgb(
lrgb2rgb( 3.1338561 * x - 1.6168667 * y - 0.4906146 * z),
lrgb2rgb(-0.9787684 * x + 1.9161415 * y + 0.0334540 * z),
lrgb2rgb( 0.0719453 * x - 0.2289914 * y + 1.4052427 * z),
this.opacity
);
}
}));
function xyz2lab(t) {
return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0;
}
function lab2xyz(t) {
return t > t1 ? t * t * t : t2 * (t - t0);
}
function lrgb2rgb(x) {
return 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055);
}
function rgb2lrgb(x) {
return (x /= 255) <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}
function hclConvert(o) {
if (o instanceof Hcl) return new Hcl(o.h, o.c, o.l, o.opacity);
if (!(o instanceof Lab)) o = labConvert(o);
if (o.a === 0 && o.b === 0) return new Hcl(NaN, 0 < o.l && o.l < 100 ? 0 : NaN, o.l, o.opacity);
var h = Math.atan2(o.b, o.a) * rad2deg;
return new Hcl(h < 0 ? h + 360 : h, Math.sqrt(o.a * o.a + o.b * o.b), o.l, o.opacity);
}
function hcl(h, c, l, opacity) {
return arguments.length === 1 ? hclConvert(h) : new Hcl(h, c, l, opacity == null ? 1 : opacity);
}
function Hcl(h, c, l, opacity) {
this.h = +h;
this.c = +c;
this.l = +l;
this.opacity = +opacity;
}
function hcl2lab(o) {
if (isNaN(o.h)) return new Lab(o.l, 0, 0, o.opacity);
var h = o.h * deg2rad;
return new Lab(o.l, Math.cos(h) * o.c, Math.sin(h) * o.c, o.opacity);
}
define(Hcl, hcl, extend(Color, {
brighter: function(k) {
return new Hcl(this.h, this.c, this.l + K * (k == null ? 1 : k), this.opacity);
},
darker: function(k) {
return new Hcl(this.h, this.c, this.l - K * (k == null ? 1 : k), this.opacity);
},
rgb: function() {
return hcl2lab(this).rgb();
}
}));
var A = -0.14861,
B = +1.78277,
C = -0.29227,
D = -0.90649,
E = +1.97294,
ED = E * D,
EB = E * B,
BC_DA = B * C - D * A;
function cubehelixConvert(o) {
if (o instanceof Cubehelix) return new Cubehelix(o.h, o.s, o.l, o.opacity);
if (!(o instanceof Rgb)) o = rgbConvert(o);
var r = o.r / 255,
g = o.g / 255,
b = o.b / 255,
l = (BC_DA * b + ED * r - EB * g) / (BC_DA + ED - EB),
bl = b - l,
k = (E * (g - l) - C * bl) / D,
s = Math.sqrt(k * k + bl * bl) / (E * l * (1 - l)), // NaN if l=0 or l=1
h = s ? Math.atan2(k, bl) * rad2deg - 120 : NaN;
return new Cubehelix(h < 0 ? h + 360 : h, s, l, o.opacity);
}
function cubehelix(h, s, l, opacity) {
return arguments.length === 1 ? cubehelixConvert(h) : new Cubehelix(h, s, l, opacity == null ? 1 : opacity);
}
function Cubehelix(h, s, l, opacity) {
this.h = +h;
this.s = +s;
this.l = +l;
this.opacity = +opacity;
}
define(Cubehelix, cubehelix, extend(Color, {
brighter: function(k) {
k = k == null ? brighter : Math.pow(brighter, k);
return new Cubehelix(this.h, this.s, this.l * k, this.opacity);
},
darker: function(k) {
k = k == null ? darker : Math.pow(darker, k);
return new Cubehelix(this.h, this.s, this.l * k, this.opacity);
},
rgb: function() {
var h = isNaN(this.h) ? 0 : (this.h + 120) * deg2rad,
l = +this.l,
a = isNaN(this.s) ? 0 : this.s * l * (1 - l),
cosh = Math.cos(h),
sinh = Math.sin(h);
return new Rgb(
255 * (l + a * (A * cosh + B * sinh)),
255 * (l + a * (C * cosh + D * sinh)),
255 * (l + a * (E * cosh)),
this.opacity
);
}
}));
var prefix = "$";

@@ -816,4 +668,4 @@

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

@@ -889,70 +741,130 @@ function remToPx(rem) {

function wrapStringToLines(text, font_styles, text_max_lines, max_width) {
var lines = [];
var max_line_width = 0;
function wrapStringToLines(text, font_styles, max_lines, max_width, allow_single_word_truncation = true) {
if (typeof text !== "string") text = String(text);
const isOverflow = string => ctx.measureText(string).width > max_width;
const truncateStringToWidth = (string, width) => {
let string_width = ctx.measureText(string).width;
// Check if the string needs truncation
if (string_width <= width) {
return string;
}
// if no lines fit, return max_line_width of 0 (i.e. hide string)
if (text_max_lines !== null && text_max_lines <= 0) {
lines.widest_line = max_line_width;
let remove_counter = 1;
let truncated_string = string;
do {
truncated_string = string.substring(0, string.length - remove_counter) + "…";
string_width = ctx.measureText(truncated_string).width;
} while ((string_width > width) && (++remove_counter < string.length));
if (truncated_string.length < 1) truncated_string = string.substring(0, 1) + "…";
return truncated_string;
};
ctx.font = font_styles;
let lines = [];
Object.defineProperty(lines, "widest_line", {
value: 0,
enumerable: true,
writable: true
});
Object.defineProperty(lines, "has_truncated", {
value: false,
enumerable: true,
writable: true
});
if (typeof text === "undefined" || (Number.isInteger(max_lines) && max_lines <= 0)) return lines;
if (text.length === 0) {
lines.push("");
return lines;
}
if (max_width <= 0) {
if (text.length > 0) {
lines.push(text.slice(0, 1) + "…");
lines.widest_line = ctx.measureText(lines[0]).width;
}
return lines;
}
ctx.font = font_styles;
if (!text || text.length === 0 || max_lines === 0 || max_width <= 0) return lines;
const all_words = text.split(/\s+/g);
// if everything fits in one line, leave as a single string
var text_width = ctx.measureText(text).width;
if (text_width <= max_width) {
// Check if whole label fits ok - no work needed - return it
if (!isOverflow(text)) {
lines.push(text);
max_line_width = text_width;
lines.widest_line = ctx.measureText(text).width;
lines.has_truncated = false;
return lines;
}
// Otherwise…
else {
// Create array of words
var words = text.trim().split(/\s+/g);
// Loop through the words, adding them until they won't fit
var current_line = "";
for (var i = 0; i < words.length; i++) {
var word = words[i];
var string = current_line + (current_line ? " " : "") + word;
var string_width = ctx.measureText(string).width;
// If the string including the next word fits, update the
// current_line and the max_width
if (string_width <= max_width) {
current_line = string;
max_line_width = Math.max(max_line_width, string_width);
}
// If we reach this point, the text has overflowed,
// so should always result in a truncation or a new line
else {
var last_line = lines.length + 1 == text_max_lines;
if (!last_line && current_line) {
lines.push(current_line);
var word_width = ctx.measureText(word).width;
if (word_width <= max_width) {
current_line = word;
max_line_width = Math.max(max_line_width, word_width);
continue;
}
// Check if the first word doesn't even fit in the space - return truncated first word
let first_word = new String(all_words[0]);
if (first_word && isOverflow(first_word)) {
if (allow_single_word_truncation) {
first_word = truncateStringToWidth(first_word, max_width);
}
else if (all_words.length > 1) {
first_word += "…";
}
lines.push(first_word);
lines.widest_line = ctx.measureText(first_word).width;
lines.has_truncated = true;
return lines;
}
// Fit words in line by line, truncating if necessary - return
let currentLine = "";
let has_truncated = false;
all_words.forEach(word => {
if (has_truncated) return;
if (isOverflow(currentLine + (currentLine ? " " : "") + word)) {
if (currentLine) {
const currentLineWidth = ctx.measureText(currentLine).width;
if (currentLineWidth > max_width) {
currentLine = truncateStringToWidth(currentLine, max_width);
has_truncated = true;
}
// Truncate
var remove_counter = 1;
var truncated_string = current_line;
do {
truncated_string = current_line.substring(0, string.length - remove_counter) + "…";
string_width = ctx.measureText(truncated_string).width;
}
while ((string_width > max_width) && (++remove_counter < string.length));
max_line_width = Math.max(max_line_width, string_width);
current_line = truncated_string;
break;
lines.push(currentLine);
currentLine = "";
}
currentLine = word;
}
lines.push(current_line);
else {
currentLine += (currentLine ? " " : "") + word;
}
});
if (currentLine && !has_truncated) {
lines.push(truncateStringToWidth(currentLine, max_width));
}
lines.widest_line = max_line_width;
if (lines.length > max_lines) {
// can't have more lines than max_lines, so slice and add ellipses.
const to_truncate = lines.length > max_lines;
if (to_truncate && Number.isInteger(max_lines)) {
lines.splice(max_lines, lines.length - max_lines);
let last_line = lines[lines.length - 1];
// check if adding an ellipsis will exceed max_width
if (isOverflow(last_line + "…")) {
// truncate further before adding ellipsis
last_line = truncateStringToWidth(last_line, max_width - ctx.measureText("…").width);
}
// add ellipsis if not already present
if (last_line.slice(-1) !== "…") {
last_line += "…";
}
lines[lines.length - 1] = last_line;
has_truncated = true;
}
}
// find the widest entry in lines and measure it
lines.widest_line = Math.max(...lines.map(line => ctx.measureText(line).width));
lines.has_truncated = has_truncated;
return lines;
}
function getUniqueValuesFromBinding(data, binding, index) {

@@ -986,4 +898,2 @@ if (data.column_names[binding] === undefined) return [];

Object.defineProperty(exports, '__esModule', { value: true });
})));
}));

@@ -0,1 +1,7 @@

# 1.3.2
* Ensure non-string inputs to wrapStringToLines are forced into strings
# 1.3.1
* Changed the wrapStringToLinesFunction to add new functionality but not effect on existing usage
# 1.3.0

@@ -2,0 +8,0 @@ * Improve contrast checking in `isPale()`

@@ -1,3 +0,6 @@

import nodeResolve from "rollup-plugin-node-resolve";
import nodeResolve from "@rollup/plugin-node-resolve";
import babel from "@rollup/plugin-babel";
const is_production = process.env.NODE_ENV === "production" || !!process.env.CI;
export default {

@@ -11,3 +14,4 @@ input: "src/index.js",

plugins: [
nodeResolve()
nodeResolve(),
is_production && babel({ babelHelpers: "bundled", rootMode: "upward" }),
],

@@ -14,0 +18,0 @@ onwarn: function (warning, warn) {

@@ -7,4 +7,4 @@ import { color } from "d3-color";

var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

@@ -80,70 +80,130 @@ function remToPx(rem) {

function wrapStringToLines(text, font_styles, text_max_lines, max_width) {
var lines = [];
var max_line_width = 0;
function wrapStringToLines(text, font_styles, max_lines, max_width, allow_single_word_truncation = true) {
if (typeof text !== "string") text = String(text);
const isOverflow = string => ctx.measureText(string).width > max_width;
const truncateStringToWidth = (string, width) => {
let string_width = ctx.measureText(string).width;
// Check if the string needs truncation
if (string_width <= width) {
return string;
}
// if no lines fit, return max_line_width of 0 (i.e. hide string)
if (text_max_lines !== null && text_max_lines <= 0) {
lines.widest_line = max_line_width;
let remove_counter = 1;
let truncated_string = string;
do {
truncated_string = string.substring(0, string.length - remove_counter) + "…";
string_width = ctx.measureText(truncated_string).width;
} while ((string_width > width) && (++remove_counter < string.length));
if (truncated_string.length < 1) truncated_string = string.substring(0, 1) + "…";
return truncated_string;
};
ctx.font = font_styles;
let lines = [];
Object.defineProperty(lines, "widest_line", {
value: 0,
enumerable: true,
writable: true
});
Object.defineProperty(lines, "has_truncated", {
value: false,
enumerable: true,
writable: true
});
if (typeof text === "undefined" || (Number.isInteger(max_lines) && max_lines <= 0)) return lines;
if (text.length === 0) {
lines.push("");
return lines;
}
if (max_width <= 0) {
if (text.length > 0) {
lines.push(text.slice(0, 1) + "…");
lines.widest_line = ctx.measureText(lines[0]).width;
}
return lines;
}
ctx.font = font_styles;
if (!text || text.length === 0 || max_lines === 0 || max_width <= 0) return lines;
const all_words = text.split(/\s+/g);
// if everything fits in one line, leave as a single string
var text_width = ctx.measureText(text).width;
if (text_width <= max_width) {
// Check if whole label fits ok - no work needed - return it
if (!isOverflow(text)) {
lines.push(text);
max_line_width = text_width;
lines.widest_line = ctx.measureText(text).width;
lines.has_truncated = false;
return lines;
}
// Otherwise…
else {
// Create array of words
var words = text.trim().split(/\s+/g);
// Loop through the words, adding them until they won't fit
var current_line = "";
for (var i = 0; i < words.length; i++) {
var word = words[i];
var string = current_line + (current_line ? " " : "") + word;
var string_width = ctx.measureText(string).width;
// If the string including the next word fits, update the
// current_line and the max_width
if (string_width <= max_width) {
current_line = string;
max_line_width = Math.max(max_line_width, string_width);
}
// If we reach this point, the text has overflowed,
// so should always result in a truncation or a new line
else {
var last_line = lines.length + 1 == text_max_lines;
if (!last_line && current_line) {
lines.push(current_line);
var word_width = ctx.measureText(word).width;
if (word_width <= max_width) {
current_line = word;
max_line_width = Math.max(max_line_width, word_width);
continue;
}
// Check if the first word doesn't even fit in the space - return truncated first word
let first_word = new String(all_words[0]);
if (first_word && isOverflow(first_word)) {
if (allow_single_word_truncation) {
first_word = truncateStringToWidth(first_word, max_width);
}
else if (all_words.length > 1) {
first_word += "…";
}
lines.push(first_word);
lines.widest_line = ctx.measureText(first_word).width;
lines.has_truncated = true;
return lines;
}
// Fit words in line by line, truncating if necessary - return
let currentLine = "";
let has_truncated = false;
all_words.forEach(word => {
if (has_truncated) return;
if (isOverflow(currentLine + (currentLine ? " " : "") + word)) {
if (currentLine) {
const currentLineWidth = ctx.measureText(currentLine).width;
if (currentLineWidth > max_width) {
currentLine = truncateStringToWidth(currentLine, max_width);
has_truncated = true;
}
// Truncate
var remove_counter = 1;
var truncated_string = current_line;
do {
truncated_string = current_line.substring(0, string.length - remove_counter) + "…";
string_width = ctx.measureText(truncated_string).width;
}
while ((string_width > max_width) && (++remove_counter < string.length));
max_line_width = Math.max(max_line_width, string_width);
current_line = truncated_string;
break;
lines.push(currentLine);
currentLine = "";
}
currentLine = word;
}
lines.push(current_line);
else {
currentLine += (currentLine ? " " : "") + word;
}
});
if (currentLine && !has_truncated) {
lines.push(truncateStringToWidth(currentLine, max_width));
}
lines.widest_line = max_line_width;
if (lines.length > max_lines) {
// can't have more lines than max_lines, so slice and add ellipses.
const to_truncate = lines.length > max_lines;
if (to_truncate && Number.isInteger(max_lines)) {
lines.splice(max_lines, lines.length - max_lines);
let last_line = lines[lines.length - 1];
// check if adding an ellipsis will exceed max_width
if (isOverflow(last_line + "…")) {
// truncate further before adding ellipsis
last_line = truncateStringToWidth(last_line, max_width - ctx.measureText("…").width);
}
// add ellipsis if not already present
if (last_line.slice(-1) !== "…") {
last_line += "…";
}
lines[lines.length - 1] = last_line;
has_truncated = true;
}
}
// find the widest entry in lines and measure it
lines.widest_line = Math.max(...lines.map(line => ctx.measureText(line).width));
lines.has_truncated = has_truncated;
return lines;
}
function getUniqueValuesFromBinding(data, binding, index) {

@@ -163,3 +223,2 @@ if (data.column_names[binding] === undefined) return [];

export { remToPx, getTextDimensions, getTextWidth, getTextHeight, getTextDirection, isImage, isPale, isUrl, hexToColor, hexToRgba, wrapStringToLines, getUniqueValuesFromBinding, createScreenshotSVG };

@@ -10,3 +10,3 @@ import { expect } from "chai";

it("correctly truncates multiple lines", () => {
const lines = wrapStringToLines("This is a particularly long Y axis tick that should wrap onto multiple lines", "Arial 12px", 3, 100);
const lines = wrapStringToLines("This is a particularly long Y axis tick that should wrap onto multiple lines", "normal 11px Arial", 3, 100);

@@ -18,6 +18,35 @@ expect(lines).deep.eq([

]);
const lines_2 = wrapStringToLines("Trent Alexander-Arnold", "normal 12px Arial", 3, 66);
expect(lines_2).deep.eq([
"Trent",
"Alexande…"
]);
const lines_3 = wrapStringToLines("Alexander-Arnold Trent", "normal 12px Arial", 3, 66);
expect(lines_3).deep.eq([
"Alexande…"
]);
const lines_4 = wrapStringToLines("Trent Alexander-Arnold", "normal 12px Arial", 3, 28);
expect(lines_4).deep.eq([
"Trent",
"Al…"
]);
const lines_5 = wrapStringToLines("Virgil van Dijk", "normal 10px Arial", 3, 51);
expect(lines_5).deep.eq([
"Virgil van",
"Dijk"
]);
const lines_6 = wrapStringToLines("Virgil van Dijk", "normal 14.399999999999999px Arial", 3, 53.6);
expect(lines_6).deep.eq([
"Virgil",
"van Dijk"
]);
});
it("returns no lines when max_lines is less than 1", () => {
const lines = wrapStringToLines("This is a string", "Arial 12px", 0, 100);
const lines = wrapStringToLines("This is a string", "normal 14px Arial", 0, 100);

@@ -28,6 +57,91 @@ expect(lines).deep.eq([]);

it("does not return no lines when max_lines is null", () => {
const lines =wrapStringToLines("This is a string", "Arial 12px", null, 100);
const lines = wrapStringToLines("This is a string", "normal 14px Arial", null, 100);
expect(lines).deep.eq(["This is a string"]);
});
it("handles number input", () => {
const number_input = 43;
const number_output = wrapStringToLines(number_input, "normal 14px Arial", 2, 100);
expect(number_output).deep.eq(["43"]);
});
it("handles empty string input", () => {
const empty_string_input = "";
const empty_string_output = wrapStringToLines(empty_string_input, "normal 14px Arial", 2, 100);
expect(empty_string_output).deep.eq([""]);
});
it("handles whitespace input", () => {
const whitespace_input = " ";
const whitespace_output = wrapStringToLines(whitespace_input, "normal 14px Arial", 2, 100);
expect(whitespace_output).deep.eq([" "]);
});
it("handles negative max_width", () => {
const negative_max_width_input = -50;
const negative_max_width_output = wrapStringToLines("Test string", "normal 14px Arial", 2, negative_max_width_input);
expect(negative_max_width_output).deep.eq(["T…"]);
});
it("handles zero max_width", () => {
const zero_max_width_input = "Test string";
const zero_max_width_output = wrapStringToLines(zero_max_width_input, "normal 14px Arial", 2, 0);
expect(zero_max_width_output).deep.eq(["T…"]);
});
it("handles a single long word", () => {
const single_long_word_input = "Supercalifragilisticexpialidocious";
const single_long_word_output = wrapStringToLines(single_long_word_input, "normal 14px Arial", 2, 50);
expect(single_long_word_output).deep.eq(["Supe…"]);
});
it("handles different font styles", () => {
const different_font_styles_input = "normal Times New Roman 12px";
const different_font_styles_output = wrapStringToLines("Test string", different_font_styles_input, 2, 100);
expect(different_font_styles_output).deep.eq(["Test string"]);
});
it("handles extremely small font size", () => {
const small_font_size_input = "normal 1px Arial";
const small_font_size_output = wrapStringToLines("Test string", small_font_size_input, 2, 100);
expect(small_font_size_output).deep.eq(["Test string"]);
});
it("handles extremely large font size", () => {
const large_font_size_input = "normal 100px Arial";
const large_font_size_output = wrapStringToLines("Test string", large_font_size_input, 2, 100);
expect(large_font_size_output).deep.eq(["T…"]);
});
it("handles non-boolean allow_single_word_truncation", () => {
const non_boolean_truncation_input = "Test string";
const non_boolean_truncation_output = wrapStringToLines(non_boolean_truncation_input, "normal 14px Arial", 2, 50, "true");
expect(non_boolean_truncation_output).length.above(0);
});
it("handles very long text input", () => {
const long_text_input = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.".repeat(10);
const very_long_input_output = wrapStringToLines(long_text_input, "normal 14px Arial", 5, 100);
expect(very_long_input_output).deep.eq([
"Lorem ipsum",
"dolor sit amet,",
"consectetur",
"adipiscing elit,",
"sed do…",
]);
});
it("handles special characters", () => {
const special_characters_input = "!@#$%^&*()";
const special_characters_output = wrapStringToLines(special_characters_input, "normal 14px Arial", 2, 100);
expect(special_characters_output).deep.eq([ "!@#$%^&*()" ]);
});
it("handles numerical inputs", () => {
const numerical_inputs = "1234567890";
const numerical_inputs_output = wrapStringToLines(numerical_inputs, "normal 14px Arial", 2, 50);
expect(numerical_inputs_output).deep.eq([ "1234…" ]);
});
});