1 // Written in the D programming language.
2
3 /**
4 This module contains charts widgets implementation.
5 Currently only SimpleBarChart.
6
7
8 Synopsis:
9
10 ----
11 import dlangui.widgets.charts;
12
13 // creation of simple bar chart
14 SimpleBarChart chart = new SimpleBarChart("chart");
15
16 // add bars
17 chart.addBar(12.2, makeRGBA(255, 0, 0, 0), "new bar"c);
18
19 // update bar with index 0
20 chart.updateBar(0, 10, makeRGBA(255, 255, 0, 0), "new bar updated"c);
21 chart.updateBar(0, 20);
22
23 // remove bars with index 0
24 chart.removeBar(0, 20);
25
26 // change title
27 chart.title = "new title"d;
28
29 // change min axis ratio
30 chart.axisRatio = 0.3; // y axis length will be 0.3 of x axis
31
32 ----
33
34 Copyright: Andrzej Kilijański, 2017
35 License: Boost License 1.0
36 Authors: Andrzej Kilijański, and3md@gmail.com
37 */
38
39 module dlangui.widgets.charts;
40
41 import dlangui.widgets.widget;
42 import std.math;
43 import std.algorithm.comparison;
44 import std.algorithm : remove;
45 import std.conv;
46
47 class SimpleBarChart : Widget {
48
49 this(string ID = null) {
50 super(ID);
51 clickable = false;
52 focusable = false;
53 trackHover = false;
54 styleId = "SIMPLE_BAR_CHART";
55 _axisX.arrowSize = 1;
56 title = UIString.fromId("TITLE_NEW_CHART"c);
57 measureTextToSetWidgetSize();
58 }
59
60 this(string ID, string titleResourceId) {
61 this(ID);
62 title = UIString.fromId(titleResourceId);
63 }
64
65 this(string ID, dstring title) {
66 this(ID);
67 this.title = UIString.fromRaw(title);
68 }
69
70 this(string ID, UIString title) {
71 this(ID);
72 this.title = title;
73 }
74
75 struct BarData {
76 double y;
77 UIString title;
78 private Point _titleSize;
79 uint color;
80
81 this (double y, uint color, UIString title) {
82 this.y = y;
83 this.color = color;
84 this.title = title;
85 }
86 }
87
88 protected BarData[] _bars;
89 protected double _maxY = 0;
90
91 size_t barCount() {
92 return _bars.length;
93 }
94
95 void addBar(double y, uint color, UIString barTitle) {
96 if (y < 0)
97 return; // current limitation only positive values
98 _bars ~= BarData(y, color, barTitle);
99 if (y > _maxY)
100 _maxY = y;
101 requestLayout();
102 }
103
104 void addBar(double y, uint color, string barTitle) {
105 addBar(y, color, UIString.fromId(barTitle));
106 }
107
108 void addBar(double y, uint color, dstring barTitle) {
109 addBar(y, color, UIString.fromRaw(barTitle));
110 }
111
112 void removeBar(size_t index) {
113 _bars = remove(_bars, index);
114 // update _maxY
115 _maxY = 0;
116 foreach (ref bar ; _bars) {
117 if (bar.y > _maxY)
118 _maxY = bar.y;
119 }
120 requestLayout();
121 }
122
123 void removeAllBars() {
124 _bars = [];
125 _maxY = 0;
126 requestLayout();
127 }
128
129 void updateBar(size_t index, double y, uint color, string barTitle) {
130 updateBar(index, y, color, UIString.fromId(barTitle));
131 }
132
133 void updateBar(size_t index, double y, uint color, dstring barTitle) {
134 updateBar(index, y, color, UIString.fromRaw(barTitle));
135 }
136
137 void updateBar(size_t index, double y, uint color, UIString barTitle) {
138 if (y < 0)
139 return; // current limitation only positive values
140 _bars[index].y = y;
141 _bars[index].color = color;
142 _bars[index].title = barTitle;
143
144 // update _maxY
145 _maxY = 0;
146 foreach (ref bar ; _bars) {
147 if (bar.y > _maxY)
148 _maxY = bar.y;
149 }
150 requestLayout();
151 }
152
153 void updateBar(size_t index, double y) {
154 if (y < 0)
155 return; // curent limitation only positive values
156 _bars[index].y = y;
157
158 // update _maxY
159 _maxY = 0;
160 foreach (ref bar ; _bars) {
161 if (bar.y > _maxY)
162 _maxY = bar.y;
163 }
164 requestLayout();
165 }
166
167 protected UIString _title;
168 protected bool _showTitle = true;
169 protected Point _titleSize;
170 protected int _marginAfterTitle = 2;
171
172 /// set title to show
173 @property Widget title(string s) {
174 return title(UIString.fromId(s));
175 }
176
177 @property Widget title(dstring s) {
178 return title(UIString.fromRaw(s));
179 }
180
181 /// set title to show
182 @property Widget title(UIString s) {
183 _title = s;
184 measureTitleSize();
185 if (_showTitle)
186 requestLayout();
187 return this;
188 }
189
190 /// get title value
191 @property dstring title() {
192 return _title;
193 }
194
195 /// show title?
196 @property bool showTitle() {
197 return _showTitle;
198 }
199
200 @property void showTitle(bool show) {
201 if (_showTitle != show) {
202 _showTitle = show;
203 requestLayout();
204 }
205 }
206
207 override protected void handleFontChanged() {
208 measureTitleSize();
209 measureTextToSetWidgetSize();
210 }
211
212 protected void measureTitleSize() {
213 FontRef font = font();
214 _titleSize = font.textSize(_title, MAX_WIDTH_UNSPECIFIED, 4, 0, textFlags); //todo: more than one line title support
215 }
216
217 @property uint chartBackgroundColor() {return ownStyle.customColor("chart_background_color"); }
218
219 @property Widget chartBackgroundColor(uint newColor) {
220 ownStyle.setCustomColor("chart_background_color",newColor);
221 invalidate();
222 return this;
223 }
224
225 @property uint chartAxisColor() {return ownStyle.customColor("chart_axis_color"); }
226
227 @property Widget chartAxisColor(uint newColor) {
228 ownStyle.setCustomColor("chart_axis_color",newColor);
229 invalidate();
230 return this;
231 }
232
233 @property uint chartSegmentTagColor() {return ownStyle.customColor("chart_segment_tag_color"); }
234
235 @property Widget chartSegmentTagColor(uint newColor) {
236 ownStyle.setCustomColor("chart_segment_tag_color",newColor);
237 invalidate();
238 return this;
239 }
240
241 struct AxisData {
242 Point maxDescriptionSize = Point(30,20);
243 int thickness = 1;
244 int arrowSize = 20;
245 int segmentTagLength = 4;
246 int zeroValueDist = 3;
247 int lengthFromZeroToArrow = 200;
248 }
249
250 AxisData _axisX;
251 AxisData _axisY;
252
253 protected int _axisYMaxValueDescWidth = 30;
254 protected int _axisYAvgValueDescWidth = 30;
255
256 protected double _axisRatio = 0.6;
257
258 @property double axisRatio() {
259 return _axisRatio;
260 }
261
262 @property void axisRatio(double newRatio) {
263 _axisRatio = newRatio;
264 requestLayout();
265 }
266
267 protected int _minBarWidth = 10;
268 protected int _barWidth = 10;
269 protected int _barDistance = 3;
270
271 protected int _axisXMinWfromZero = 150;
272 protected int _axisYMinDescWidth = 30;
273
274 protected dstring _textToSetDescLineSize = "aaaaaaaaaa";
275 protected Point _measuredTextToSetDescLineSize;
276
277 @property dstring textToSetDescLineSize() {
278 return _textToSetDescLineSize;
279 }
280
281 @property void textToSetDescLineSize(dstring newText) {
282 _textToSetDescLineSize = newText;
283 measureTextToSetWidgetSize();
284 requestLayout();
285 }
286
287 private int[] _charWidths;
288 protected Point measureTextToSetWidgetSize() {
289 FontRef font = font();
290 _charWidths.length = _textToSetDescLineSize.length;
291 int charsMeasured = font.measureText(_textToSetDescLineSize, _charWidths, MAX_WIDTH_UNSPECIFIED, 4);
292 _measuredTextToSetDescLineSize.x = charsMeasured > 0 ? _charWidths[charsMeasured - 1]: 0;
293 _measuredTextToSetDescLineSize.y = font.height;
294 return _measuredTextToSetDescLineSize;
295 }
296
297 override void measure(int parentWidth, int parentHeight) {
298 FontRef font = font();
299
300 int mWidth = minWidth;
301 int mHeight = minHeight;
302
303 int chartW = 0;
304 int chartH = 0;
305
306 _axisY.maxDescriptionSize = measureAxisYDesc();
307
308 int usedWidth = _axisY.maxDescriptionSize.x + _axisY.thickness + _axisY.segmentTagLength + _axisX.zeroValueDist + margins.left + padding.left + margins.right + padding.right + _axisX.arrowSize;
309
310 int currentMinBarWidth = max(_minBarWidth, _measuredTextToSetDescLineSize.x);
311 _axisX.maxDescriptionSize.y = _measuredTextToSetDescLineSize.y;
312
313 // axis length
314 _axisX.lengthFromZeroToArrow = cast(uint) barCount * (currentMinBarWidth + _barDistance);
315
316 if (_axisX.lengthFromZeroToArrow < _axisXMinWfromZero) {
317 _axisX.lengthFromZeroToArrow = _axisXMinWfromZero;
318 if (barCount > 0)
319 _barWidth = cast (int) ((_axisX.lengthFromZeroToArrow - (_barDistance * barCount)) / barCount);
320 }
321
322 // minWidth and minHeight check
323
324 if (minWidth > _axisX.lengthFromZeroToArrow + usedWidth) {
325 _axisX.lengthFromZeroToArrow = minWidth-usedWidth;
326 if (barCount > 0)
327 _barWidth = cast (int) ((_axisX.lengthFromZeroToArrow - (_barDistance * barCount)) / barCount);
328 }
329
330 // width FILL_PARENT support
331 if (parentWidth != SIZE_UNSPECIFIED && layoutWidth == FILL_PARENT) {
332 if (_axisX.lengthFromZeroToArrow < parentWidth - usedWidth) {
333 _axisX.lengthFromZeroToArrow = parentWidth - usedWidth;
334 if (barCount > 0)
335 _barWidth = cast (int) ((_axisX.lengthFromZeroToArrow - (_barDistance * barCount)) / barCount);
336 }
337 }
338
339
340 // initialize axis y length
341 _axisY.lengthFromZeroToArrow = cast(int) round(_axisRatio * _axisX.lengthFromZeroToArrow);
342
343 // is axis Y enought long
344 int usedHeight = _axisX.maxDescriptionSize.y + _axisX.thickness + _axisX.segmentTagLength + _axisY.zeroValueDist + ((_showTitle) ? _titleSize.y + _marginAfterTitle : 0) + margins.top + padding.top + margins.bottom + padding.bottom + _axisY.arrowSize;
345 if (minHeight > _axisY.lengthFromZeroToArrow + usedHeight) {
346 _axisY.lengthFromZeroToArrow = minHeight - usedHeight;
347 _axisX.lengthFromZeroToArrow = cast (int) round(_axisY.lengthFromZeroToArrow / _axisRatio);
348 }
349
350 // height FILL_PARENT support
351 if (parentHeight != SIZE_UNSPECIFIED && layoutHeight == FILL_PARENT) {
352 if (_axisY.lengthFromZeroToArrow < parentHeight - usedHeight)
353 _axisY.lengthFromZeroToArrow = parentHeight - usedHeight;
354 }
355
356 if (barCount > 0)
357 _barWidth = cast (int) ((_axisX.lengthFromZeroToArrow - (_barDistance * barCount)) / barCount);
358
359 // compute X axis max description height
360 _axisX.maxDescriptionSize = measureAxisXDesc();
361
362 // compute chart size
363 chartW = _axisY.maxDescriptionSize.x + _axisY.thickness + _axisY.segmentTagLength + _axisX.zeroValueDist + _axisX.lengthFromZeroToArrow + _axisX.arrowSize;
364 if (_showTitle && chartW < _titleSize.y)
365 chartW = _titleSize.y;
366
367 chartH = _axisX.maxDescriptionSize.y + _axisX.thickness + _axisX.segmentTagLength + _axisY.zeroValueDist + _axisY.lengthFromZeroToArrow + ((_showTitle) ? _titleSize.y + _marginAfterTitle : 0) + _axisY.arrowSize;
368 measuredContent(parentWidth, parentHeight, chartW, chartH);
369 }
370
371
372 protected Point measureAxisXDesc() {
373 Point sz;
374 foreach (ref bar ; _bars) {
375 bar._titleSize = font.measureMultilineText(bar.title, 0, _barWidth, 4, 0, textFlags);
376 if (sz.y < bar._titleSize.y)
377 sz.y = bar._titleSize.y;
378 if (sz.x < bar._titleSize.x)
379 sz.x = bar._titleSize.y;
380 }
381 return sz;
382 }
383
384 protected Point measureAxisYDesc() {
385 int maxDescWidth = _axisYMinDescWidth;
386 double currentMaxValue = _maxY;
387 if (isClose(_maxY, 0, 0.0000001, 0.0000001))
388 currentMaxValue = 100;
389
390 Point sz = font.textSize(to!dstring(currentMaxValue), MAX_WIDTH_UNSPECIFIED, 4, 0, textFlags);
391 if (maxDescWidth<sz.x)
392 maxDescWidth=sz.x;
393 _axisYMaxValueDescWidth = sz.x;
394 sz = font.textSize(to!dstring(currentMaxValue / 2), MAX_WIDTH_UNSPECIFIED, 4, 0, textFlags);
395 if (maxDescWidth<sz.x)
396 maxDescWidth=sz.x;
397 _axisYAvgValueDescWidth = sz.x;
398 return Point(maxDescWidth, sz.y);
399 }
400
401 protected int barYValueToPixels(int axisInPixels, double barYValue ) {
402 double currentMaxValue = _maxY;
403 if (isClose(_maxY, 0, 0.0000001, 0.0000001))
404 currentMaxValue = 100;
405
406 double pixValue = axisInPixels / currentMaxValue;
407 return cast(int) round(barYValue * pixValue);
408 }
409
410 override void onDraw(DrawBuf buf) {
411 if (visibility != Visibility.Visible)
412 return;
413 super.onDraw(buf);
414
415 Rect rc = _pos;
416 applyMargins(rc);
417 applyPadding(rc);
418
419 auto saver = ClipRectSaver(buf, rc, alpha);
420
421 FontRef font = font();
422 if (_showTitle)
423 font.drawText(buf, rc.left+ (_measuredWidth - _titleSize.x)/2 , rc.top, _title, textColor, 4, 0, textFlags);
424
425 // draw axises and
426 int x1 = rc.left + _axisY.maxDescriptionSize.x + _axisY.segmentTagLength;
427 int x2 = rc.left + _axisY.maxDescriptionSize.x + _axisY.segmentTagLength + _axisY.thickness + _axisX.zeroValueDist + _axisX.lengthFromZeroToArrow + _axisX.arrowSize;
428 int y1 = rc.bottom - _axisX.maxDescriptionSize.y - _axisX.segmentTagLength - _axisX.thickness - _axisY.zeroValueDist - _axisY.lengthFromZeroToArrow - _axisY.arrowSize;
429 int y2 = rc.bottom - _axisX.maxDescriptionSize.y - _axisX.segmentTagLength;
430
431 buf.fillRect(Rect(x1, y1, x2, y2), chartBackgroundColor);
432
433 // y axis
434 buf.drawLine(Point(x1 + 1, y1), Point(x1 + 1, y2), chartAxisColor);
435
436 // x axis
437 buf.drawLine(Point(x1, y2 - 1), Point(x2, y2 - 1), chartAxisColor);
438
439 // top line - will be optional in the future
440 buf.drawLine(Point(x1, y1), Point(x2, y1), chartAxisColor);
441
442 // right line - will be optional in the future
443 buf.drawLine(Point(x2, y1), Point(x2, y2), chartAxisColor);
444
445 // draw bars
446
447 int firstBarX = x1 + _axisY.thickness + _axisX.zeroValueDist;
448 int firstBarY = y2 - _axisX.thickness - _axisY.zeroValueDist;
449
450 SimpleTextFormatter fmt;
451 foreach (ref bar ; _bars) {
452 // draw bar
453 buf.fillRect(Rect(firstBarX, firstBarY - barYValueToPixels(_axisY.lengthFromZeroToArrow, bar.y), firstBarX + _barWidth, firstBarY), bar.color);
454
455 // draw x axis segment under bar
456 buf.drawLine(Point(firstBarX + _barWidth / 2, y2), Point(firstBarX + _barWidth / 2, rc.bottom - _axisX.maxDescriptionSize.y), chartSegmentTagColor);
457
458 // draw x axis description
459 fmt.format(bar.title, font, 0, _barWidth, 4, 0, textFlags);
460 fmt.draw(buf, firstBarX + (_barWidth - bar._titleSize.x) / 2, rc.bottom - _axisX.maxDescriptionSize.y + (_axisX.maxDescriptionSize.y - bar._titleSize.y) / 2, font, textColor, Align.HCenter);
461
462 firstBarX += _barWidth + _barDistance;
463 }
464
465 // segments on y axis and values (now only max and max/2)
466 double currentMaxValue = _maxY;
467 if (isClose(_maxY, 0, 0.0000001, 0.0000001))
468 currentMaxValue = 100;
469
470 int yZero = rc.bottom - _axisX.maxDescriptionSize.y - _axisX.segmentTagLength - _axisX.thickness - _axisY.zeroValueDist;
471 int yMax = yZero - _axisY.lengthFromZeroToArrow;
472 int yAvg = (yZero + yMax) / 2;
473
474 buf.drawLine(Point(rc.left + _axisY.maxDescriptionSize.x, yMax), Point(rc.left + _axisY.maxDescriptionSize.x + _axisY.segmentTagLength, yMax), chartSegmentTagColor);
475 buf.drawLine(Point(rc.left + _axisY.maxDescriptionSize.x, yAvg), Point(rc.left + _axisY.maxDescriptionSize.x + _axisY.segmentTagLength, yAvg), chartSegmentTagColor);
476
477 font.drawText(buf, rc.left + (_axisY.maxDescriptionSize.x - _axisYMaxValueDescWidth), yMax - _axisY.maxDescriptionSize.y / 2, to!dstring(currentMaxValue), textColor, 4, 0, textFlags);
478 font.drawText(buf, rc.left + (_axisY.maxDescriptionSize.x - _axisYAvgValueDescWidth), yAvg - _axisY.maxDescriptionSize.y / 2, to!dstring(currentMaxValue / 2), textColor, 4, 0, textFlags);
479
480 }
481
482 override void onThemeChanged() {
483 super.onThemeChanged();
484 handleFontChanged();
485 }
486 }
487