shapeData.forEach(d => { const s = new shapes[d.key]().config(shapeConfig).data(d.values); if (d.key === "Bar") { let space; const scale = this._discrete === "x" ? x : y; const vals = (this._discrete === "x" ? xDomain : yDomain).filter(d => typeof d !== "string" || d.indexOf("d3plus-buffer-") < 0); const range = this._discrete === "x" ? xRange : yRange; if (vals.length > 1) space = scale(vals[1]) - scale(vals[0]); else space = range[range.length - 1] - range[0]; space -= this._groupPadding; let barSize = space; const groups = nest() .key(d => d[this._discrete]) .key(d => d.group) .entries(d.values); const ids = merge(groups.map(d => d.values.map(v => v.key))); const uniqueIds = Array.from(new Set(ids)); if (max(groups.map(d => d.values.length)) === 1) { s[this._discrete]((d, i) => shapeConfig[this._discrete](d, i)); } else { barSize = (barSize - this._barPadding * uniqueIds.length - 1) / uniqueIds.length; const offset = space / 2 - barSize / 2; const xMod = scales.scaleLinear() .domain([0, uniqueIds.length - 1]) .range([-offset, offset]); s[this._discrete]((d, i) => shapeConfig[this._discrete](d, i) + xMod(uniqueIds.indexOf(d.group))); } s.width(barSize); s.height(barSize); } else if (d.key === "Line" && this._confidence) { const areaConfig = Object.assign({}, shapeConfig); const key = this._discrete === "x" ? "y" : "x"; const scaleFunction = this._discrete === "x" ? y : x; areaConfig[`${key}0`] = d => scaleFunction(this._confidence[0] ? d.lci : d[key]); areaConfig[`${key}1`] = d => scaleFunction(this._confidence[1] ? d.hci : d[key]); const area = new shapes.Area().config(areaConfig).data(d.values); const confidenceConfig = Object.assign(this._shapeConfig, this._confidenceConfig); area.config(configPrep.bind(this)(confidenceConfig, "shape", "Area")).render(); this._shapes.push(area); } const classEvents = events.filter(e => e.includes(`.${d.key}`)), globalEvents = events.filter(e => !e.includes(".")), shapeEvents = events.filter(e => e.includes(".shape")); for (let e = 0; e < globalEvents.length; e++) s.on(globalEvents[e], d => this._on[globalEvents[e]](d.data, d.i)); for (let e = 0; e < shapeEvents.length; e++) s.on(shapeEvents[e], d => this._on[shapeEvents[e]](d.data, d.i)); for (let e = 0; e < classEvents.length; e++) s.on(classEvents[e], d => this._on[classEvents[e]](d.data, d.i)); s.config(configPrep.bind(this)(this._shapeConfig, "shape", d.key)).render(); this._shapes.push(s); });
/** @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; }
/** Extends the draw behavior of the abstract Viz class. @private */ _draw(callback) { super._draw(callback); const height = this._height - this._margin.top - this._margin.bottom, width = this._width - this._margin.left - this._margin.right; const radius = min([height, width]) / 2 - this._outerPadding, transform = `translate(${width / 2}, ${height / 2})`; const nestedAxisData = nest() .key(this._metric) .entries(this._filteredData), nestedGroupData = nest() .key(this._id) .key(this._metric) .entries(this._filteredData); const maxValue = max(nestedGroupData.map(h => h.values.map(d => sum(d.values, (x, i) => this._value(x, i)))).flat()); const circularAxis = Array.from(Array(this._levels).keys()).map(d => ({ id: d, r: radius * ((d + 1) / this._levels) })); new Circle() .data(circularAxis) .select( elem("g.d3plus-Radar-radial-circles", { parent: this._select, enter: {transform}, update: {transform} }).node() ) .config(configPrep.bind(this)(this._axisConfig.shapeConfig, "shape", "Circle")) .render(); const totalAxis = nestedAxisData.length; const polarAxis = nestedAxisData .map((d, i) => { const width = this._outerPadding; const fontSize = this._shapeConfig.labelConfig.fontSize && this._shapeConfig.labelConfig.fontSize(d, i) || 11; const lineHeight = fontSize * 1.4; const height = lineHeight * 2; const padding = 10, quadrant = parseInt(360 - 360 / totalAxis * i / 90, 10) % 4 + 1, radians = tau / totalAxis * i; let angle = 360 / totalAxis * i; let textAnchor = "start"; let x = padding; if (quadrant === 2 || quadrant === 3) { x = -width - padding; textAnchor = "end"; angle += 180; } const labelBounds = { x, y: -height / 2, width, height }; return { id: d.key, angle, textAnchor, labelBounds, rotateAnchor: [-x, height / 2], x: radius * Math.cos(radians), y: radius * Math.sin(radians) }; }) .sort((a, b) => a.key - b.key); new Rect() .data(polarAxis) .rotate(d => d.angle) .width(0) .height(0) .x(d => d.x) .y(d => d.y) .label(d => d.id) .labelBounds(d => d.labelBounds) .labelConfig(this._axisConfig.shapeConfig.labelConfig) .select( elem("g.d3plus-Radar-text", { parent: this._select, enter: {transform}, update: {transform} }).node() ) .render(); new Path() .data(polarAxis) .d(d => `M${0},${0} ${-d.x},${-d.y}`) .select( elem("g.d3plus-Radar-axis", { parent: this._select, enter: {transform}, update: {transform} }).node() ) .config(configPrep.bind(this)(this._axisConfig.shapeConfig, "shape", "Path")) .render(); const groupData = nestedGroupData.map(h => { const q = h.values.map((d, i) => { const value = sum(d.values, (x, i) => this._value(x, i)); const r = value / maxValue * radius, radians = tau / totalAxis * i; return { x: r * Math.cos(radians), y: r * Math.sin(radians) }; }); const d = `M ${q[0].x} ${q[0].y} ${q .map(l => `L ${l.x} ${l.y}`) .join(" ")} L ${q[0].x} ${q[0].y}`; return { arr: h.values.map(d => merge(d.values)), id: h.key, points: q, d, __d3plus__: true, data: merge(h.values.map(d => merge(d.values))) }; }); const pathConfig = configPrep.bind(this)(this._shapeConfig, "shape", "Path"); const events = Object.keys(pathConfig.on); pathConfig.on = {}; for (let e = 0; e < events.length; e++) { const event = events[e]; pathConfig.on[event] = (d, i) => { const x = d.points.map(p => p.x + width / 2); const y = d.points.map(p => p.y + height / 2); const cursor = mouse(this._select.node()); const xDist = x.map(p => Math.abs(p - cursor[0])); const yDist = y.map(p => Math.abs(p - cursor[1])); const dists = xDist.map((d, i) => d + yDist[i]); this._on[event].bind(this)(d.arr[dists.indexOf(min(dists))], i); }; } this._shapes.push( new Path() .data(groupData) .d(d => d.d) .select( elem("g.d3plus-Radar-items", { parent: this._select, enter: {transform}, update: {transform} }).node() ) .config(pathConfig) .render() ); return this; }