|
1 | 1 | /** |
2 | | - * Combined jqPlot Donut Overrides |
3 | | - * Fixes: |
4 | | - * 1. Continuous color consumption across rings. |
5 | | - * 2. Comprehensive legend for all rings sorted by length. |
6 | | - * 3. Respects numberColumns (multi-column support). |
7 | | - * 4. Measurement and padding fixes to prevent cutoff. |
| 2 | + * jqPlot Donut & Legend Overrides |
| 3 | + * Fixes: |
| 4 | + * 1. Continuous color consumption across nested rings. |
| 5 | + * 2. Legend sorting (shortest first), truncation (max 20), and 2-column force. |
| 6 | + * 3. IndexSizeError fix for 6+ rings by clamping radius at 0. |
| 7 | + * 4. Dynamic thickness scaling to prevent ring collapse. |
8 | 8 | */ |
9 | 9 | (function($) { |
10 | 10 | if ($.jqplot.DonutRenderer) { |
11 | 11 | $.jqplot.DonutRenderer.prototype.draw = function (ctx, gd, options, plot) { |
12 | 12 | var i, opts = (options != undefined) ? options : {}, offx = 0, offy = 0, trans = 1; |
| 13 | + |
| 14 | + // Fix 1: Use the plot-level color generator for continuous colors across rings |
13 | 15 | var colorGenerator = plot.colorGenerator; |
| 16 | + |
14 | 17 | if (options.legendInfo && options.legendInfo.placement == 'insideGrid') { |
15 | 18 | var li = options.legendInfo; |
16 | 19 | switch (li.location) { |
|
24 | 27 | case 's': offy = li.height + li.yoffset; trans = -1; break; |
25 | 28 | } |
26 | 29 | } |
27 | | - var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow; |
28 | | - var fill = (opts.fill != undefined) ? opts.fill : this.fill; |
| 30 | + |
29 | 31 | var cw = ctx.canvas.width, ch = ctx.canvas.height; |
30 | 32 | var w = cw - offx - 2 * this.padding, h = ch - offy - 2 * this.padding; |
31 | 33 | var mindim = Math.min(w, h), d = mindim; |
32 | 34 | var ringmargin = (this.ringMargin == null) ? this.sliceMargin * 2.0 : this.ringMargin; |
| 35 | + |
33 | 36 | for (i = 0; i < this._previousSeries.length; i++) { |
34 | 37 | d -= 2.0 * this._previousSeries[i]._thickness + 2.0 * ringmargin; |
35 | 38 | } |
| 39 | + |
36 | 40 | this._diameter = this.diameter || d; |
| 41 | + |
| 42 | + // Fix 3 & 4: Improved thickness logic for many rings |
37 | 43 | if (this.innerDiameter != null) { |
38 | 44 | var od = (this._numberSeries > 1 && this.index > 0) ? this._previousSeries[0]._diameter : this._diameter; |
39 | 45 | this._thickness = this.thickness || (od - this.innerDiameter - 2.0 * ringmargin * this._numberSeries) / this._numberSeries / 2.0; |
40 | 46 | } else { |
41 | | - this._thickness = this.thickness || mindim / 2 / (this._numberSeries + 1) * 0.85; |
| 47 | + // Scale thickness based on the number of rings to prevent IndexSizeError |
| 48 | + var availableRadius = mindim / 2; |
| 49 | + var totalRings = this._numberSeries; |
| 50 | + this._thickness = this.thickness || (availableRadius / (totalRings + 0.5)) * 0.8; |
42 | 51 | } |
| 52 | + |
43 | 53 | var r = this._radius = this._diameter / 2; |
44 | | - this._innerRadius = this._radius - this._thickness; |
| 54 | + |
| 55 | + // Safety Clamp: Prevent negative radius error |
| 56 | + if (r < 0) { r = 0; this._radius = 0; } |
| 57 | + |
| 58 | + this._innerRadius = r - this._thickness; |
| 59 | + |
| 60 | + // Safety Clamp: Prevent inner radius from going negative |
| 61 | + if (this._innerRadius < 0) { |
| 62 | + this._innerRadius = 0; |
| 63 | + this._thickness = r; |
| 64 | + } |
| 65 | + |
45 | 66 | var sa = this.startAngle / 180 * Math.PI; |
46 | 67 | this._center = [(cw - trans * offx) / 2 + trans * offx, (ch - trans * offy) / 2 + trans * offy]; |
| 68 | + |
47 | 69 | if (this.shadow) { |
48 | 70 | var shadowColor = 'rgba(0,0,0,' + this.shadowAlpha + ')'; |
49 | 71 | for (i = 0; i < gd.length; i++) { |
|
57 | 79 | ang1 += this.sliceMargin / 180 * Math.PI; |
58 | 80 | var ang2 = gd[i][1] + sa; |
59 | 81 | this._sliceAngles.push([ang1, ang2]); |
| 82 | + |
| 83 | + // Use the global colorGenerator |
60 | 84 | var sliceColor = colorGenerator.next(); |
61 | 85 | this.renderer.drawSlice.call(this, ctx, ang1, ang2, sliceColor, false); |
| 86 | + |
62 | 87 | if (this.showDataLabels && gd[i][2] * 100 >= this.dataLabelThreshold) { |
63 | 88 | var fstr, avgang = (ang1 + ang2) / 2, label; |
64 | 89 | if (this.dataLabels == 'label') { fstr = this.dataLabelFormatString || '%s'; label = $.jqplot.sprintf(fstr, gd[i][0]); } |
65 | 90 | else if (this.dataLabels == 'value') { fstr = this.dataLabelFormatString || '%d'; label = $.jqplot.sprintf(fstr, this.data[i][1]); } |
66 | 91 | else if (this.dataLabels == 'percent') { fstr = this.dataLabelFormatString || '%d%%'; label = $.jqplot.sprintf(fstr, gd[i][2] * 100); } |
67 | 92 | else if (this.dataLabels.constructor == Array) { fstr = this.dataLabelFormatString || '%s'; label = $.jqplot.sprintf(fstr, this.dataLabels[i]); } |
| 93 | + |
68 | 94 | var fact = this._innerRadius + this._thickness * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge; |
69 | 95 | var lx = this._center[0] + Math.cos(avgang) * fact + this.canvas._offsets.left; |
70 | 96 | var ly = this._center[1] + Math.sin(avgang) * fact + this.canvas._offsets.top; |
|
76 | 102 | } |
77 | 103 |
|
78 | 104 | if ($.jqplot.DonutLegendRenderer) { |
79 | | - var originalLegendInit = $.jqplot.DonutLegendRenderer.prototype.init; |
80 | 105 | $.jqplot.DonutLegendRenderer.prototype.init = function(options) { |
| 106 | + this.numberRows = null; |
| 107 | + this.numberColumns = null; |
81 | 108 | this.preDraw = true; |
82 | | - originalLegendInit.call(this, options); |
| 109 | + $.extend(true, this, options); |
83 | 110 | }; |
84 | 111 |
|
85 | 112 | $.jqplot.DonutLegendRenderer.prototype.draw = function() { |
86 | 113 | var legend = this; |
87 | 114 | if (this.show) { |
88 | 115 | var series = this._series; |
| 116 | + |
| 117 | + // Fix 2: Retrieve limits from discovery path series[0] |
| 118 | + var maxItems = series[0].maxLegendItems || 0; |
| 119 | + var maxCols = series[0].maxLegendColumns || 1; |
| 120 | + |
89 | 121 | this._elem = $(document.createElement('table')).addClass('jqplot-table-legend'); |
90 | 122 | var ss = { position:'absolute' }; |
91 | | - if (this.background) { ss['background'] = this.background; } |
92 | | - if (this.border) { ss['border'] = this.border; } |
93 | | - if (this.fontSize) { ss['fontSize'] = this.fontSize; } |
94 | | - if (this.fontFamily) { ss['fontFamily'] = this.fontFamily; } |
95 | | - if (this.textColor) { ss['textColor'] = this.textColor; } |
96 | | - if (this.marginTop != null) { ss['marginTop'] = this.marginTop; } |
97 | | - if (this.marginBottom != null) { ss['marginBottom'] = this.marginBottom; } |
98 | | - if (this.marginLeft != null) { ss['marginLeft'] = this.marginLeft; } |
99 | | - if (this.marginRight != null) { ss['marginRight'] = this.marginRight; } |
| 123 | + if (this.background) ss.background = this.background; |
| 124 | + if (this.border) ss.border = this.border; |
| 125 | + if (this.fontSize) ss.fontSize = this.fontSize; |
| 126 | + if (this.fontFamily) ss.fontFamily = this.fontFamily; |
| 127 | + if (this.textColor) ss.textColor = this.textColor; |
100 | 128 | if (this.placement === 'outsideGrid') { ss['white-space'] = 'nowrap'; ss['z-index'] = '101'; } |
101 | 129 | this._elem.css(ss); |
102 | 130 |
|
|
117 | 145 | } |
118 | 146 | } |
119 | 147 | } |
| 148 | + |
| 149 | + // Sort shortest to longest |
120 | 150 | legendItems.sort(function(a, b) { return a.label.length - b.label.length; }); |
121 | 151 |
|
122 | | - // --- START MULTI-COLUMN LOGIC --- |
| 152 | + // Truncate logic |
| 153 | + var wasTruncated = false; |
| 154 | + if (maxItems > 0 && legendItems.length > maxItems) { |
| 155 | + legendItems = legendItems.slice(0, maxItems); |
| 156 | + wasTruncated = true; |
| 157 | + } |
| 158 | + |
123 | 159 | var numItems = legendItems.length; |
124 | | - var numCols = this.numberColumns || 1; |
125 | | - var numRows = Math.ceil(numItems / numCols); |
| 160 | + var targetCols = wasTruncated ? maxCols : (this.numberColumns || 1); |
| 161 | + var numRows = Math.ceil(numItems / targetCols); |
126 | 162 |
|
127 | 163 | for (var rIdx = 0; rIdx < numRows; rIdx++) { |
128 | 164 | var tr = $(document.createElement('tr')).addClass('jqplot-table-legend').appendTo(this._elem); |
129 | | - for (var cIdx = 0; cIdx < numCols; cIdx++) { |
130 | | - var k = rIdx + (cIdx * numRows); // Populate columns vertically first |
| 165 | + for (var cIdx = 0; cIdx < targetCols; cIdx++) { |
| 166 | + var k = rIdx + (cIdx * numRows); |
131 | 167 | if (k < numItems) { |
132 | 168 | var item = legendItems[k]; |
133 | 169 | var rs = (rIdx > 0) ? this.rowSpacing : '0'; |
134 | | - var ps = (cIdx > 0) ? '15px' : '0'; // Add horizontal spacing between columns |
| 170 | + var ps = (cIdx > 0) ? '15px' : '0'; |
135 | 171 |
|
136 | 172 | var td1 = $(document.createElement('td')).addClass('jqplot-table-legend jqplot-table-legend-swatch').css({textAlign: 'center', paddingTop: rs, paddingLeft: ps}); |
137 | | - var div0 = $(document.createElement('div')).addClass('jqplot-table-legend-swatch-outline'); |
138 | | - var div1 = $(document.createElement('div')).addClass('jqplot-table-legend-swatch').css({backgroundColor: item.color, borderColor: item.color}); |
139 | | - td1.append(div0.append(div1)).appendTo(tr); |
| 173 | + var divOutline = $(document.createElement('div')).addClass('jqplot-table-legend-swatch-outline'); |
| 174 | + var divSwatch = $(document.createElement('div')).addClass('jqplot-table-legend-swatch').css({backgroundColor: item.color, borderColor: item.color}); |
| 175 | + td1.append(divOutline.append(divSwatch)).appendTo(tr); |
140 | 176 |
|
141 | 177 | var td2 = $(document.createElement('td')).addClass('jqplot-table-legend jqplot-table-legend-label').css({paddingTop: rs, paddingLeft: '5px'}); |
142 | 178 | if (this.escapeHtml) { td2.text(item.label); } else { td2.html(item.label); } |
143 | 179 | td2.appendTo(tr); |
144 | 180 | } |
145 | 181 | } |
146 | 182 | } |
147 | | - // --- END MULTI-COLUMN LOGIC --- |
148 | 183 | } |
149 | 184 | return this._elem; |
150 | 185 | }; |
|
0 commit comments