diff --git a/src/utils.js b/src/utils.js
index b4d59868d..ddfa70151 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -106,8 +106,8 @@ export const detectInit = function(text) {
* ```
*
* @param {string} text The text defining the graph
- * @param {string|RegExp} type The directive to return (default: null
- * @returns {object | Array} An object or Array representing the directive(s): { type: string, args: object|null } matchd by the input type
+ * @param {string|RegExp} type The directive to return (default: null)
+ * @returns {object | Array} An object or Array representing the directive(s): { type: string, args: object|null } matched by the input type
* if a single directive was found, that directive object will be returned.
*/
export const detectDirective = function(text, type = null) {
@@ -221,6 +221,20 @@ export const detectType = function(text) {
return 'flowchart';
};
+const memoize = (fn, resolver) => {
+ let cache = {};
+ return (...args) => {
+ let n = resolver ? resolver.apply(this, args) : args[0];
+ if (n in cache) {
+ return cache[n];
+ } else {
+ let result = fn(...args);
+ cache[n] = result;
+ return result;
+ }
+ };
+};
+
/**
* @function isSubstringInArray
* Detects whether a substring in present in a given array
@@ -256,13 +270,13 @@ export const formatUrl = (linkStr, config) => {
};
export const runFunc = (functionName, ...params) => {
- var arrPaths = functionName.split('.');
+ const arrPaths = functionName.split('.');
- var len = arrPaths.length - 1;
- var fnName = arrPaths[len];
+ const len = arrPaths.length - 1;
+ const fnName = arrPaths[len];
- var obj = window;
- for (var i = 0; i < len; i++) {
+ let obj = window;
+ for (let i = 0; i < len; i++) {
obj = obj[arrPaths[i]];
if (!obj) return;
}
@@ -283,10 +297,8 @@ const traverseEdge = points => {
});
// Traverse half of total distance along points
- const distanceToLabel = totalDistance / 2;
-
- let remainingDistance = distanceToLabel;
- let center;
+ let remainingDistance = totalDistance / 2;
+ let center = undefined;
prevPoint = undefined;
points.forEach(point => {
if (prevPoint && !center) {
@@ -313,8 +325,7 @@ const traverseEdge = points => {
};
const calcLabelPosition = points => {
- const p = traverseEdge(points);
- return p;
+ return traverseEdge(points);
};
const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition) => {
@@ -332,7 +343,7 @@ const calcCardinalityPosition = (isRelationTypePresent, points, initialPosition)
const distanceToCardinalityPoint = 25;
let remainingDistance = distanceToCardinalityPoint;
- let center;
+ let center = { x: 0, y: 0 };
prevPoint = undefined;
points.forEach(point => {
if (prevPoint && !center) {
@@ -498,81 +509,73 @@ export const drawSimpleText = function(elem, textData) {
return textElem;
};
-export const wrapLabel = (label, maxWidth, config) => {
- if (!wrapLabel.cache) {
- // until memoize PR
- wrapLabel.cache = {};
- }
- if (!label) {
- return label;
- }
- config = Object.assign(
- { fontSize: 12, fontWeight: 400, fontFamily: 'Arial', joinWith: '
' },
- config
- );
- const cacheKey = `${label}-${maxWidth}-${JSON.stringify(config)}`;
- if (wrapLabel.cache[cacheKey]) {
- return wrapLabel.cache[cacheKey];
- }
- if (common.lineBreakRegex.test(label)) {
- return label;
- }
- const words = label.split(' ');
- const completedLines = [];
- let nextLine = '';
- words.forEach((word, index) => {
- const wordLength = calculateTextWidth(`${word} `, config);
- const nextLineLength = calculateTextWidth(nextLine, config);
- if (wordLength > maxWidth) {
- const { hyphenatedStrings, remainingWord } = breakString(word, maxWidth, '-', config);
- completedLines.push(nextLine, ...hyphenatedStrings);
- nextLine = remainingWord;
- } else if (nextLineLength + wordLength >= maxWidth) {
- completedLines.push(nextLine);
- nextLine = word;
- } else {
- nextLine = [nextLine, word].filter(Boolean).join(' ');
+export const wrapLabel = memoize(
+ (label, maxWidth, config) => {
+ if (!label) {
+ return label;
}
- const currentWord = index + 1;
- const isLastWord = currentWord === words.length;
- if (isLastWord) {
- completedLines.push(nextLine);
+ config = Object.assign(
+ { fontSize: 12, fontWeight: 400, fontFamily: 'Arial', joinWith: '
' },
+ config
+ );
+ if (common.lineBreakRegex.test(label)) {
+ return label;
}
- });
- const result = completedLines.filter(line => line !== '').join(config.joinWith);
- wrapLabel.cache[cacheKey] = result;
- return result;
-};
+ const words = label.split(' ');
+ const completedLines = [];
+ let nextLine = '';
+ words.forEach((word, index) => {
+ const wordLength = calculateTextWidth(`${word} `, config);
+ const nextLineLength = calculateTextWidth(nextLine, config);
+ if (wordLength > maxWidth) {
+ const { hyphenatedStrings, remainingWord } = breakString(word, maxWidth, '-', config);
+ completedLines.push(nextLine, ...hyphenatedStrings);
+ nextLine = remainingWord;
+ } else if (nextLineLength + wordLength >= maxWidth) {
+ completedLines.push(nextLine);
+ nextLine = word;
+ } else {
+ nextLine = [nextLine, word].filter(Boolean).join(' ');
+ }
+ const currentWord = index + 1;
+ const isLastWord = currentWord === words.length;
+ if (isLastWord) {
+ completedLines.push(nextLine);
+ }
+ });
+ return completedLines.filter(line => line !== '').join(config.joinWith);
+ },
+ (label, maxWidth, config) =>
+ `${label}-${maxWidth}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}-${config.joinWith}`
+);
-const breakString = (word, maxWidth, hyphenCharacter = '-', config) => {
- if (!breakString.cache) {
- breakString.cache = {};
- }
- config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
- const cacheKey = `${word}-${maxWidth}-${hyphenCharacter}-${JSON.stringify(config)}`;
- if (breakString.cache[cacheKey]) {
- return breakString.cache[cacheKey];
- }
- const characters = word.split('');
- const lines = [];
- let currentLine = '';
- characters.forEach((character, index) => {
- const nextLine = `${currentLine}${character}`;
- const lineWidth = calculateTextWidth(nextLine, config);
- if (lineWidth >= maxWidth) {
- const currentCharacter = index + 1;
- const isLastLine = characters.length === currentCharacter;
- const hyphenatedNextLine = `${nextLine}${hyphenCharacter}`;
- lines.push(isLastLine ? nextLine : hyphenatedNextLine);
- currentLine = '';
- } else {
- currentLine = nextLine;
- }
- });
- const result = { hyphenatedStrings: lines, remainingWord: currentLine };
- breakString.cache[cacheKey] = result;
- return result;
-};
+const breakString = memoize(
+ (word, maxWidth, hyphenCharacter = '-', config) => {
+ config = Object.assign(
+ { fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 0 },
+ config
+ );
+ const characters = word.split('');
+ const lines = [];
+ let currentLine = '';
+ characters.forEach((character, index) => {
+ const nextLine = `${currentLine}${character}`;
+ const lineWidth = calculateTextWidth(nextLine, config);
+ if (lineWidth >= maxWidth) {
+ const currentCharacter = index + 1;
+ const isLastLine = characters.length === currentCharacter;
+ const hyphenatedNextLine = `${nextLine}${hyphenCharacter}`;
+ lines.push(isLastLine ? nextLine : hyphenatedNextLine);
+ currentLine = '';
+ } else {
+ currentLine = nextLine;
+ }
+ });
+ return { hyphenatedStrings: lines, remainingWord: currentLine };
+ },
+ (word, maxWidth, hyphenCharacter = '-', config) =>
+ `${word}-${maxWidth}-${hyphenCharacter}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}`
+);
/**
* This calculates the text's height, taking into account the wrap breaks and
@@ -583,10 +586,13 @@ const breakString = (word, maxWidth, hyphenCharacter = '-', config) => {
*
* @return - The height for the given text
* @param text the text to measure
- * @param config - the config for fontSize, fontFamily, fontWeight, and margin all impacting the resulting size
+ * @param config - the config for fontSize, fontFamily, and fontWeight all impacting the resulting size
*/
export const calculateTextHeight = function(text, config) {
- config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
+ config = Object.assign(
+ { fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 15 },
+ config
+ );
return calculateTextDimensions(text, config).height;
};
@@ -595,7 +601,7 @@ export const calculateTextHeight = function(text, config) {
*
* @return - The width for the given text
* @param text - The text to calculate the width of
- * @param config - the config for fontSize, fontFamily, fontWeight, and margin all impacting the resulting size
+ * @param config - the config for fontSize, fontFamily, and fontWeight all impacting the resulting size
*/
export const calculateTextWidth = function(text, config) {
config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
@@ -609,71 +615,65 @@ export const calculateTextWidth = function(text, config) {
* @param text - The text to calculate the width of
* @param config - the config for fontSize, fontFamily, fontWeight, and margin all impacting the resulting size
*/
-export const calculateTextDimensions = function(text, config) {
- if (!calculateTextDimensions.cache) {
- calculateTextDimensions.cache = {};
- }
- config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
- const { fontSize, fontFamily, fontWeight } = config;
- if (!text) {
- return { width: 0, height: 0 };
- }
- const cacheKey = `${text}-${JSON.stringify(config)}`;
- if (calculateTextDimensions.cache[cacheKey]) {
- return calculateTextDimensions.cache[cacheKey];
- }
-
- // We can't really know if the user supplied font family will render on the user agent;
- // thus, we'll take the max width between the user supplied font family, and a default
- // of sans-serif.
- const fontFamilies = ['sans-serif', fontFamily];
- const lines = common.splitBreaks(text);
- let dims = [];
-
- const body = select('body');
- // We don't want to leak DOM elements - if a removal operation isn't available
- // for any reason, do not continue.
- if (!body.remove) {
- return { width: 0, height: 0, lineHeight: 0 };
- }
-
- const g = body.append('svg');
-
- for (let fontFamily of fontFamilies) {
- let cheight = 0;
- let dim = { width: 0, height: 0, lineHeight: 0 };
- for (let line of lines) {
- const textObj = getTextObj();
- textObj.text = line;
- const textElem = drawSimpleText(g, textObj)
- .style('font-size', fontSize)
- .style('font-weight', fontWeight)
- .style('font-family', fontFamily);
-
- let bBox = (textElem._groups || textElem)[0][0].getBBox();
- dim.width = Math.round(Math.max(dim.width, bBox.width));
- cheight = Math.round(bBox.height);
- dim.height += cheight;
- dim.lineHeight = Math.round(Math.max(dim.lineHeight, cheight));
+export const calculateTextDimensions = memoize(
+ function(text, config) {
+ config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config);
+ const { fontSize, fontFamily, fontWeight } = config;
+ if (!text) {
+ return { width: 0, height: 0 };
}
- dims.push(dim);
- }
- g.remove();
+ // We can't really know if the user supplied font family will render on the user agent;
+ // thus, we'll take the max width between the user supplied font family, and a default
+ // of sans-serif.
+ const fontFamilies = ['sans-serif', fontFamily];
+ const lines = text.split(common.lineBreakRegex);
+ let dims = [];
- let index =
- isNaN(dims[1].height) ||
- isNaN(dims[1].width) ||
- isNaN(dims[1].lineHeight) ||
- (dims[0].height > dims[1].height &&
- dims[0].width > dims[1].width &&
- dims[0].lineHeight > dims[1].lineHeight)
- ? 0
- : 1;
- const result = dims[index];
- calculateTextDimensions.cache[cacheKey] = result;
- return result;
-};
+ const body = select('body');
+ // We don't want to leak DOM elements - if a removal operation isn't available
+ // for any reason, do not continue.
+ if (!body.remove) {
+ return { width: 0, height: 0, lineHeight: 0 };
+ }
+
+ const g = body.append('svg');
+
+ for (let fontFamily of fontFamilies) {
+ let cheight = 0;
+ let dim = { width: 0, height: 0, lineHeight: 0 };
+ for (let line of lines) {
+ const textObj = getTextObj();
+ textObj.text = line;
+ const textElem = drawSimpleText(g, textObj)
+ .style('font-size', fontSize)
+ .style('font-weight', fontWeight)
+ .style('font-family', fontFamily);
+
+ let bBox = (textElem._groups || textElem)[0][0].getBBox();
+ dim.width = Math.round(Math.max(dim.width, bBox.width));
+ cheight = Math.round(bBox.height);
+ dim.height += cheight;
+ dim.lineHeight = Math.round(Math.max(dim.lineHeight, cheight));
+ }
+ dims.push(dim);
+ }
+
+ g.remove();
+
+ let index =
+ isNaN(dims[1].height) ||
+ isNaN(dims[1].width) ||
+ isNaN(dims[1].lineHeight) ||
+ (dims[0].height > dims[1].height &&
+ dims[0].width > dims[1].width &&
+ dims[0].lineHeight > dims[1].lineHeight)
+ ? 0
+ : 1;
+ return dims[index];
+ },
+ (text, config) => `${text}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}`
+);
export default {
assignWithDepth,
@@ -692,5 +692,6 @@ export default {
getStylesFromArray,
generateId,
random,
+ memoize,
runFunc
};
diff --git a/src/utils.spec.js b/src/utils.spec.js
index 72036e917..85e9b1120 100644
--- a/src/utils.spec.js
+++ b/src/utils.spec.js
@@ -69,6 +69,25 @@ describe('when assignWithDepth: should merge objects within objects', function()
expect(result).toEqual({ foo: "foo", bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } }, foobar: "foobar", boofar: 1 });
});
});
+describe('when memoizing', function() {
+ it('should return the same value', function() {
+ const fib = utils.memoize(function(n, canary) {
+ canary.flag = true;
+ if (n < 2){
+ return 1;
+ }else{
+ //We'll console.log a loader every time we have to recurse
+ return fib(n-2, canary) + fib(n-1, canary);
+ }
+ });
+ let canary = {flag: false};
+ fib(10, canary);
+ expect(canary.flag).toBe(true);
+ canary = {flag: false};
+ fib(10, canary);
+ expect(canary.flag).toBe(false);
+ });
+})
describe('when detecting chart type ', function() {
it('should handle a graph definition', function() {
const str = 'graph TB\nbfs1:queue';
@@ -103,7 +122,7 @@ Alice->Bob: hi`;
const type = utils.detectType(str);
const init = utils.detectInit(str);
expect(type).toBe('sequence');
- expect(init).toEqual({logLevel:0,theme:"dark", sequence: { wrapEnabled: true }});
+ expect(init).toEqual({logLevel:0, theme:"dark", sequence: { wrapEnabled: true }});
});
it('should handle a multiline init definition', function() {
const str = `