Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <private/scatterchartitem_p.h>
#include <QtCharts/QScatterSeries>
#include <private/qscatterseries_p.h>
#include <private/chartpresenter_p.h>
#include <private/abstractdomain_p.h>
#include <QtCharts/QChart>
#include <QtGui/QPainter>
#include <QtWidgets/QGraphicsScene>
#include <QtCore/QDebug>
#include <QtWidgets/QGraphicsSceneMouseEvent>
QT_BEGIN_NAMESPACE
namespace {
constexpr short STAR_SPIKES = 5;
}
ScatterChartItem::ScatterChartItem(QScatterSeries *series, QGraphicsItem *item)
: XYChart(series,item),
m_series(series),
m_items(this),
m_visible(true),
m_markerShape(QScatterSeries::MarkerShapeRectangle),
m_pointsVisible(true),
m_pointLabelsVisible(false),
m_markerSize(series->markerSize()),
m_pointLabelsFormat(series->pointLabelsFormat()),
m_pointLabelsFont(series->pointLabelsFont()),
m_pointLabelsColor(series->pointLabelsColor()),
m_pointLabelsClipping(true),
m_lastHoveredPoint(QPointF(qQNaN(), qQNaN())),
m_mousePressed(false)
{
connect(series->d_func(), &QXYSeriesPrivate::seriesUpdated,
this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::lightMarkerChanged, this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::selectedLightMarkerChanged, this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::markerSizeChanged, this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::visibleChanged, this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::opacityChanged, this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::pointLabelsFormatChanged,
this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::pointLabelsVisibilityChanged,
this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::pointLabelsFontChanged,
this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::pointLabelsColorChanged,
this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::pointLabelsClippingChanged,
this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::selectedColorChanged,
this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QXYSeries::selectedPointsChanged,
this, &ScatterChartItem::handleSeriesUpdated);
connect(series, &QScatterSeries::pointsConfigurationChanged,
this, &ScatterChartItem::handleSeriesUpdated);
setZValue(ChartPresenter::ScatterSeriesZValue);
setFlags(QGraphicsItem::ItemClipsChildrenToShape | QGraphicsItem::ItemIsSelectable);
handleSeriesUpdated();
m_items.setHandlesChildEvents(false);
}
QRectF ScatterChartItem::boundingRect() const
{
return m_rect;
}
void ScatterChartItem::createPoints(int count)
{
for (int i = 0; i < count; ++i) {
QGraphicsItem *item = 0;
switch (m_markerShape) {
case QScatterSeries::MarkerShapeCircle: {
item = new ChartMarker<QGraphicsEllipseItem>(0, 0, m_markerSize, m_markerSize, this);
break;
}
case QScatterSeries::MarkerShapeRectangle: {
item = new ChartMarker<QGraphicsRectItem>(0, 0, m_markerSize, m_markerSize, this);
break;
}
case QScatterSeries::MarkerShapeRotatedRectangle: {
item = new RotatedRectangleMarker(0, 0, m_markerSize, m_markerSize, this);
break;
}
case QScatterSeries::MarkerShapeTriangle: {
item = new TriangleMarker(0, 0, m_markerSize, m_markerSize, this);
break;
}
case QScatterSeries::MarkerShapeStar: {
item = new StarMarker(0, 0, m_markerSize, m_markerSize, this);
break;
}
case QScatterSeries::MarkerShapePentagon: {
item = new PentagonMarker(0, 0, m_markerSize, m_markerSize, this);
break;
}
default:
qWarning() << "Unsupported marker type";
break;
}
m_items.addToGroup(item);
}
}
void ScatterChartItem::deletePoints(int count)
{
QList<QGraphicsItem *> items = m_items.childItems();
for (int i = 0; i < count; ++i) {
QGraphicsItem *item = items.takeLast();
m_markerMap.remove(item);
delete(item);
}
}
void ScatterChartItem::resizeMarker(QGraphicsItem *marker, const int size)
{
switch (m_markerShape) {
case QScatterSeries::MarkerShapeCircle: {
QGraphicsEllipseItem *item = static_cast<QGraphicsEllipseItem *>(marker);
item->setRect(0, 0, size, size);
break;
}
case QScatterSeries::MarkerShapeRectangle: {
QGraphicsRectItem *item = static_cast<QGraphicsRectItem *>(marker);
item->setRect(0, 0, size, size);
break;
}
case QScatterSeries::MarkerShapeRotatedRectangle: {
QGraphicsPolygonItem *item = static_cast<QGraphicsPolygonItem *>(marker);
item->setPolygon(RotatedRectangleMarker::polygon(0, 0, size, size));
break;
}
case QScatterSeries::MarkerShapeTriangle: {
QGraphicsPolygonItem *item = static_cast<QGraphicsPolygonItem *>(marker);
item->setPolygon(TriangleMarker::polygon(0, 0, size, size));
break;
}
case QScatterSeries::MarkerShapeStar: {
QGraphicsPolygonItem *item = static_cast<QGraphicsPolygonItem *>(marker);
item->setPolygon(StarMarker::polygon(0, 0, size, size));
break;
}
case QScatterSeries::MarkerShapePentagon: {
QGraphicsPolygonItem *item = static_cast<QGraphicsPolygonItem *>(marker);
item->setPolygon(PentagonMarker::polygon(0, 0, size, size));
break;
}
default:
qWarning() << "Unsupported marker type";
break;
}
}
void ScatterChartItem::markerSelected(QGraphicsItem *marker)
{
emit XYChart::clicked(m_markerMap[marker]);
}
void ScatterChartItem::markerHovered(QGraphicsItem *marker, bool state)
{
emit XYChart::hovered(m_markerMap[marker], state);
}
void ScatterChartItem::markerPressed(QGraphicsItem *marker)
{
emit XYChart::pressed(m_markerMap[marker]);
}
void ScatterChartItem::markerReleased(QGraphicsItem *marker)
{
emit XYChart::released(m_markerMap[marker]);
}
void ScatterChartItem::markerDoubleClicked(QGraphicsItem *marker)
{
emit XYChart::doubleClicked(m_markerMap[marker]);
}
void ScatterChartItem::updateGeometry()
{
if (m_series->useOpenGL()) {
if (m_items.childItems().size())
deletePoints(m_items.childItems().size());
if (!m_rect.isEmpty()) {
prepareGeometryChange();
// Changed signal seems to trigger even with empty region
m_rect = QRectF();
}
update();
return;
}
const QList<QPointF> &points = geometryPoints();
if (points.size() == 0) {
deletePoints(m_items.childItems().size());
return;
}
int diff = m_items.childItems().size() - points.size();
if (diff > 0)
deletePoints(diff);
else if (diff < 0)
createPoints(-diff);
if (diff != 0)
handleSeriesUpdated();
QList<QGraphicsItem *> items = m_items.childItems();
QRectF clipRect(QPointF(0,0),domain()->size());
// Only zoom in if the clipRect fits inside int limits. QWidget::update() uses
// a region that has to be compatible with QRect.
if (clipRect.height() <= INT_MAX
&& clipRect.width() <= INT_MAX) {
const QList<bool> offGridStatus = offGridStatusVector();
const int seriesLastIndex = m_series->count() - 1;
for (int i = 0; i < points.size(); i++) {
QAbstractGraphicsShapeItem *item =
static_cast<QAbstractGraphicsShapeItem *>(items.at(i));
const QPointF &point = points.at(i);
if (m_pointsConfiguration.contains(i)) {
const auto &conf = m_pointsConfiguration[i];
if (conf.contains(QXYSeries::PointConfiguration::Size))
resizeMarker(
item,
m_pointsConfiguration[i][QXYSeries::PointConfiguration::Size].toReal());
}
const QRectF &rect = item->boundingRect();
// During remove animation series may have different number of points,
// so ensure we don't go over the index. Animation handling itself ensures that
// if there is actually no points in the series, then it won't generate a fake point,
// so we can be assured there is always at least one point in m_series here.
// Note that marker map values can be technically incorrect during the animation,
// if it was caused by an insert, but this shouldn't be a problem as the points are
// fake anyway. After remove animation stops, geometry is updated to correct one.
m_markerMap[item] = m_series->at(qMin(seriesLastIndex, i));
QPointF position;
position.setX(point.x() - rect.width() / 2);
position.setY(point.y() - rect.height() / 2);
item->setPos(position);
if (!m_visible || offGridStatus.at(i)) {
item->setVisible(false);
} else {
bool drawPoint = m_pointsVisible;
if (m_pointsConfiguration.contains(i)) {
const auto &conf = m_pointsConfiguration[i];
if (conf.contains(QXYSeries::PointConfiguration::Visibility)) {
drawPoint
= m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility]
.toBool();
}
if (drawPoint && conf.contains(QXYSeries::PointConfiguration::Color)) {
item->setBrush(
m_pointsConfiguration[i][QXYSeries::PointConfiguration::Color]
.value<QColor>());
}
}
if (m_series->isPointSelected(i)) {
drawPoint = m_series->selectedLightMarker().isNull();
if (drawPoint && m_selectedColor.isValid())
item->setBrush(m_selectedColor);
}
item->setVisible(drawPoint);
}
}
prepareGeometryChange();
m_rect = clipRect;
}
}
void ScatterChartItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
QPointF matchedP = matchForLightMarker(event->pos());
if (!qIsNaN(matchedP.x())) {
emit XYChart::pressed(matchedP);
m_lastMousePos = event->pos();
m_mousePressed = true;
} else {
event->ignore();
}
QGraphicsItem::mousePressEvent(event);
}
void ScatterChartItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
{
QPointF matchedP = matchForLightMarker(event->pos());
if (!qIsNaN(matchedP.x())) {
if (matchedP != m_lastHoveredPoint) {
if (!qIsNaN(m_lastHoveredPoint.x()))
emit XYChart::hovered(m_lastHoveredPoint, false);
m_lastHoveredPoint = matchedP;
emit XYChart::hovered(matchedP, true);
}
} else if (!qIsNaN(m_lastHoveredPoint.x())) {
emit XYChart::hovered(m_lastHoveredPoint, false);
m_lastHoveredPoint = QPointF(qQNaN(), qQNaN());
}
QGraphicsItem::hoverMoveEvent(event);
}
void ScatterChartItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
QPointF result;
QPointF matchedP = matchForLightMarker(m_lastMousePos);
if (!qIsNaN(matchedP.x()) && m_mousePressed) {
result = matchedP;
emit XYChart::released(result);
emit XYChart::clicked(result);
}
m_mousePressed = false;
QGraphicsItem::mouseReleaseEvent(event);
}
void ScatterChartItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
QPointF matchedP = matchForLightMarker(event->pos());
if (!qIsNaN(matchedP.x()))
emit XYChart::doubleClicked(matchedP);
else
emit XYChart::doubleClicked(domain()->calculateDomainPoint(m_lastMousePos));
QGraphicsItem::mouseDoubleClickEvent(event);
}
void ScatterChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
Q_UNUSED(option);
Q_UNUSED(widget);
if (m_series->useOpenGL())
return;
QRectF clipRect = QRectF(QPointF(0, 0), domain()->size());
painter->save();
painter->setClipRect(clipRect);
// Draw markers if a marker or marker for selected points only has been
// set (set to QImage() to disable)
if (!m_series->lightMarker().isNull() || !m_series->selectedLightMarker().isNull()) {
const QImage &marker = m_series->lightMarker();
const QImage &selectedMarker = m_series->selectedLightMarker();
qreal markerHalfSize = m_markerSize / 2.0;
for (int i = 0; i < m_points.size(); ++i) {
// Documentation of light markers says that points visibility and
// light markers are independent features. Therefore m_pointsVisible
// is not used here as light markers are drawn if lightMarker is not null.
// However points visibility configuration can be still used here.
bool drawPoint = !m_series->lightMarker().isNull();
if (m_pointsConfiguration.contains(i)) {
const auto &conf = m_pointsConfiguration[i];
if (conf.contains(QXYSeries::PointConfiguration::Visibility)) {
drawPoint = m_pointsConfiguration[i][QXYSeries::PointConfiguration::Visibility]
.toBool();
}
}
bool drawSelectedPoint = false;
if (m_series->isPointSelected(i)) {
drawPoint = true;
drawSelectedPoint = !selectedMarker.isNull();
}
if (drawPoint) {
const QRectF rect(m_points[i].x() - markerHalfSize,
m_points[i].y() - markerHalfSize,
m_markerSize, m_markerSize);
painter->drawImage(rect, drawSelectedPoint ? selectedMarker : marker);
}
}
}
if (m_series->bestFitLineVisible())
m_series->d_func()->drawBestFitLine(painter, clipRect);
m_series->d_func()->drawPointLabels(painter, m_points, m_series->markerSize() / 2 + m_series->pen().width());
painter->restore();
}
void ScatterChartItem::setPen(const QPen &pen)
{
QPen penToUse(pen);
if (!m_series->lightMarker().isNull())
penToUse.setColor(Qt::transparent);
const auto &items = m_items.childItems();
for (auto item : items)
static_cast<QAbstractGraphicsShapeItem*>(item)->setPen(penToUse);
}
void ScatterChartItem::setBrush(const QBrush &brush)
{
const auto &items = m_items.childItems();
for (auto item : items) {
if (m_series->lightMarker().isNull()) {
if (m_markerMap.contains(item)) {
auto index = m_series->points().indexOf(m_markerMap[item]);
if (m_selectedPoints.contains(index) && m_selectedColor.isValid()) {
static_cast<QAbstractGraphicsShapeItem *>(item)->setBrush(m_selectedColor);
} else {
bool useBrush = true;
if (m_pointsConfiguration.contains(index)) {
const auto &conf = m_pointsConfiguration[index];
if (conf.contains(QXYSeries::PointConfiguration::Color))
useBrush = false;
}
if (useBrush)
static_cast<QAbstractGraphicsShapeItem *>(item)->setBrush(brush);
}
} else {
static_cast<QAbstractGraphicsShapeItem *>(item)->setBrush(brush);
}
} else {
QBrush brushToUse;
brushToUse.setColor(Qt::transparent);
static_cast<QAbstractGraphicsShapeItem *>(item)->setBrush(brushToUse);
}
}
}
void ScatterChartItem::handleSeriesUpdated()
{
if (m_series->useOpenGL()) {
if ((m_series->isVisible() != m_visible)) {
m_visible = m_series->isVisible();
refreshGlChart();
}
return;
}
int count = m_items.childItems().size();
if (count == 0)
return;
const bool pointsConfigurationDirty =
m_series->pointsConfiguration() != m_pointsConfiguration;
bool recreate = m_visible != m_series->isVisible()
|| m_pointsVisible != m_series->pointsVisible()
|| m_markerSize != m_series->markerSize()
|| m_markerShape != m_series->markerShape()
|| m_selectedColor != m_series->selectedColor()
|| m_selectedPoints != m_series->selectedPoints()
|| pointsConfigurationDirty;
m_visible = m_series->isVisible();
m_markerSize = m_series->markerSize();
m_markerShape = m_series->markerShape();
setVisible(m_visible);
setOpacity(m_series->opacity());
m_pointsVisible = m_series->pointsVisible();
m_pointLabelsFormat = m_series->pointLabelsFormat();
m_pointLabelsVisible = m_series->pointLabelsVisible();
m_pointLabelsFont = m_series->pointLabelsFont();
m_pointLabelsColor = m_series->pointLabelsColor();
m_selectedColor = m_series->selectedColor();
m_selectedPoints = m_series->selectedPoints();
m_pointsConfiguration = m_series->pointsConfiguration();
bool labelClippingChanged = m_pointLabelsClipping != m_series->pointLabelsClipping();
m_pointLabelsClipping = m_series->pointLabelsClipping();
if (recreate) {
deletePoints(count);
createPoints(count);
// Updating geometry is now safe, because it won't call handleSeriesUpdated unless it
// creates/deletes points
updateGeometry();
}
// Only accept hover events when light/selection markers are in use so we don't unnecessarily
// eat the events in the regular case
setAcceptHoverEvents(!(m_series->lightMarker().isNull()
&& (m_series->selectedLightMarker().isNull()
|| m_series->selectedPoints().isEmpty())));
setPen(m_series->pen());
setBrush(m_series->brush());
// Update whole chart in case label clipping changed as labels can be outside series area
if (labelClippingChanged)
m_series->chart()->update();
else
update();
}
void ScatterChartItem::handleMarkerMouseReleaseEvent(QGraphicsItem *item)
{
markerReleased(item);
if (mousePressed())
markerSelected(item);
setMousePressed(false);
}
template<class T>
ChartMarker<T>::ChartMarker(qreal x, qreal y, qreal w, qreal h, ScatterChartItem *parent)
: T(x, y, w, h, parent)
, m_parent(parent)
{
T::setAcceptHoverEvents(true);
T::setFlag(QGraphicsItem::ItemIsSelectable);
}
template<class T>
ChartMarker<T>::ChartMarker(ScatterChartItem *parent)
: T(parent)
, m_parent(parent)
{
T::setAcceptHoverEvents(true);
T::setFlag(QGraphicsItem::ItemIsSelectable);
}
template<class T>
void ChartMarker<T>::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
T::mousePressEvent(event);
m_parent->markerPressed(this);
m_parent->setMousePressed();
}
template<class T>
void ChartMarker<T>::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
T::hoverEnterEvent(event);
m_parent->markerHovered(this, true);
}
template<class T>
void ChartMarker<T>::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
T::hoverLeaveEvent(event);
m_parent->markerHovered(this, false);
}
template<class T>
void ChartMarker<T>::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
T::mouseReleaseEvent(event);
m_parent->handleMarkerMouseReleaseEvent(this);
}
template<class T>
void ChartMarker<T>::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
T::mouseDoubleClickEvent(event);
m_parent->markerDoubleClicked(this);
}
RotatedRectangleMarker::RotatedRectangleMarker(qreal x, qreal y, qreal w, qreal h,
ScatterChartItem *parent)
: ChartMarker<QGraphicsPolygonItem>(parent)
{
setPolygon(RotatedRectangleMarker::polygon(x, y, w, h));
}
QPolygonF RotatedRectangleMarker::polygon(qreal x, qreal y, qreal w, qreal h)
{
QPolygonF rotatedRectPoly;
rotatedRectPoly << QPointF(x, y + h / 2.0);
rotatedRectPoly << QPointF(x + w / 2.0, y + h);
rotatedRectPoly << QPointF(x + w, y + h / 2.0);
rotatedRectPoly << QPointF(x + w / 2.0, y);
return rotatedRectPoly;
}
TriangleMarker::TriangleMarker(qreal x, qreal y, qreal w, qreal h, ScatterChartItem *parent)
: ChartMarker<QGraphicsPolygonItem>(parent)
{
setPolygon(TriangleMarker::polygon(x, y, w, h));
}
QPolygonF TriangleMarker::polygon(qreal x, qreal y, qreal w, qreal h)
{
QPolygonF trianglePoly;
trianglePoly << QPointF(x, y + h);
trianglePoly << QPointF(x + w, y + h);
trianglePoly << QPointF(x + w / 2.0, y);
return trianglePoly;
}
StarMarker::StarMarker(qreal x, qreal y, qreal w, qreal h, ScatterChartItem *parent)
: ChartMarker<QGraphicsPolygonItem>(parent)
{
setPolygon(StarMarker::polygon(x, y, w, h));
}
QPolygonF StarMarker::polygon(qreal x, qreal y, qreal w, qreal h)
{
QPolygonF starPoly;
constexpr qreal step = M_PI / STAR_SPIKES;
const qreal radius = w / 2.0;
const qreal innerRadius = radius * 0.5;
const QPointF ¢er = QPointF(x + w / 2.0, y + h / 2.0);
qreal rot = M_PI / 2 * 3;
for (int i = 0; i < STAR_SPIKES; ++i) {
starPoly << QPointF(center.x() + std::cos(rot) * radius,
center.y() + std::sin(rot) * radius);
rot += step;
starPoly << QPointF(center.x() + std::cos(rot) * innerRadius,
center.y() + std::sin(rot) * innerRadius);
rot += step;
}
return starPoly;
}
PentagonMarker::PentagonMarker(qreal x, qreal y, qreal w, qreal h, ScatterChartItem *parent)
: ChartMarker<QGraphicsPolygonItem>(parent)
{
setPolygon(PentagonMarker::polygon(x, y, w, h));
}
QPolygonF PentagonMarker::polygon(qreal x, qreal y, qreal w, qreal h)
{
QPolygonF pentagonPoly;
constexpr qreal step = 2 * M_PI / 5;
const qreal radius = w / 2.0;
const QPointF ¢er = QPointF(x + w / 2.0, y + h / 2.0);
qreal rot = M_PI / 2 * 3;
for (int i = 0; i < 5; ++i) {
pentagonPoly << QPointF(center.x() + std::cos(rot) * radius,
center.y() + std::sin(rot) * radius);
rot += step;
}
return pentagonPoly;
}
QT_END_NAMESPACE
#include "moc_scatterchartitem_p.cpp"