this._wrapLines = function() { lines++; if (lines > maxLines) return; const wrappable = lines === 1 ? this._lineData.slice() : this._lineData.filter(d => d.width + d.shapeWidth + this._padding * (d.width ? 2 : 1) > availableWidth && d.words.length >= lines) .sort((a, b) => b.sentence.length - a.sentence.length); if (wrappable.length && availableHeight > wrappable[0].height * lines) { let truncated = false; for (let x = 0; x < wrappable.length; x++) { const label = wrappable[x]; const h = label.og.height * lines, w = label.og.width * (1.5 * (1 / lines)); const res = textWrap().fontFamily(label.f).fontSize(label.s).lineHeight(label.lh).width(w).height(h)(label.sentence); if (!res.truncated) { label.width = Math.ceil(max(res.lines.map(t => textWidth(t, {"font-family": label.f, "font-size": label.s})))) + label.s; label.height = res.lines.length * (label.lh + 1); } else { truncated = true; break; } } if (!truncated) this._wrapRows(); } else { newRows = []; return; } };
this._lineData = this._data.map((d, i) => { const label = this._label(d, i); let res = { data: d, i, id: this._id(d, i), shapeWidth: this._fetchConfig("width", d, i), shapeHeight: this._fetchConfig("height", d, i), y: 0 }; if (!label) { res.sentence = false; res.words = []; res.height = 0; res.width = 0; return res; } const f = this._fetchConfig("fontFamily", d, i), lh = this._fetchConfig("lineHeight", d, i), s = this._fetchConfig("fontSize", d, i); const h = availableHeight - (this._data.length + 1) * this._padding, w = this._width; res = Object.assign(res, textWrap() .fontFamily(f) .fontSize(s) .lineHeight(lh) .width(w) .height(h) (label)); res.width = Math.ceil(max(res.lines.map(t => textWidth(t, {"font-family": f, "font-size": s})))) + s * 0.75; res.height = Math.ceil(res.lines.length * (lh + 1)); res.og = {height: res.height, width: res.width}; res.f = f; res.s = s; res.lh = lh; return res; });
/** @memberof Legend @desc Renders the current Legend to the page. If a *callback* is specified, it will be called once the legend is done drawing. @param {Function} [*callback* = undefined] @chainable */ render(callback) { if (this._select === void 0) this.select(select("body").append("svg").attr("width", `${this._width}px`).attr("height", `${this._height}px`).node()); // Shape <g> Group this._group = elem("g.d3plus-Legend", {parent: this._select}); let availableHeight = this._height; this._titleHeight = 0; this._titleWidth = 0; if (this._title) { const f = this._titleConfig.fontFamily || this._titleClass.fontFamily()(), s = this._titleConfig.fontSize || this._titleClass.fontSize()(); let lH = lH = this._titleConfig.lineHeight || this._titleClass.lineHeight(); lH = lH ? lH() : s * 1.4; const res = textWrap() .fontFamily(f) .fontSize(s) .lineHeight(lH) .width(this._width) .height(this._height) (this._title); this._titleHeight = lH + res.lines.length + this._padding; this._titleWidth = max(res.widths); availableHeight -= this._titleHeight; } // Calculate Text Sizes this._lineData = this._data.map((d, i) => { const label = this._label(d, i); let res = { data: d, i, id: this._id(d, i), shapeWidth: this._fetchConfig("width", d, i), shapeHeight: this._fetchConfig("height", d, i), y: 0 }; if (!label) { res.sentence = false; res.words = []; res.height = 0; res.width = 0; return res; } const f = this._fetchConfig("fontFamily", d, i), lh = this._fetchConfig("lineHeight", d, i), s = this._fetchConfig("fontSize", d, i); const h = availableHeight - (this._data.length + 1) * this._padding, w = this._width; res = Object.assign(res, textWrap() .fontFamily(f) .fontSize(s) .lineHeight(lh) .width(w) .height(h) (label)); res.width = Math.ceil(max(res.lines.map(t => textWidth(t, {"font-family": f, "font-size": s})))) + s * 0.75; res.height = Math.ceil(res.lines.length * (lh + 1)); res.og = {height: res.height, width: res.width}; res.f = f; res.s = s; res.lh = lh; return res; }); let spaceNeeded; const availableWidth = this._width - this._padding * 2; spaceNeeded = this._rowWidth(this._lineData); if (this._direction === "column" || spaceNeeded > availableWidth) { let lines = 1, newRows = []; const maxLines = max(this._lineData.map(d => d.words.length)); this._wrapLines = function() { lines++; if (lines > maxLines) return; const wrappable = lines === 1 ? this._lineData.slice() : this._lineData.filter(d => d.width + d.shapeWidth + this._padding * (d.width ? 2 : 1) > availableWidth && d.words.length >= lines) .sort((a, b) => b.sentence.length - a.sentence.length); if (wrappable.length && availableHeight > wrappable[0].height * lines) { let truncated = false; for (let x = 0; x < wrappable.length; x++) { const label = wrappable[x]; const h = label.og.height * lines, w = label.og.width * (1.5 * (1 / lines)); const res = textWrap().fontFamily(label.f).fontSize(label.s).lineHeight(label.lh).width(w).height(h)(label.sentence); if (!res.truncated) { label.width = Math.ceil(max(res.lines.map(t => textWidth(t, {"font-family": label.f, "font-size": label.s})))) + label.s; label.height = res.lines.length * (label.lh + 1); } else { truncated = true; break; } } if (!truncated) this._wrapRows(); } else { newRows = []; return; } }; this._wrapRows = function() { newRows = []; let row = 1, rowWidth = 0; for (let i = 0; i < this._lineData.length; i++) { const d = this._lineData[i], w = d.width + this._padding * (d.width ? 2 : 1) + d.shapeWidth; if (sum(newRows.map(row => max(row, d => max([d.height, d.shapeHeight])))) > availableHeight) { newRows = []; break; } if (w > availableWidth) { newRows = []; this._wrapLines(); break; } else if (rowWidth + w < availableWidth) { rowWidth += w; } else if (this._direction !== "column") { rowWidth = w; row++; } if (!newRows[row - 1]) newRows[row - 1] = []; newRows[row - 1].push(d); if (this._direction === "column") { rowWidth = 0; row++; } } }; this._wrapRows(); if (!newRows.length || sum(newRows, this._rowHeight.bind(this)) + this._padding > availableHeight) { spaceNeeded = sum(this._lineData.map(d => d.shapeWidth + this._padding)) - this._padding; for (let i = 0; i < this._lineData.length; i++) { this._lineData[i].width = 0; this._lineData[i].height = 0; } this._wrapRows(); } if (newRows.length && sum(newRows, this._rowHeight.bind(this)) + this._padding < availableHeight) { newRows.forEach((row, i) => { row.forEach(d => { if (i) { d.y = sum(newRows.slice(0, i), this._rowHeight.bind(this)); } }); }); spaceNeeded = max(newRows, this._rowWidth.bind(this)); } } const innerHeight = max(this._lineData, (d, i) => max([d.height, this._fetchConfig("height", d.data, i)]) + d.y) + this._titleHeight, innerWidth = max([spaceNeeded, this._titleWidth]); this._outerBounds.width = innerWidth; this._outerBounds.height = innerHeight; let xOffset = this._padding, yOffset = this._padding; if (this._align === "center") xOffset = (this._width - innerWidth) / 2; else if (this._align === "right") xOffset = this._width - this._padding - innerWidth; if (this._verticalAlign === "middle") yOffset = (this._height - innerHeight) / 2; else if (this._verticalAlign === "bottom") yOffset = this._height - this._padding - innerHeight; this._outerBounds.x = xOffset; this._outerBounds.y = yOffset; this._titleClass .data(this._title ? [{text: this._title}] : []) .duration(this._duration) .select(this._group.node()) .textAnchor({left: "start", center: "middle", right: "end"}[this._align]) .width(this._width - this._padding * 2) .x(this._padding) .y(this._outerBounds.y) .config(this._titleConfig) .render(); this._shapes = []; const baseConfig = configPrep.bind(this)(this._shapeConfig, "legend"), config = { id: d => d.id, label: d => d.label, lineHeight: d => d.lH }; const data = this._data.map((d, i) => { const obj = { __d3plus__: true, data: d, i, id: this._id(d, i), label: this._lineData[i].width ? this._label(d, i) : false, lH: this._fetchConfig("lineHeight", d, i), shape: this._shape(d, i) }; return obj; }); // Legend Shapes this._shapes = []; ["Circle", "Rect"].forEach(Shape => { this._shapes.push(new shapes[Shape]() .data(data.filter(d => d.shape === Shape)) .duration(this._duration) .labelConfig({padding: 0}) .select(this._group.node()) .verticalAlign("top") .config(assign({}, baseConfig, config)) .render()); }); if (callback) setTimeout(callback, this._duration + 100); return this; }