1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the Qt Charts module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 or (at your option) any later version |
20 | ** approved by the KDE Free Qt Foundation. The licenses are as published by |
21 | ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
22 | ** included in the packaging of this file. Please review the following |
23 | ** information to ensure the GNU General Public License requirements will |
24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
25 | ** |
26 | ** $QT_END_LICENSE$ |
27 | ** |
28 | ****************************************************************************/ |
29 | |
30 | #include <private/areachartitem_p.h> |
31 | #include <QtCharts/QAreaSeries> |
32 | #include <private/qareaseries_p.h> |
33 | #include <QtCharts/QLineSeries> |
34 | #include <private/chartpresenter_p.h> |
35 | #include <private/abstractdomain_p.h> |
36 | #include <private/chartdataset_p.h> |
37 | #include <QtGui/QPainter> |
38 | #include <QtWidgets/QGraphicsSceneMouseEvent> |
39 | #include <QtCore/QDebug> |
40 | |
41 | |
42 | QT_CHARTS_BEGIN_NAMESPACE |
43 | |
44 | AreaChartItem::AreaChartItem(QAreaSeries *areaSeries, QGraphicsItem* item) |
45 | : ChartItem(areaSeries->d_func(),item), |
46 | m_series(areaSeries), |
47 | m_upper(0), |
48 | m_lower(0), |
49 | m_pointsVisible(false), |
50 | m_pointLabelsVisible(false), |
51 | m_pointLabelsFormat(areaSeries->pointLabelsFormat()), |
52 | m_pointLabelsFont(areaSeries->pointLabelsFont()), |
53 | m_pointLabelsColor(areaSeries->pointLabelsColor()), |
54 | m_pointLabelsClipping(true), |
55 | m_mousePressed(false) |
56 | { |
57 | setAcceptHoverEvents(true); |
58 | setFlag(flag: QGraphicsItem::ItemIsSelectable, enabled: true); |
59 | setZValue(ChartPresenter::LineChartZValue); |
60 | if (m_series->upperSeries()) |
61 | m_upper = new AreaBoundItem(this, m_series->upperSeries()); |
62 | if (m_series->lowerSeries()) |
63 | m_lower = new AreaBoundItem(this, m_series->lowerSeries()); |
64 | |
65 | QObject::connect(sender: m_series->d_func(), SIGNAL(updated()), receiver: this, SLOT(handleUpdated())); |
66 | QObject::connect(sender: m_series, SIGNAL(visibleChanged()), receiver: this, SLOT(handleUpdated())); |
67 | QObject::connect(sender: m_series, SIGNAL(opacityChanged()), receiver: this, SLOT(handleUpdated())); |
68 | QObject::connect(sender: this, SIGNAL(clicked(QPointF)), receiver: areaSeries, SIGNAL(clicked(QPointF))); |
69 | QObject::connect(sender: this, SIGNAL(hovered(QPointF,bool)), receiver: areaSeries, SIGNAL(hovered(QPointF,bool))); |
70 | QObject::connect(sender: this, SIGNAL(pressed(QPointF)), receiver: areaSeries, SIGNAL(pressed(QPointF))); |
71 | QObject::connect(sender: this, SIGNAL(released(QPointF)), receiver: areaSeries, SIGNAL(released(QPointF))); |
72 | QObject::connect(sender: this, SIGNAL(doubleClicked(QPointF)), |
73 | receiver: areaSeries, SIGNAL(doubleClicked(QPointF))); |
74 | QObject::connect(sender: areaSeries, SIGNAL(pointLabelsFormatChanged(QString)), |
75 | receiver: this, SLOT(handleUpdated())); |
76 | QObject::connect(sender: areaSeries, SIGNAL(pointLabelsVisibilityChanged(bool)), |
77 | receiver: this, SLOT(handleUpdated())); |
78 | QObject::connect(sender: areaSeries, SIGNAL(pointLabelsFontChanged(QFont)), |
79 | receiver: this, SLOT(handleUpdated())); |
80 | QObject::connect(sender: areaSeries, SIGNAL(pointLabelsColorChanged(QColor)), |
81 | receiver: this, SLOT(handleUpdated())); |
82 | QObject::connect(sender: areaSeries, SIGNAL(pointLabelsClippingChanged(bool)), |
83 | receiver: this, SLOT(handleUpdated())); |
84 | |
85 | handleUpdated(); |
86 | } |
87 | |
88 | AreaChartItem::~AreaChartItem() |
89 | { |
90 | delete m_upper; |
91 | delete m_lower; |
92 | } |
93 | |
94 | void AreaChartItem::setPresenter(ChartPresenter *presenter) |
95 | { |
96 | if (m_upper) |
97 | m_upper->setPresenter(presenter); |
98 | if (m_lower) |
99 | m_lower->setPresenter(presenter); |
100 | ChartItem::setPresenter(presenter); |
101 | } |
102 | |
103 | void AreaChartItem::setUpperSeries(QLineSeries *series) |
104 | { |
105 | delete m_upper; |
106 | if (series) |
107 | m_upper = new AreaBoundItem(this, series); |
108 | else |
109 | m_upper = 0; |
110 | if (m_upper) { |
111 | m_upper->setPresenter(presenter()); |
112 | fixEdgeSeriesDomain(edgeSeries: m_upper); |
113 | } else { |
114 | updatePath(); |
115 | } |
116 | } |
117 | |
118 | void AreaChartItem::setLowerSeries(QLineSeries *series) |
119 | { |
120 | delete m_lower; |
121 | if (series) |
122 | m_lower = new AreaBoundItem(this, series); |
123 | else |
124 | m_lower = 0; |
125 | if (m_lower) { |
126 | m_lower->setPresenter(presenter()); |
127 | fixEdgeSeriesDomain(edgeSeries: m_lower); |
128 | } else { |
129 | updatePath(); |
130 | } |
131 | } |
132 | |
133 | QRectF AreaChartItem::boundingRect() const |
134 | { |
135 | return m_rect; |
136 | } |
137 | |
138 | QPainterPath AreaChartItem::shape() const |
139 | { |
140 | return m_path; |
141 | } |
142 | |
143 | void AreaChartItem::updatePath() |
144 | { |
145 | QPainterPath path; |
146 | QRectF rect(QPointF(0,0),domain()->size()); |
147 | |
148 | if (m_upper) { |
149 | path = m_upper->path(); |
150 | |
151 | if (m_lower) { |
152 | // Note: Polarcharts draw area correctly only when both series have equal width or are |
153 | // fully displayed. If one series is partally off-chart, the connecting line between |
154 | // the series does not attach to the end of the partially hidden series but to the point |
155 | // where it intersects the axis line. The problem is especially noticeable when one of |
156 | // the series is entirely off-chart, in which case the connecting line connects two |
157 | // ends of the visible series. |
158 | // This happens because we get the paths from linechart, which omits off-chart segments. |
159 | // To properly fix, linechart would need to provide true full path, in right, left, |
160 | // and the rest portions to enable proper clipping. However, combining those to single |
161 | // visually unified area would be a nightmare, since they would have to be painted |
162 | // separately. |
163 | path.connectPath(path: m_lower->path().toReversed()); |
164 | } else { |
165 | QPointF first = path.pointAtPercent(t: 0); |
166 | QPointF last = path.pointAtPercent(t: 1); |
167 | if (presenter()->chartType() == QChart::ChartTypeCartesian) { |
168 | path.lineTo(x: last.x(), y: rect.bottom()); |
169 | path.lineTo(x: first.x(), y: rect.bottom()); |
170 | } else { // polar |
171 | path.lineTo(p: rect.center()); |
172 | } |
173 | } |
174 | path.closeSubpath(); |
175 | } |
176 | |
177 | // Only zoom in if the bounding rect of the path fits inside int limits. QWidget::update() uses |
178 | // a region that has to be compatible with QRect. |
179 | if (path.boundingRect().height() <= INT_MAX |
180 | && path.boundingRect().width() <= INT_MAX) { |
181 | prepareGeometryChange(); |
182 | m_path = path; |
183 | m_rect = path.boundingRect(); |
184 | update(); |
185 | } |
186 | } |
187 | |
188 | void AreaChartItem::handleUpdated() |
189 | { |
190 | setVisible(m_series->isVisible()); |
191 | m_pointsVisible = m_series->pointsVisible(); |
192 | m_linePen = m_series->pen(); |
193 | m_brush = m_series->brush(); |
194 | m_pointPen = m_series->pen(); |
195 | m_pointPen.setWidthF(2 * m_pointPen.width()); |
196 | setOpacity(m_series->opacity()); |
197 | m_pointLabelsFormat = m_series->pointLabelsFormat(); |
198 | m_pointLabelsVisible = m_series->pointLabelsVisible(); |
199 | m_pointLabelsFont = m_series->pointLabelsFont(); |
200 | m_pointLabelsColor = m_series->pointLabelsColor(); |
201 | bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping(); |
202 | m_pointLabelsClipping = m_series->pointLabelsClipping(); |
203 | // Update whole chart in case label clipping changed as labels can be outside series area |
204 | if (labelClippingChanged) |
205 | m_series->chart()->update(); |
206 | else |
207 | update(); |
208 | } |
209 | |
210 | void AreaChartItem::handleDomainUpdated() |
211 | { |
212 | fixEdgeSeriesDomain(edgeSeries: m_upper); |
213 | fixEdgeSeriesDomain(edgeSeries: m_lower); |
214 | } |
215 | |
216 | void AreaChartItem::fixEdgeSeriesDomain(LineChartItem *edgeSeries) |
217 | { |
218 | if (edgeSeries) { |
219 | AbstractDomain* mainDomain = domain(); |
220 | AbstractDomain* edgeDomain = edgeSeries->domain(); |
221 | |
222 | if (edgeDomain->type() != mainDomain->type()) { |
223 | // Change the domain of edge series to the same type as the area series |
224 | edgeDomain = dataSet()->createDomain(type: mainDomain->type()); |
225 | edgeSeries->seriesPrivate()->setDomain(edgeDomain); |
226 | } |
227 | edgeDomain->setSize(mainDomain->size()); |
228 | edgeDomain->setRange(minX: mainDomain->minX(), maxX: mainDomain->maxX(), minY: mainDomain->minY(), maxY: mainDomain->maxY()); |
229 | edgeDomain->setReverseX(mainDomain->isReverseX()); |
230 | edgeDomain->setReverseY(mainDomain->isReverseY()); |
231 | edgeSeries->handleDomainUpdated(); |
232 | } |
233 | } |
234 | |
235 | void AreaChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) |
236 | { |
237 | Q_UNUSED(widget) |
238 | Q_UNUSED(option) |
239 | |
240 | painter->save(); |
241 | painter->setPen(m_linePen); |
242 | painter->setBrush(m_brush); |
243 | QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); |
244 | if (presenter()->chartType() == QChart::ChartTypePolar) |
245 | painter->setClipRegion(QRegion(clipRect.toRect(), QRegion::Ellipse)); |
246 | else |
247 | painter->setClipRect(clipRect); |
248 | |
249 | painter->drawPath(path: m_path); |
250 | if (m_pointsVisible) { |
251 | painter->setPen(m_pointPen); |
252 | if (m_upper) |
253 | painter->drawPoints(points: m_upper->geometryPoints()); |
254 | if (m_lower) |
255 | painter->drawPoints(points: m_lower->geometryPoints()); |
256 | } |
257 | |
258 | // Draw series point label |
259 | if (m_pointLabelsVisible) { |
260 | static const QString xPointTag(QLatin1String("@xPoint" )); |
261 | static const QString yPointTag(QLatin1String("@yPoint" )); |
262 | const int labelOffset = 2; |
263 | |
264 | if (m_pointLabelsClipping) |
265 | painter->setClipping(true); |
266 | else |
267 | painter->setClipping(false); |
268 | |
269 | QFont f(m_pointLabelsFont); |
270 | f.setPixelSize(QFontInfo(m_pointLabelsFont).pixelSize()); |
271 | painter->setFont(f); |
272 | painter->setPen(QPen(m_pointLabelsColor)); |
273 | QFontMetrics fm(painter->font()); |
274 | |
275 | QString pointLabel; |
276 | |
277 | if (m_series->upperSeries()) { |
278 | for (int i(0); i < m_series->upperSeries()->count(); i++) { |
279 | pointLabel = m_pointLabelsFormat; |
280 | pointLabel.replace(before: xPointTag, |
281 | after: presenter()->numberToString(value: m_series->upperSeries()->at(index: i).x())); |
282 | pointLabel.replace(before: yPointTag, |
283 | after: presenter()->numberToString(value: m_series->upperSeries()->at(index: i).y())); |
284 | |
285 | // Position text in relation to the point |
286 | int pointLabelWidth = fm.horizontalAdvance(pointLabel); |
287 | QPointF position(m_upper->geometryPoints().at(i)); |
288 | position.setX(position.x() - pointLabelWidth / 2); |
289 | position.setY(position.y() - m_series->upperSeries()->pen().width() / 2 |
290 | - labelOffset); |
291 | painter->drawText(p: position, s: pointLabel); |
292 | } |
293 | } |
294 | |
295 | if (m_series->lowerSeries()) { |
296 | for (int i(0); i < m_series->lowerSeries()->count(); i++) { |
297 | pointLabel = m_pointLabelsFormat; |
298 | pointLabel.replace(before: xPointTag, |
299 | after: presenter()->numberToString(value: m_series->lowerSeries()->at(index: i).x())); |
300 | pointLabel.replace(before: yPointTag, |
301 | after: presenter()->numberToString(value: m_series->lowerSeries()->at(index: i).y())); |
302 | |
303 | // Position text in relation to the point |
304 | int pointLabelWidth = fm.horizontalAdvance(pointLabel); |
305 | QPointF position(m_lower->geometryPoints().at(i)); |
306 | position.setX(position.x() - pointLabelWidth / 2); |
307 | position.setY(position.y() - m_series->lowerSeries()->pen().width() / 2 |
308 | - labelOffset); |
309 | painter->drawText(p: position, s: pointLabel); |
310 | } |
311 | } |
312 | } |
313 | |
314 | painter->restore(); |
315 | } |
316 | |
317 | void AreaChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event) |
318 | { |
319 | emit pressed(point: domain()->calculateDomainPoint(point: event->pos())); |
320 | m_lastMousePos = event->pos(); |
321 | m_mousePressed = true; |
322 | ChartItem::mousePressEvent(event); |
323 | } |
324 | |
325 | void AreaChartItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) |
326 | { |
327 | emit hovered(point: domain()->calculateDomainPoint(point: event->pos()), state: true); |
328 | event->accept(); |
329 | // QGraphicsItem::hoverEnterEvent(event); |
330 | } |
331 | |
332 | void AreaChartItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) |
333 | { |
334 | emit hovered(point: domain()->calculateDomainPoint(point: event->pos()), state: false); |
335 | event->accept(); |
336 | // QGraphicsItem::hoverEnterEvent(event); |
337 | } |
338 | |
339 | void AreaChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) |
340 | { |
341 | emit released(point: domain()->calculateDomainPoint(point: m_lastMousePos)); |
342 | if (m_mousePressed) |
343 | emit clicked(point: domain()->calculateDomainPoint(point: m_lastMousePos)); |
344 | m_mousePressed = false; |
345 | ChartItem::mouseReleaseEvent(event); |
346 | } |
347 | |
348 | void AreaChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) |
349 | { |
350 | emit doubleClicked(point: domain()->calculateDomainPoint(point: m_lastMousePos)); |
351 | ChartItem::mouseDoubleClickEvent(event); |
352 | } |
353 | |
354 | QT_CHARTS_END_NAMESPACE |
355 | |
356 | #include "moc_areachartitem_p.cpp" |
357 | |