1 /*
2 * Copyright (C) 2015 Emweb bv, Herent, Belgium.
3 *
4 * See the LICENSE file for terms of use.
5 */
6
7 #include "Wt/Chart/WAxisSliderWidget.h"
8
9 #include "Wt/WApplication.h"
10 #include "Wt/WAny.h"
11 #include "Wt/WBrush.h"
12 #include "Wt/WColor.h"
13 #include "Wt/WJavaScript.h"
14 #include "Wt/WLogger.h"
15 #include "Wt/WPainter.h"
16 #include "Wt/WStringStream.h"
17 #include "Wt/WTransform.h"
18
19 #include "Wt/Chart/WCartesianChart.h"
20
21 #ifndef WT_DEBUG_JS
22 #include "js/WAxisSliderWidget.min.js"
23 #endif
24
25 #include <algorithm>
26
27 namespace Wt {
28
29 LOGGER("Chart.WAxisSliderWidget");
30
31 namespace Chart {
32
WAxisSliderWidget()33 WAxisSliderWidget::WAxisSliderWidget()
34 : series_(nullptr),
35 selectedSeriesPen_(&seriesPen_),
36 handleBrush_(WColor(0,0,200)),
37 background_(WColor(230, 230, 230)),
38 selectedAreaBrush_(WColor(255, 255, 255)),
39 autoPadding_(false),
40 labelsEnabled_(true),
41 yAxisZoomEnabled_(true)
42 {
43 init();
44 }
45
WAxisSliderWidget(WDataSeries * series)46 WAxisSliderWidget::WAxisSliderWidget(WDataSeries *series)
47 : series_(series),
48 selectedSeriesPen_(&seriesPen_),
49 handleBrush_(WColor(0,0,200)),
50 background_(WColor(230, 230, 230)),
51 selectedAreaBrush_(WColor(255, 255, 255)),
52 autoPadding_(false),
53 labelsEnabled_(true),
54 yAxisZoomEnabled_(true)
55 {
56 init();
57 }
58
init()59 void WAxisSliderWidget::init()
60 {
61 transform_ = createJSTransform();
62
63 mouseWentDown().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.mouseDown(o, e);}}");
64 mouseWentUp().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.mouseUp(o, e);}}");
65 mouseDragged().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.mouseDrag(o, e);}}");
66 mouseMoved().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.mouseMoved(o, e);}}");
67 touchStarted().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.touchStarted(o, e);}}");
68 touchEnded().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.touchEnded(o, e);}}");
69 touchMoved().connect("function(o, e){var o=" + this->sObjJsRef() + ";if(o){o.touchMoved(o, e);}}");
70
71 setSelectionAreaPadding(0, Side::Top);
72 setSelectionAreaPadding(20, WFlags<Side>(Side::Left) | Side::Right);
73 setSelectionAreaPadding(30, Side::Bottom);
74
75 if (chart()) {
76 chart()->addAxisSliderWidget(this);
77 }
78 }
79
~WAxisSliderWidget()80 WAxisSliderWidget::~WAxisSliderWidget()
81 {
82 if (chart()) {
83 chart()->removeAxisSliderWidget(this);
84 }
85 if (selectedSeriesPen_ != &seriesPen_) {
86 delete selectedSeriesPen_;
87 }
88 }
89
setSeries(WDataSeries * series)90 void WAxisSliderWidget::setSeries(WDataSeries *series)
91 {
92 if (series_ != series) {
93 if (series_)
94 chart()->removeAxisSliderWidget(this);
95 series_ = series;
96 if (series_)
97 chart()->addAxisSliderWidget(this);
98 update();
99 }
100 }
101
setSeriesPen(const WPen & pen)102 void WAxisSliderWidget::setSeriesPen(const WPen& pen)
103 {
104 if (pen != seriesPen_) {
105 seriesPen_ = pen;
106 update();
107 }
108 }
109
setSelectedSeriesPen(const WPen & pen)110 void WAxisSliderWidget::setSelectedSeriesPen(const WPen& pen)
111 {
112 if (selectedSeriesPen_ != &seriesPen_) {
113 delete selectedSeriesPen_;
114 }
115 selectedSeriesPen_ = new WPen(pen);
116 update();
117 }
118
setHandleBrush(const WBrush & brush)119 void WAxisSliderWidget::setHandleBrush(const WBrush& brush)
120 {
121 if (brush != handleBrush_) {
122 handleBrush_ = brush;
123 update();
124 }
125 }
126
setBackground(const WBrush & brush)127 void WAxisSliderWidget::setBackground(const WBrush& brush)
128 {
129 if (brush != background_) {
130 background_ = brush;
131 update();
132 }
133 }
134
setSelectedAreaBrush(const WBrush & brush)135 void WAxisSliderWidget::setSelectedAreaBrush(const WBrush& brush)
136 {
137 if (brush != selectedAreaBrush_) {
138 selectedAreaBrush_ = brush;
139 update();
140 }
141 }
142
render(WFlags<RenderFlag> flags)143 void WAxisSliderWidget::render(WFlags<RenderFlag> flags)
144 {
145 WPaintedWidget::render(flags);
146
147 WApplication *app = WApplication::instance();
148
149 LOAD_JAVASCRIPT(app, "js/WAxisSliderWidget.js", "WAxisSliderWidget", wtjs1);
150 }
151
setSelectionAreaPadding(int padding,WFlags<Side> sides)152 void WAxisSliderWidget::setSelectionAreaPadding(int padding, WFlags<Side> sides)
153 {
154 if (sides.test(Side::Top))
155 padding_[0] = padding;
156 if (sides.test(Side::Right))
157 padding_[1] = padding;
158 if (sides.test(Side::Bottom))
159 padding_[2] = padding;
160 if (sides.test(Side::Left))
161 padding_[3] = padding;
162 }
163
selectionAreaPadding(Side side)164 int WAxisSliderWidget::selectionAreaPadding(Side side) const
165 {
166 switch (side) {
167 case Side::Top:
168 return padding_[0];
169 case Side::Right:
170 return padding_[1];
171 case Side::Bottom:
172 return padding_[2];
173 case Side::Left:
174 return padding_[3];
175 default:
176 LOG_ERROR("selectionAreaPadding(): improper side.");
177 return 0;
178 }
179 }
180
setAutoLayoutEnabled(bool enabled)181 void WAxisSliderWidget::setAutoLayoutEnabled(bool enabled)
182 {
183 autoPadding_ = enabled;
184 }
185
setLabelsEnabled(bool enabled)186 void WAxisSliderWidget::setLabelsEnabled(bool enabled)
187 {
188 if (enabled != labelsEnabled_) {
189 labelsEnabled_ = enabled;
190 update();
191 }
192 }
193
setYAxisZoomEnabled(bool enabled)194 void WAxisSliderWidget::setYAxisZoomEnabled(bool enabled)
195 {
196 if (enabled != yAxisZoomEnabled_) {
197 yAxisZoomEnabled_ = enabled;
198 update();
199 }
200 }
201
hv(const WRectF & rect)202 WRectF WAxisSliderWidget::hv(const WRectF& rect) const
203 {
204 bool horizontal = chart()->orientation() == Orientation::Vertical; // yes, vertical chart means horizontal X axis slider
205
206 if (horizontal) {
207 return rect;
208 } else {
209 return WRectF(width().value() - rect.y() - rect.height(), rect.x(), rect.height(), rect.width());
210 }
211 }
212
hv(const WTransform & t)213 WTransform WAxisSliderWidget::hv(const WTransform& t) const
214 {
215 bool horizontal = chart()->orientation() == Orientation::Vertical; // yes, vertical chart means horizontal X axis slider
216
217 if (horizontal) {
218 return t;
219 } else {
220 return WTransform(0,1,1,0,0,0) * t * WTransform(0,1,1,0,0,0);
221 }
222 }
223
paintEvent(WPaintDevice * paintDevice)224 void WAxisSliderWidget::paintEvent(WPaintDevice *paintDevice)
225 {
226 if (!chart()) {
227 LOG_ERROR("Attempted to draw a slider widget not associated with a chart.");
228 return;
229 }
230 // Don't paint anything, unless we're associated to a chart,
231 // and the chart has been painted.
232 if (!chart()->cObjCreated_ || chart()->needRerender())
233 return;
234
235 if (series_->type() != SeriesType::Line &&
236 series_->type() != SeriesType::Curve) {
237 if (getMethod() == RenderMethod::HtmlCanvas) {
238 WStringStream ss;
239 ss << "\ndelete " << jsRef() << ".wtSObj;";
240 ss << "\nif (" << objJsRef() << ") {"
241 << objJsRef() << ".canvas.style.cursor = 'auto';"
242 << "setTimeout(" << objJsRef() << ".repaint,0);"
243 "}\n";
244 doJavaScript(ss.str());
245 }
246 LOG_ERROR("WAxisSliderWidget is not associated with a line or curve series.");
247 return;
248 }
249
250 WAxis &xAxis = chart()->xAxis(series_->xAxis());
251 WCartesianChart::AxisStruct &xAxisStruct = chart()->xAxes_[series_->xAxis()];
252
253 WPainter painter(paintDevice);
254
255 bool horizontal = chart()->orientation() == Orientation::Vertical; // yes, vertical chart means horizontal X axis slider
256
257 double w = horizontal ? width().value() : height().value(),
258 h = horizontal ? height().value() : width().value();
259
260 bool autoPadding = autoPadding_;
261 if (autoPadding && !(paintDevice->features() &
262 PaintDeviceFeatureFlag::FontMetrics) &&
263 labelsEnabled_) {
264 LOG_ERROR("setAutoLayout(): device does not have font metrics "
265 "(not even server-side font metrics).");
266 autoPadding = false;
267 }
268
269 if (autoPadding) {
270 if (horizontal) {
271 if (labelsEnabled_) {
272 setSelectionAreaPadding(0, Side::Top);
273 setSelectionAreaPadding(
274 static_cast<int>(xAxis.calcMaxTickLabelSize(
275 paintDevice,
276 Orientation::Vertical
277 ) + 10), Side::Bottom);
278 setSelectionAreaPadding(
279 static_cast<int>(std::max(xAxis.calcMaxTickLabelSize(
280 paintDevice,
281 Orientation::Horizontal
282 ) / 2, 10.0)), WFlags<Side>(Side::Left) | Side::Right);
283 } else {
284 setSelectionAreaPadding(0, Side::Top);
285 setSelectionAreaPadding(5, WFlags<Side>(Side::Left) | Side::Right | Side::Bottom);
286 }
287 } else {
288 if (labelsEnabled_) {
289 setSelectionAreaPadding(0, Side::Right);
290 setSelectionAreaPadding(
291 static_cast<int>(std::max(xAxis.calcMaxTickLabelSize(
292 paintDevice,
293 Orientation::Vertical
294 ) / 2, 10.0)), WFlags<Side>(Side::Top) | Side::Bottom);
295 setSelectionAreaPadding(
296 static_cast<int>(xAxis.calcMaxTickLabelSize(
297 paintDevice,
298 Orientation::Horizontal
299 ) + 10), Side::Left);
300 } else {
301 setSelectionAreaPadding(0, Side::Right);
302 setSelectionAreaPadding(5, WFlags<Side>(Side::Top) | Side::Bottom | Side::Left);
303 }
304 }
305 }
306
307 double left = horizontal ? selectionAreaPadding(Side::Left)
308 : selectionAreaPadding(Side::Top);
309 double right = horizontal ? selectionAreaPadding(Side::Right)
310 : selectionAreaPadding(Side::Bottom);
311 double top = horizontal ? selectionAreaPadding(Side::Top)
312 : selectionAreaPadding(Side::Right);
313 double bottom = horizontal ? selectionAreaPadding(Side::Bottom)
314 : selectionAreaPadding(Side::Left);
315
316 double maxW = w - left - right;
317 WRectF drawArea(left, 0, maxW, h);
318 #ifndef WT_TARGET_JAVA
319 std::vector<WAxis::Segment> segmentsBak = xAxis.segments_;
320 #else
321 std::vector<WAxis::Segment> segmentsBak;
322 for (std::size_t i = 0; i < xAxis.segments_.size(); ++i) {
323 segmentsBak.push_back(WAxis::Segment(xAxis.segments_[i]));
324 }
325 #endif
326 double renderIntervalBak = xAxis.renderInterval_;
327 double fullRenderLengthBak = xAxis.fullRenderLength_;
328 xAxis.prepareRender(horizontal ? Orientation::Horizontal : Orientation::Vertical, drawArea.width());
329
330 const WRectF& chartArea = chart()->chartArea_;
331 WRectF selectionRect;
332 {
333 // Determine initial position based on xTransform of chart
334 double u = -xAxisStruct.transformHandle.value().dx() /
335 (chartArea.width() * xAxisStruct.transformHandle.value().m11());
336 selectionRect = WRectF(0, top, maxW, h - (top + bottom));
337 transform_.setValue(
338 WTransform(1 / xAxisStruct.transformHandle.value().m11(), 0, 0, 1, u * maxW, 0));
339 }
340 WRectF seriesArea(left, top + 5, maxW, h - (top + bottom + 5));
341 WTransform selectionTransform = hv(WTransform(1,0,0,1,left,0) * transform_.value());
342 WRectF rect = selectionTransform.map(hv(selectionRect));
343
344 painter.fillRect(hv(WRectF(left, top, maxW, h - top - bottom)), background_);
345 painter.fillRect(rect, selectedAreaBrush_);
346
347 // FIXME: Refactor this code? We have very similar code now in WCartesianChart and
348 // WCartesian3DChart too.
349 const double TICK_LENGTH = 5;
350 const double ANGLE1 = 15;
351 const double ANGLE2 = 80;
352
353 double tickStart = 0.0, tickEnd = 0.0, labelPos = 0.0;
354 AlignmentFlag labelHFlag = AlignmentFlag::Center, labelVFlag = AlignmentFlag::Middle;
355
356 if (horizontal) {
357 tickStart = 0;
358 tickEnd = TICK_LENGTH;
359 labelPos = TICK_LENGTH;
360 labelVFlag = AlignmentFlag::Top;
361 } else {
362 tickStart = -TICK_LENGTH;
363 tickEnd = 0;
364 labelPos = -TICK_LENGTH;
365 labelHFlag = AlignmentFlag::Right;
366 }
367
368 if (horizontal) {
369 if (xAxis.labelAngle() > ANGLE1) {
370 labelHFlag = AlignmentFlag::Right;
371 if (xAxis.labelAngle() > ANGLE2)
372 labelVFlag = AlignmentFlag::Middle;
373 } else if (xAxis.labelAngle() < -ANGLE1) {
374 labelHFlag = AlignmentFlag::Left;
375 if (xAxis.labelAngle() < -ANGLE2)
376 labelVFlag = AlignmentFlag::Middle;
377 }
378 } else {
379 if (xAxis.labelAngle() > ANGLE1) {
380 labelVFlag = AlignmentFlag::Bottom;
381 if (xAxis.labelAngle() > ANGLE2)
382 labelHFlag = AlignmentFlag::Center;
383 } else if (xAxis.labelAngle() < -ANGLE1) {
384 labelVFlag = AlignmentFlag::Top;
385 if (xAxis.labelAngle() < -ANGLE2)
386 labelHFlag = AlignmentFlag::Center;
387 }
388 }
389
390 WFlags<AxisProperty> axisProperties = AxisProperty::Line;
391 if (labelsEnabled_) {
392 axisProperties |= AxisProperty::Labels;
393 }
394
395 if (horizontal) {
396 xAxis.render(
397 painter,
398 axisProperties,
399 WPointF(drawArea.left(), h - bottom),
400 WPointF(drawArea.right(), h - bottom),
401 tickStart, tickEnd, labelPos,
402 WFlags<AlignmentFlag>(labelHFlag) | labelVFlag);
403 WPainterPath line;
404 line.moveTo(drawArea.left() + 0.5, h - (bottom - 0.5));
405 line.lineTo(drawArea.right(), h - (bottom - 0.5));
406 painter.strokePath(line, xAxis.pen());
407 } else {
408 xAxis.render(
409 painter,
410 axisProperties,
411 WPointF(selectionAreaPadding(Side::Left) - 1, drawArea.left()),
412 WPointF(selectionAreaPadding(Side::Left) - 1, drawArea.right()),
413 tickStart, tickEnd, labelPos,
414 WFlags<AlignmentFlag>(labelHFlag) | labelVFlag);
415 WPainterPath line;
416 line.moveTo(selectionAreaPadding(Side::Left) - 0.5, drawArea.left() + 0.5);
417 line.lineTo(selectionAreaPadding(Side::Left) - 0.5, drawArea.right());
418 painter.strokePath(line, xAxis.pen());
419 }
420
421 WPainterPath curve;
422 {
423 WTransform t = WTransform(1,0,0,1,seriesArea.left(),seriesArea.top()) *
424 WTransform(seriesArea.width() / chartArea.width(), 0, 0, seriesArea.height() / chartArea.height(), 0, 0) *
425 WTransform(1,0,0,1,-chartArea.left(),-chartArea.top());
426 if (!horizontal) {
427 t = WTransform(0,1,1,0,selectionAreaPadding(Side::Left) - selectionAreaPadding(Side::Right) - 5,0) * t * WTransform(0,1,1,0,0,0);
428 }
429 curve = t.map(chart()->pathForSeries(*series_));
430 }
431
432 {
433 WRectF leftHandle = hv(WRectF(-5, top, 5, h - top - bottom));
434 WTransform t = (WTransform(1,0,0,1,left,-top) *
435 (WTransform().translate(transform_.value().map(selectionRect.topLeft()))));
436 painter.fillRect(hv(t).map(leftHandle), handleBrush_);
437 }
438
439 {
440 WRectF rightHandle = hv(WRectF(0, top, 5, h - top - bottom));
441 WTransform t = (WTransform(1,0,0,1,left,-top) *
442 (WTransform().translate(transform_.value().map(selectionRect.topRight()))));
443 painter.fillRect(hv(t).map(rightHandle), handleBrush_);
444 }
445
446 if (selectedSeriesPen_ != &seriesPen_ && *selectedSeriesPen_ != seriesPen_) {
447 WPainterPath clipPath;
448 clipPath.addRect(hv(selectionRect));
449 painter.setClipPath(selectionTransform.map(clipPath));
450 painter.setClipping(true);
451
452 painter.setPen(selectedSeriesPen());
453 painter.drawPath(curve);
454
455 WPainterPath leftClipPath;
456 leftClipPath.addRect(hv(WTransform(1,0,0,1,-selectionRect.width(),0).map(selectionRect)));
457 painter.setClipPath(hv(
458 WTransform(1,0,0,1,left,-top) *
459 (WTransform().translate(transform_.value().map(selectionRect.topLeft())))
460 ).map(leftClipPath));
461
462 painter.setPen(seriesPen());
463 painter.drawPath(curve);
464
465 WPainterPath rightClipPath;
466 rightClipPath.addRect(hv(WTransform(1,0,0,1,selectionRect.width(),0).map(selectionRect)));
467 painter.setClipPath(hv(
468 WTransform(1,0,0,1,left - selectionRect.right(),-top) *
469 (WTransform().translate(transform_.value().map(selectionRect.topRight())))
470 ).map(rightClipPath));
471
472 painter.drawPath(curve);
473
474 painter.setClipping(false);
475 } else {
476 WPainterPath clipPath;
477 clipPath.addRect(hv(WRectF(left, top, maxW, h - top - bottom)));
478 painter.setClipPath(clipPath);
479 painter.setClipping(true);
480
481 painter.setPen(seriesPen());
482 painter.drawPath(curve);
483
484 painter.setClipping(false);
485 }
486
487 if (getMethod() == RenderMethod::HtmlCanvas) {
488 WApplication *app = WApplication::instance();
489 WStringStream ss;
490 ss << "new " WT_CLASS ".WAxisSliderWidget("
491 << app->javaScriptClass() << ","
492 << jsRef() << ","
493 << objJsRef() << ","
494 << "{"
495 "chart:" << chart()->cObjJsRef() << ","
496 "transform:" << transform_.jsRef() << ","
497 "rect:function(){return " << rect.jsRef() << "},"
498 "drawArea:" << drawArea.jsRef() << ","
499 "series:" << chart()->seriesIndexOf(*series_) << ","
500 "updateYAxis:" << asString(yAxisZoomEnabled_).toUTF8() <<
501 "});";
502 doJavaScript(ss.str());
503 }
504
505 #ifndef WT_TARGET_JAVA
506 xAxis.segments_ = segmentsBak;
507 #else
508 xAxis.segments_.clear();
509 for (std::size_t i = 0; i < segmentsBak.size(); ++i) {
510 xAxis.segments_.push_back(WAxis::Segment(segmentsBak[i]));
511 }
512 #endif
513 xAxis.renderInterval_ = renderIntervalBak;
514 xAxis.fullRenderLength_ = fullRenderLengthBak;
515 }
516
sObjJsRef()517 std::string WAxisSliderWidget::sObjJsRef() const
518 {
519 return jsRef() + ".wtSObj";
520 }
521
chart()522 WCartesianChart *WAxisSliderWidget::chart()
523 {
524 if (series_) {
525 return series_->chart();
526 } else {
527 return nullptr;
528 }
529 }
530
chart()531 const WCartesianChart *WAxisSliderWidget::chart() const
532 {
533 return const_cast<WAxisSliderWidget *>(this)->chart();
534 }
535
536 }
537 }
538