1 /*
2 * Copyright 2009, Ingo Weinhold, ingo_weinhold@gmx.de.
3 * Distributed under the terms of the MIT License.
4 */
5
6 #include "chart/LegendChartAxis.h"
7
8 #include <limits.h>
9 #include <stdio.h>
10
11 #include <algorithm>
12 #include <new>
13
14 #include <Font.h>
15 #include <View.h>
16
17 #include "chart/ChartLegend.h"
18 #include "chart/ChartAxisLegendSource.h"
19
20
21 static const int32 kChartRulerDistance = 2;
22 static const int32 kRulerSize = 3;
23 static const int32 kRulerMarkSize = 3;
24 static const int32 kRulerLegendDistance = 2;
25 static const int32 kChartLegendDistance
26 = kChartRulerDistance + kRulerSize + kRulerMarkSize + kRulerLegendDistance;
27
28
29
30 struct LegendChartAxis::LegendInfo {
31 ChartLegend* legend;
32 double value;
33 BSize size;
34
LegendInfoLegendChartAxis::LegendInfo35 LegendInfo(ChartLegend* legend, double value, BSize size)
36 :
37 legend(legend),
38 value(value),
39 size(size)
40 {
41 }
42
~LegendInfoLegendChartAxis::LegendInfo43 ~LegendInfo()
44 {
45 delete legend;
46 }
47 };
48
49
50 float
_LegendPosition(double value,float legendSize,float totalSize,double scale)51 LegendChartAxis::_LegendPosition(double value, float legendSize,
52 float totalSize, double scale)
53 {
54 float position = (value - fRange.min) * scale - legendSize / 2;
55 if (position + legendSize > totalSize)
56 position = totalSize - legendSize;
57 if (position < 0)
58 position = 0;
59 return position;
60 }
61
62
63 void
_FilterLegends(int32 totalSize,int32 spacing,float BSize::* sizeField)64 LegendChartAxis::_FilterLegends(int32 totalSize, int32 spacing,
65 float BSize::* sizeField)
66 {
67 // compute the min/max legend levels
68 int32 legendCount = fLegends.CountItems();
69 int32 minLevel = INT_MAX;
70 int32 maxLevel = 0;
71
72 for (int32 i = 0; i < legendCount; i++) {
73 LegendInfo* info = fLegends.ItemAt(i);
74 int32 level = info->legend->Level();
75 if (level < minLevel)
76 minLevel = level;
77 if (level > maxLevel)
78 maxLevel = level;
79 }
80
81 if (maxLevel <= 0)
82 return;
83
84 double rangeSize = fRange.max - fRange.min;
85 if (rangeSize == 0)
86 rangeSize = 1.0;
87 double scale = (double)totalSize / rangeSize;
88
89 // Filter out all higher level legends colliding with lower level or
90 // preceeding same-level legends. We iterate backwards from the lower to
91 // the higher levels
92 for (int32 level = std::max(minLevel, (int32)0); level <= maxLevel;) {
93 legendCount = fLegends.CountItems();
94
95 // get the first legend position/end
96 LegendInfo* info = fLegends.ItemAt(0);
97 float position = _LegendPosition(info->value, info->size.*sizeField,
98 (float)totalSize, scale);;
99
100 int32 previousEnd = (int32)ceilf(position + info->size.*sizeField);
101 int32 previousLevel = info->legend->Level();
102
103 for (int32 i = 1; (info = fLegends.ItemAt(i)) != NULL; i++) {
104 float position = _LegendPosition(info->value, info->size.*sizeField,
105 (float)totalSize, scale);;
106
107 if (position - spacing < previousEnd
108 && (previousLevel <= level
109 || info->legend->Level() <= level)
110 && std::max(previousLevel, info->legend->Level()) > 0) {
111 // The item intersects with the previous one -- remove the
112 // one at the higher level.
113 if (info->legend->Level() >= previousLevel) {
114 // This item is at the higher level -- remove it.
115 delete fLegends.RemoveItemAt(i);
116 i--;
117 continue;
118 }
119
120 // The previous item is at the higher level -- remove it.
121 delete fLegends.RemoveItemAt(i - 1);
122 i--;
123 }
124
125 if (i == 0 && position < 0)
126 position = 0;
127 previousEnd = (int32)ceilf(position + info->size.*sizeField);
128 previousLevel = info->legend->Level();
129 }
130
131 // repeat with the level as long as we've removed something
132 if (legendCount == fLegends.CountItems())
133 level++;
134 }
135 }
136
137
LegendChartAxis(ChartAxisLegendSource * legendSource,ChartLegendRenderer * legendRenderer)138 LegendChartAxis::LegendChartAxis(ChartAxisLegendSource* legendSource,
139 ChartLegendRenderer* legendRenderer)
140 :
141 fLegendSource(legendSource),
142 fLegendRenderer(legendRenderer),
143 fLocation(CHART_AXIS_BOTTOM),
144 fRange(),
145 fFrame(),
146 fLegends(20, true),
147 fHorizontalSpacing(20),
148 fVerticalSpacing(10),
149 fLayoutValid(false)
150 {
151 }
152
153
~LegendChartAxis()154 LegendChartAxis::~LegendChartAxis()
155 {
156 }
157
158
159 void
SetLocation(ChartAxisLocation location)160 LegendChartAxis::SetLocation(ChartAxisLocation location)
161 {
162 if (location != fLocation) {
163 fLocation = location;
164 _InvalidateLayout();
165 }
166 }
167
168
169 void
SetRange(const ChartDataRange & range)170 LegendChartAxis::SetRange(const ChartDataRange& range)
171 {
172 if (range != fRange) {
173 fRange = range;
174 _InvalidateLayout();
175 }
176 }
177
178
179 void
SetFrame(BRect frame)180 LegendChartAxis::SetFrame(BRect frame)
181 {
182 if (frame != fFrame) {
183 fFrame = frame;
184 _InvalidateLayout();
185 }
186 }
187
188
189 BSize
PreferredSize(BView * view,BSize maxSize)190 LegendChartAxis::PreferredSize(BView* view, BSize maxSize)
191 {
192 // estimate the maximum legend count we might need
193 float hSpacing, vSpacing;
194 int32 maxLegends = _EstimateMaxLegendCount(view, maxSize, &hSpacing,
195 &vSpacing);
196 BSize spacing(hSpacing, vSpacing);
197 if (maxLegends < 4)
198 maxLegends = 4;
199
200 // get the legends
201 ChartLegend* legends[maxLegends];
202 double values[maxLegends];
203
204 int32 legendCount = fLegendSource->GetAxisLegends(fRange, legends, values,
205 maxLegends);
206
207 // get the sizes, delete the legends, and compute the preferred size
208 float BSize::* sizeField;
209 float BSize::* otherSizeField;
210 if (fLocation == CHART_AXIS_LEFT || fLocation == CHART_AXIS_RIGHT) {
211 sizeField = &BSize::height;
212 otherSizeField = &BSize::width;
213 } else {
214 sizeField = &BSize::width;
215 otherSizeField = &BSize::height;
216 }
217
218 BSize preferredSize;
219
220 for (int32 i = 0; i < legendCount; i++) {
221 ChartLegend* legend = legends[i];
222 BSize size = fLegendRenderer->LegendSize(legend, view);
223 delete legend;
224
225 if (size.*sizeField > preferredSize.*sizeField)
226 preferredSize.*sizeField = size.*sizeField;
227 if (size.*otherSizeField > preferredSize.*otherSizeField)
228 preferredSize.*otherSizeField = size.*otherSizeField;
229 }
230
231 // Suppose we want to have at least 2 legends.
232 preferredSize.*sizeField
233 = ceilf(preferredSize.*sizeField * 2 + spacing.*sizeField);
234 preferredSize.*otherSizeField += kChartLegendDistance;
235
236 return preferredSize;
237 }
238
239
240 void
Render(BView * view,BRect updateRect)241 LegendChartAxis::Render(BView* view, BRect updateRect)
242 {
243 if (!_ValidateLayout(view))
244 return;
245
246 float valueDirection;
247 float rulerDirection;
248 float BSize::* sizeField;
249 float BSize::* otherSizeField;
250 float BPoint::* pointField;
251 float BPoint::* otherPointField;
252
253 switch (fLocation) {
254 case CHART_AXIS_LEFT:
255 case CHART_AXIS_RIGHT:
256 valueDirection = -1;
257 rulerDirection = fLocation == CHART_AXIS_LEFT ? -1 : 1;
258 sizeField = &BSize::height;
259 otherSizeField = &BSize::width;
260 pointField = &BPoint::y;
261 otherPointField = &BPoint::x;
262 break;
263 case CHART_AXIS_TOP:
264 case CHART_AXIS_BOTTOM:
265 valueDirection = 1;
266 rulerDirection = fLocation == CHART_AXIS_TOP ? -1 : 1;
267 sizeField = &BSize::width;
268 otherSizeField = &BSize::height;
269 pointField = &BPoint::x;
270 otherPointField = &BPoint::y;
271 break;
272 default:
273 return;
274 }
275
276 float totalSize = floorf(fFrame.Size().*sizeField) + 1;
277 double rangeSize = fRange.max - fRange.min;
278 if (rangeSize == 0)
279 rangeSize = 1.0;
280 double scale = (double)totalSize / rangeSize;
281
282 // draw the ruler
283 float rulerStart = fFrame.LeftBottom().*pointField;
284 float rulerChartClosest = rulerDirection == 1
285 ? fFrame.LeftTop().*otherPointField + kChartRulerDistance
286 : fFrame.RightBottom().*otherPointField - kChartRulerDistance;
287 float rulerEnd = fFrame.RightTop().*pointField;
288 float rulerChartDistant = rulerChartClosest + rulerDirection * kRulerSize;
289
290 rgb_color black = { 0, 0, 0, 255 };
291 view->BeginLineArray(3 + fLegends.CountItems());
292 BPoint first;
293 first.*pointField = rulerStart;
294 first.*otherPointField = rulerChartClosest;
295 BPoint second = first;
296 second.*otherPointField = rulerChartDistant;
297 BPoint third = second;
298 third.*pointField = rulerEnd;
299 BPoint fourth = third;
300 fourth.*otherPointField = rulerChartClosest;
301 view->AddLine(first, second, black);
302 view->AddLine(second, third, black);
303 view->AddLine(third, fourth, black);
304
305 // marks
306 for (int32 i = 0; LegendInfo* info = fLegends.ItemAt(i); i++) {
307 float position = (info->value - fRange.min) * scale;
308 position = rulerStart + valueDirection * position;
309 first.*pointField = position;
310 first.*otherPointField = rulerChartDistant;
311 second.*pointField = position;
312 second.*otherPointField = rulerChartDistant
313 + rulerDirection * kRulerMarkSize;
314 view->AddLine(first, second, black);
315 }
316 view->EndLineArray();
317
318 // draw the legends
319 float legendRulerClosest = rulerChartDistant
320 + rulerDirection * (kRulerMarkSize + kRulerLegendDistance);
321
322 for (int32 i = 0; LegendInfo* info = fLegends.ItemAt(i); i++) {
323 float position = _LegendPosition(info->value, info->size.*sizeField,
324 (float)totalSize, scale);;
325
326 first.*pointField = rulerStart
327 + (valueDirection == 1
328 ? position : -position - info->size.*sizeField);
329 first.*otherPointField = rulerDirection == 1
330 ? legendRulerClosest
331 : legendRulerClosest - info->size.*otherSizeField;
332
333 fLegendRenderer->RenderLegend(info->legend, view, first);
334 }
335 }
336
337
338 void
_InvalidateLayout()339 LegendChartAxis::_InvalidateLayout()
340 {
341 fLayoutValid = false;
342 }
343
344
345 bool
_ValidateLayout(BView * view)346 LegendChartAxis::_ValidateLayout(BView* view)
347 {
348 if (fLayoutValid)
349 return true;
350
351 fLegends.MakeEmpty();
352
353 int32 width = fFrame.IntegerWidth() + 1;
354 int32 height = fFrame.IntegerHeight() + 1;
355
356 // estimate the maximum legend count we might need
357 int32 maxLegends = _EstimateMaxLegendCount(view, fFrame.Size(),
358 &fHorizontalSpacing, &fVerticalSpacing);
359
360 if (maxLegends == 0)
361 return false;
362
363 // get the legends
364 ChartLegend* legends[maxLegends];
365 double values[maxLegends];
366
367 int32 legendCount = fLegendSource->GetAxisLegends(fRange, legends, values,
368 maxLegends);
369 if (legendCount == 0)
370 return false;
371
372 // create legend infos
373 for (int32 i = 0; i < legendCount; i++) {
374 ChartLegend* legend = legends[i];
375 BSize size = fLegendRenderer->LegendSize(legend, view);
376 LegendInfo* info = new(std::nothrow) LegendInfo(legend, values[i],
377 size);
378 if (info == NULL || !fLegends.AddItem(info)) {
379 // TODO: Report error!
380 delete info;
381 for (int32 k = i; k < legendCount; k++)
382 delete legends[k];
383 return false;
384 }
385 }
386
387 if (fLocation == CHART_AXIS_LEFT || fLocation == CHART_AXIS_RIGHT)
388 _FilterLegends(height, fVerticalSpacing, &BSize::height);
389 else
390 _FilterLegends(width, fHorizontalSpacing, &BSize::width);
391
392 fLayoutValid = true;
393 return true;
394 }
395
396
397 int32
_EstimateMaxLegendCount(BView * view,BSize size,float * _hSpacing,float * _vSpacing)398 LegendChartAxis::_EstimateMaxLegendCount(BView* view, BSize size,
399 float* _hSpacing, float* _vSpacing)
400 {
401 // get the legend spacing
402 fLegendRenderer->GetMinimumLegendSpacing(view, _hSpacing, _vSpacing);
403
404 // estimate the maximum legend count we might need
405 if (fLocation == CHART_AXIS_LEFT || fLocation == CHART_AXIS_RIGHT)
406 return (int32)((size.IntegerHeight() + 1) / (10 + *_vSpacing));
407 return (int32)((size.IntegerWidth() + 1) / (20 + *_hSpacing));
408 }
409