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 (approxEqual(_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 (approxEqual(_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 (approxEqual(_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