From f494279b6366b06e3eeeb4f8c006ce76b08f10d7 2013-04-17 07:14:43 From: Miikka Heikkinen Date: 2013-04-17 07:14:43 Subject: [PATCH] Add Polar chart support This commit also heavily refactors things as polar chart needs separate implementation of various classes that previously only needed one, such as ChartAxis and ChartLayout. Task-number: QTRD-1757 Change-Id: I3d3db23920314987ceef3ae92879960b833b7136 Reviewed-by: Miikka Heikkinen --- diff --git a/demos/chartinteractions/chart.cpp b/demos/chartinteractions/chart.cpp index 17ee90f..6043c9b 100644 --- a/demos/chartinteractions/chart.cpp +++ b/demos/chartinteractions/chart.cpp @@ -24,7 +24,7 @@ #include Chart::Chart(QGraphicsItem *parent, Qt::WindowFlags wFlags, QLineSeries *series) - : QChart(parent, wFlags), m_series(series) + : QChart(QChart::ChartTypeCartesian, parent, wFlags), m_series(series) { m_clicked = false; } diff --git a/demos/demos.pro b/demos/demos.pro index 43f6c93..76a53c5 100644 --- a/demos/demos.pro +++ b/demos/demos.pro @@ -15,7 +15,8 @@ SUBDIRS += piechartcustomization \ chartinteractions \ qmlaxes \ qmlcustomlegend \ - callout + callout \ + qmlpolarchart contains(QT_CONFIG, opengl) { SUBDIRS += chartthemes \ diff --git a/demos/dynamicspline/chart.cpp b/demos/dynamicspline/chart.cpp index c7e111b..2845777 100644 --- a/demos/dynamicspline/chart.cpp +++ b/demos/dynamicspline/chart.cpp @@ -26,7 +26,7 @@ #include Chart::Chart(QGraphicsItem *parent, Qt::WindowFlags wFlags): - QChart(parent, wFlags), + QChart(QChart::ChartTypeCartesian, parent, wFlags), m_series(0), m_axis(new QValueAxis), m_step(0), diff --git a/demos/qmlpolarchart/main.cpp b/demos/qmlpolarchart/main.cpp new file mode 100644 index 0000000..f5f00cc --- /dev/null +++ b/demos/qmlpolarchart/main.cpp @@ -0,0 +1,36 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include "qmlapplicationviewer.h" + +Q_DECL_EXPORT int main(int argc, char *argv[]) +{ + QScopedPointer app(createApplication(argc, argv)); + QScopedPointer viewer(QmlApplicationViewer::create()); + + // // viewer->setOrientation(QmlApplicationViewer::ScreenOrientationAuto); + viewer->setSource(QUrl("qrc:/qml/qmlpolarchart/loader.qml")); + viewer->setRenderHint(QPainter::Antialiasing, true); + viewer->showExpanded(); + + return app->exec(); +} diff --git a/demos/qmlpolarchart/qml/qmlpolarchart/View1.qml b/demos/qmlpolarchart/qml/qmlpolarchart/View1.qml new file mode 100644 index 0000000..c3b849b --- /dev/null +++ b/demos/qmlpolarchart/qml/qmlpolarchart/View1.qml @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 1.0 +import QtCommercial.Chart 1.3 + +Rectangle { + anchors.fill: parent + //![1] + PolarChartView { + title: "Two Series, Common Axes" + anchors.fill: parent + legend.visible: false + + ValueAxis { + id: axisAngular + min: 0 + max: 20 + tickCount: 9 + } + + ValueAxis { + id: axisRadial + min: -0.5 + max: 1.5 + } + + SplineSeries { + id: series1 + axisAngular: axisAngular + axisRadial: axisRadial + pointsVisible: true + } + + ScatterSeries { + id: series2 + axisAngular: axisAngular + axisRadial: axisRadial + markerSize: 10 + } + } + + // Add data dynamically to the series + Component.onCompleted: { + for (var i = 0; i <= 20; i++) { + series1.append(i, Math.random()); + series2.append(i, Math.random()); + } + } + //![1] +} diff --git a/demos/qmlpolarchart/qml/qmlpolarchart/View2.qml b/demos/qmlpolarchart/qml/qmlpolarchart/View2.qml new file mode 100644 index 0000000..ff8ac32 --- /dev/null +++ b/demos/qmlpolarchart/qml/qmlpolarchart/View2.qml @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 1.0 +import QtCommercial.Chart 1.3 + +Rectangle { + anchors.fill: parent + + //![1] + PolarChartView { + title: "Historical Area Series" + anchors.fill: parent + legend.visible: false + + DateTimeAxis { + id: axis1 + format: "yyyy MMM" + tickCount: 13 + } + ValueAxis { + id: axis2 + } + LineSeries { + id: lowerLine + axisAngular: axis1 + axisRadial: axis2 + + // Please note that month in JavaScript months are zero based, so 2 means March + XYPoint { x: toMsecsSinceEpoch(new Date(1950, 0, 1)); y: 15 } + XYPoint { x: toMsecsSinceEpoch(new Date(1962, 4, 1)); y: 35 } + XYPoint { x: toMsecsSinceEpoch(new Date(1970, 0, 1)); y: 50 } + XYPoint { x: toMsecsSinceEpoch(new Date(1978, 2, 1)); y: 75 } + XYPoint { x: toMsecsSinceEpoch(new Date(1987, 11, 1)); y: 102 } + XYPoint { x: toMsecsSinceEpoch(new Date(1992, 1, 1)); y: 132 } + XYPoint { x: toMsecsSinceEpoch(new Date(1998, 7, 1)); y: 100 } + XYPoint { x: toMsecsSinceEpoch(new Date(2002, 4, 1)); y: 120 } + XYPoint { x: toMsecsSinceEpoch(new Date(2012, 8, 1)); y: 140 } + XYPoint { x: toMsecsSinceEpoch(new Date(2013, 5, 1)); y: 150 } + } + LineSeries { + id: upperLine + axisAngular: axis1 + axisRadial: axis2 + + // Please note that month in JavaScript months are zero based, so 2 means March + XYPoint { x: toMsecsSinceEpoch(new Date(1950, 0, 1)); y: 30 } + XYPoint { x: toMsecsSinceEpoch(new Date(1962, 4, 1)); y: 55 } + XYPoint { x: toMsecsSinceEpoch(new Date(1970, 0, 1)); y: 80 } + XYPoint { x: toMsecsSinceEpoch(new Date(1978, 2, 1)); y: 105 } + XYPoint { x: toMsecsSinceEpoch(new Date(1987, 11, 1)); y: 125 } + XYPoint { x: toMsecsSinceEpoch(new Date(1992, 1, 1)); y: 160 } + XYPoint { x: toMsecsSinceEpoch(new Date(1998, 7, 1)); y: 140 } + XYPoint { x: toMsecsSinceEpoch(new Date(2002, 4, 1)); y: 140 } + XYPoint { x: toMsecsSinceEpoch(new Date(2012, 8, 1)); y: 170 } + XYPoint { x: toMsecsSinceEpoch(new Date(2013, 5, 1)); y: 200 } + } + AreaSeries { + axisAngular: axis1 + axisRadial: axis2 + lowerSeries: lowerLine + upperSeries: upperLine + } + } + // DateTimeAxis is based on QDateTimes so we must convert our JavaScript dates to + // milliseconds since epoch to make them match the DateTimeAxis values + function toMsecsSinceEpoch(date) { + var msecs = date.getTime(); + return msecs; + } + //![1] +} diff --git a/demos/qmlpolarchart/qml/qmlpolarchart/View3.qml b/demos/qmlpolarchart/qml/qmlpolarchart/View3.qml new file mode 100644 index 0000000..27c1210 --- /dev/null +++ b/demos/qmlpolarchart/qml/qmlpolarchart/View3.qml @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 1.0 +import QtCommercial.Chart 1.3 + +Rectangle { + anchors.fill: parent + + //![1] + PolarChartView { + title: "Numerical Data for Dummies" + anchors.fill: parent + legend.visible: false + + LineSeries { + axisRadial: CategoryAxis { + min: 0 + max: 30 + CategoryRange { + label: "critical" + endValue: 2 + } + CategoryRange { + label: "low" + endValue: 4 + } + CategoryRange { + label: "normal" + endValue: 7 + } + CategoryRange { + label: "high" + endValue: 15 + } + CategoryRange { + label: "extremely high" + endValue: 30 + } + } + + axisAngular: ValueAxis { + tickCount: 13 + } + + XYPoint { x: 0; y: 4.3 } + XYPoint { x: 1; y: 4.1 } + XYPoint { x: 2; y: 4.7 } + XYPoint { x: 3; y: 3.9 } + XYPoint { x: 4; y: 5.2 } + XYPoint { x: 5; y: 5.3 } + XYPoint { x: 6; y: 6.1 } + XYPoint { x: 7; y: 7.7 } + XYPoint { x: 8; y: 12.9 } + XYPoint { x: 9; y: 19.2 } + } + } + //![1] +} diff --git a/demos/qmlpolarchart/qml/qmlpolarchart/loader.qml b/demos/qmlpolarchart/qml/qmlpolarchart/loader.qml new file mode 100644 index 0000000..20627ab --- /dev/null +++ b/demos/qmlpolarchart/qml/qmlpolarchart/loader.qml @@ -0,0 +1,37 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 1.0 + +Item { + id: container + width: 400 + height: 300 + Component.onCompleted: { + var co = Qt.createComponent("main.qml") + if (co.status == Component.Ready) { + var o = co.createObject(container) + } else { + console.log(co.errorString()) + console.log("QtCommercial.Chart 1.3 not available") + console.log("Please use correct QML_IMPORT_PATH export") + } + } +} diff --git a/demos/qmlpolarchart/qml/qmlpolarchart/main.qml b/demos/qmlpolarchart/qml/qmlpolarchart/main.qml new file mode 100644 index 0000000..e6ce656 --- /dev/null +++ b/demos/qmlpolarchart/qml/qmlpolarchart/main.qml @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 1.0 + +Rectangle { + width: parent.width + height: parent.height + property int viewNumber: 1 + property int viewCount: 3 + + Loader { + id: loader + anchors.fill: parent + source: "View" + viewNumber + ".qml"; + } + + Rectangle { + id: infoText + anchors.centerIn: parent + width: parent.width + height: 40 + color: "black" + Text { + color: "white" + anchors.centerIn: parent + text: "Use left and right arrow keys to navigate" + } + + Behavior on opacity { + NumberAnimation { duration: 400 } + } + } + + MouseArea { + focus: true + anchors.fill: parent + onClicked: { + if (infoText.opacity > 0) { + infoText.opacity = 0.0; + } else { + nextView(); + } + } + Keys.onPressed: { + if (infoText.opacity > 0) { + infoText.opacity = 0.0; + } else { + if (event.key == Qt.Key_Left) { + previousView(); + } else { + nextView(); + } + } + } + } + + function nextView() { + var i = viewNumber + 1; + if (i > viewCount) + viewNumber = 1; + else + viewNumber = i; + } + + function previousView() { + var i = viewNumber - 1; + if (i <= 0) + viewNumber = viewCount; + else + viewNumber = i; + } +} diff --git a/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.cpp b/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.cpp new file mode 100644 index 0000000..0f12265 --- /dev/null +++ b/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.cpp @@ -0,0 +1,177 @@ +// checksum 0xbd34 version 0x80016 +/* + This file was generated by the Qt Quick Application wizard of Qt Creator. + QmlApplicationViewer is a convenience class containing mobile device specific + code such as screen orientation handling. Also QML paths and debugging are + handled here. + It is recommended not to modify this file, since newer versions of Qt Creator + may offer an updated version of it. +*/ + +#include "qmlapplicationviewer.h" + +#include +#include +#include +#include +#include +#include + +#include // MEEGO_EDITION_HARMATTAN + +#ifdef HARMATTAN_BOOSTER +#include +#endif + +#if defined(QMLJSDEBUGGER) && QT_VERSION < 0x040800 + +#include + +#if !defined(NO_JSDEBUGGER) +#include +#endif +#if !defined(NO_QMLOBSERVER) +#include +#endif + +// Enable debugging before any QDeclarativeEngine is created +struct QmlJsDebuggingEnabler +{ + QmlJsDebuggingEnabler() + { + QDeclarativeDebugHelper::enableDebugging(); + } +}; + +// Execute code in constructor before first QDeclarativeEngine is instantiated +static QmlJsDebuggingEnabler enableDebuggingHelper; + +#endif // QMLJSDEBUGGER + +class QmlApplicationViewerPrivate +{ + QString mainQmlFile; + friend class QmlApplicationViewer; + static QString adjustPath(const QString &path); +}; + +QString QmlApplicationViewerPrivate::adjustPath(const QString &path) +{ +#ifdef Q_OS_MAC + if (!QDir::isAbsolutePath(path)) + return QString::fromLatin1("%1/../Resources/%2") + .arg(QCoreApplication::applicationDirPath(), path); +#elif defined(Q_OS_QNX) + if (!QDir::isAbsolutePath(path)) + return QString::fromLatin1("app/native/%1").arg(path); +#elif !defined(Q_OS_ANDROID) + QString pathInInstallDir = + QString::fromLatin1("%1/../%2").arg(QCoreApplication::applicationDirPath(), path); + if (QFileInfo(pathInInstallDir).exists()) + return pathInInstallDir; + pathInInstallDir = + QString::fromLatin1("%1/%2").arg(QCoreApplication::applicationDirPath(), path); + if (QFileInfo(pathInInstallDir).exists()) + return pathInInstallDir; +#endif + return path; +} + +QmlApplicationViewer::QmlApplicationViewer(QWidget *parent) + : QDeclarativeView(parent) + , d(new QmlApplicationViewerPrivate()) +{ + connect(engine(), SIGNAL(quit()), SLOT(close())); + setResizeMode(QDeclarativeView::SizeRootObjectToView); + + // Qt versions prior to 4.8.0 don't have QML/JS debugging services built in +#if defined(QMLJSDEBUGGER) && QT_VERSION < 0x040800 +#if !defined(NO_JSDEBUGGER) + new QmlJSDebugger::JSDebuggerAgent(engine()); +#endif +#if !defined(NO_QMLOBSERVER) + new QmlJSDebugger::QDeclarativeViewObserver(this, this); +#endif +#endif +} + +QmlApplicationViewer::~QmlApplicationViewer() +{ + delete d; +} + +QmlApplicationViewer *QmlApplicationViewer::create() +{ + return new QmlApplicationViewer(); +} + +void QmlApplicationViewer::setMainQmlFile(const QString &file) +{ + d->mainQmlFile = QmlApplicationViewerPrivate::adjustPath(file); +#ifdef Q_OS_ANDROID + setSource(QUrl(QLatin1String("assets:/")+d->mainQmlFile)); +#else + setSource(QUrl::fromLocalFile(d->mainQmlFile)); +#endif +} + +void QmlApplicationViewer::addImportPath(const QString &path) +{ + engine()->addImportPath(QmlApplicationViewerPrivate::adjustPath(path)); +} + +void QmlApplicationViewer::setOrientation(ScreenOrientation orientation) +{ +#if QT_VERSION < 0x050000 + Qt::WidgetAttribute attribute; + switch (orientation) { +#if QT_VERSION < 0x040702 + // Qt < 4.7.2 does not yet have the Qt::WA_*Orientation attributes + case ScreenOrientationLockPortrait: + attribute = static_cast(128); + break; + case ScreenOrientationLockLandscape: + attribute = static_cast(129); + break; + default: + case ScreenOrientationAuto: + attribute = static_cast(130); + break; +#else // QT_VERSION < 0x040702 + case ScreenOrientationLockPortrait: + attribute = Qt::WA_LockPortraitOrientation; + break; + case ScreenOrientationLockLandscape: + attribute = Qt::WA_LockLandscapeOrientation; + break; + default: + case ScreenOrientationAuto: + attribute = Qt::WA_AutoOrientation; + break; +#endif // QT_VERSION < 0x040702 + }; + setAttribute(attribute, true); +#else // QT_VERSION < 0x050000 + Q_UNUSED(orientation) +#endif // QT_VERSION < 0x050000 +} + +void QmlApplicationViewer::showExpanded() +{ +#if defined(MEEGO_EDITION_HARMATTAN) || defined(Q_WS_SIMULATOR) + showFullScreen(); +#elif defined(Q_WS_MAEMO_5) || defined(Q_OS_QNX) + showMaximized(); +#else + show(); +#endif +} + +QApplication *createApplication(int &argc, char **argv) +{ +#ifdef HARMATTAN_BOOSTER + return MDeclarativeCache::qApplication(argc, argv); +#else + return new QApplication(argc, argv); +#endif +} diff --git a/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.h b/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.h new file mode 100644 index 0000000..fba2d52 --- /dev/null +++ b/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.h @@ -0,0 +1,46 @@ +// checksum 0xc67a version 0x80016 +/* + This file was generated by the Qt Quick Application wizard of Qt Creator. + QmlApplicationViewer is a convenience class containing mobile device specific + code such as screen orientation handling. Also QML paths and debugging are + handled here. + It is recommended not to modify this file, since newer versions of Qt Creator + may offer an updated version of it. +*/ + +#ifndef QMLAPPLICATIONVIEWER_H +#define QMLAPPLICATIONVIEWER_H + +#include + +class QmlApplicationViewer : public QDeclarativeView +{ + Q_OBJECT + +public: + enum ScreenOrientation { + ScreenOrientationLockPortrait, + ScreenOrientationLockLandscape, + ScreenOrientationAuto + }; + + explicit QmlApplicationViewer(QWidget *parent = 0); + virtual ~QmlApplicationViewer(); + + static QmlApplicationViewer *create(); + + void setMainQmlFile(const QString &file); + void addImportPath(const QString &path); + + // Note that this will only have an effect on Fremantle. + void setOrientation(ScreenOrientation orientation); + + void showExpanded(); + +private: + class QmlApplicationViewerPrivate *d; +}; + +QApplication *createApplication(int &argc, char **argv); + +#endif // QMLAPPLICATIONVIEWER_H diff --git a/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.pri b/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.pri new file mode 100644 index 0000000..567c6dc --- /dev/null +++ b/demos/qmlpolarchart/qmlapplicationviewer/qmlapplicationviewer.pri @@ -0,0 +1,13 @@ +QT += declarative + +SOURCES += $$PWD/qmlapplicationviewer.cpp +HEADERS += $$PWD/qmlapplicationviewer.h +INCLUDEPATH += $$PWD + +# Include JS debugger library if QMLJSDEBUGGER_PATH is set +!isEmpty(QMLJSDEBUGGER_PATH) { + include($$QMLJSDEBUGGER_PATH/qmljsdebugger-lib.pri) +} else { + DEFINES -= QMLJSDEBUGGER +} + diff --git a/demos/qmlpolarchart/qmlpolarchart.pro b/demos/qmlpolarchart/qmlpolarchart.pro new file mode 100644 index 0000000..f661312 --- /dev/null +++ b/demos/qmlpolarchart/qmlpolarchart.pro @@ -0,0 +1,9 @@ +!include( ../demos.pri ) { + error( "Couldn't find the demos.pri file!" ) +} + +RESOURCES += resources.qrc +SOURCES += main.cpp +OTHER_FILES += qml/qmlpolarchart/* + +include(qmlapplicationviewer/qmlapplicationviewer.pri) diff --git a/demos/qmlpolarchart/resources.qrc b/demos/qmlpolarchart/resources.qrc new file mode 100644 index 0000000..854acbe --- /dev/null +++ b/demos/qmlpolarchart/resources.qrc @@ -0,0 +1,9 @@ + + + qml/qmlpolarchart/loader.qml + qml/qmlpolarchart/main.qml + qml/qmlpolarchart/View1.qml + qml/qmlpolarchart/View2.qml + qml/qmlpolarchart/View3.qml + + diff --git a/doc/images/demos_qmlpolarchart1.png b/doc/images/demos_qmlpolarchart1.png new file mode 100644 index 0000000000000000000000000000000000000000..58be80e6d2cb1e7a1629a7c749c48f3addcd1f49 GIT binary patch literal 41088 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 S$G$+|Xa7Fyy-!Kh$o~LqX!P{} literal 0 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 L8tD|CatQq&b;|pP literal 0 Hc$@y%1PJaSxVzsb&-;Dr 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�lS)#_}@dz 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
  • QChart
  • +
  • QPolarChart
  • QChartView
  • QAbstractAxis
  • QValueAxis
  • diff --git a/doc/src/demos-qmlpolarchart.qdoc b/doc/src/demos-qmlpolarchart.qdoc new file mode 100644 index 0000000..04c7037 --- /dev/null +++ b/doc/src/demos-qmlpolarchart.qdoc @@ -0,0 +1,33 @@ +/*! + \example demos/qmlpolarchart + \title Qml Polar Chart + \subtitle + + This is a demonstration of how to use polar chart in your QML application. + + \table + \row + \o \br + We begin with a chart that has a spline series and a scatter series with random + data. Both series use the same axes. + \br + \br + \snippet ../demos/qmlpolarchart/qml/qmlpolarchart/View1.qml 1 + \o \inlineimage demos_qmlpolarchart1.png + \row + \o \br + The next example shows a chart with some accurate historical data that makes us to use a DateTimeAxis + and AreaSeries. + \br + \br + \snippet ../demos/qmlpolarchart/qml/qmlpolarchart/View2.qml 1 + \o \inlineimage demos_qmlpolarchart2.png + \row + \o \br + And the final example with a chart that uses a CategoryAxis to make the data easier to understand. + \br + \br + \snippet ../demos/qmlpolarchart/qml/qmlpolarchart/View3.qml 1 + \o \inlineimage demos_qmlpolarchart3.png + \endtable +*/ diff --git a/doc/src/demos.qdoc b/doc/src/demos.qdoc index 573b416..6316617 100644 --- a/doc/src/demos.qdoc +++ b/doc/src/demos.qdoc @@ -8,6 +8,14 @@ + + + + + + + + @@ -15,7 +23,6 @@ - @@ -24,7 +31,6 @@ - @@ -33,7 +39,6 @@ - @@ -42,7 +47,6 @@ - @@ -51,7 +55,6 @@ - @@ -61,14 +64,11 @@ - - + - - + -
    AudioCallout
    AudioCallout
    Chart Themes Dynamic Spline Chart
    Chart Themes Dynamic Spline
    Nested Donuts Chart Pie Chart CustomizationNested Donuts Chart Pie Chart Customization
    Qml Basic Charts Qml AxesQml Basic Charts Qml Axes
    Qml Customizations Qml Custom ModelQml Customizations Qml Custom Model
    Qml F1 Legends Qml OscilloscopeQml F1 Legends Qml Oscilloscope
    Qml Weather Qml Custom LegendQml Custom Legend
    CalloutAudioQml Polar Chart
    CalloutAudioQml Polar Chart
    \endraw diff --git a/doc/src/examples-polarchart.qdoc b/doc/src/examples-polarchart.qdoc new file mode 100644 index 0000000..d9f976d --- /dev/null +++ b/doc/src/examples-polarchart.qdoc @@ -0,0 +1,33 @@ +/*! + \example examples/polarchart + \title Polar chart example + \subtitle + + The example shows how to create simple polar chart with multiple different series. + It also shows how to implement scrolling and zooming of the polar chart as well as + visually demonstrate how polar chart and cartesian chart relate to each other. + + \image examples_polarchart.png + + Creating polar chart is done via QPolarChart instance instead of QChart. + + \snippet ../examples/polarchart/main.cpp 1 + + Axes are created similarly to cartesian charts, but when axes are added to the chart, + you can use polar orientations instead of alignments. + + \snippet ../examples/polarchart/main.cpp 2 + + Zooming and scrolling of the polar chart is logically nearly identical to zooming and scrolling of cartesian chart. + The main difference is that when scrolling along X-axis (angular axis), angle is used instead of number of pixels. + Another difference is that zooming to a rectangle cannot be done. + + \snippet ../examples/polarchart/chartview.cpp 1 + + Same axes and series can be used in both cartesian and polar charts, though not simultaneously. + To switch between chart types, you need to first remove series and axes from the old chart and then add + them to the new chart. If you want to preserve axis ranges, those need to be copied, too. + + \snippet ../examples/polarchart/chartview.cpp 2 + +*/ diff --git a/doc/src/examples.qdoc b/doc/src/examples.qdoc index f0588be..f5c736e 100644 --- a/doc/src/examples.qdoc +++ b/doc/src/examples.qdoc @@ -98,36 +98,38 @@ + Polar chart Scatter chart - Scatter Interactions + Polar chart Scatter chart - Scatter Interactions + Scatter Interactions Spline Chart - Stacked Bar Chart + Scatter Interactions Spline Chart - Stacked Bar Chart + Stacked Bar Chart Stacked Bar Chart Drilldown - Temperature Records + Stacked Bar Chart Stacked Bar Chart Drilldown - Temperature Records + Temperature Records Zoom Line + Temperature Records Zoom Line diff --git a/doc/src/qml.qdoc b/doc/src/qml.qdoc index 9fbb9fe..d7fae7e 100644 --- a/doc/src/qml.qdoc +++ b/doc/src/qml.qdoc @@ -27,6 +27,7 @@
    • ChartView
    • +
    • PolarChartView
    • AbstractAxis
    • ValueAxis
    • CategoryAxis
    • diff --git a/examples/donutbreakdown/donutbreakdownchart.cpp b/examples/donutbreakdown/donutbreakdownchart.cpp index 6509e46..37130f0 100644 --- a/examples/donutbreakdown/donutbreakdownchart.cpp +++ b/examples/donutbreakdown/donutbreakdownchart.cpp @@ -26,7 +26,7 @@ QTCOMMERCIALCHART_USE_NAMESPACE //![1] DonutBreakdownChart::DonutBreakdownChart(QGraphicsItem *parent, Qt::WindowFlags wFlags) - : QChart(parent, wFlags) + : QChart(QChart::ChartTypeCartesian, parent, wFlags) { // create the series for main center pie m_mainSeries = new QPieSeries(); diff --git a/examples/examples.pro b/examples/examples.pro index ab406af..716dce5 100644 --- a/examples/examples.pro +++ b/examples/examples.pro @@ -31,7 +31,8 @@ SUBDIRS += \ donutchart \ multiaxis \ legendmarkers \ - logvalueaxis + logvalueaxis \ + polarchart !linux-arm*: { SUBDIRS += \ diff --git a/examples/piechartdrilldown/drilldownchart.cpp b/examples/piechartdrilldown/drilldownchart.cpp index 96de458..47d83ad 100644 --- a/examples/piechartdrilldown/drilldownchart.cpp +++ b/examples/piechartdrilldown/drilldownchart.cpp @@ -23,7 +23,7 @@ QTCOMMERCIALCHART_USE_NAMESPACE DrilldownChart::DrilldownChart(QGraphicsItem *parent, Qt::WindowFlags wFlags) - : QChart(parent, wFlags), + : QChart(QChart::ChartTypeCartesian, parent, wFlags), m_currentSeries(0) { diff --git a/examples/polarchart/chartview.cpp b/examples/polarchart/chartview.cpp new file mode 100644 index 0000000..fc53a53 --- /dev/null +++ b/examples/polarchart/chartview.cpp @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "chartview.h" +#include +#include +#include +#include + +QTCOMMERCIALCHART_USE_NAMESPACE + +ChartView::ChartView(QWidget *parent) + : QChartView(parent) +{ +} + +//![1] +void ChartView::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Plus: + chart()->zoomIn(); + break; + case Qt::Key_Minus: + chart()->zoomOut(); + break; + case Qt::Key_Left: + chart()->scroll(-1.0, 0); + break; + case Qt::Key_Right: + chart()->scroll(1.0, 0); + break; + case Qt::Key_Up: + chart()->scroll(0, 1.0); + break; + case Qt::Key_Down: + chart()->scroll(0, -1.0); + break; + case Qt::Key_Space: + switchChartType(); + break; + default: + QGraphicsView::keyPressEvent(event); + break; + } +} +//![1] + +//![2] +void ChartView::switchChartType() +{ + QChart *newChart; + QChart *oldChart = chart(); + + if (oldChart->chartType() == QChart::ChartTypeCartesian) + newChart = new QPolarChart(); + else + newChart = new QChart(); + + // Move series and axes from old chart to new one + QList seriesList = oldChart->series(); + QList axisList = oldChart->axes(); + QList> axisRanges; + + foreach (QAbstractAxis *axis, axisList) { + QValueAxis *valueAxis = static_cast(axis); + axisRanges.append(QPair(valueAxis->min(), valueAxis->max())); + } + + foreach (QAbstractSeries *series, seriesList) + oldChart->removeSeries(series); + + foreach (QAbstractAxis *axis, axisList) { + oldChart->removeAxis(axis); + newChart->addAxis(axis, axis->alignment()); + } + + foreach (QAbstractSeries *series, seriesList) { + newChart->addSeries(series); + foreach (QAbstractAxis *axis, axisList) + series->attachAxis(axis); + } + + int count = 0; + foreach (QAbstractAxis *axis, axisList) { + axis->setRange(axisRanges[count].first, axisRanges[count].second); + count++; + } + + newChart->setTitle(oldChart->title()); + setChart(newChart); + delete oldChart; +} +//![2] diff --git a/examples/polarchart/chartview.h b/examples/polarchart/chartview.h new file mode 100644 index 0000000..c4584ac --- /dev/null +++ b/examples/polarchart/chartview.h @@ -0,0 +1,41 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef CHARTVIEW_H +#define CHARTVIEW_H + +#include +#include + +QTCOMMERCIALCHART_USE_NAMESPACE + +class ChartView : public QChartView +{ +public: + ChartView(QWidget *parent = 0); + +protected: + void keyPressEvent(QKeyEvent *event); + +private: + void switchChartType(); +}; + +#endif diff --git a/examples/polarchart/main.cpp b/examples/polarchart/main.cpp new file mode 100644 index 0000000..7b3b38d --- /dev/null +++ b/examples/polarchart/main.cpp @@ -0,0 +1,137 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "chartview.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QTCOMMERCIALCHART_USE_NAMESPACE + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + const qreal angularMin = -100; + const qreal angularMax = 100; + + const qreal radialMin = -100; + const qreal radialMax = 100; + + QScatterSeries *series1 = new QScatterSeries(); + series1->setName("scatter"); + for (int i = angularMin; i <= angularMax; i += 10) + series1->append(i, (i / radialMax) * radialMax + 8.0); + + QSplineSeries *series2 = new QSplineSeries(); + series2->setName("spline"); + for (int i = angularMin; i <= angularMax; i += 10) + series2->append(i, (i / radialMax) * radialMax); + + QLineSeries *series3 = new QLineSeries(); + series3->setName("star outer"); + qreal ad = (angularMax - angularMin) / 8; + qreal rd = (radialMax - radialMin) / 3 * 1.3; + series3->append(angularMin, radialMax); + series3->append(angularMin + ad*1, radialMin + rd); + series3->append(angularMin + ad*2, radialMax); + series3->append(angularMin + ad*3, radialMin + rd); + series3->append(angularMin + ad*4, radialMax); + series3->append(angularMin + ad*5, radialMin + rd); + series3->append(angularMin + ad*6, radialMax); + series3->append(angularMin + ad*7, radialMin + rd); + series3->append(angularMin + ad*8, radialMax); + + QLineSeries *series4 = new QLineSeries(); + series4->setName("star inner"); + ad = (angularMax - angularMin) / 8; + rd = (radialMax - radialMin) / 3; + series4->append(angularMin, radialMax); + series4->append(angularMin + ad*1, radialMin + rd); + series4->append(angularMin + ad*2, radialMax); + series4->append(angularMin + ad*3, radialMin + rd); + series4->append(angularMin + ad*4, radialMax); + series4->append(angularMin + ad*5, radialMin + rd); + series4->append(angularMin + ad*6, radialMax); + series4->append(angularMin + ad*7, radialMin + rd); + series4->append(angularMin + ad*8, radialMax); + + QAreaSeries *series5 = new QAreaSeries(); + series5->setName("star area"); + series5->setUpperSeries(series3); + series5->setLowerSeries(series4); + series5->setOpacity(0.5); + + //![1] + QPolarChart *chart = new QPolarChart(); + //![1] + chart->addSeries(series1); + chart->addSeries(series2); + chart->addSeries(series3); + chart->addSeries(series4); + chart->addSeries(series5); + + chart->setTitle("Use arrow keys to scroll, +/- to zoom, and space to switch chart type."); + + //![2] + QValueAxis *angularAxis = new QValueAxis(); + angularAxis->setTickCount(9); // First and last ticks are co-located on 0/360 angle. + angularAxis->setLabelFormat("%.1f"); + angularAxis->setShadesVisible(true); + angularAxis->setShadesBrush(QBrush(QColor(249, 249, 255))); + chart->addAxis(angularAxis, QPolarChart::PolarOrientationAngular); + + QValueAxis *radialAxis = new QValueAxis(); + radialAxis->setTickCount(9); + radialAxis->setLabelFormat("%d"); + chart->addAxis(radialAxis, QPolarChart::PolarOrientationRadial); + //![2] + + series1->attachAxis(radialAxis); + series1->attachAxis(angularAxis); + series2->attachAxis(radialAxis); + series2->attachAxis(angularAxis); + series3->attachAxis(radialAxis); + series3->attachAxis(angularAxis); + series4->attachAxis(radialAxis); + series4->attachAxis(angularAxis); + series5->attachAxis(radialAxis); + series5->attachAxis(angularAxis); + + radialAxis->setRange(radialMin, radialMax); + angularAxis->setRange(angularMin, angularMax); + + ChartView *chartView = new ChartView(); + chartView->setChart(chart); + chartView->setRenderHint(QPainter::Antialiasing); + + QMainWindow window; + window.setCentralWidget(chartView); + window.resize(800, 600); + window.show(); + + return a.exec(); +} diff --git a/examples/polarchart/polarchart.pro b/examples/polarchart/polarchart.pro new file mode 100644 index 0000000..8759c1c --- /dev/null +++ b/examples/polarchart/polarchart.pro @@ -0,0 +1,6 @@ +!include( ../examples.pri ) { + error( "Couldn't find the examples.pri file!" ) +} +TARGET = polarchart +SOURCES += main.cpp chartview.cpp +HEADERS += chartview.h diff --git a/examples/stackedbarchartdrilldown/drilldownchart.cpp b/examples/stackedbarchartdrilldown/drilldownchart.cpp index 51c9147..b2f5120 100644 --- a/examples/stackedbarchartdrilldown/drilldownchart.cpp +++ b/examples/stackedbarchartdrilldown/drilldownchart.cpp @@ -24,7 +24,7 @@ QTCOMMERCIALCHART_USE_NAMESPACE DrilldownChart::DrilldownChart(QGraphicsItem *parent, Qt::WindowFlags wFlags) - : QChart(parent, wFlags), + : QChart(QChart::ChartTypeCartesian, parent, wFlags), m_currentSeries(0) { } diff --git a/examples/zoomlinechart/chart.cpp b/examples/zoomlinechart/chart.cpp index aa109f2..4083434 100644 --- a/examples/zoomlinechart/chart.cpp +++ b/examples/zoomlinechart/chart.cpp @@ -24,7 +24,7 @@ #include Chart::Chart(QGraphicsItem *parent, Qt::WindowFlags wFlags) - : QChart(parent, wFlags) + : QChart(QChart::ChartTypeCartesian, parent, wFlags) { // Seems that QGraphicsView (QChartView) does not grab gestures. // They can only be grabbed here in the QGraphicsWidget (QChart). diff --git a/plugins/declarative/declarative.pro b/plugins/declarative/declarative.pro index 7641b2b..12d4883 100644 --- a/plugins/declarative/declarative.pro +++ b/plugins/declarative/declarative.pro @@ -25,7 +25,8 @@ SOURCES += \ declarativebarseries.cpp \ declarativecategoryaxis.cpp \ declarativemargins.cpp \ - declarativeaxes.cpp + declarativeaxes.cpp \ + declarativepolarchart.cpp HEADERS += \ declarativechart.h \ @@ -39,7 +40,8 @@ HEADERS += \ declarativebarseries.h \ declarativecategoryaxis.h \ declarativemargins.h \ - declarativeaxes.h + declarativeaxes.h \ + declarativepolarchart.h TARGETPATH = QtCommercial/Chart target.path = $$[QT_INSTALL_IMPORTS]/$$TARGETPATH diff --git a/plugins/declarative/declarativeareaseries.cpp b/plugins/declarative/declarativeareaseries.cpp index cda4db6..fabbc61 100644 --- a/plugins/declarative/declarativeareaseries.cpp +++ b/plugins/declarative/declarativeareaseries.cpp @@ -31,6 +31,8 @@ DeclarativeAreaSeries::DeclarativeAreaSeries(QObject *parent) : connect(m_axes, SIGNAL(axisYChanged(QAbstractAxis*)), this, SIGNAL(axisYChanged(QAbstractAxis*))); connect(m_axes, SIGNAL(axisXTopChanged(QAbstractAxis*)), this, SIGNAL(axisXTopChanged(QAbstractAxis*))); connect(m_axes, SIGNAL(axisYRightChanged(QAbstractAxis*)), this, SIGNAL(axisYRightChanged(QAbstractAxis*))); + connect(m_axes, SIGNAL(axisXChanged(QAbstractAxis*)), this, SIGNAL(axisAngularChanged(QAbstractAxis*))); + connect(m_axes, SIGNAL(axisYChanged(QAbstractAxis*)), this, SIGNAL(axisRadialChanged(QAbstractAxis*))); } void DeclarativeAreaSeries::setUpperSeries(DeclarativeLineSeries *series) diff --git a/plugins/declarative/declarativeareaseries.h b/plugins/declarative/declarativeareaseries.h index 3b3fc11..7c1c0ab 100644 --- a/plugins/declarative/declarativeareaseries.h +++ b/plugins/declarative/declarativeareaseries.h @@ -36,6 +36,8 @@ class DeclarativeAreaSeries : public QAreaSeries Q_PROPERTY(QAbstractAxis *axisY READ axisY WRITE setAxisY NOTIFY axisYChanged REVISION 1) Q_PROPERTY(QAbstractAxis *axisXTop READ axisXTop WRITE setAxisXTop NOTIFY axisXTopChanged REVISION 2) Q_PROPERTY(QAbstractAxis *axisYRight READ axisYRight WRITE setAxisYRight NOTIFY axisYRightChanged REVISION 2) + Q_PROPERTY(QAbstractAxis *axisAngular READ axisAngular WRITE setAxisAngular NOTIFY axisAngularChanged REVISION 3) + Q_PROPERTY(QAbstractAxis *axisRadial READ axisRadial WRITE setAxisRadial NOTIFY axisRadialChanged REVISION 3) Q_PROPERTY(qreal borderWidth READ borderWidth WRITE setBorderWidth NOTIFY borderWidthChanged REVISION 1) public: @@ -52,6 +54,10 @@ public: Q_REVISION(2) void setAxisXTop(QAbstractAxis *axis) { m_axes->setAxisXTop(axis); } Q_REVISION(2) QAbstractAxis *axisYRight() { return m_axes->axisYRight(); } Q_REVISION(2) void setAxisYRight(QAbstractAxis *axis) { m_axes->setAxisYRight(axis); } + Q_REVISION(3) QAbstractAxis *axisAngular() { return m_axes->axisX(); } + Q_REVISION(3) void setAxisAngular(QAbstractAxis *axis) { m_axes->setAxisX(axis); } + Q_REVISION(3) QAbstractAxis *axisRadial() { return m_axes->axisY(); } + Q_REVISION(3) void setAxisRadial(QAbstractAxis *axis) { m_axes->setAxisY(axis); } qreal borderWidth() const; void setBorderWidth(qreal borderWidth); @@ -61,6 +67,8 @@ Q_SIGNALS: Q_REVISION(1) void borderWidthChanged(qreal width); Q_REVISION(2) void axisXTopChanged(QAbstractAxis *axis); Q_REVISION(2) void axisYRightChanged(QAbstractAxis *axis); + Q_REVISION(3) void axisAngularChanged(QAbstractAxis *axis); + Q_REVISION(3) void axisRadialChanged(QAbstractAxis *axis); public: DeclarativeAxes *m_axes; diff --git a/plugins/declarative/declarativechart.cpp b/plugins/declarative/declarativechart.cpp index 7e5500b..344f5e6 100644 --- a/plugins/declarative/declarativechart.cpp +++ b/plugins/declarative/declarativechart.cpp @@ -35,6 +35,7 @@ #include "chartdataset_p.h" #include "declarativeaxes.h" #include "qchart_p.h" +#include "qpolarchart.h" #ifndef QT_ON_ARM #include "qdatetimeaxis.h" @@ -235,9 +236,24 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE */ DeclarativeChart::DeclarativeChart(QDeclarativeItem *parent) - : QDeclarativeItem(parent), - m_chart(new QChart(this)) + : QDeclarativeItem(parent) { + initChart(QChart::ChartTypeCartesian); +} + +DeclarativeChart::DeclarativeChart(QChart::ChartType type, QDeclarativeItem *parent) + : QDeclarativeItem(parent) +{ + initChart(type); +} + +void DeclarativeChart::initChart(QChart::ChartType type) +{ + if (type == QChart::ChartTypePolar) + m_chart = new QPolarChart(this); + else + m_chart = new QChart(this); + setFlag(QGraphicsItem::ItemHasNoContents, false); m_margins = new DeclarativeMargins(this); m_margins->setTop(m_chart->margins().top()); diff --git a/plugins/declarative/declarativechart.h b/plugins/declarative/declarativechart.h index 6911606..cc6295e 100644 --- a/plugins/declarative/declarativechart.h +++ b/plugins/declarative/declarativechart.h @@ -170,7 +170,11 @@ private Q_SLOTS: void handleAxisYRightSet(QAbstractAxis *axis); void handleSeriesAdded(QAbstractSeries *series); +protected: + explicit DeclarativeChart(QChart::ChartType type, QDeclarativeItem *parent); + private: + void initChart(QChart::ChartType type); // Extending QChart with DeclarativeChart is not possible because QObject does not support // multi inheritance, so we now have a QChart as a member instead QChart *m_chart; diff --git a/plugins/declarative/declarativelineseries.cpp b/plugins/declarative/declarativelineseries.cpp index aae3eed..735b296 100644 --- a/plugins/declarative/declarativelineseries.cpp +++ b/plugins/declarative/declarativelineseries.cpp @@ -30,6 +30,8 @@ DeclarativeLineSeries::DeclarativeLineSeries(QObject *parent) : connect(m_axes, SIGNAL(axisYChanged(QAbstractAxis*)), this, SIGNAL(axisYChanged(QAbstractAxis*))); connect(m_axes, SIGNAL(axisXTopChanged(QAbstractAxis*)), this, SIGNAL(axisXTopChanged(QAbstractAxis*))); connect(m_axes, SIGNAL(axisYRightChanged(QAbstractAxis*)), this, SIGNAL(axisYRightChanged(QAbstractAxis*))); + connect(m_axes, SIGNAL(axisXChanged(QAbstractAxis*)), this, SIGNAL(axisAngularChanged(QAbstractAxis*))); + connect(m_axes, SIGNAL(axisYChanged(QAbstractAxis*)), this, SIGNAL(axisRadialChanged(QAbstractAxis*))); connect(this, SIGNAL(pointAdded(int)), this, SLOT(handleCountChanged(int))); connect(this, SIGNAL(pointRemoved(int)), this, SLOT(handleCountChanged(int))); } diff --git a/plugins/declarative/declarativelineseries.h b/plugins/declarative/declarativelineseries.h index 21e2ac1..05222a6 100644 --- a/plugins/declarative/declarativelineseries.h +++ b/plugins/declarative/declarativelineseries.h @@ -38,6 +38,8 @@ class DeclarativeLineSeries : public QLineSeries, public DeclarativeXySeries, pu Q_PROPERTY(QAbstractAxis *axisY READ axisY WRITE setAxisY NOTIFY axisYChanged REVISION 1) Q_PROPERTY(QAbstractAxis *axisXTop READ axisXTop WRITE setAxisXTop NOTIFY axisXTopChanged REVISION 2) Q_PROPERTY(QAbstractAxis *axisYRight READ axisYRight WRITE setAxisYRight NOTIFY axisYRightChanged REVISION 2) + Q_PROPERTY(QAbstractAxis *axisAngular READ axisAngular WRITE setAxisAngular NOTIFY axisAngularChanged REVISION 3) + Q_PROPERTY(QAbstractAxis *axisRadial READ axisRadial WRITE setAxisRadial NOTIFY axisRadialChanged REVISION 3) Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged REVISION 1) Q_PROPERTY(Qt::PenStyle style READ style WRITE setStyle NOTIFY styleChanged REVISION 1) Q_PROPERTY(Qt::PenCapStyle capStyle READ capStyle WRITE setCapStyle NOTIFY capStyleChanged REVISION 1) @@ -55,6 +57,10 @@ public: Q_REVISION(2) void setAxisXTop(QAbstractAxis *axis) { m_axes->setAxisXTop(axis); } Q_REVISION(2) QAbstractAxis *axisYRight() { return m_axes->axisYRight(); } Q_REVISION(2) void setAxisYRight(QAbstractAxis *axis) { m_axes->setAxisYRight(axis); } + Q_REVISION(3) QAbstractAxis *axisAngular() { return m_axes->axisX(); } + Q_REVISION(3) void setAxisAngular(QAbstractAxis *axis) { m_axes->setAxisX(axis); } + Q_REVISION(3) QAbstractAxis *axisRadial() { return m_axes->axisY(); } + Q_REVISION(3) void setAxisRadial(QAbstractAxis *axis) { m_axes->setAxisY(axis); } qreal width() const; void setWidth(qreal width); Qt::PenStyle style() const; @@ -81,6 +87,8 @@ Q_SIGNALS: Q_REVISION(1) void axisYChanged(QAbstractAxis *axis); Q_REVISION(2) void axisXTopChanged(QAbstractAxis *axis); Q_REVISION(2) void axisYRightChanged(QAbstractAxis *axis); + Q_REVISION(3) void axisAngularChanged(QAbstractAxis *axis); + Q_REVISION(3) void axisRadialChanged(QAbstractAxis *axis); Q_REVISION(1) void widthChanged(qreal width); Q_REVISION(1) void styleChanged(Qt::PenStyle style); Q_REVISION(1) void capStyleChanged(Qt::PenCapStyle capStyle); diff --git a/plugins/declarative/declarativepolarchart.cpp b/plugins/declarative/declarativepolarchart.cpp new file mode 100644 index 0000000..3be90d7 --- /dev/null +++ b/plugins/declarative/declarativepolarchart.cpp @@ -0,0 +1,64 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "declarativepolarchart.h" +#include "qchart.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +/*! + \qmlclass PolarChartView DeclarativePolarChart + + PolarChartView element is the parent that is responsible for showing different chart series types + in a polar chart. + + Polar charts support line, spline, area, and scatter series, and all axis types + supported by those series. + + \note When setting ticks to an angular ValueAxis, keep in mind that the first and last tick + are co-located at 0/360 degree angle. + + \note If the angular distance between two consecutive points in a series is more than 180 degrees, + any line connecting the two points becomes meaningless, so choose the axis ranges accordingly + when displaying line, spline, or area series. + + \note Polar charts do not support multiple axes of same orientation. + + The following QML shows how to create a polar chart with two series: + \snippet ../demos/qmlpolarchart/qml/qmlpolarchart/view1.qml 1 + + \beginfloatleft + \image demos_qmlpolarchart1.png + \endfloat + \clearfloat +*/ + +DeclarativePolarChart::DeclarativePolarChart(QDeclarativeItem *parent) + : DeclarativeChart(QChart::ChartTypePolar, parent) +{ +} + +DeclarativePolarChart::~DeclarativePolarChart() +{ +} + +#include "moc_declarativepolarchart.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/plugins/declarative/declarativepolarchart.h b/plugins/declarative/declarativepolarchart.h new file mode 100644 index 0000000..598de6e --- /dev/null +++ b/plugins/declarative/declarativepolarchart.h @@ -0,0 +1,40 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef DECLARATIVEPOLARCHART_H +#define DECLARATIVEPOLARCHART_H + +#include +#include +#include "declarativechart.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class DeclarativePolarChart : public DeclarativeChart +{ + Q_OBJECT +public: + DeclarativePolarChart(QDeclarativeItem *parent = 0); + ~DeclarativePolarChart(); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // DECLARATIVEPOLARCHART_H diff --git a/plugins/declarative/declarativescatterseries.cpp b/plugins/declarative/declarativescatterseries.cpp index 209333e..af7b41a 100644 --- a/plugins/declarative/declarativescatterseries.cpp +++ b/plugins/declarative/declarativescatterseries.cpp @@ -30,6 +30,8 @@ DeclarativeScatterSeries::DeclarativeScatterSeries(QObject *parent) : connect(m_axes, SIGNAL(axisYChanged(QAbstractAxis*)), this, SIGNAL(axisYChanged(QAbstractAxis*))); connect(m_axes, SIGNAL(axisXTopChanged(QAbstractAxis*)), this, SIGNAL(axisXTopChanged(QAbstractAxis*))); connect(m_axes, SIGNAL(axisYRightChanged(QAbstractAxis*)), this, SIGNAL(axisYRightChanged(QAbstractAxis*))); + connect(m_axes, SIGNAL(axisXChanged(QAbstractAxis*)), this, SIGNAL(axisAngularChanged(QAbstractAxis*))); + connect(m_axes, SIGNAL(axisYChanged(QAbstractAxis*)), this, SIGNAL(axisRadialChanged(QAbstractAxis*))); connect(this, SIGNAL(pointAdded(int)), this, SLOT(handleCountChanged(int))); connect(this, SIGNAL(pointRemoved(int)), this, SLOT(handleCountChanged(int))); } diff --git a/plugins/declarative/declarativescatterseries.h b/plugins/declarative/declarativescatterseries.h index a6b4b1b..2400242 100644 --- a/plugins/declarative/declarativescatterseries.h +++ b/plugins/declarative/declarativescatterseries.h @@ -38,6 +38,8 @@ class DeclarativeScatterSeries : public QScatterSeries, public DeclarativeXySeri Q_PROPERTY(QAbstractAxis *axisY READ axisY WRITE setAxisY NOTIFY axisYChanged REVISION 1) Q_PROPERTY(QAbstractAxis *axisXTop READ axisXTop WRITE setAxisXTop NOTIFY axisXTopChanged REVISION 2) Q_PROPERTY(QAbstractAxis *axisYRight READ axisYRight WRITE setAxisYRight NOTIFY axisYRightChanged REVISION 2) + Q_PROPERTY(QAbstractAxis *axisAngular READ axisAngular WRITE setAxisAngular NOTIFY axisAngularChanged REVISION 3) + Q_PROPERTY(QAbstractAxis *axisRadial READ axisRadial WRITE setAxisRadial NOTIFY axisRadialChanged REVISION 3) Q_PROPERTY(qreal borderWidth READ borderWidth WRITE setBorderWidth NOTIFY borderWidthChanged REVISION 1) Q_PROPERTY(QDeclarativeListProperty declarativeChildren READ declarativeChildren) Q_CLASSINFO("DefaultProperty", "declarativeChildren") @@ -53,6 +55,10 @@ public: Q_REVISION(2) void setAxisXTop(QAbstractAxis *axis) { m_axes->setAxisXTop(axis); } Q_REVISION(2) QAbstractAxis *axisYRight() { return m_axes->axisYRight(); } Q_REVISION(2) void setAxisYRight(QAbstractAxis *axis) { m_axes->setAxisYRight(axis); } + Q_REVISION(3) QAbstractAxis *axisAngular() { return m_axes->axisX(); } + Q_REVISION(3) void setAxisAngular(QAbstractAxis *axis) { m_axes->setAxisX(axis); } + Q_REVISION(3) QAbstractAxis *axisRadial() { return m_axes->axisY(); } + Q_REVISION(3) void setAxisRadial(QAbstractAxis *axis) { m_axes->setAxisY(axis); } qreal borderWidth() const; void setBorderWidth(qreal borderWidth); QDeclarativeListProperty declarativeChildren(); @@ -76,6 +82,8 @@ Q_SIGNALS: Q_REVISION(1) void borderWidthChanged(qreal width); Q_REVISION(2) void axisXTopChanged(QAbstractAxis *axis); Q_REVISION(2) void axisYRightChanged(QAbstractAxis *axis); + Q_REVISION(3) void axisAngularChanged(QAbstractAxis *axis); + Q_REVISION(3) void axisRadialChanged(QAbstractAxis *axis); public Q_SLOTS: static void appendDeclarativeChildren(QDeclarativeListProperty *list, QObject *element); diff --git a/plugins/declarative/declarativesplineseries.cpp b/plugins/declarative/declarativesplineseries.cpp index 7825b59..f390041 100644 --- a/plugins/declarative/declarativesplineseries.cpp +++ b/plugins/declarative/declarativesplineseries.cpp @@ -30,6 +30,8 @@ DeclarativeSplineSeries::DeclarativeSplineSeries(QObject *parent) : connect(m_axes, SIGNAL(axisYChanged(QAbstractAxis*)), this, SIGNAL(axisYChanged(QAbstractAxis*))); connect(m_axes, SIGNAL(axisXTopChanged(QAbstractAxis*)), this, SIGNAL(axisXTopChanged(QAbstractAxis*))); connect(m_axes, SIGNAL(axisYRightChanged(QAbstractAxis*)), this, SIGNAL(axisYRightChanged(QAbstractAxis*))); + connect(m_axes, SIGNAL(axisXChanged(QAbstractAxis*)), this, SIGNAL(axisAngularChanged(QAbstractAxis*))); + connect(m_axes, SIGNAL(axisYChanged(QAbstractAxis*)), this, SIGNAL(axisRadialChanged(QAbstractAxis*))); connect(this, SIGNAL(pointAdded(int)), this, SLOT(handleCountChanged(int))); connect(this, SIGNAL(pointRemoved(int)), this, SLOT(handleCountChanged(int))); } diff --git a/plugins/declarative/declarativesplineseries.h b/plugins/declarative/declarativesplineseries.h index f9e1a3e..cfa0a7e 100644 --- a/plugins/declarative/declarativesplineseries.h +++ b/plugins/declarative/declarativesplineseries.h @@ -38,6 +38,8 @@ class DeclarativeSplineSeries : public QSplineSeries, public DeclarativeXySeries Q_PROPERTY(QAbstractAxis *axisY READ axisY WRITE setAxisY NOTIFY axisYChanged REVISION 1) Q_PROPERTY(QAbstractAxis *axisXTop READ axisXTop WRITE setAxisXTop NOTIFY axisXTopChanged REVISION 2) Q_PROPERTY(QAbstractAxis *axisYRight READ axisYRight WRITE setAxisYRight NOTIFY axisYRightChanged REVISION 2) + Q_PROPERTY(QAbstractAxis *axisAngular READ axisAngular WRITE setAxisAngular NOTIFY axisAngularChanged REVISION 3) + Q_PROPERTY(QAbstractAxis *axisRadial READ axisRadial WRITE setAxisRadial NOTIFY axisRadialChanged REVISION 3) Q_PROPERTY(qreal width READ width WRITE setWidth NOTIFY widthChanged REVISION 1) Q_PROPERTY(Qt::PenStyle style READ style WRITE setStyle NOTIFY styleChanged REVISION 1) Q_PROPERTY(Qt::PenCapStyle capStyle READ capStyle WRITE setCapStyle NOTIFY capStyleChanged REVISION 1) @@ -55,6 +57,10 @@ public: Q_REVISION(2) void setAxisXTop(QAbstractAxis *axis) { m_axes->setAxisXTop(axis); } Q_REVISION(2) QAbstractAxis *axisYRight() { return m_axes->axisYRight(); } Q_REVISION(2) void setAxisYRight(QAbstractAxis *axis) { m_axes->setAxisYRight(axis); } + Q_REVISION(3) QAbstractAxis *axisAngular() { return m_axes->axisX(); } + Q_REVISION(3) void setAxisAngular(QAbstractAxis *axis) { m_axes->setAxisX(axis); } + Q_REVISION(3) QAbstractAxis *axisRadial() { return m_axes->axisY(); } + Q_REVISION(3) void setAxisRadial(QAbstractAxis *axis) { m_axes->setAxisY(axis); } qreal width() const; void setWidth(qreal width); Qt::PenStyle style() const; @@ -81,6 +87,8 @@ Q_SIGNALS: Q_REVISION(1) void axisYChanged(QAbstractAxis *axis); Q_REVISION(2) void axisXTopChanged(QAbstractAxis *axis); Q_REVISION(2) void axisYRightChanged(QAbstractAxis *axis); + Q_REVISION(3) void axisAngularChanged(QAbstractAxis *axis); + Q_REVISION(3) void axisRadialChanged(QAbstractAxis *axis); Q_REVISION(1) void widthChanged(qreal width); Q_REVISION(1) void styleChanged(Qt::PenStyle style); Q_REVISION(1) void capStyleChanged(Qt::PenCapStyle capStyle); diff --git a/plugins/declarative/plugin.cpp b/plugins/declarative/plugin.cpp index 2b4fd0a..bf560e5 100644 --- a/plugins/declarative/plugin.cpp +++ b/plugins/declarative/plugin.cpp @@ -24,6 +24,7 @@ #include "declarativecategoryaxis.h" #include "qbarcategoryaxis.h" #include "declarativechart.h" +#include "declarativepolarchart.h" #include "declarativexypoint.h" #include "declarativelineseries.h" #include "declarativesplineseries.h" @@ -62,6 +63,7 @@ Q_DECLARE_METATYPE(QList) // These should not be needed or at least they are not needed in Qt4. Q_DECLARE_METATYPE(DeclarativeChart *) +Q_DECLARE_METATYPE(DeclarativePolarChart *) Q_DECLARE_METATYPE(DeclarativeMargins *) Q_DECLARE_METATYPE(DeclarativeAreaSeries *) Q_DECLARE_METATYPE(DeclarativeBarSeries *) @@ -213,6 +215,13 @@ public: qmlRegisterType(uri, 1, 2, "HorizontalBarSeries"); qmlRegisterType(uri, 1, 2, "HorizontalStackedBarSeries"); qmlRegisterType(uri, 1, 2, "HorizontalPercentBarSeries"); + + // QtCommercial.Chart 1.3 + qmlRegisterType(uri, 1, 3, "PolarChartView"); + qmlRegisterType(uri, 1, 3, "SplineSeries"); + qmlRegisterType(uri, 1, 3, "ScatterSeries"); + qmlRegisterType(uri, 1, 3, "LineSeries"); + qmlRegisterType(uri, 1, 3, "AreaSeries"); } }; diff --git a/src/animations/axisanimation.cpp b/src/animations/axisanimation.cpp index e59d9aa..fab017e 100644 --- a/src/animations/axisanimation.cpp +++ b/src/animations/axisanimation.cpp @@ -19,14 +19,15 @@ ****************************************************************************/ #include "axisanimation_p.h" -#include "chartaxis_p.h" +#include "chartaxiselement_p.h" +#include "qabstractaxis_p.h" Q_DECLARE_METATYPE(QVector) QTCOMMERCIALCHART_BEGIN_NAMESPACE -AxisAnimation::AxisAnimation(ChartAxis *axis) +AxisAnimation::AxisAnimation(ChartAxisElement *axis) : ChartAnimation(axis), m_axis(axis), m_type(DefaultAnimation) @@ -68,13 +69,13 @@ void AxisAnimation::setValues(QVector &oldLayout, QVector &newLayo oldLayout.resize(newLayout.count()); for (int i = 0, j = oldLayout.count() - 1; i < (oldLayout.count() + 1) / 2; ++i, --j) { - oldLayout[i] = m_axis->orientation() == Qt::Horizontal ? rect.left() : rect.bottom(); - oldLayout[j] = m_axis->orientation() == Qt::Horizontal ? rect.right() : rect.top(); + oldLayout[i] = m_axis->axis()->orientation() == Qt::Horizontal ? rect.left() : rect.bottom(); + oldLayout[j] = m_axis->axis()->orientation() == Qt::Horizontal ? rect.right() : rect.top(); } } break; case ZoomInAnimation: { - int index = qMin(oldLayout.count() * (m_axis->orientation() == Qt::Horizontal ? m_point.x() : (1 - m_point.y())), newLayout.count() - (qreal)1.0); + int index = qMin(oldLayout.count() * (m_axis->axis()->orientation() == Qt::Horizontal ? m_point.x() : (1 - m_point.y())), newLayout.count() - (qreal)1.0); oldLayout.resize(newLayout.count()); for (int i = 0; i < oldLayout.count(); i++) @@ -99,7 +100,7 @@ void AxisAnimation::setValues(QVector &oldLayout, QVector &newLayo oldLayout.resize(newLayout.count()); QRectF rect = m_axis->gridGeometry(); for (int i = 0, j = oldLayout.count() - 1; i < oldLayout.count(); ++i, --j) - oldLayout[i] = m_axis->orientation() == Qt::Horizontal ? rect.left() : rect.top(); + oldLayout[i] = m_axis->axis()->orientation() == Qt::Horizontal ? rect.left() : rect.top(); } break; } diff --git a/src/animations/axisanimation_p.h b/src/animations/axisanimation_p.h index 6a213ea..d25cced 100644 --- a/src/animations/axisanimation_p.h +++ b/src/animations/axisanimation_p.h @@ -35,13 +35,13 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -class ChartAxis; +class ChartAxisElement; class AxisAnimation: public ChartAnimation { public: enum Animation { DefaultAnimation, ZoomOutAnimation, ZoomInAnimation, MoveForwardAnimation, MoveBackwordAnimation}; - AxisAnimation(ChartAxis *axis); + AxisAnimation(ChartAxisElement *axis); ~AxisAnimation(); void setAnimationType(Animation type); void setAnimationPoint(const QPointF &point); @@ -50,7 +50,7 @@ protected: QVariant interpolated(const QVariant &from, const QVariant &to, qreal progress) const; void updateCurrentValue(const QVariant &value); private: - ChartAxis *m_axis; + ChartAxisElement *m_axis; Animation m_type; QPointF m_point; }; diff --git a/src/areachart/areachartitem.cpp b/src/areachart/areachartitem.cpp index 92f8999..10adfa6 100644 --- a/src/areachart/areachartitem.cpp +++ b/src/areachart/areachartitem.cpp @@ -88,12 +88,26 @@ void AreaChartItem::updatePath() path = m_upper->path(); if (m_lower) { + // Note: Polarcharts always draw area correctly only when both series have equal width or are + // fully displayed. If one series is partally off-chart, the connecting line between + // the series does not attach to the end of the partially hidden series but to the point + // where it intersects the axis line. The problem is especially noticeable when one of the series + // is entirely off-chart, in which case the connecting line connects two ends of the + // visible series. + // This happens because we get the paths from linechart, which omits off-chart segments. + // To properly fix, linechart would need to provide true full path, in right, left, and the rest + // portions to enable proper clipping. However, combining those to single visually unified area + // would be a nightmare, since they would have to be painted separately. path.connectPath(m_lower->path().toReversed()); } else { QPointF first = path.pointAtPercent(0); QPointF last = path.pointAtPercent(1); - path.lineTo(last.x(), rect.bottom()); - path.lineTo(first.x(), rect.bottom()); + if (presenter()->chartType() == QChart::ChartTypeCartesian) { + path.lineTo(last.x(), rect.bottom()); + path.lineTo(first.x(), rect.bottom()); + } else { // polar + path.lineTo(rect.center()); + } } path.closeSubpath(); prepareGeometryChange(); @@ -137,7 +151,11 @@ void AreaChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt painter->save(); painter->setPen(m_linePen); painter->setBrush(m_brush); - painter->setClipRect(QRectF(QPointF(0,0),domain()->size())); + QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); + if (presenter()->chartType() == QChart::ChartTypePolar) + painter->setClipRegion(QRegion(clipRect.toRect(), QRegion::Ellipse)); + else + painter->setClipRect(clipRect); painter->drawPath(m_path); if (m_pointsVisible) { painter->setPen(m_pointPen); diff --git a/src/areachart/areachartitem_p.h b/src/areachart/areachartitem_p.h index 0c09bad..60d3e07 100644 --- a/src/areachart/areachartitem_p.h +++ b/src/areachart/areachartitem_p.h @@ -32,11 +32,11 @@ #include "qchartglobal.h" #include "linechartitem_p.h" +#include "qareaseries.h" #include QTCOMMERCIALCHART_BEGIN_NAMESPACE -class QAreaSeries; class AreaChartItem; class AreaChartItem : public ChartItem @@ -57,6 +57,8 @@ public: void updatePath(); void setPresenter(ChartPresenter *presenter); + QAreaSeries *series() const { return m_series; } + protected: void mousePressEvent(QGraphicsSceneMouseEvent *event); void hoverEnterEvent(QGraphicsSceneHoverEvent *event); @@ -89,6 +91,9 @@ public: AreaBoundItem(AreaChartItem *area, QLineSeries *lineSeries,QGraphicsItem* item = 0) : LineChartItem(lineSeries, item), m_item(area) { + // We do not actually want to draw anything from LineChartItem. + // Drawing is done in AreaChartItem only. + setVisible(false); } ~AreaBoundItem() {} @@ -97,6 +102,9 @@ public: // Turn off points drawing from component line chart item, as that // messes up the fill for area series. suppressPoints(); + // Component lineseries are not necessarily themselves on the chart, + // so get the chart type for them from area chart. + forceChartType(m_item->series()->chart()->chartType()); LineChartItem::updateGeometry(); m_item->updatePath(); } diff --git a/src/axis/axis.pri b/src/axis/axis.pri index 264ed89..6f706aa 100644 --- a/src/axis/axis.pri +++ b/src/axis/axis.pri @@ -13,7 +13,8 @@ DEPENDPATH += $$PWD \ $$PWD/logvalueaxis SOURCES += \ - $$PWD/chartaxis.cpp \ + $$PWD/chartaxiselement.cpp \ + $$PWD/cartesianchartaxis.cpp \ $$PWD/qabstractaxis.cpp \ $$PWD/verticalaxis.cpp \ $$PWD/horizontalaxis.cpp \ @@ -31,10 +32,12 @@ SOURCES += \ $$PWD/logvalueaxis/qlogvalueaxis.cpp PRIVATE_HEADERS += \ - $$PWD/chartaxis_p.h \ + $$PWD/chartaxiselement_p.h \ + $$PWD/cartesianchartaxis_p.h \ $$PWD/qabstractaxis_p.h \ $$PWD/verticalaxis_p.h \ $$PWD/horizontalaxis_p.h \ + $$PWD/linearrowitem_p.h \ $$PWD/valueaxis/chartvalueaxisx_p.h \ $$PWD/valueaxis/chartvalueaxisy_p.h \ $$PWD/valueaxis/qvalueaxis_p.h \ @@ -55,6 +58,29 @@ PUBLIC_HEADERS += \ $$PWD/categoryaxis/qcategoryaxis.h \ $$PWD/logvalueaxis/qlogvalueaxis.h \ +# polar +SOURCES += \ + $$PWD/polarchartaxis.cpp \ + $$PWD/polarchartaxisangular.cpp \ + $$PWD/polarchartaxisradial.cpp \ + $$PWD/valueaxis/polarchartvalueaxisangular.cpp \ + $$PWD/valueaxis/polarchartvalueaxisradial.cpp \ + $$PWD/logvalueaxis/polarchartlogvalueaxisangular.cpp \ + $$PWD/logvalueaxis/polarchartlogvalueaxisradial.cpp \ + $$PWD/categoryaxis/polarchartcategoryaxisangular.cpp \ + $$PWD/categoryaxis/polarchartcategoryaxisradial.cpp + +PRIVATE_HEADERS += \ + $$PWD/polarchartaxis_p.h \ + $$PWD/polarchartaxisangular_p.h \ + $$PWD/polarchartaxisradial_p.h \ + $$PWD/valueaxis/polarchartvalueaxisangular_p.h \ + $$PWD/valueaxis/polarchartvalueaxisradial_p.h \ + $$PWD/logvalueaxis/polarchartlogvalueaxisangular_p.h \ + $$PWD/logvalueaxis/polarchartlogvalueaxisradial_p.h \ + $$PWD/categoryaxis/polarchartcategoryaxisangular_p.h \ + $$PWD/categoryaxis/polarchartcategoryaxisradial_p.h + !linux-arm*: { INCLUDEPATH += \ $$PWD/datetimeaxis @@ -65,12 +91,16 @@ DEPENDPATH += \ SOURCES += \ $$PWD/datetimeaxis/chartdatetimeaxisx.cpp \ $$PWD/datetimeaxis/chartdatetimeaxisy.cpp \ - $$PWD/datetimeaxis/qdatetimeaxis.cpp + $$PWD/datetimeaxis/qdatetimeaxis.cpp \ + $$PWD/datetimeaxis/polarchartdatetimeaxisangular.cpp \ + $$PWD/datetimeaxis/polarchartdatetimeaxisradial.cpp PRIVATE_HEADERS += \ $$PWD/datetimeaxis/chartdatetimeaxisx_p.h \ $$PWD/datetimeaxis/chartdatetimeaxisy_p.h \ - $$PWD/datetimeaxis/qdatetimeaxis_p.h + $$PWD/datetimeaxis/qdatetimeaxis_p.h \ + $$PWD/datetimeaxis/polarchartdatetimeaxisangular_p.h \ + $$PWD/datetimeaxis/polarchartdatetimeaxisradial_p.h PUBLIC_HEADERS += \ $$PWD/datetimeaxis/qdatetimeaxis.h diff --git a/src/axis/barcategoryaxis/chartbarcategoryaxisx.cpp b/src/axis/barcategoryaxis/chartbarcategoryaxisx.cpp index 8d86b12..9d09aeb 100644 --- a/src/axis/barcategoryaxis/chartbarcategoryaxisx.cpp +++ b/src/axis/barcategoryaxis/chartbarcategoryaxisx.cpp @@ -21,7 +21,7 @@ #include "chartbarcategoryaxisx_p.h" #include "chartpresenter_p.h" #include "qbarcategoryaxis_p.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -87,7 +87,7 @@ QStringList ChartBarCategoryAxisX::createCategoryLabels(const QVector& la void ChartBarCategoryAxisX::updateGeometry() { - const QVector& layout = ChartAxis::layout(); + const QVector& layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; setLabels(createCategoryLabels(layout)); @@ -104,7 +104,7 @@ QSizeF ChartBarCategoryAxisX::sizeHint(Qt::SizeHint which, const QSizeF &constra { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = HorizontalAxis::sizeHint(which, constraint); QStringList ticksList = m_categoriesAxis->categories(); diff --git a/src/axis/barcategoryaxis/chartbarcategoryaxisy.cpp b/src/axis/barcategoryaxis/chartbarcategoryaxisy.cpp index 0ca8116..20194bb 100644 --- a/src/axis/barcategoryaxis/chartbarcategoryaxisy.cpp +++ b/src/axis/barcategoryaxis/chartbarcategoryaxisy.cpp @@ -21,7 +21,7 @@ #include "chartbarcategoryaxisy_p.h" #include "chartpresenter_p.h" #include "qbarcategoryaxis_p.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -86,7 +86,7 @@ QStringList ChartBarCategoryAxisY::createCategoryLabels(const QVector& la void ChartBarCategoryAxisY::updateGeometry() { - const QVector& layout = ChartAxis::layout(); + const QVector& layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; setLabels(createCategoryLabels(layout)); @@ -103,7 +103,7 @@ QSizeF ChartBarCategoryAxisY::sizeHint(Qt::SizeHint which, const QSizeF &constra { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = VerticalAxis::sizeHint(which, constraint); QStringList ticksList = m_categoriesAxis->categories(); diff --git a/src/axis/barcategoryaxis/qbarcategoryaxis.cpp b/src/axis/barcategoryaxis/qbarcategoryaxis.cpp index 0dffff0..0dfbea1 100644 --- a/src/axis/barcategoryaxis/qbarcategoryaxis.cpp +++ b/src/axis/barcategoryaxis/qbarcategoryaxis.cpp @@ -111,7 +111,7 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE /*! \fn void QBarCategoryAxis::categoriesChanged() - Axis emits signal when the categories of the axis has changed. + Axis emits signal when the categories of the axis have changed. */ /*! @@ -550,7 +550,7 @@ void QBarCategoryAxisPrivate::setRange(const QString &minCategory, const QStrin void QBarCategoryAxisPrivate::initializeGraphics(QGraphicsItem* parent) { Q_Q(QBarCategoryAxis); - ChartAxis* axis(0); + ChartAxisElement* axis(0); if (orientation() == Qt::Vertical) axis = new ChartBarCategoryAxisY(q,parent); if (orientation() == Qt::Horizontal) diff --git a/src/axis/cartesianchartaxis.cpp b/src/axis/cartesianchartaxis.cpp new file mode 100644 index 0000000..8597820 --- /dev/null +++ b/src/axis/cartesianchartaxis.cpp @@ -0,0 +1,197 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "cartesianchartaxis_p.h" +#include "qabstractaxis.h" +#include "qabstractaxis_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" +#include "abstractdomain_p.h" +#include "linearrowitem_p.h" +#include +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +CartesianChartAxis::CartesianChartAxis(QAbstractAxis *axis, QGraphicsItem *item , bool intervalAxis) + : ChartAxisElement(axis, item, intervalAxis) +{ + Q_ASSERT(item); +} + + +CartesianChartAxis::~CartesianChartAxis() +{ +} + +void CartesianChartAxis::createItems(int count) +{ + if (arrowItems().size() == 0) { + QGraphicsLineItem *arrow = new LineArrowItem(this, this); + arrow->setPen(axis()->linePen()); + arrowGroup()->addToGroup(arrow); + } + + if (intervalAxis() && gridItems().size() == 0) { + for (int i = 0 ; i < 2 ; i ++){ + QGraphicsLineItem *item = new QGraphicsLineItem(this); + item->setPen(axis()->gridLinePen()); + gridGroup()->addToGroup(item); + } + } + + for (int i = 0; i < count; ++i) { + QGraphicsLineItem *arrow = new QGraphicsLineItem(this); + QGraphicsLineItem *grid = new QGraphicsLineItem(this); + QGraphicsSimpleTextItem *label = new QGraphicsSimpleTextItem(this); + QGraphicsSimpleTextItem *title = titleItem(); + arrow->setPen(axis()->linePen()); + grid->setPen(axis()->gridLinePen()); + label->setFont(axis()->labelsFont()); + label->setPen(axis()->labelsPen()); + label->setBrush(axis()->labelsBrush()); + label->setRotation(axis()->labelsAngle()); + title->setFont(axis()->titleFont()); + title->setPen(axis()->titlePen()); + title->setBrush(axis()->titleBrush()); + title->setText(axis()->titleText()); + arrowGroup()->addToGroup(arrow); + gridGroup()->addToGroup(grid); + labelGroup()->addToGroup(label); + + if ((gridItems().size()) % 2 && gridItems().size() > 2) { + QGraphicsRectItem* shades = new QGraphicsRectItem(this); + shades->setPen(axis()->shadesPen()); + shades->setBrush(axis()->shadesBrush()); + shadeGroup()->addToGroup(shades); + } + } + +} + +void CartesianChartAxis::deleteItems(int count) +{ + QList lines = gridItems(); + QList labels = labelItems(); + QList shades = shadeItems(); + QList axis = arrowItems(); + + for (int i = 0; i < count; ++i) { + if (lines.size() % 2 && lines.size() > 1) + delete(shades.takeLast()); + delete(lines.takeLast()); + delete(labels.takeLast()); + delete(axis.takeLast()); + } +} + +void CartesianChartAxis::updateLayout(QVector &layout) +{ + int diff = ChartAxisElement::layout().size() - layout.size(); + + if (diff > 0) + deleteItems(diff); + else if (diff < 0) + createItems(-diff); + + if (animation()) { + switch (presenter()->state()) { + case ChartPresenter::ZoomInState: + animation()->setAnimationType(AxisAnimation::ZoomInAnimation); + animation()->setAnimationPoint(presenter()->statePoint()); + break; + case ChartPresenter::ZoomOutState: + animation()->setAnimationType(AxisAnimation::ZoomOutAnimation); + animation()->setAnimationPoint(presenter()->statePoint()); + break; + case ChartPresenter::ScrollUpState: + case ChartPresenter::ScrollLeftState: + animation()->setAnimationType(AxisAnimation::MoveBackwordAnimation); + break; + case ChartPresenter::ScrollDownState: + case ChartPresenter::ScrollRightState: + animation()->setAnimationType(AxisAnimation::MoveForwardAnimation); + break; + case ChartPresenter::ShowState: + animation()->setAnimationType(AxisAnimation::DefaultAnimation); + break; + } + animation()->setValues(ChartAxisElement::layout(), layout); + presenter()->startAnimation(animation()); + } else { + setLayout(layout); + updateGeometry(); + } +} + +bool CartesianChartAxis::isEmpty() +{ + return axisGeometry().isEmpty() + || gridGeometry().isEmpty() + || qFuzzyCompare(min(), max()); +} + +void CartesianChartAxis::setGeometry(const QRectF &axis, const QRectF &grid) +{ + m_gridRect = grid; + setAxisGeometry(axis); + + if (isEmpty()) + return; + + QVector layout = calculateLayout(); + updateLayout(layout); +} + +QSizeF CartesianChartAxis::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const +{ + Q_UNUSED(which); + Q_UNUSED(constraint); + return QSizeF(); +} + +void CartesianChartAxis::handleArrowPenChanged(const QPen &pen) +{ + foreach (QGraphicsItem *item, arrowItems()) + static_cast(item)->setPen(pen); +} + +void CartesianChartAxis::handleGridPenChanged(const QPen &pen) +{ + foreach (QGraphicsItem *item, gridItems()) + static_cast(item)->setPen(pen); +} + +void CartesianChartAxis::handleShadesBrushChanged(const QBrush &brush) +{ + foreach (QGraphicsItem *item, shadeItems()) + static_cast(item)->setBrush(brush); +} + +void CartesianChartAxis::handleShadesPenChanged(const QPen &pen) +{ + foreach (QGraphicsItem *item, shadeItems()) + static_cast(item)->setPen(pen); +} + +#include "moc_cartesianchartaxis_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/cartesianchartaxis_p.h b/src/axis/cartesianchartaxis_p.h new file mode 100644 index 0000000..51ed132 --- /dev/null +++ b/src/axis/cartesianchartaxis_p.h @@ -0,0 +1,79 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef CARTESIANCHARTAXIS_H +#define CARTESIANCHARTAXIS_H + +#include "qchartglobal.h" +#include "chartaxiselement_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QAbstractAxis; + +class CartesianChartAxis : public ChartAxisElement +{ + Q_OBJECT + Q_INTERFACES(QGraphicsLayoutItem) +public: + + CartesianChartAxis(QAbstractAxis *axis, QGraphicsItem *item = 0, bool intervalAxis = false); + ~CartesianChartAxis(); + + void setGeometry(const QRectF &axis, const QRectF &grid); + QRectF gridGeometry() const { return m_gridRect; } + bool isEmpty(); + + virtual QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const; + +protected: + void setGeometry(const QRectF &size) { Q_UNUSED(size);} + virtual void updateGeometry() = 0; + void updateLayout(QVector &layout); + +public Q_SLOTS: + virtual void handleArrowPenChanged(const QPen &pen); + virtual void handleGridPenChanged(const QPen &pen); + virtual void handleShadesBrushChanged(const QBrush &brush); + virtual void handleShadesPenChanged(const QPen &pen); + +private: + void createItems(int count); + void deleteItems(int count); + +private: + QRectF m_gridRect; + + friend class AxisAnimation; + friend class LineArrowItem; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif /* CARTESIANCHARTAXIS_H */ diff --git a/src/axis/categoryaxis/chartcategoryaxisx.cpp b/src/axis/categoryaxis/chartcategoryaxisx.cpp index 9c9813c..cb1ede6 100644 --- a/src/axis/categoryaxis/chartcategoryaxisx.cpp +++ b/src/axis/categoryaxis/chartcategoryaxisx.cpp @@ -22,6 +22,7 @@ #include "qcategoryaxis.h" #include "qabstractaxis.h" #include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -32,6 +33,7 @@ ChartCategoryAxisX::ChartCategoryAxisX(QCategoryAxis *axis, QGraphicsItem* item) : HorizontalAxis(axis, item, true), m_axis(axis) { + QObject::connect(axis, SIGNAL(categoriesChanged()), this, SLOT(handleCategoriesChanged())); } ChartCategoryAxisX::~ChartCategoryAxisX() @@ -72,16 +74,11 @@ void ChartCategoryAxisX::updateGeometry() HorizontalAxis::updateGeometry(); } -void ChartCategoryAxisX::handleAxisUpdated() -{ - updateGeometry(); -} - QSizeF ChartCategoryAxisX::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = HorizontalAxis::sizeHint(which, constraint); QStringList ticksList = m_axis->categoriesLabels(); @@ -114,4 +111,12 @@ QSizeF ChartCategoryAxisX::sizeHint(Qt::SizeHint which, const QSizeF &constraint return sh; } +void ChartCategoryAxisX::handleCategoriesChanged() +{ + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); +} + +#include "moc_chartcategoryaxisx_p.cpp" + QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/categoryaxis/chartcategoryaxisx_p.h b/src/axis/categoryaxis/chartcategoryaxisx_p.h index df338cc..2fced9c 100644 --- a/src/axis/categoryaxis/chartcategoryaxisx_p.h +++ b/src/axis/categoryaxis/chartcategoryaxisx_p.h @@ -35,23 +35,23 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE class QCategoryAxis; -class ChartPresenter; class ChartCategoryAxisX : public HorizontalAxis { + Q_OBJECT public: ChartCategoryAxisX(QCategoryAxis *axis, QGraphicsItem* item = 0); ~ChartCategoryAxisX(); QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint) const; +public Q_SLOTS: + void handleCategoriesChanged(); + protected: QVector calculateLayout() const; void updateGeometry(); -public Q_SLOTS: - void handleAxisUpdated(); - private: QCategoryAxis *m_axis; }; diff --git a/src/axis/categoryaxis/chartcategoryaxisy.cpp b/src/axis/categoryaxis/chartcategoryaxisy.cpp index ea86b56..96f5a2d 100644 --- a/src/axis/categoryaxis/chartcategoryaxisy.cpp +++ b/src/axis/categoryaxis/chartcategoryaxisy.cpp @@ -22,6 +22,7 @@ #include "qcategoryaxis.h" #include "qabstractaxis.h" #include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -33,6 +34,7 @@ ChartCategoryAxisY::ChartCategoryAxisY(QCategoryAxis *axis, QGraphicsItem* item) : VerticalAxis(axis, item, true), m_axis(axis) { + QObject::connect(axis, SIGNAL(categoriesChanged()), this, SLOT(handleCategoriesChanged())); } ChartCategoryAxisY::~ChartCategoryAxisY() @@ -72,16 +74,11 @@ void ChartCategoryAxisY::updateGeometry() VerticalAxis::updateGeometry(); } -void ChartCategoryAxisY::handleAxisUpdated() -{ - updateGeometry(); -} - QSizeF ChartCategoryAxisY::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = VerticalAxis::sizeHint(which, constraint); QStringList ticksList = m_axis->categoriesLabels(); @@ -113,4 +110,12 @@ QSizeF ChartCategoryAxisY::sizeHint(Qt::SizeHint which, const QSizeF &constraint return sh; } +void ChartCategoryAxisY::handleCategoriesChanged() +{ + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); +} + +#include "moc_chartcategoryaxisy_p.cpp" + QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/categoryaxis/chartcategoryaxisy_p.h b/src/axis/categoryaxis/chartcategoryaxisy_p.h index a9dcbd8..20760b8 100644 --- a/src/axis/categoryaxis/chartcategoryaxisy_p.h +++ b/src/axis/categoryaxis/chartcategoryaxisy_p.h @@ -35,23 +35,23 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE class QCategoryAxis; -class ChartPresenter; class ChartCategoryAxisY : public VerticalAxis { + Q_OBJECT public: ChartCategoryAxisY(QCategoryAxis *axis, QGraphicsItem* item = 0); ~ChartCategoryAxisY(); QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint) const; +public Q_SLOTS: + void handleCategoriesChanged(); + protected: QVector calculateLayout() const; void updateGeometry(); -public Q_SLOTS: - void handleAxisUpdated(); - private: QCategoryAxis *m_axis; }; diff --git a/src/axis/categoryaxis/polarchartcategoryaxisangular.cpp b/src/axis/categoryaxis/polarchartcategoryaxisangular.cpp new file mode 100644 index 0000000..ead087f --- /dev/null +++ b/src/axis/categoryaxis/polarchartcategoryaxisangular.cpp @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartcategoryaxisangular_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" +#include "qcategoryaxis.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartCategoryAxisAngular::PolarChartCategoryAxisAngular(QCategoryAxis *axis, QGraphicsItem *item) + : PolarChartAxisAngular(axis, item, true) +{ + QObject::connect(axis, SIGNAL(categoriesChanged()), this, SLOT(handleCategoriesChanged())); +} + +PolarChartCategoryAxisAngular::~PolarChartCategoryAxisAngular() +{ +} + +QVector PolarChartCategoryAxisAngular::calculateLayout() const +{ + QCategoryAxis *catAxis = static_cast(axis()); + int tickCount = catAxis->categoriesLabels().count() + 1; + QVector points; + + if (tickCount < 2) + return points; + + qreal range = max() - min(); + if (range > 0) { + points.resize(tickCount); + qreal scale = 360.0 / range; + qreal angle; + for (int i = 0; i < tickCount; ++i) { + if (i < tickCount - 1) + angle = (catAxis->startValue(catAxis->categoriesLabels().at(i)) - min()) * scale; + else + angle = (catAxis->endValue(catAxis->categoriesLabels().at(i - 1)) - min()) * scale; + points[i] = angle; + } + } + + return points; +} + +void PolarChartCategoryAxisAngular::createAxisLabels(const QVector &layout) +{ + Q_UNUSED(layout); + setLabels(static_cast(axis())->categoriesLabels() << ""); +} + +void PolarChartCategoryAxisAngular::handleCategoriesChanged() +{ + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); +} + + +#include "moc_polarchartcategoryaxisangular_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/categoryaxis/polarchartcategoryaxisangular_p.h b/src/axis/categoryaxis/polarchartcategoryaxisangular_p.h new file mode 100644 index 0000000..40d8d92 --- /dev/null +++ b/src/axis/categoryaxis/polarchartcategoryaxisangular_p.h @@ -0,0 +1,56 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTCATEGORYAXISANGULAR_P_H +#define POLARCHARTCATEGORYAXISANGULAR_P_H + +#include "polarchartaxisangular_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QCategoryAxis; + +class PolarChartCategoryAxisAngular : public PolarChartAxisAngular +{ + Q_OBJECT + +public: + PolarChartCategoryAxisAngular(QCategoryAxis *axis, QGraphicsItem *item); + ~PolarChartCategoryAxisAngular(); + + virtual QVector calculateLayout() const; + virtual void createAxisLabels(const QVector &layout); + +public Q_SLOTS: + void handleCategoriesChanged(); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTCATEGORYAXISANGULAR_P_H diff --git a/src/axis/categoryaxis/polarchartcategoryaxisradial.cpp b/src/axis/categoryaxis/polarchartcategoryaxisradial.cpp new file mode 100644 index 0000000..f4fe3b7 --- /dev/null +++ b/src/axis/categoryaxis/polarchartcategoryaxisradial.cpp @@ -0,0 +1,79 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartcategoryaxisradial_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" +#include "qcategoryaxis.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartCategoryAxisRadial::PolarChartCategoryAxisRadial(QCategoryAxis *axis, QGraphicsItem *item) + : PolarChartAxisRadial(axis, item, true) +{ + QObject::connect(axis, SIGNAL(categoriesChanged()), this, SLOT(handleCategoriesChanged())); +} + +PolarChartCategoryAxisRadial::~PolarChartCategoryAxisRadial() +{ +} + +QVector PolarChartCategoryAxisRadial::calculateLayout() const +{ + QCategoryAxis *catAxis = static_cast(axis()); + int tickCount = catAxis->categoriesLabels().count() + 1; + QVector points; + + if (tickCount < 2) + return points; + + qreal range = max() - min(); + if (range > 0) { + points.resize(tickCount); + qreal scale = (axisGeometry().width() / 2) / range; + qreal angle; + for (int i = 0; i < tickCount; ++i) { + if (i < tickCount - 1) + angle = (catAxis->startValue(catAxis->categoriesLabels().at(i)) - min()) * scale; + else + angle = (catAxis->endValue(catAxis->categoriesLabels().at(i - 1)) - min()) * scale; + points[i] = angle; + } + } + + return points; +} + +void PolarChartCategoryAxisRadial::createAxisLabels(const QVector &layout) +{ + Q_UNUSED(layout); + setLabels(static_cast(axis())->categoriesLabels() << ""); +} + +void PolarChartCategoryAxisRadial::handleCategoriesChanged() +{ + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); +} + +#include "moc_polarchartcategoryaxisradial_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/categoryaxis/polarchartcategoryaxisradial_p.h b/src/axis/categoryaxis/polarchartcategoryaxisradial_p.h new file mode 100644 index 0000000..83be8d8 --- /dev/null +++ b/src/axis/categoryaxis/polarchartcategoryaxisradial_p.h @@ -0,0 +1,56 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTCATEGORYAXISRADIAL_P_H +#define POLARCHARTCATEGORYAXISRADIAL_P_H + +#include "polarchartaxisradial_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QCategoryAxis; + +class PolarChartCategoryAxisRadial : public PolarChartAxisRadial +{ + Q_OBJECT + +public: + PolarChartCategoryAxisRadial(QCategoryAxis *axis, QGraphicsItem *item); + ~PolarChartCategoryAxisRadial(); + + virtual QVector calculateLayout() const; + virtual void createAxisLabels(const QVector &layout); + +public Q_SLOTS: + void handleCategoriesChanged(); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTCATEGORYAXISRADIAL_P_H diff --git a/src/axis/categoryaxis/qcategoryaxis.cpp b/src/axis/categoryaxis/qcategoryaxis.cpp index 40d8cc2..d187da8 100644 --- a/src/axis/categoryaxis/qcategoryaxis.cpp +++ b/src/axis/categoryaxis/qcategoryaxis.cpp @@ -22,6 +22,8 @@ #include "qcategoryaxis_p.h" #include "chartcategoryaxisx_p.h" #include "chartcategoryaxisy_p.h" +#include "polarchartcategoryaxisangular_p.h" +#include "polarchartcategoryaxisradial_p.h" #include "qchart.h" #include #include @@ -103,6 +105,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE */ /*! + \fn void QCategoryAxis::categoriesChanged() + Axis emits signal when the categories of the axis have changed. +*/ + + +/*! Constructs an axis object which is a child of \a parent. */ QCategoryAxis::QCategoryAxis(QObject *parent): @@ -151,10 +159,12 @@ void QCategoryAxis::append(const QString &categoryLabel, qreal categoryEndValue) Range range(d->m_categoryMinimum, categoryEndValue); d->m_categoriesMap.insert(categoryLabel, range); d->m_categories.append(categoryLabel); + emit categoriesChanged(); } else if (categoryEndValue > endValue(d->m_categories.last())) { Range previousRange = d->m_categoriesMap.value(d->m_categories.last()); d->m_categoriesMap.insert(categoryLabel, Range(previousRange.second, categoryEndValue)); d->m_categories.append(categoryLabel); + emit categoriesChanged(); } } } @@ -169,10 +179,13 @@ void QCategoryAxis::setStartValue(qreal min) Q_D(QCategoryAxis); if (d->m_categories.isEmpty()) { d->m_categoryMinimum = min; + emit categoriesChanged(); } else { Range range = d->m_categoriesMap.value(d->m_categories.first()); - if (min < range.second) + if (min < range.second) { d->m_categoriesMap.insert(d->m_categories.first(), Range(min, range.second)); + emit categoriesChanged(); + } } } @@ -227,7 +240,7 @@ void QCategoryAxis::remove(const QString &categoryLabel) d->m_categoriesMap.insert(label, range); } } - //TODO:: d->emitUpdated(); + emit categoriesChanged(); } } @@ -251,7 +264,7 @@ void QCategoryAxis::replaceLabel(const QString &oldLabel, const QString &newLabe Range range = d->m_categoriesMap.value(oldLabel); d->m_categoriesMap.remove(oldLabel); d->m_categoriesMap.insert(newLabel, range); - //TODO:: d->emitUpdated(); + emit categoriesChanged(); } } @@ -300,14 +313,23 @@ int QCategoryAxisPrivate::ticksCount() const return m_categories.count() + 1; } -void QCategoryAxisPrivate::initializeGraphics(QGraphicsItem* parent) +void QCategoryAxisPrivate::initializeGraphics(QGraphicsItem *parent) { Q_Q(QCategoryAxis); - ChartAxis* axis(0); - if (orientation() == Qt::Vertical) - axis = new ChartCategoryAxisY(q,parent); - else if(orientation() == Qt::Horizontal) - axis = new ChartCategoryAxisX(q,parent); + ChartAxisElement *axis(0); + if (m_chart->chartType() == QChart::ChartTypeCartesian) { + if (orientation() == Qt::Vertical) + axis = new ChartCategoryAxisY(q,parent); + else if (orientation() == Qt::Horizontal) + axis = new ChartCategoryAxisX(q,parent); + } + + if (m_chart->chartType() == QChart::ChartTypePolar) { + if (orientation() == Qt::Vertical) + axis = new PolarChartCategoryAxisRadial(q, parent); + if (orientation() == Qt::Horizontal) + axis = new PolarChartCategoryAxisAngular(q, parent); + } m_item.reset(axis); QAbstractAxisPrivate::initializeGraphics(parent); diff --git a/src/axis/categoryaxis/qcategoryaxis.h b/src/axis/categoryaxis/qcategoryaxis.h index a778927..73a33e2 100644 --- a/src/axis/categoryaxis/qcategoryaxis.h +++ b/src/axis/categoryaxis/qcategoryaxis.h @@ -57,6 +57,9 @@ public: QStringList categoriesLabels(); int count() const; +Q_SIGNALS: + void categoriesChanged(); + private: Q_DECLARE_PRIVATE(QCategoryAxis) Q_DISABLE_COPY(QCategoryAxis) diff --git a/src/axis/categoryaxis/qcategoryaxis_p.h b/src/axis/categoryaxis/qcategoryaxis_p.h index 09d80a5..466deae 100644 --- a/src/axis/categoryaxis/qcategoryaxis_p.h +++ b/src/axis/categoryaxis/qcategoryaxis_p.h @@ -48,9 +48,6 @@ public: void initializeGraphics(QGraphicsItem* parent); int ticksCount() const; -Q_SIGNALS: - void changed(qreal min, qreal max, int tickCount, bool niceNumbers); - private: QMap m_categoriesMap; QStringList m_categories; diff --git a/src/axis/chartaxis.cpp b/src/axis/chartaxis.cpp deleted file mode 100644 index 7e565ac..0000000 --- a/src/axis/chartaxis.cpp +++ /dev/null @@ -1,547 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2013 Digia Plc -** All rights reserved. -** For any questions to Digia, please use contact form at http://qt.digia.com -** -** This file is part of the Qt Commercial Charts Add-on. -** -** $QT_BEGIN_LICENSE$ -** Licensees holding valid Qt Commercial licenses may use this file in -** accordance with the Qt Commercial License Agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and Digia. -** -** If you have questions regarding the use of this file, please use -** contact form at http://qt.digia.com -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "chartaxis_p.h" -#include "qabstractaxis.h" -#include "qabstractaxis_p.h" -#include "chartpresenter_p.h" -#include "chartlayout_p.h" -#include "abstractdomain_p.h" -#include -#include -#include -#include -#include -#include - -QTCOMMERCIALCHART_BEGIN_NAMESPACE - -ChartAxis::ChartAxis(QAbstractAxis *axis, QGraphicsItem* item , bool intervalAxis) - : ChartElement(item), - m_axis(axis), - m_labelsAngle(axis->labelsAngle()), - m_grid(new QGraphicsItemGroup(item)), - m_arrow(new QGraphicsItemGroup(item)), - m_shades(new QGraphicsItemGroup(item)), - m_labels(new QGraphicsItemGroup(item)), - m_title(new QGraphicsSimpleTextItem(item)), - m_animation(0), - m_labelPadding(5), - m_intervalAxis(intervalAxis), - m_titlePadding(3) -{ - Q_ASSERT(item); - //initial initialization - m_arrow->setHandlesChildEvents(false); - m_arrow->setZValue(ChartPresenter::AxisZValue); - m_labels->setZValue(ChartPresenter::AxisZValue); - m_shades->setZValue(ChartPresenter::ShadesZValue); - m_grid->setZValue(ChartPresenter::GridZValue); - m_title->setZValue(ChartPresenter::GridZValue); - handleVisibleChanged(m_axis->isVisible()); - connectSlots(); - - setFlag(QGraphicsItem::ItemHasNoContents,true); -} - -void ChartAxis::connectSlots() -{ - QObject::connect(m_axis,SIGNAL(visibleChanged(bool)),this,SLOT(handleVisibleChanged(bool))); - QObject::connect(m_axis,SIGNAL(lineVisibleChanged(bool)),this,SLOT(handleArrowVisibleChanged(bool))); - QObject::connect(m_axis,SIGNAL(gridVisibleChanged(bool)),this,SLOT(handleGridVisibleChanged(bool))); - QObject::connect(m_axis,SIGNAL(labelsVisibleChanged(bool)),this,SLOT(handleLabelsVisibleChanged(bool))); - QObject::connect(m_axis,SIGNAL(shadesVisibleChanged(bool)),this,SLOT(handleShadesVisibleChanged(bool))); - QObject::connect(m_axis,SIGNAL(labelsAngleChanged(int)),this,SLOT(handleLabelsAngleChanged(int))); - QObject::connect(m_axis,SIGNAL(linePenChanged(const QPen&)),this,SLOT(handleArrowPenChanged(const QPen&))); - QObject::connect(m_axis,SIGNAL(labelsPenChanged(const QPen&)),this,SLOT(handleLabelsPenChanged(const QPen&))); - QObject::connect(m_axis,SIGNAL(labelsBrushChanged(const QBrush&)),this,SLOT(handleLabelsBrushChanged(const QBrush&))); - QObject::connect(m_axis,SIGNAL(labelsFontChanged(const QFont&)),this,SLOT(handleLabelsFontChanged(const QFont&))); - QObject::connect(m_axis,SIGNAL(gridLinePenChanged(const QPen&)),this,SLOT(handleGridPenChanged(const QPen&))); - QObject::connect(m_axis,SIGNAL(shadesPenChanged(const QPen&)),this,SLOT(handleShadesPenChanged(const QPen&))); - QObject::connect(m_axis,SIGNAL(shadesBrushChanged(const QBrush&)),this,SLOT(handleShadesBrushChanged(const QBrush&))); - QObject::connect(m_axis,SIGNAL(titleTextChanged(const QString&)),this,SLOT(handleTitleTextChanged(const QString&))); - QObject::connect(m_axis,SIGNAL(titleFontChanged(const QFont&)),this,SLOT(handleTitleFontChanged(const QFont&))); - QObject::connect(m_axis,SIGNAL(titlePenChanged(const QPen&)),this,SLOT(handleTitlePenChanged(const QPen&))); - QObject::connect(m_axis,SIGNAL(titleBrushChanged(const QBrush&)),this,SLOT(handleTitleBrushChanged(const QBrush&))); - QObject::connect(m_axis,SIGNAL(titleVisibleChanged(bool)),this,SLOT(handleTitleVisibleChanged(bool))); - QObject::connect(m_axis->d_ptr.data(),SIGNAL(rangeChanged(qreal,qreal)),this,SLOT(handleRangeChanged(qreal,qreal))); -} - -ChartAxis::~ChartAxis() -{ -} - -void ChartAxis::setAnimation(AxisAnimation *animation) -{ - m_animation = animation; -} - -void ChartAxis::setLayout(QVector &layout) -{ - m_layoutVector = layout; -} - -void ChartAxis::createItems(int count) -{ - if (m_arrow->childItems().size() == 0){ - QGraphicsLineItem* arrow = new ArrowItem(this, this); - arrow->setPen(m_axis->linePen()); - m_arrow->addToGroup(arrow); - } - - if (m_intervalAxis && m_grid->childItems().size() == 0) { - for (int i = 0 ; i < 2 ; i ++){ - QGraphicsLineItem* item = new QGraphicsLineItem(this); - item->setPen(m_axis->gridLinePen()); - m_grid->addToGroup(item); - } - } - - for (int i = 0; i < count; ++i) { - QGraphicsLineItem* arrow = new QGraphicsLineItem(this); - arrow->setPen(m_axis->linePen()); - QGraphicsLineItem* grid = new QGraphicsLineItem(this); - grid->setPen(m_axis->gridLinePen()); - QGraphicsSimpleTextItem* label = new QGraphicsSimpleTextItem(this); - label->setFont(m_axis->labelsFont()); - label->setPen(m_axis->labelsPen()); - label->setBrush(m_axis->labelsBrush()); - label->setRotation(m_labelsAngle); - m_arrow->addToGroup(arrow); - m_grid->addToGroup(grid); - m_labels->addToGroup(label); - - if ((m_grid->childItems().size()) % 2 && m_grid->childItems().size() > 2){ - QGraphicsRectItem* shades = new QGraphicsRectItem(this); - shades->setPen(m_axis->shadesPen()); - shades->setBrush(m_axis->shadesBrush()); - m_shades->addToGroup(shades); - } - } - -} - -void ChartAxis::deleteItems(int count) -{ - QList lines = m_grid->childItems(); - QList labels = m_labels->childItems(); - QList shades = m_shades->childItems(); - QList axis = m_arrow->childItems(); - - for (int i = 0; i < count; ++i) { - if (lines.size() % 2 && lines.size() > 1) - delete(shades.takeLast()); - delete(lines.takeLast()); - delete(labels.takeLast()); - delete(axis.takeLast()); - } -} - -void ChartAxis::updateLayout(QVector &layout) -{ - int diff = m_layoutVector.size() - layout.size(); - - if (diff > 0) - deleteItems(diff); - else if (diff < 0) - createItems(-diff); - - if (m_animation) { - switch (presenter()->state()) { - case ChartPresenter::ZoomInState: - m_animation->setAnimationType(AxisAnimation::ZoomInAnimation); - m_animation->setAnimationPoint(presenter()->statePoint()); - break; - case ChartPresenter::ZoomOutState: - m_animation->setAnimationType(AxisAnimation::ZoomOutAnimation); - m_animation->setAnimationPoint(presenter()->statePoint()); - break; - case ChartPresenter::ScrollUpState: - case ChartPresenter::ScrollLeftState: - m_animation->setAnimationType(AxisAnimation::MoveBackwordAnimation); - break; - case ChartPresenter::ScrollDownState: - case ChartPresenter::ScrollRightState: - m_animation->setAnimationType(AxisAnimation::MoveForwardAnimation); - break; - case ChartPresenter::ShowState: - m_animation->setAnimationType(AxisAnimation::DefaultAnimation); - break; - } - m_animation->setValues(m_layoutVector, layout); - presenter()->startAnimation(m_animation); - } else { - setLayout(layout); - updateGeometry(); - } -} - -void ChartAxis::setLabelPadding(int padding) -{ - m_labelPadding = padding; -} - -void ChartAxis::setTitlePadding(int padding) -{ - m_titlePadding = padding; -} - -bool ChartAxis::isEmpty() -{ - return m_axisRect.isEmpty() || m_gridRect.isEmpty() || qFuzzyCompare(min(),max()); -} - -void ChartAxis::setGeometry(const QRectF &axis, const QRectF &grid) -{ - m_gridRect = grid; - m_axisRect = axis; - - if (isEmpty()) - return; - - QVector layout = calculateLayout(); - updateLayout(layout); -} - -qreal ChartAxis::min() const -{ - return m_axis->d_ptr->min(); -} - -qreal ChartAxis::max() const -{ - return m_axis->d_ptr->max(); -} - -QFont ChartAxis::font() const -{ - return m_axis->labelsFont(); -} - -QFont ChartAxis::titleFont() const -{ - return m_axis->titleFont(); -} - -QString ChartAxis::titleText() const -{ - return m_axis->titleText(); -} - -void ChartAxis::axisSelected() -{ - emit clicked(); -} - -Qt::Orientation ChartAxis::orientation() const -{ - return m_axis->orientation(); -} - -Qt::Alignment ChartAxis::alignment() const -{ - return m_axis->alignment(); -} - -void ChartAxis::setLabels(const QStringList &labels) -{ - m_labelsList = labels; -} - -QSizeF ChartAxis::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const -{ - Q_UNUSED(which); - Q_UNUSED(constraint); - return QSizeF(); -} - -//handlers - -void ChartAxis::handleArrowVisibleChanged(bool visible) -{ - m_arrow->setVisible(visible); -} - -void ChartAxis::handleGridVisibleChanged(bool visible) -{ - m_grid->setVisible(visible); -} - -void ChartAxis::handleLabelsVisibleChanged(bool visible) -{ - m_labels->setVisible(visible); -} - -void ChartAxis::handleShadesVisibleChanged(bool visible) -{ - m_shades->setVisible(visible); -} - -void ChartAxis::handleTitleVisibleChanged(bool visible) -{ - QGraphicsLayoutItem::updateGeometry(); - presenter()->layout()->invalidate(); - m_title->setVisible(visible); -} - -void ChartAxis::handleLabelsAngleChanged(int angle) -{ - foreach (QGraphicsItem *item, m_labels->childItems()) - item->setRotation(angle); - - m_labelsAngle = angle; -} - -void ChartAxis::handleLabelsPenChanged(const QPen &pen) -{ - foreach (QGraphicsItem *item , m_labels->childItems()) - static_cast(item)->setPen(pen); -} - -void ChartAxis::handleLabelsBrushChanged(const QBrush &brush) -{ - foreach (QGraphicsItem *item , m_labels->childItems()) - static_cast(item)->setBrush(brush); -} - -void ChartAxis::handleLabelsFontChanged(const QFont &font) -{ - foreach (QGraphicsItem *item , m_labels->childItems()) - static_cast(item)->setFont(font); - QGraphicsLayoutItem::updateGeometry(); - presenter()->layout()->invalidate(); -} - -void ChartAxis::handleShadesBrushChanged(const QBrush &brush) -{ - foreach (QGraphicsItem *item , m_shades->childItems()) - static_cast(item)->setBrush(brush); -} - -void ChartAxis::handleShadesPenChanged(const QPen &pen) -{ - foreach (QGraphicsItem *item , m_shades->childItems()) - static_cast(item)->setPen(pen); -} - -void ChartAxis::handleArrowPenChanged(const QPen &pen) -{ - foreach (QGraphicsItem *item , m_arrow->childItems()) - static_cast(item)->setPen(pen); -} - -void ChartAxis::handleGridPenChanged(const QPen &pen) -{ - foreach (QGraphicsItem *item , m_grid->childItems()) - static_cast(item)->setPen(pen); -} - -void ChartAxis::handleTitleTextChanged(const QString &title) -{ - QGraphicsLayoutItem::updateGeometry(); - presenter()->layout()->invalidate(); - m_title->setText(title); -} - - -void ChartAxis::handleTitlePenChanged(const QPen &pen) -{ - m_title->setPen(pen); -} - -void ChartAxis::handleTitleBrushChanged(const QBrush &brush) -{ - m_title->setBrush(brush); -} - -void ChartAxis::handleTitleFontChanged(const QFont &font) -{ - if(m_title->font() != font){ - m_title->setFont(font); - QGraphicsLayoutItem::updateGeometry(); - presenter()->layout()->invalidate(); - } -} - -void ChartAxis::handleVisibleChanged(bool visible) -{ - setVisible(visible); - if(!visible) { - m_grid->setVisible(visible); - m_arrow->setVisible(visible); - m_shades->setVisible(visible); - m_labels->setVisible(visible); - m_title->setVisible(visible); - }else { - m_grid->setVisible(m_axis->isGridLineVisible()); - m_arrow->setVisible(m_axis->isLineVisible()); - m_shades->setVisible(m_axis->shadesVisible()); - m_labels->setVisible(m_axis->labelsVisible()); - m_title->setVisible(m_axis->isTitleVisible()); - } - - if(presenter()) presenter()->layout()->invalidate(); -} - -void ChartAxis::handleRangeChanged(qreal min, qreal max) -{ - Q_UNUSED(min); - Q_UNUSED(max); - - if (!isEmpty()) { - - QVector layout = calculateLayout(); - updateLayout(layout); - QSizeF before = effectiveSizeHint(Qt::PreferredSize); - QSizeF after = sizeHint(Qt::PreferredSize); - - if (before != after) { - QGraphicsLayoutItem::updateGeometry(); - //we don't want to call invalidate on layout, since it will change minimum size of component, - //which we would like to avoid since it causes nasty flips when scrolling or zooming, - //instead recalculate layout and use plotArea for extra space. - presenter()->layout()->setGeometry(presenter()->layout()->geometry()); - } - } - -} - -//helpers - -QStringList ChartAxis::createValueLabels(qreal min, qreal max, int ticks,const QString& format) -{ - QStringList labels; - - if (max <= min || ticks < 1) - return labels; - - int n = qMax(int(-qFloor(log10((max - min) / (ticks - 1)))), 0); - n++; - - if (format.isNull()) { - for (int i = 0; i < ticks; i++) { - qreal value = min + (i * (max - min) / (ticks - 1)); - labels << QString::number(value, 'f', n); - } - } else { - QByteArray array = format.toLatin1(); - for (int i = 0; i < ticks; i++) { - qreal value = min + (i * (max - min) / (ticks - 1)); - if (format.contains("d") - || format.contains("i") - || format.contains("c")) - labels << QString().sprintf(array, (qint64)value); - else if (format.contains("u") - || format.contains("o") - || format.contains("x", Qt::CaseInsensitive)) - labels << QString().sprintf(array, (quint64)value); - else if (format.contains("f", Qt::CaseInsensitive) - || format.contains("e", Qt::CaseInsensitive) - || format.contains("g", Qt::CaseInsensitive)) - labels << QString().sprintf(array, value); - else - labels << QString(); - } - } - - return labels; -} - -QStringList ChartAxis::createLogValueLabels(qreal min, qreal max, qreal base, int ticks, const QString& format) -{ -// Q_ASSERT(m_max > m_min); - // Q_ASSERT(ticks > 1); - - QStringList labels; - - int n = 0; - if (ticks > 1) - n = qMax(int(-qFloor(log10((max - min) / (ticks - 1)))), 0); - n++; - - int firstTick; - if (base > 1) - firstTick = ceil(log10(min) / log10(base)); - else - firstTick = ceil(log10(max) / log10(base)); - - if (format.isNull()) { - for (int i = firstTick; i < ticks + firstTick; i++) { - qreal value = qPow(base, i); - labels << QString::number(value, 'f', n); - } - } else { - QByteArray array = format.toLatin1(); - for (int i = firstTick; i < ticks + firstTick; i++) { - qreal value = qPow(base, i); - if (format.contains("d") - || format.contains("i") - || format.contains("c")) - labels << QString().sprintf(array, (qint64)value); - else if (format.contains("u") - || format.contains("o") - || format.contains("x", Qt::CaseInsensitive)) - labels << QString().sprintf(array, (quint64)value); - else if (format.contains("f", Qt::CaseInsensitive) - || format.contains("e", Qt::CaseInsensitive) - || format.contains("g", Qt::CaseInsensitive)) - labels << QString().sprintf(array, value); - else - labels << QString(); - } - } - - return labels; -} - -QStringList ChartAxis::createDateTimeLabels(qreal min, qreal max,int ticks,const QString& format) -{ - QStringList labels; - - if (max <= min || ticks < 1) { - return labels; - } - - int n = qMax(int(-floor(log10((max - min) / (ticks - 1)))), 0); - n++; - for (int i = 0; i < ticks; i++) { - qreal value = min + (i * (max - min) / (ticks - 1)); - labels << QDateTime::fromMSecsSinceEpoch(value).toString(format); - } - return labels; -} - -QRect ChartAxis::labelBoundingRect(const QFontMetrics &fn, const QString &label) const -{ - QRect boundingRect = fn.boundingRect(label); - - // Take label rotation into account - if (m_labelsAngle) { - QTransform transform; - transform.rotate(m_labelsAngle); - boundingRect = transform.mapRect(boundingRect); - } - - return boundingRect; -} - -#include "moc_chartaxis_p.cpp" - -QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/chartaxiselement.cpp b/src/axis/chartaxiselement.cpp new file mode 100644 index 0000000..19fa8e1 --- /dev/null +++ b/src/axis/chartaxiselement.cpp @@ -0,0 +1,349 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "chartaxiselement_p.h" +#include "qabstractaxis_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" +#include +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +ChartAxisElement::ChartAxisElement(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis) + : ChartElement(item), + m_axis(axis), + m_animation(0), + m_grid(new QGraphicsItemGroup(item)), + m_arrow(new QGraphicsItemGroup(item)), + m_shades(new QGraphicsItemGroup(item)), + m_labels(new QGraphicsItemGroup(item)), + m_title(new QGraphicsSimpleTextItem(item)), + m_intervalAxis(intervalAxis) + +{ + //initial initialization + m_arrow->setHandlesChildEvents(false); + m_arrow->setZValue(ChartPresenter::AxisZValue); + m_labels->setZValue(ChartPresenter::AxisZValue); + m_shades->setZValue(ChartPresenter::ShadesZValue); + m_grid->setZValue(ChartPresenter::GridZValue); + m_title->setZValue(ChartPresenter::GridZValue); + handleVisibleChanged(axis->isVisible()); + connectSlots(); + + setFlag(QGraphicsItem::ItemHasNoContents, true); +} + +ChartAxisElement::~ChartAxisElement() +{ +} + +void ChartAxisElement::connectSlots() +{ + QObject::connect(axis(), SIGNAL(visibleChanged(bool)), this, SLOT(handleVisibleChanged(bool))); + QObject::connect(axis(), SIGNAL(lineVisibleChanged(bool)), this, SLOT(handleArrowVisibleChanged(bool))); + QObject::connect(axis(), SIGNAL(gridVisibleChanged(bool)), this, SLOT(handleGridVisibleChanged(bool))); + QObject::connect(axis(), SIGNAL(labelsVisibleChanged(bool)), this, SLOT(handleLabelsVisibleChanged(bool))); + QObject::connect(axis(), SIGNAL(shadesVisibleChanged(bool)), this, SLOT(handleShadesVisibleChanged(bool))); + QObject::connect(axis(), SIGNAL(labelsAngleChanged(int)), this, SLOT(handleLabelsAngleChanged(int))); + QObject::connect(axis(), SIGNAL(linePenChanged(const QPen&)), this, SLOT(handleArrowPenChanged(const QPen&))); + QObject::connect(axis(), SIGNAL(labelsPenChanged(const QPen&)), this, SLOT(handleLabelsPenChanged(const QPen&))); + QObject::connect(axis(), SIGNAL(labelsBrushChanged(const QBrush&)), this, SLOT(handleLabelsBrushChanged(const QBrush&))); + QObject::connect(axis(), SIGNAL(labelsFontChanged(const QFont&)), this, SLOT(handleLabelsFontChanged(const QFont&))); + QObject::connect(axis(), SIGNAL(gridLinePenChanged(const QPen&)), this, SLOT(handleGridPenChanged(const QPen&))); + QObject::connect(axis(), SIGNAL(shadesPenChanged(const QPen&)), this, SLOT(handleShadesPenChanged(const QPen&))); + QObject::connect(axis(), SIGNAL(shadesBrushChanged(const QBrush&)), this, SLOT(handleShadesBrushChanged(const QBrush&))); + QObject::connect(axis(), SIGNAL(titleTextChanged(const QString&)), this, SLOT(handleTitleTextChanged(const QString&))); + QObject::connect(axis(), SIGNAL(titleFontChanged(const QFont&)), this, SLOT(handleTitleFontChanged(const QFont&))); + QObject::connect(axis(), SIGNAL(titlePenChanged(const QPen&)), this, SLOT(handleTitlePenChanged(const QPen&))); + QObject::connect(axis(), SIGNAL(titleBrushChanged(const QBrush&)), this, SLOT(handleTitleBrushChanged(const QBrush&))); + QObject::connect(axis(), SIGNAL(titleVisibleChanged(bool)), this, SLOT(handleTitleVisibleChanged(bool))); + QObject::connect(axis()->d_ptr.data(), SIGNAL(rangeChanged(qreal, qreal)), this, SLOT(handleRangeChanged(qreal, qreal))); +} + +void ChartAxisElement::handleArrowVisibleChanged(bool visible) +{ + m_arrow->setVisible(visible); +} + +void ChartAxisElement::handleGridVisibleChanged(bool visible) +{ + m_grid->setVisible(visible); +} + +void ChartAxisElement::handleLabelsVisibleChanged(bool visible) +{ + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); + m_labels->setVisible(visible); +} + +void ChartAxisElement::handleShadesVisibleChanged(bool visible) +{ + m_shades->setVisible(visible); +} + +void ChartAxisElement::handleTitleVisibleChanged(bool visible) +{ + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); + m_title->setVisible(visible); +} + +void ChartAxisElement::handleLabelsAngleChanged(int angle) +{ + foreach (QGraphicsItem *item, m_labels->childItems()) + item->setRotation(angle); + + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); +} + +void ChartAxisElement::handleLabelsPenChanged(const QPen &pen) +{ + foreach (QGraphicsItem *item, m_labels->childItems()) + static_cast(item)->setPen(pen); +} + +void ChartAxisElement::handleLabelsBrushChanged(const QBrush &brush) +{ + foreach (QGraphicsItem *item, m_labels->childItems()) + static_cast(item)->setBrush(brush); +} + +void ChartAxisElement::handleLabelsFontChanged(const QFont &font) +{ + foreach (QGraphicsItem *item, m_labels->childItems()) + static_cast(item)->setFont(font); + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); +} + +void ChartAxisElement::handleTitleTextChanged(const QString &title) +{ + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); + m_title->setText(title); +} + +void ChartAxisElement::handleTitlePenChanged(const QPen &pen) +{ + m_title->setPen(pen); +} + +void ChartAxisElement::handleTitleBrushChanged(const QBrush &brush) +{ + m_title->setBrush(brush); +} + +void ChartAxisElement::handleTitleFontChanged(const QFont &font) +{ + if (m_title->font() != font) { + m_title->setFont(font); + QGraphicsLayoutItem::updateGeometry(); + presenter()->layout()->invalidate(); + } +} + +void ChartAxisElement::handleVisibleChanged(bool visible) +{ + setVisible(visible); + if (!visible) { + m_grid->setVisible(visible); + m_arrow->setVisible(visible); + m_shades->setVisible(visible); + m_labels->setVisible(visible); + m_title->setVisible(visible); + } else { + m_grid->setVisible(axis()->isGridLineVisible()); + m_arrow->setVisible(axis()->isLineVisible()); + m_shades->setVisible(axis()->shadesVisible()); + m_labels->setVisible(axis()->labelsVisible()); + m_title->setVisible(axis()->isTitleVisible()); + } + + if (presenter()) presenter()->layout()->invalidate(); +} + +QRect ChartAxisElement::labelBoundingRect(const QFontMetrics &fn, const QString &label) const +{ + QRect boundingRect = fn.boundingRect(label); + // Take label rotation into account + if (axis()->labelsAngle()) { + QTransform transform; + transform.rotate(axis()->labelsAngle()); + boundingRect = transform.mapRect(boundingRect); + } + + return boundingRect; +} + +void ChartAxisElement::handleRangeChanged(qreal min, qreal max) +{ + Q_UNUSED(min); + Q_UNUSED(max); + + if (!isEmpty()) { + QVector layout = calculateLayout(); + updateLayout(layout); + QSizeF before = effectiveSizeHint(Qt::PreferredSize); + QSizeF after = sizeHint(Qt::PreferredSize); + + if (before != after) { + QGraphicsLayoutItem::updateGeometry(); + // We don't want to call invalidate on layout, since it will change minimum size of + // component, which we would like to avoid since it causes nasty flips when scrolling + // or zooming, instead recalculate layout and use plotArea for extra space. + presenter()->layout()->setGeometry(presenter()->layout()->geometry()); + } + } +} + +bool ChartAxisElement::isEmpty() +{ + return axisGeometry().isEmpty() + || gridGeometry().isEmpty() + || qFuzzyCompare(min(), max()); +} + +qreal ChartAxisElement::min() const +{ + return m_axis->d_ptr->min(); +} + +qreal ChartAxisElement::max() const +{ + return m_axis->d_ptr->max(); +} + +QStringList ChartAxisElement::createValueLabels(qreal min, qreal max, int ticks, const QString &format) +{ + QStringList labels; + + if (max <= min || ticks < 1) + return labels; + + int n = qMax(int(-qFloor(log10((max - min) / (ticks - 1)))), 0); + n++; + + if (format.isNull()) { + for (int i = 0; i < ticks; i++) { + qreal value = min + (i * (max - min) / (ticks - 1)); + labels << QString::number(value, 'f', n); + } + } else { + QByteArray array = format.toLatin1(); + for (int i = 0; i < ticks; i++) { + qreal value = min + (i * (max - min) / (ticks - 1)); + if (format.contains("d") + || format.contains("i") + || format.contains("c")) { + labels << QString().sprintf(array, (qint64)value); + } else if (format.contains("u") + || format.contains("o") + || format.contains("x", Qt::CaseInsensitive)) { + labels << QString().sprintf(array, (quint64)value); + } else if (format.contains("f", Qt::CaseInsensitive) + || format.contains("e", Qt::CaseInsensitive) + || format.contains("g", Qt::CaseInsensitive)) { + labels << QString().sprintf(array, value); + } else { + labels << QString(); + } + } + } + + return labels; +} + +QStringList ChartAxisElement::createLogValueLabels(qreal min, qreal max, qreal base, int ticks, const QString &format) +{ + QStringList labels; + + if (max <= min || ticks < 1) + return labels; + + int n = 0; + if (ticks > 1) + n = qMax(int(-qFloor(log10((max - min) / (ticks - 1)))), 0); + n++; + + int firstTick; + if (base > 1) + firstTick = ceil(log10(min) / log10(base)); + else + firstTick = ceil(log10(max) / log10(base)); + + if (format.isNull()) { + for (int i = firstTick; i < ticks + firstTick; i++) { + qreal value = qPow(base, i); + labels << QString::number(value, 'f', n); + } + } else { + QByteArray array = format.toLatin1(); + for (int i = firstTick; i < ticks + firstTick; i++) { + qreal value = qPow(base, i); + if (format.contains("d") + || format.contains("i") + || format.contains("c")) { + labels << QString().sprintf(array, (qint64)value); + } else if (format.contains("u") + || format.contains("o") + || format.contains("x", Qt::CaseInsensitive)) { + labels << QString().sprintf(array, (quint64)value); + } else if (format.contains("f", Qt::CaseInsensitive) + || format.contains("e", Qt::CaseInsensitive) + || format.contains("g", Qt::CaseInsensitive)) { + labels << QString().sprintf(array, value); + } else { + labels << QString(); + } + } + } + + return labels; +} + +QStringList ChartAxisElement::createDateTimeLabels(qreal min, qreal max,int ticks,const QString &format) +{ + QStringList labels; + + if (max <= min || ticks < 1) + return labels; + + int n = qMax(int(-floor(log10((max - min) / (ticks - 1)))), 0); + n++; + for (int i = 0; i < ticks; i++) { + qreal value = min + (i * (max - min) / (ticks - 1)); + labels << QDateTime::fromMSecsSinceEpoch(value).toString(format); + } + return labels; +} + +void ChartAxisElement::axisSelected() +{ + emit clicked(); +} + +#include "moc_chartaxiselement_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/chartaxis_p.h b/src/axis/chartaxiselement_p.h similarity index 61% rename from src/axis/chartaxis_p.h rename to src/axis/chartaxiselement_p.h index 681613f..0fdd207 100644 --- a/src/axis/chartaxis_p.h +++ b/src/axis/chartaxiselement_p.h @@ -27,8 +27,8 @@ // // We mean it. -#ifndef CHARTAXIS_H -#define CHARTAXIS_H +#ifndef CHARTAXISELEMENT_H +#define CHARTAXISELEMENT_H #include "qchartglobal.h" #include "chartelement_p.h" @@ -39,163 +39,113 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -class QAbstractAxis; class ChartPresenter; +class QAbstractAxis; -class ChartAxis : public ChartElement, public QGraphicsLayoutItem +class ChartAxisElement : public ChartElement, public QGraphicsLayoutItem { Q_OBJECT - Q_INTERFACES(QGraphicsLayoutItem) public: + ChartAxisElement(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis = false); + ~ChartAxisElement(); + + virtual QRectF gridGeometry() const = 0; + virtual void setGeometry(const QRectF &axis, const QRectF &grid) = 0; + virtual bool isEmpty() = 0; + + void setAnimation(AxisAnimation *animation) { m_animation = animation; } + AxisAnimation *animation() const { return m_animation; } + + QAbstractAxis *axis() const { return m_axis; } + void setLayout(QVector &layout) { m_layout = layout; } + QVector &layout() { return m_layout; } // Modifiable reference + int labelPadding() const { return 5; } + int titlePadding() const { return 3; } + void setLabels(const QStringList &labels) { m_labelsList = labels; } + QStringList labels() const { return m_labelsList; } - ChartAxis(QAbstractAxis *axis, QGraphicsItem* item = 0, bool intervalAxis = false); - ~ChartAxis(); - - QAbstractAxis* axis() const { return m_axis; } - - void setLabelPadding(int padding); - int labelPadding() const { return m_labelPadding;}; - - void setTitlePadding(int padding); - int titlePadding() const { return m_titlePadding;}; - - QFont titleFont() const; - QString titleText() const; - - void setLayout(QVector &layout); - QVector layout() const { return m_layoutVector; } - - void setAnimation(AxisAnimation *animation); - ChartAnimation *animation() const { return m_animation; }; - - Qt::Orientation orientation() const; - Qt::Alignment alignment() const; + qreal min() const; + qreal max() const; - void setGeometry(const QRectF &axis, const QRectF &grid); QRectF axisGeometry() const { return m_axisRect; } - QRectF gridGeometry() const { return m_gridRect; } + void setAxisGeometry(const QRectF &axisGeometry) { m_axisRect = axisGeometry; } - void setLabels(const QStringList &labels); - QStringList labels() const { return m_labelsList; } + QRect labelBoundingRect(const QFontMetrics &fn, const QString &label) const; + + void axisSelected(); //this flag indicates that axis is used to show intervals it means labels are in between ticks bool intervalAxis() const { return m_intervalAxis; } - virtual QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const; - + static QStringList createValueLabels(qreal max, qreal min, int ticks, const QString &format); + static QStringList createLogValueLabels(qreal min, qreal max, qreal base, int ticks, const QString &format); + static QStringList createDateTimeLabels(qreal max, qreal min, int ticks, const QString &format); - QRectF boundingRect() const{ + // from QGraphicsLayoutItem + QRectF boundingRect() const + { return QRectF(); } + // from QGraphicsLayoutItem void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*) { - } -//helpers - static QStringList createValueLabels(qreal max, qreal min, int ticks, const QString &format); - static QStringList createLogValueLabels(qreal min, qreal max, qreal base, int ticks, const QString& format); - static QStringList createDateTimeLabels(qreal max, qreal min, int ticks, const QString &format); - protected: - void setGeometry(const QRectF &size) { Q_UNUSED(size);}; - virtual void updateGeometry() = 0; virtual QVector calculateLayout() const = 0; + virtual void updateLayout(QVector &layout) = 0; + + QList gridItems() { return m_grid->childItems(); } + QList labelItems() { return m_labels->childItems(); } + QList shadeItems() { return m_shades->childItems(); } + QList arrowItems() { return m_arrow->childItems(); } + QGraphicsSimpleTextItem *titleItem() const { return m_title.data(); } + QGraphicsItemGroup *gridGroup() { return m_grid.data(); } + QGraphicsItemGroup *labelGroup() { return m_labels.data(); } + QGraphicsItemGroup *shadeGroup() { return m_shades.data(); } + QGraphicsItemGroup *arrowGroup() { return m_arrow.data(); } - QList lineItems() { return m_grid->childItems(); }; - QList labelItems() { return m_labels->childItems();}; - QList shadeItems() { return m_shades->childItems();}; - QList arrowItems() { return m_arrow->childItems();}; - QGraphicsSimpleTextItem* titleItem() const { return m_title.data();} - - QFont font() const; - qreal min() const; - qreal max() const; - QRect labelBoundingRect(const QFontMetrics &fn, const QString &label) const; - -//handlers public Q_SLOTS: - void handleVisibleChanged(bool visible); - void handleArrowVisibleChanged(bool visible); - void handleGridVisibleChanged(bool visible); - void handleLabelsVisibleChanged(bool visible); - void handleShadesVisibleChanged(bool visible); - void handleLabelsAngleChanged(int angle); - void handleShadesBrushChanged(const QBrush &brush); - void handleShadesPenChanged(const QPen &pen); - void handleArrowPenChanged(const QPen &pen); - void handleGridPenChanged(const QPen &pen); - void handleLabelsPenChanged(const QPen &pen); - void handleLabelsBrushChanged(const QBrush &brush); - void handleLabelsFontChanged(const QFont &font); + void handleVisibleChanged(bool visible); + void handleArrowVisibleChanged(bool visible); + void handleGridVisibleChanged(bool visible); + void handleLabelsVisibleChanged(bool visible); + void handleShadesVisibleChanged(bool visible); + void handleLabelsAngleChanged(int angle); + virtual void handleShadesBrushChanged(const QBrush &brush) = 0; + virtual void handleShadesPenChanged(const QPen &pen) = 0; + virtual void handleArrowPenChanged(const QPen &pen) = 0; + virtual void handleGridPenChanged(const QPen &pen) = 0; + void handleLabelsPenChanged(const QPen &pen); + void handleLabelsBrushChanged(const QBrush &brush); + void handleLabelsFontChanged(const QFont &font); void handleTitlePenChanged(const QPen &pen); void handleTitleBrushChanged(const QBrush &brush); void handleTitleFontChanged(const QFont &font); void handleTitleTextChanged(const QString &title); void handleTitleVisibleChanged(bool visible); - void handleRangeChanged(qreal min , qreal max); + void handleRangeChanged(qreal min, qreal max); Q_SIGNALS: - void clicked(); + void clicked(); private: - inline bool isEmpty(); - void createItems(int count); - void deleteItems(int count); - void updateLayout(QVector &layout); - void axisSelected(); void connectSlots(); -private: QAbstractAxis *m_axis; - int m_labelsAngle; + AxisAnimation *m_animation; + QVector m_layout; + QStringList m_labelsList; QRectF m_axisRect; - QRectF m_gridRect; QScopedPointer m_grid; QScopedPointer m_arrow; QScopedPointer m_shades; QScopedPointer m_labels; QScopedPointer m_title; - QVector m_layoutVector; - AxisAnimation *m_animation; - int m_labelPadding; - QStringList m_labelsList; bool m_intervalAxis; - int m_titlePadding; - - friend class AxisAnimation; - friend class ArrowItem; - -}; - -class ArrowItem: public QGraphicsLineItem -{ - -public: - explicit ArrowItem(ChartAxis *axis, QGraphicsItem *parent = 0) : QGraphicsLineItem(parent), m_axis(axis) {} - -protected: - void mousePressEvent(QGraphicsSceneMouseEvent *event) { - Q_UNUSED(event) - m_axis->axisSelected(); - } - - QRectF boundingRect() const { - return shape().boundingRect(); - } - - QPainterPath shape() const { - QPainterPath path = QGraphicsLineItem::shape(); - QRectF rect = path.boundingRect(); - path.addRect(rect.adjusted(0, 0, m_axis->orientation() != Qt::Horizontal ? 8 : 0, m_axis->orientation() != Qt::Vertical ? 8 : 0)); - return path; - } - -private: - ChartAxis *m_axis; }; QTCOMMERCIALCHART_END_NAMESPACE -#endif /* CHARTAXI_H */ +#endif /* CHARTAXISELEMENT_H */ diff --git a/src/axis/datetimeaxis/chartdatetimeaxisx.cpp b/src/axis/datetimeaxis/chartdatetimeaxisx.cpp index 39fc3ba..6ff32c5 100644 --- a/src/axis/datetimeaxis/chartdatetimeaxisx.cpp +++ b/src/axis/datetimeaxis/chartdatetimeaxisx.cpp @@ -21,7 +21,7 @@ #include "chartdatetimeaxisx_p.h" #include "chartpresenter_p.h" #include "qdatetimeaxis.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -29,12 +29,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -ChartDateTimeAxisX::ChartDateTimeAxisX(QDateTimeAxis *axis, QGraphicsItem* item) +ChartDateTimeAxisX::ChartDateTimeAxisX(QDateTimeAxis *axis, QGraphicsItem *item) : HorizontalAxis(axis, item), m_axis(axis) { - QObject::connect(m_axis,SIGNAL(tickCountChanged(int)),this, SLOT(handleTickCountChanged(int))); - QObject::connect(m_axis,SIGNAL(formatChanged(QString)),this, SLOT(handleFormatChanged(QString))); + QObject::connect(m_axis, SIGNAL(tickCountChanged(int)), this, SLOT(handleTickCountChanged(int))); + QObject::connect(m_axis, SIGNAL(formatChanged(QString)), this, SLOT(handleFormatChanged(QString))); } ChartDateTimeAxisX::~ChartDateTimeAxisX() @@ -58,10 +58,10 @@ QVector ChartDateTimeAxisX::calculateLayout() const void ChartDateTimeAxisX::updateGeometry() { - const QVector& layout = ChartAxis::layout(); + const QVector& layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; - setLabels(createDateTimeLabels(min(),max(), layout.size(),m_axis->format())); + setLabels(createDateTimeLabels(min(), max(), layout.size(), m_axis->format())); HorizontalAxis::updateGeometry(); } @@ -69,25 +69,27 @@ void ChartDateTimeAxisX::handleTickCountChanged(int tick) { Q_UNUSED(tick) QGraphicsLayoutItem::updateGeometry(); - if(presenter()) presenter()->layout()->invalidate(); + if (presenter()) + presenter()->layout()->invalidate(); } void ChartDateTimeAxisX::handleFormatChanged(const QString &format) { Q_UNUSED(format); QGraphicsLayoutItem::updateGeometry(); - if(presenter()) presenter()->layout()->invalidate(); + if (presenter()) + presenter()->layout()->invalidate(); } QSizeF ChartDateTimeAxisX::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = HorizontalAxis::sizeHint(which, constraint); - QStringList ticksList = createDateTimeLabels(min(),max(),m_axis->tickCount(),m_axis->format()); + QStringList ticksList = createDateTimeLabels(min(), max(), m_axis->tickCount(), m_axis->format()); // Width of horizontal axis sizeHint indicates the maximum distance labels can extend past // first and last ticks. Base width is irrelevant. qreal width = 0; @@ -97,7 +99,7 @@ QSizeF ChartDateTimeAxisX::sizeHint(Qt::SizeHint which, const QSizeF &constraint return sh; switch (which) { - case Qt::MinimumSize:{ + case Qt::MinimumSize: { QRectF boundingRect = labelBoundingRect(fn, "..."); width = boundingRect.width() / 2.0; height = boundingRect.height() + labelPadding(); diff --git a/src/axis/datetimeaxis/chartdatetimeaxisx_p.h b/src/axis/datetimeaxis/chartdatetimeaxisx_p.h index 841ae1e..03fe98b 100644 --- a/src/axis/datetimeaxis/chartdatetimeaxisx_p.h +++ b/src/axis/datetimeaxis/chartdatetimeaxisx_p.h @@ -35,7 +35,6 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE class QDateTimeAxis; -class ChartPresenter; class ChartDateTimeAxisX : public HorizontalAxis { diff --git a/src/axis/datetimeaxis/chartdatetimeaxisy.cpp b/src/axis/datetimeaxis/chartdatetimeaxisy.cpp index a534617..a8fe58a 100644 --- a/src/axis/datetimeaxis/chartdatetimeaxisy.cpp +++ b/src/axis/datetimeaxis/chartdatetimeaxisy.cpp @@ -21,7 +21,7 @@ #include "chartdatetimeaxisy_p.h" #include "chartpresenter_p.h" #include "qdatetimeaxis.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -29,12 +29,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -ChartDateTimeAxisY::ChartDateTimeAxisY(QDateTimeAxis *axis, QGraphicsItem* item) +ChartDateTimeAxisY::ChartDateTimeAxisY(QDateTimeAxis *axis, QGraphicsItem *item) : VerticalAxis(axis, item), m_axis(axis) { - QObject::connect(m_axis,SIGNAL(tickCountChanged(int)),this, SLOT(handleTickCountChanged(int))); - QObject::connect(m_axis,SIGNAL(formatChanged(QString)),this, SLOT(handleFormatChanged(QString))); + QObject::connect(m_axis, SIGNAL(tickCountChanged(int)), this, SLOT(handleTickCountChanged(int))); + QObject::connect(m_axis, SIGNAL(formatChanged(QString)), this, SLOT(handleFormatChanged(QString))); } ChartDateTimeAxisY::~ChartDateTimeAxisY() @@ -59,10 +59,10 @@ QVector ChartDateTimeAxisY::calculateLayout() const void ChartDateTimeAxisY::updateGeometry() { - const QVector &layout = ChartAxis::layout(); + const QVector &layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; - setLabels(createDateTimeLabels(min(),max(), layout.size(),m_axis->format())); + setLabels(createDateTimeLabels(min(), max(), layout.size(), m_axis->format())); VerticalAxis::updateGeometry(); } @@ -70,25 +70,27 @@ void ChartDateTimeAxisY::handleTickCountChanged(int tick) { Q_UNUSED(tick) QGraphicsLayoutItem::updateGeometry(); - if(presenter()) presenter()->layout()->invalidate(); + if (presenter()) + presenter()->layout()->invalidate(); } void ChartDateTimeAxisY::handleFormatChanged(const QString &format) { Q_UNUSED(format); QGraphicsLayoutItem::updateGeometry(); - if(presenter()) presenter()->layout()->invalidate(); + if (presenter()) + presenter()->layout()->invalidate(); } QSizeF ChartDateTimeAxisY::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = VerticalAxis::sizeHint(which, constraint); - QStringList ticksList = createDateTimeLabels(min(),max(),m_axis->tickCount(),m_axis->format()); + QStringList ticksList = createDateTimeLabels(min(), max(), m_axis->tickCount(), m_axis->format()); qreal width = 0; // Height of vertical axis sizeHint indicates the maximum distance labels can extend past // first and last ticks. Base height is irrelevant. diff --git a/src/axis/datetimeaxis/chartdatetimeaxisy_p.h b/src/axis/datetimeaxis/chartdatetimeaxisy_p.h index 0bbe0d5..be3adc5 100644 --- a/src/axis/datetimeaxis/chartdatetimeaxisy_p.h +++ b/src/axis/datetimeaxis/chartdatetimeaxisy_p.h @@ -35,7 +35,6 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE class QDateTimeAxis; -class ChartPresenter; class ChartDateTimeAxisY : public VerticalAxis { diff --git a/src/axis/datetimeaxis/polarchartdatetimeaxisangular.cpp b/src/axis/datetimeaxis/polarchartdatetimeaxisangular.cpp new file mode 100644 index 0000000..6b1d86a --- /dev/null +++ b/src/axis/datetimeaxis/polarchartdatetimeaxisangular.cpp @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartdatetimeaxisangular_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" +#include "qdatetimeaxis.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartDateTimeAxisAngular::PolarChartDateTimeAxisAngular(QDateTimeAxis *axis, QGraphicsItem *item) + : PolarChartAxisAngular(axis, item) +{ + QObject::connect(axis, SIGNAL(tickCountChanged(int)), this, SLOT(handleTickCountChanged(int))); + QObject::connect(axis, SIGNAL(formatChanged(QString)), this, SLOT(handleFormatChanged(QString))); +} + +PolarChartDateTimeAxisAngular::~PolarChartDateTimeAxisAngular() +{ +} + +QVector PolarChartDateTimeAxisAngular::calculateLayout() const +{ + int tickCount = static_cast(axis())->tickCount(); + Q_ASSERT(tickCount >= 2); + + QVector points; + points.resize(tickCount); + + const qreal d = 360.0 / qreal(tickCount - 1); + + for (int i = 0; i < tickCount; ++i) { + qreal angularCoordinate = qreal(i) * d; + points[i] = angularCoordinate; + } + + return points; +} +void PolarChartDateTimeAxisAngular::createAxisLabels(const QVector &layout) +{ + QStringList labelList = createDateTimeLabels(min(), max(), layout.size(), static_cast(axis())->format()); + setLabels(labelList); +} + +void PolarChartDateTimeAxisAngular::handleTickCountChanged(int tick) +{ + Q_UNUSED(tick); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +void PolarChartDateTimeAxisAngular::handleFormatChanged(const QString &format) +{ + Q_UNUSED(format); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +#include "moc_polarchartdatetimeaxisangular_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/datetimeaxis/polarchartdatetimeaxisangular_p.h b/src/axis/datetimeaxis/polarchartdatetimeaxisangular_p.h new file mode 100644 index 0000000..9a7ecff --- /dev/null +++ b/src/axis/datetimeaxis/polarchartdatetimeaxisangular_p.h @@ -0,0 +1,56 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTDATETIMEAXISANGULAR_P_H +#define POLARCHARTDATETIMEAXISANGULAR_P_H + +#include "polarchartaxisangular_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QDateTimeAxis; + +class PolarChartDateTimeAxisAngular : public PolarChartAxisAngular +{ + Q_OBJECT +public: + PolarChartDateTimeAxisAngular(QDateTimeAxis *axis, QGraphicsItem *item); + ~PolarChartDateTimeAxisAngular(); + + virtual QVector calculateLayout() const; + virtual void createAxisLabels(const QVector &layout); + +private Q_SLOTS: + void handleTickCountChanged(int tick); + void handleFormatChanged(const QString &format); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTDATETIMEAXISANGULAR_P_H diff --git a/src/axis/datetimeaxis/polarchartdatetimeaxisradial.cpp b/src/axis/datetimeaxis/polarchartdatetimeaxisradial.cpp new file mode 100644 index 0000000..bcd0428 --- /dev/null +++ b/src/axis/datetimeaxis/polarchartdatetimeaxisradial.cpp @@ -0,0 +1,79 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartdatetimeaxisradial_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" +#include "qdatetimeaxis.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartDateTimeAxisRadial::PolarChartDateTimeAxisRadial(QDateTimeAxis *axis, QGraphicsItem *item) + : PolarChartAxisRadial(axis, item) +{ + QObject::connect(axis, SIGNAL(tickCountChanged(int)), this, SLOT(handleTickCountChanged(int))); + QObject::connect(axis, SIGNAL(formatChanged(QString)), this, SLOT(handleFormatChanged(QString))); +} + +PolarChartDateTimeAxisRadial::~PolarChartDateTimeAxisRadial() +{ +} + +QVector PolarChartDateTimeAxisRadial::calculateLayout() const +{ + int tickCount = static_cast(axis())->tickCount(); + Q_ASSERT(tickCount >= 2); + + QVector points; + points.resize(tickCount); + + const qreal d = (axisGeometry().width() / 2) / qreal(tickCount - 1); + + for (int i = 0; i < tickCount; ++i) { + qreal radialCoordinate = qreal(i) * d; + points[i] = radialCoordinate; + } + + return points; +} +void PolarChartDateTimeAxisRadial::createAxisLabels(const QVector &layout) +{ + setLabels(createDateTimeLabels(min(), max(), layout.size(), static_cast(axis())->format())); +} + +void PolarChartDateTimeAxisRadial::handleTickCountChanged(int tick) +{ + Q_UNUSED(tick); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +void PolarChartDateTimeAxisRadial::handleFormatChanged(const QString &format) +{ + Q_UNUSED(format); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +#include "moc_polarchartdatetimeaxisradial_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/datetimeaxis/polarchartdatetimeaxisradial_p.h b/src/axis/datetimeaxis/polarchartdatetimeaxisradial_p.h new file mode 100644 index 0000000..e10e919 --- /dev/null +++ b/src/axis/datetimeaxis/polarchartdatetimeaxisradial_p.h @@ -0,0 +1,56 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTDATETIMEAXISRADIAL_P_H +#define POLARCHARTDATETIMEAXISRADIAL_P_H + +#include "polarchartaxisradial_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QDateTimeAxis; + +class PolarChartDateTimeAxisRadial : public PolarChartAxisRadial +{ + Q_OBJECT +public: + PolarChartDateTimeAxisRadial(QDateTimeAxis *axis, QGraphicsItem *item); + ~PolarChartDateTimeAxisRadial(); + + virtual QVector calculateLayout() const; + virtual void createAxisLabels(const QVector &layout); + +private Q_SLOTS: + void handleTickCountChanged(int tick); + void handleFormatChanged(const QString &format); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTDATETIMEAXISRADIAL_P_H diff --git a/src/axis/datetimeaxis/qdatetimeaxis.cpp b/src/axis/datetimeaxis/qdatetimeaxis.cpp index 3e00ee9..ed1b4a1 100644 --- a/src/axis/datetimeaxis/qdatetimeaxis.cpp +++ b/src/axis/datetimeaxis/qdatetimeaxis.cpp @@ -22,6 +22,8 @@ #include "qdatetimeaxis_p.h" #include "chartdatetimeaxisx_p.h" #include "chartdatetimeaxisy_p.h" +#include "polarchartdatetimeaxisangular_p.h" +#include "polarchartdatetimeaxisradial_p.h" #include "abstractdomain_p.h" #include "qchart.h" #include @@ -342,11 +344,20 @@ void QDateTimeAxisPrivate::setRange(const QVariant &min, const QVariant &max) void QDateTimeAxisPrivate::initializeGraphics(QGraphicsItem* parent) { Q_Q(QDateTimeAxis); - ChartAxis* axis(0); - if (orientation() == Qt::Vertical) - axis = new ChartDateTimeAxisY(q,parent); - if (orientation() == Qt::Horizontal) - axis = new ChartDateTimeAxisX(q,parent); + ChartAxisElement *axis(0); + if (m_chart->chartType() == QChart::ChartTypeCartesian) { + if (orientation() == Qt::Vertical) + axis = new ChartDateTimeAxisY(q,parent); + if (orientation() == Qt::Horizontal) + axis = new ChartDateTimeAxisX(q,parent); + } + + if (m_chart->chartType() == QChart::ChartTypePolar) { + if (orientation() == Qt::Vertical) + axis = new PolarChartDateTimeAxisRadial(q, parent); + if (orientation() == Qt::Horizontal) + axis = new PolarChartDateTimeAxisAngular(q, parent); + } m_item.reset(axis); QAbstractAxisPrivate::initializeGraphics(parent); diff --git a/src/axis/horizontalaxis.cpp b/src/axis/horizontalaxis.cpp index 9dcd775..7068d59 100644 --- a/src/axis/horizontalaxis.cpp +++ b/src/axis/horizontalaxis.cpp @@ -19,15 +19,15 @@ ****************************************************************************/ #include "horizontalaxis_p.h" -#include "qabstractaxis.h" +#include "qabstractaxis_p.h" #include #include #include QTCOMMERCIALCHART_BEGIN_NAMESPACE -HorizontalAxis::HorizontalAxis(QAbstractAxis *axis, QGraphicsItem* item , bool intervalAxis) - : ChartAxis(axis, item, intervalAxis) +HorizontalAxis::HorizontalAxis(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis) + : CartesianChartAxis(axis, item, intervalAxis) { } @@ -37,18 +37,18 @@ HorizontalAxis::~HorizontalAxis() void HorizontalAxis::updateGeometry() { - const QVector& layout = ChartAxis::layout(); + const QVector &layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; QStringList labelList = labels(); - QList lines = lineItems(); + QList lines = gridItems(); QList labels = labelItems(); QList shades = shadeItems(); - QList axis = arrowItems(); - QGraphicsSimpleTextItem* title = titleItem(); + QList arrow = arrowItems(); + QGraphicsSimpleTextItem *title = titleItem(); Q_ASSERT(labels.size() == labelList.size()); Q_ASSERT(layout.size() == labelList.size()); @@ -57,24 +57,24 @@ void HorizontalAxis::updateGeometry() const QRectF &gridRect = gridGeometry(); //arrow - QGraphicsLineItem *arrowItem = static_cast(axis.at(0)); + QGraphicsLineItem *arrowItem = static_cast(arrow.at(0)); - if (alignment() == Qt::AlignTop) + if (axis()->alignment() == Qt::AlignTop) arrowItem->setLine(gridRect.left(), axisRect.bottom(), gridRect.right(), axisRect.bottom()); - else if (alignment() == Qt::AlignBottom) + else if (axis()->alignment() == Qt::AlignBottom) arrowItem->setLine(gridRect.left(), axisRect.top(), gridRect.right(), axisRect.top()); qreal width = 0; - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); //title int titlePad = 0; QRectF titleBoundingRect; - if (!titleText().isEmpty() && titleItem()->isVisible()) { + QString titleText = axis()->titleText(); + if (!titleText.isEmpty() && titleItem()->isVisible()) { QFontMetrics fn(title->font()); int size(0); size = gridRect.width(); - QString titleText = this->titleText(); if (fn.boundingRect(titleText).width() > size) { QString string = titleText + "..."; @@ -89,9 +89,9 @@ void HorizontalAxis::updateGeometry() titleBoundingRect = title->boundingRect(); QPointF center = gridRect.center() - titleBoundingRect.center(); - if (alignment() == Qt::AlignTop) { + if (axis()->alignment() == Qt::AlignTop) { title->setPos(center.x(), axisRect.top() + titlePad); - } else if (alignment() == Qt::AlignBottom) { + } else if (axis()->alignment() == Qt::AlignBottom) { title->setPos(center.x(), axisRect.bottom() - titleBoundingRect.height() - titlePad); } } @@ -100,7 +100,7 @@ void HorizontalAxis::updateGeometry() //items QGraphicsLineItem *gridItem = static_cast(lines.at(i)); - QGraphicsLineItem *tickItem = static_cast(axis.at(i + 1)); + QGraphicsLineItem *tickItem = static_cast(arrow.at(i + 1)); QGraphicsSimpleTextItem *labelItem = static_cast(labels.at(i)); //grid line @@ -128,10 +128,10 @@ void HorizontalAxis::updateGeometry() int heightDiff = rect.height() - boundingRect.height(); //ticks and label position - if (alignment() == Qt::AlignTop) { + if (axis()->alignment() == Qt::AlignTop) { labelItem->setPos(layout[i] - center.x(), axisRect.bottom() - rect.height() + (heightDiff / 2) - labelPadding()); tickItem->setLine(layout[i], axisRect.bottom(), layout[i], axisRect.bottom() - labelPadding()); - } else if (alignment() == Qt::AlignBottom) { + } else if (axis()->alignment() == Qt::AlignBottom) { labelItem->setPos(layout[i] - center.x(), axisRect.top() - (heightDiff / 2) + labelPadding()); tickItem->setLine(layout[i], axisRect.top(), layout[i], axisRect.top() + labelPadding()); } @@ -178,7 +178,7 @@ void HorizontalAxis::updateGeometry() if (x < gridRect.left() || x > gridRect.right()) { gridItem->setVisible(false); tickItem->setVisible(false); - }else{ + } else { gridItem->setVisible(true); tickItem->setVisible(true); } @@ -200,10 +200,10 @@ void HorizontalAxis::updateGeometry() QSizeF HorizontalAxis::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const { Q_UNUSED(constraint); - QFontMetrics fn(titleFont()); + QFontMetrics fn(axis()->titleFont()); QSizeF sh(0,0); - if (titleText().isEmpty() || !titleItem()->isVisible()) + if (axis()->titleText().isEmpty() || !titleItem()->isVisible()) return sh; switch (which) { diff --git a/src/axis/horizontalaxis_p.h b/src/axis/horizontalaxis_p.h index 02aa258..2311679 100644 --- a/src/axis/horizontalaxis_p.h +++ b/src/axis/horizontalaxis_p.h @@ -30,14 +30,14 @@ #ifndef HORIZONTALAXIS_P_H_ #define HORIZONTALAXIS_P_H_ -#include "chartaxis_p.h" +#include "cartesianchartaxis_p.h" QTCOMMERCIALCHART_BEGIN_NAMESPACE -class HorizontalAxis : public ChartAxis +class HorizontalAxis : public CartesianChartAxis { public: - HorizontalAxis(QAbstractAxis *axis, QGraphicsItem* item = 0, bool intervalAxis = false); + HorizontalAxis(QAbstractAxis *axis, QGraphicsItem *item = 0, bool intervalAxis = false); ~HorizontalAxis(); QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const; protected: diff --git a/src/axis/linearrowitem_p.h b/src/axis/linearrowitem_p.h new file mode 100644 index 0000000..1adfd3b --- /dev/null +++ b/src/axis/linearrowitem_p.h @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef LINEARROWITEM_P_H +#define LINEARROWITEM_P_H + +#include "chartaxiselement_p.h" +#include "qabstractaxis_p.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class LineArrowItem: public QGraphicsLineItem +{ +public: + explicit LineArrowItem(ChartAxisElement *axis, QGraphicsItem *parent = 0) + : QGraphicsLineItem(parent), + m_axis(axis), + m_axisOrientation(axis->axis()->orientation()) + { + } + +protected: + void mousePressEvent(QGraphicsSceneMouseEvent *event) + { + Q_UNUSED(event) + m_axis->axisSelected(); + } + + QRectF boundingRect() const + { + return shape().boundingRect(); + } + + QPainterPath shape() const + { + QPainterPath path = QGraphicsLineItem::shape(); + QRectF rect = path.boundingRect(); + path.addRect(rect.adjusted(0, 0, m_axisOrientation != Qt::Horizontal ? 8 : 0, m_axisOrientation != Qt::Vertical ? 8 : 0)); + return path; + } + +private: + ChartAxisElement *m_axis; + Qt::Orientation m_axisOrientation; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif /* LINEARROWITEM_P_H */ diff --git a/src/axis/logvalueaxis/chartlogvalueaxisx.cpp b/src/axis/logvalueaxis/chartlogvalueaxisx.cpp index 3edc6d9..ac8f1f0 100644 --- a/src/axis/logvalueaxis/chartlogvalueaxisx.cpp +++ b/src/axis/logvalueaxis/chartlogvalueaxisx.cpp @@ -21,7 +21,7 @@ #include "chartlogvalueaxisx_p.h" #include "chartpresenter_p.h" #include "qlogvalueaxis.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -29,12 +29,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -ChartLogValueAxisX::ChartLogValueAxisX(QLogValueAxis *axis, QGraphicsItem* item) +ChartLogValueAxisX::ChartLogValueAxisX(QLogValueAxis *axis, QGraphicsItem *item) : HorizontalAxis(axis, item), m_axis(axis) { - QObject::connect(m_axis,SIGNAL(baseChanged(qreal)),this, SLOT(handleBaseChanged(qreal))); - QObject::connect(m_axis,SIGNAL(labelFormatChanged(QString)),this, SLOT(handleLabelFormatChanged(QString))); + QObject::connect(m_axis, SIGNAL(baseChanged(qreal)), this, SLOT(handleBaseChanged(qreal))); + QObject::connect(m_axis, SIGNAL(labelFormatChanged(QString)), this, SLOT(handleLabelFormatChanged(QString))); } ChartLogValueAxisX::~ChartLogValueAxisX() @@ -55,14 +55,14 @@ QVector ChartLogValueAxisX::calculateLayout() const const QRectF &gridRect = gridGeometry(); const qreal deltaX = gridRect.width() / qAbs(logMax - logMin); for (int i = 0; i < tickCount; ++i) - points[i] = (ceilEdge + i) * deltaX - leftEdge * deltaX + gridRect.left(); + points[i] = (ceilEdge + qreal(i)) * deltaX - leftEdge * deltaX + gridRect.left(); return points; } void ChartLogValueAxisX::updateGeometry() { - const QVector& layout = ChartAxis::layout(); + const QVector& layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; setLabels(createLogValueLabels(m_axis->min(), m_axis->max(), m_axis->base(), layout.size(), m_axis->labelFormat())); @@ -87,7 +87,7 @@ QSizeF ChartLogValueAxisX::sizeHint(Qt::SizeHint which, const QSizeF &constraint { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = HorizontalAxis::sizeHint(which, constraint); diff --git a/src/axis/logvalueaxis/chartlogvalueaxisx_p.h b/src/axis/logvalueaxis/chartlogvalueaxisx_p.h index c0b24a5..3017e26 100644 --- a/src/axis/logvalueaxis/chartlogvalueaxisx_p.h +++ b/src/axis/logvalueaxis/chartlogvalueaxisx_p.h @@ -35,20 +35,18 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE class QLogValueAxis; -class ChartPresenter; class ChartLogValueAxisX : public HorizontalAxis { Q_OBJECT public: - ChartLogValueAxisX(QLogValueAxis *axis, QGraphicsItem* item); + ChartLogValueAxisX(QLogValueAxis *axis, QGraphicsItem *item); ~ChartLogValueAxisX(); QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint) const; protected: - void handleAxisUpdated(); QVector calculateLayout() const; void updateGeometry(); diff --git a/src/axis/logvalueaxis/chartlogvalueaxisy.cpp b/src/axis/logvalueaxis/chartlogvalueaxisy.cpp index ec474e2..42ae875 100644 --- a/src/axis/logvalueaxis/chartlogvalueaxisy.cpp +++ b/src/axis/logvalueaxis/chartlogvalueaxisy.cpp @@ -21,7 +21,7 @@ #include "chartlogvalueaxisy_p.h" #include "chartpresenter_p.h" #include "qlogvalueaxis.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -29,12 +29,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -ChartLogValueAxisY::ChartLogValueAxisY(QLogValueAxis *axis, QGraphicsItem* item) +ChartLogValueAxisY::ChartLogValueAxisY(QLogValueAxis *axis, QGraphicsItem *item) : VerticalAxis(axis, item), m_axis(axis) { - QObject::connect(m_axis, SIGNAL(baseChanged(qreal)),this, SLOT(handleBaseChanged(qreal))); - QObject::connect(m_axis,SIGNAL(labelFormatChanged(QString)),this, SLOT(handleLabelFormatChanged(QString))); + QObject::connect(m_axis, SIGNAL(baseChanged(qreal)), this, SLOT(handleBaseChanged(qreal))); + QObject::connect(m_axis, SIGNAL(labelFormatChanged(QString)), this, SLOT(handleLabelFormatChanged(QString))); } ChartLogValueAxisY::~ChartLogValueAxisY() @@ -54,7 +54,7 @@ QVector ChartLogValueAxisY::calculateLayout() const const QRectF &gridRect = gridGeometry(); const qreal deltaY = gridRect.height() / qAbs(logMax - logMin); for (int i = 0; i < tickCount; ++i) - points[i] = (ceilEdge + i) * -deltaY - leftEdge * -deltaY + gridRect.bottom(); + points[i] = (ceilEdge + qreal(i)) * -deltaY - leftEdge * -deltaY + gridRect.bottom(); return points; } @@ -62,7 +62,7 @@ QVector ChartLogValueAxisY::calculateLayout() const void ChartLogValueAxisY::updateGeometry() { - const QVector &layout = ChartAxis::layout(); + const QVector &layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; setLabels(createLogValueLabels(m_axis->min(), m_axis->max(), m_axis->base(), layout.size(), m_axis->labelFormat())); @@ -87,7 +87,7 @@ QSizeF ChartLogValueAxisY::sizeHint(Qt::SizeHint which, const QSizeF &constraint { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = VerticalAxis::sizeHint(which, constraint); diff --git a/src/axis/logvalueaxis/chartlogvalueaxisy_p.h b/src/axis/logvalueaxis/chartlogvalueaxisy_p.h index 8506cc2..3975097 100644 --- a/src/axis/logvalueaxis/chartlogvalueaxisy_p.h +++ b/src/axis/logvalueaxis/chartlogvalueaxisy_p.h @@ -35,20 +35,18 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE class QLogValueAxis; -class ChartPresenter; class ChartLogValueAxisY : public VerticalAxis { Q_OBJECT public: - ChartLogValueAxisY(QLogValueAxis *axis, QGraphicsItem* item); + ChartLogValueAxisY(QLogValueAxis *axis, QGraphicsItem *item); ~ChartLogValueAxisY(); QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint) const; protected: - void handleAxisUpdated(); QVector calculateLayout() const; void updateGeometry(); diff --git a/src/axis/logvalueaxis/polarchartlogvalueaxisangular.cpp b/src/axis/logvalueaxis/polarchartlogvalueaxisangular.cpp new file mode 100644 index 0000000..ddbd9fe --- /dev/null +++ b/src/axis/logvalueaxis/polarchartlogvalueaxisangular.cpp @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartlogvalueaxisangular_p.h" +#include "abstractchartlayout_p.h" +#include "chartpresenter_p.h" +#include "qlogvalueaxis.h" +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartLogValueAxisAngular::PolarChartLogValueAxisAngular(QLogValueAxis *axis, QGraphicsItem *item) + : PolarChartAxisAngular(axis, item) +{ + QObject::connect(axis, SIGNAL(baseChanged(qreal)), this, SLOT(handleBaseChanged(qreal))); + QObject::connect(axis, SIGNAL(labelFormatChanged(QString)), this, SLOT(handleLabelFormatChanged(QString))); +} + +PolarChartLogValueAxisAngular::~PolarChartLogValueAxisAngular() +{ +} + +QVector PolarChartLogValueAxisAngular::calculateLayout() const +{ + QLogValueAxis *logValueAxis = static_cast(axis()); + const qreal logMax = log10(logValueAxis->max()) / log10(logValueAxis->base()); + const qreal logMin = log10(logValueAxis->min()) / log10(logValueAxis->base()); + const qreal startEdge = logMin < logMax ? logMin : logMax; + const qreal delta = 360.0 / qAbs(logMax - logMin); + const qreal initialSpan = (ceil(startEdge) - startEdge) * delta; + int tickCount = qAbs(ceil(logMax) - ceil(logMin)); + + QVector points; + points.resize(tickCount); + + for (int i = 0; i < tickCount; ++i) { + qreal angularCoordinate = initialSpan + (delta * qreal(i)); + points[i] = angularCoordinate; + } + + return points; +} + +void PolarChartLogValueAxisAngular::createAxisLabels(const QVector &layout) +{ + QLogValueAxis *logValueAxis = static_cast(axis()); + setLabels(createLogValueLabels(logValueAxis->min(), + logValueAxis->max(), + logValueAxis->base(), + layout.size(), + logValueAxis->labelFormat())); +} + +void PolarChartLogValueAxisAngular::handleBaseChanged(qreal base) +{ + Q_UNUSED(base); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +void PolarChartLogValueAxisAngular::handleLabelFormatChanged(const QString &format) +{ + Q_UNUSED(format); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +#include "moc_polarchartlogvalueaxisangular_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/logvalueaxis/polarchartlogvalueaxisangular_p.h b/src/axis/logvalueaxis/polarchartlogvalueaxisangular_p.h new file mode 100644 index 0000000..231172a --- /dev/null +++ b/src/axis/logvalueaxis/polarchartlogvalueaxisangular_p.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTLOGVALUEAXISANGULAR_P_H +#define POLARCHARTLOGVALUEAXISANGULAR_P_H + +#include "polarchartaxisangular_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QLogValueAxis; + +class PolarChartLogValueAxisAngular : public PolarChartAxisAngular +{ + Q_OBJECT +public: + PolarChartLogValueAxisAngular(QLogValueAxis *axis, QGraphicsItem *item); + ~PolarChartLogValueAxisAngular(); + +protected: + virtual QVector calculateLayout() const; + virtual void createAxisLabels(const QVector &layout); + +private Q_SLOTS: + void handleBaseChanged(qreal base); + void handleLabelFormatChanged(const QString &format); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTLOGVALUEAXISANGULAR_P_H diff --git a/src/axis/logvalueaxis/polarchartlogvalueaxisradial.cpp b/src/axis/logvalueaxis/polarchartlogvalueaxisradial.cpp new file mode 100644 index 0000000..d0719db --- /dev/null +++ b/src/axis/logvalueaxis/polarchartlogvalueaxisradial.cpp @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartlogvalueaxisradial_p.h" +#include "abstractchartlayout_p.h" +#include "chartpresenter_p.h" +#include "qlogvalueaxis.h" +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartLogValueAxisRadial::PolarChartLogValueAxisRadial(QLogValueAxis *axis, QGraphicsItem *item) + : PolarChartAxisRadial(axis, item) +{ + QObject::connect(axis, SIGNAL(baseChanged(qreal)), this, SLOT(handleBaseChanged(qreal))); + QObject::connect(axis, SIGNAL(labelFormatChanged(QString)), this, SLOT(handleLabelFormatChanged(QString))); +} + +PolarChartLogValueAxisRadial::~PolarChartLogValueAxisRadial() +{ +} + +QVector PolarChartLogValueAxisRadial::calculateLayout() const +{ + QLogValueAxis *logValueAxis = static_cast(axis()); + const qreal logMax = log10(logValueAxis->max()) / log10(logValueAxis->base()); + const qreal logMin = log10(logValueAxis->min()) / log10(logValueAxis->base()); + const qreal innerEdge = logMin < logMax ? logMin : logMax; + const qreal outerEdge = logMin > logMax ? logMin : logMax; + const qreal delta = (axisGeometry().width() / 2.0) / qAbs(logMax - logMin); + const qreal initialSpan = (ceil(innerEdge) - innerEdge) * delta; + int tickCount = qAbs(ceil(logMax) - ceil(logMin)); + + // Extra tick if outer edge is exactly at the tick + if (outerEdge == ceil(outerEdge)) + tickCount++; + + QVector points; + points.resize(tickCount); + + for (int i = 0; i < tickCount; ++i) { + qreal radialCoordinate = initialSpan + (delta * qreal(i)); + points[i] = radialCoordinate; + } + + return points; +} + +void PolarChartLogValueAxisRadial::createAxisLabels(const QVector &layout) +{ + QLogValueAxis *logValueAxis = static_cast(axis()); + setLabels(createLogValueLabels(logValueAxis->min(), + logValueAxis->max(), + logValueAxis->base(), + layout.size(), + logValueAxis->labelFormat())); +} + +void PolarChartLogValueAxisRadial::handleBaseChanged(qreal base) +{ + Q_UNUSED(base); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +void PolarChartLogValueAxisRadial::handleLabelFormatChanged(const QString &format) +{ + Q_UNUSED(format); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +#include "moc_polarchartlogvalueaxisradial_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/logvalueaxis/polarchartlogvalueaxisradial_p.h b/src/axis/logvalueaxis/polarchartlogvalueaxisradial_p.h new file mode 100644 index 0000000..bc7e898 --- /dev/null +++ b/src/axis/logvalueaxis/polarchartlogvalueaxisradial_p.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTLOGVALUEAXISRADIAL_P_H +#define POLARCHARTLOGVALUEAXISRADIAL_P_H + +#include "polarchartaxisradial_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QLogValueAxis; + +class PolarChartLogValueAxisRadial : public PolarChartAxisRadial +{ + Q_OBJECT +public: + PolarChartLogValueAxisRadial(QLogValueAxis *axis, QGraphicsItem *item); + ~PolarChartLogValueAxisRadial(); + +protected: + virtual QVector calculateLayout() const; + virtual void createAxisLabels(const QVector &layout); + +private Q_SLOTS: + void handleBaseChanged(qreal base); + void handleLabelFormatChanged(const QString &format); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTLOGVALUEAXISRADIAL_P_H diff --git a/src/axis/logvalueaxis/qlogvalueaxis.cpp b/src/axis/logvalueaxis/qlogvalueaxis.cpp index 12170b8..27896b5 100644 --- a/src/axis/logvalueaxis/qlogvalueaxis.cpp +++ b/src/axis/logvalueaxis/qlogvalueaxis.cpp @@ -22,6 +22,8 @@ #include "qlogvalueaxis_p.h" #include "chartlogvalueaxisx_p.h" #include "chartlogvalueaxisy_p.h" +#include "polarchartlogvalueaxisangular_p.h" +#include "polarchartlogvalueaxisradial_p.h" #include "abstractdomain_p.h" #include #include @@ -287,14 +289,24 @@ void QLogValueAxisPrivate::setRange(qreal min, qreal max) } } -void QLogValueAxisPrivate::initializeGraphics(QGraphicsItem* parent) +void QLogValueAxisPrivate::initializeGraphics(QGraphicsItem *parent) { Q_Q(QLogValueAxis); - ChartAxis* axis(0); - if (orientation() == Qt::Vertical) - axis = new ChartLogValueAxisY(q,parent); - if (orientation() == Qt::Horizontal) - axis = new ChartLogValueAxisX(q,parent); + ChartAxisElement *axis(0); + + if (m_chart->chartType() == QChart::ChartTypeCartesian) { + if (orientation() == Qt::Vertical) + axis = new ChartLogValueAxisY(q,parent); + if (orientation() == Qt::Horizontal) + axis = new ChartLogValueAxisX(q,parent); + } + + if (m_chart->chartType() == QChart::ChartTypePolar) { + if (orientation() == Qt::Vertical) + axis = new PolarChartLogValueAxisRadial(q, parent); + if (orientation() == Qt::Horizontal) + axis = new PolarChartLogValueAxisAngular(q, parent); + } m_item.reset(axis); QAbstractAxisPrivate::initializeGraphics(parent); @@ -304,7 +316,7 @@ void QLogValueAxisPrivate::initializeGraphics(QGraphicsItem* parent) void QLogValueAxisPrivate::initializeDomain(AbstractDomain *domain) { if (orientation() == Qt::Vertical) { - if(!qFuzzyCompare(m_max, m_min)) { + if (!qFuzzyCompare(m_max, m_min)) { domain->setRangeY(m_min, m_max); } else if ( domain->minY() > 0) { setRange(domain->minY(), domain->maxY()); @@ -315,7 +327,7 @@ void QLogValueAxisPrivate::initializeDomain(AbstractDomain *domain) } } if (orientation() == Qt::Horizontal) { - if(!qFuzzyCompare(m_max, m_min)) { + if (!qFuzzyCompare(m_max, m_min)) { domain->setRangeX(m_min, m_max); } else if (domain->minX() > 0){ setRange(domain->minX(), domain->maxX()); diff --git a/src/axis/polarchartaxis.cpp b/src/axis/polarchartaxis.cpp new file mode 100644 index 0000000..8bb8042 --- /dev/null +++ b/src/axis/polarchartaxis.cpp @@ -0,0 +1,120 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartaxis_p.h" +#include "qabstractaxis_p.h" +#include "chartpresenter_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartAxis::PolarChartAxis(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis) + : ChartAxisElement(axis, item, intervalAxis) +{ +} + +PolarChartAxis::~PolarChartAxis() +{ + +} + +void PolarChartAxis::setGeometry(const QRectF &axis, const QRectF &grid) +{ + Q_UNUSED(grid); + setAxisGeometry(axis); + + if (isEmpty()) + return; + + QVector layout = calculateLayout(); + updateLayout(layout); +} + +QRectF PolarChartAxis::gridGeometry() const +{ + return QRectF(); +} + +void PolarChartAxis::updateLayout(QVector &layout) +{ + int diff = ChartAxisElement::layout().size() - layout.size(); + + if (animation()) { + switch (presenter()->state()) { + case ChartPresenter::ShowState: + animation()->setAnimationType(AxisAnimation::DefaultAnimation); + break; + } + // Update to "old" geometry before starting animation to avoid incorrectly sized + // axes lingering in wrong position compared to series plot before animation can kick in. + // Note that the position mismatch still exists even with this update, but it will be + // far less ugly. + updateGeometry(); + } + + if (diff > 0) + deleteItems(diff); + else if (diff < 0) + createItems(-diff); + + if (animation()) { + animation()->setValues(ChartAxisElement::layout(), layout); + presenter()->startAnimation(animation()); + } else { + setLayout(layout); + updateGeometry(); + } +} + +bool PolarChartAxis::isEmpty() +{ + return !axisGeometry().isValid() || qFuzzyIsNull(min() - max()); +} + +void PolarChartAxis::deleteItems(int count) +{ + QList gridLines = gridItems(); + QList labels = labelItems(); + QList shades = shadeItems(); + QList axis = arrowItems(); + + for (int i = 0; i < count; ++i) { + if (gridItems().size() == 1 || (((gridLines.size() + 1) % 2) && gridLines.size() > 0)) + delete(shades.takeLast()); + delete(gridLines.takeLast()); + delete(labels.takeLast()); + delete(axis.takeLast()); + } +} + +void PolarChartAxis::handleShadesBrushChanged(const QBrush &brush) +{ + foreach (QGraphicsItem *item, shadeItems()) + static_cast(item)->setBrush(brush); +} + +void PolarChartAxis::handleShadesPenChanged(const QPen &pen) +{ + foreach (QGraphicsItem *item, shadeItems()) + static_cast(item)->setPen(pen); +} + +#include "moc_polarchartaxis_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/polarchartaxis_p.h b/src/axis/polarchartaxis_p.h new file mode 100644 index 0000000..605b8fe --- /dev/null +++ b/src/axis/polarchartaxis_p.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTAXIS_P_H +#define POLARCHARTAXIS_P_H + +#include "chartaxiselement_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class PolarChartAxis : public ChartAxisElement +{ + Q_OBJECT + Q_INTERFACES(QGraphicsLayoutItem) +public: + PolarChartAxis(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis = false); + ~PolarChartAxis(); + + void setGeometry(const QRectF &axis, const QRectF &grid); + virtual qreal preferredAxisRadius(const QSizeF &maxSize) = 0; + int tickWidth() { return 3; } + +public: // from ChartAxisElement + QRectF gridGeometry() const; + bool isEmpty(); + +protected: + void updateLayout(QVector &layout); + +protected: // virtual functions + virtual void createItems(int count) = 0; + virtual void createAxisLabels(const QVector &layout) = 0; + +public Q_SLOTS: + virtual void handleShadesBrushChanged(const QBrush &brush); + virtual void handleShadesPenChanged(const QPen &pen); + +private: + void deleteItems(int count); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTAXIS_P_H diff --git a/src/axis/polarchartaxisangular.cpp b/src/axis/polarchartaxisangular.cpp new file mode 100644 index 0000000..8eed1f9 --- /dev/null +++ b/src/axis/polarchartaxisangular.cpp @@ -0,0 +1,428 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartaxisangular_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" +#include "qabstractaxis.h" +#include "qabstractaxis_p.h" +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartAxisAngular::PolarChartAxisAngular(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis) + : PolarChartAxis(axis, item, intervalAxis) +{ +} + +PolarChartAxisAngular::~PolarChartAxisAngular() +{ +} + +void PolarChartAxisAngular::updateGeometry() +{ + QGraphicsLayoutItem::updateGeometry(); + + const QVector &layout = this->layout(); + if (layout.isEmpty()) + return; + + createAxisLabels(layout); + QStringList labelList = labels(); + QPointF center = axisGeometry().center(); + QList arrowItemList = arrowItems(); + QList gridItemList = gridItems(); + QList labelItemList = labelItems(); + QList shadeItemList = shadeItems(); + QGraphicsSimpleTextItem *title = titleItem(); + + QGraphicsEllipseItem *axisLine = static_cast(arrowItemList.at(0)); + axisLine->setRect(axisGeometry()); + + qreal radius = axisGeometry().height() / 2.0; + + QFontMetrics fn(axis()->labelsFont()); + QRectF previousLabelRect; + QRectF firstLabelRect; + + qreal labelHeight = 0; + + bool firstShade = true; + bool nextTickVisible = false; + if (layout.size()) + nextTickVisible = !(layout.at(0) < 0.0 || layout.at(0) > 360.0); + + for (int i = 0; i < layout.size(); ++i) { + qreal angularCoordinate = layout.at(i); + + QGraphicsLineItem *gridLineItem = static_cast(gridItemList.at(i)); + QGraphicsLineItem *tickItem = static_cast(arrowItemList.at(i + 1)); + QGraphicsSimpleTextItem *labelItem = static_cast(labelItemList.at(i)); + QGraphicsPathItem *shadeItem = 0; + if (i == 0) + shadeItem = static_cast(shadeItemList.at(0)); + else if (i % 2) + shadeItem = static_cast(shadeItemList.at((i / 2) + 1)); + + // Ignore ticks outside valid range + bool currentTickVisible = nextTickVisible; + if ((i == layout.size() - 1) + || layout.at(i + 1) < 0.0 + || layout.at(i + 1) > 360.0) { + nextTickVisible = false; + } else { + nextTickVisible = true; + } + + qreal labelCoordinate = angularCoordinate; + qreal labelVisible = currentTickVisible; + if (intervalAxis()) { + qreal farEdge; + if (i == (layout.size() - 1)) + farEdge = 360.0; + else + farEdge = qMin(360.0, layout.at(i + 1)); + + // Adjust the labelCoordinate to show it if next tick is visible + if (nextTickVisible) + labelCoordinate = qMax(0.0, labelCoordinate); + + labelCoordinate = (labelCoordinate + farEdge) / 2.0; + // Don't display label once the category gets too small near the axis + if (labelCoordinate < 5.0 || labelCoordinate > 355.0) + labelVisible = false; + else + labelVisible = true; + } + + // Need this also in label calculations, so determine it first + QLineF tickLine(QLineF::fromPolar(radius - tickWidth(), 90.0 - angularCoordinate).p2(), + QLineF::fromPolar(radius + tickWidth(), 90.0 - angularCoordinate).p2()); + tickLine.translate(center); + + // Angular axis label + if (axis()->labelsVisible() && labelVisible) { + labelItem->setText(labelList.at(i)); + const QRectF &rect = labelItem->boundingRect(); + QPointF labelCenter = rect.center(); + labelItem->setTransformOriginPoint(labelCenter.x(), labelCenter.y()); + QRectF boundingRect = labelBoundingRect(fn, labelList.at(i)); + boundingRect.moveCenter(labelCenter); + QPointF positionDiff(rect.topLeft() - boundingRect.topLeft()); + + QPointF labelPoint; + if (intervalAxis()) { + QLineF labelLine = QLineF::fromPolar(radius + tickWidth(), 90.0 - labelCoordinate); + labelLine.translate(center); + labelPoint = labelLine.p2(); + } else { + labelPoint = tickLine.p2(); + } + + QRectF labelRect = moveLabelToPosition(labelCoordinate, labelPoint, boundingRect); + labelItem->setPos(labelRect.topLeft() + positionDiff); + + // Store height for title calculations + qreal labelClearance = axisGeometry().top() - labelRect.top(); + labelHeight = qMax(labelHeight, labelClearance); + + // Label overlap detection + if (i && (previousLabelRect.intersects(labelRect) || firstLabelRect.intersects(labelRect))) { + labelVisible = false; + } else { + // Store labelRect for future comparison. Some area is deducted to make things look + // little nicer, as usually intersection happens at label corner with angular labels. + labelRect.adjust(-2.0, -4.0, -2.0, -4.0); + if (firstLabelRect.isEmpty()) + firstLabelRect = labelRect; + + previousLabelRect = labelRect; + labelVisible = true; + } + } + + labelItem->setVisible(labelVisible); + if (!currentTickVisible) { + gridLineItem->setVisible(false); + tickItem->setVisible(false); + if (shadeItem) + shadeItem->setVisible(false); + continue; + } + + // Angular grid line + QLineF gridLine = QLineF::fromPolar(radius, 90.0 - angularCoordinate); + gridLine.translate(center); + gridLineItem->setLine(gridLine); + gridLineItem->setVisible(true); + + // Tick + tickItem->setLine(tickLine); + tickItem->setVisible(true); + + // Shades + if (i % 2 || (i == 0 && !nextTickVisible)) { + QPainterPath path; + path.moveTo(center); + if (i == 0) { + // If first tick is also the last, we need to custom fill the first partial arc + // or it won't get filled. + path.arcTo(axisGeometry(), 90.0 - layout.at(0), layout.at(0)); + path.closeSubpath(); + } else { + qreal nextCoordinate; + if (!nextTickVisible) // Last visible tick + nextCoordinate = 360.0; + else + nextCoordinate = layout.at(i + 1); + qreal arcSpan = angularCoordinate - nextCoordinate; + path.arcTo(axisGeometry(), 90.0 - angularCoordinate, arcSpan); + path.closeSubpath(); + + // Add additional arc for first shade item if there is a partial arc to be filled + if (firstShade) { + QGraphicsPathItem *specialShadeItem = static_cast(shadeItemList.at(0)); + if (layout.at(i - 1) > 0.0) { + QPainterPath specialPath; + specialPath.moveTo(center); + specialPath.arcTo(axisGeometry(), 90.0 - layout.at(i - 1), layout.at(i - 1)); + specialPath.closeSubpath(); + specialShadeItem->setPath(specialPath); + specialShadeItem->setVisible(true); + } else { + specialShadeItem->setVisible(false); + } + } + } + shadeItem->setPath(path); + shadeItem->setVisible(true); + firstShade = false; + } + } + + // Title, centered above the chart + QString titleText = axis()->titleText(); + if (!titleText.isEmpty() && axis()->isTitleVisible()) { + int size(0); + size = axisGeometry().width(); + + QFontMetrics titleMetrics(axis()->titleFont()); + if (titleMetrics.boundingRect(titleText).width() > size) { + QString string = titleText + "..."; + while (titleMetrics.boundingRect(string).width() > size && string.length() > 3) + string.remove(string.length() - 4, 1); + title->setText(string); + } else { + title->setText(titleText); + } + + QRectF titleBoundingRect; + titleBoundingRect = title->boundingRect(); + QPointF titleCenter = center - titleBoundingRect.center(); + title->setPos(titleCenter.x(), axisGeometry().top() - qreal(titlePadding()) * 2.0 - titleBoundingRect.height() - labelHeight); + } +} + +Qt::Orientation PolarChartAxisAngular::orientation() const +{ + return Qt::Horizontal; +} + +void PolarChartAxisAngular::createItems(int count) +{ + if (arrowItems().count() == 0) { + // angular axis line + // TODO: need class similar to LineArrowItem for click handling? + QGraphicsEllipseItem *arrow = new QGraphicsEllipseItem(presenter()->rootItem()); + arrow->setPen(axis()->linePen()); + arrowGroup()->addToGroup(arrow); + } + + for (int i = 0; i < count; ++i) { + QGraphicsLineItem *arrow = new QGraphicsLineItem(presenter()->rootItem()); + QGraphicsLineItem *grid = new QGraphicsLineItem(presenter()->rootItem()); + QGraphicsSimpleTextItem *label = new QGraphicsSimpleTextItem(presenter()->rootItem()); + QGraphicsSimpleTextItem *title = titleItem(); + arrow->setPen(axis()->linePen()); + grid->setPen(axis()->gridLinePen()); + label->setFont(axis()->labelsFont()); + label->setPen(axis()->labelsPen()); + label->setBrush(axis()->labelsBrush()); + label->setRotation(axis()->labelsAngle()); + title->setFont(axis()->titleFont()); + title->setPen(axis()->titlePen()); + title->setBrush(axis()->titleBrush()); + title->setText(axis()->titleText()); + arrowGroup()->addToGroup(arrow); + gridGroup()->addToGroup(grid); + labelGroup()->addToGroup(label); + if (gridItems().size() == 1 || (((gridItems().size() + 1) % 2) && gridItems().size() > 0)) { + QGraphicsPathItem *shade = new QGraphicsPathItem(presenter()->rootItem()); + shade->setPen(axis()->shadesPen()); + shade->setBrush(axis()->shadesBrush()); + shadeGroup()->addToGroup(shade); + } + } +} + +void PolarChartAxisAngular::handleArrowPenChanged(const QPen &pen) +{ + bool first = true; + foreach (QGraphicsItem *item, arrowItems()) { + if (first) { + first = false; + // First arrow item is the outer circle of axis + static_cast(item)->setPen(pen); + } else { + static_cast(item)->setPen(pen); + } + } +} + +void PolarChartAxisAngular::handleGridPenChanged(const QPen &pen) +{ + foreach (QGraphicsItem *item, gridItems()) + static_cast(item)->setPen(pen); +} + +QSizeF PolarChartAxisAngular::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const +{ + Q_UNUSED(which); + Q_UNUSED(constraint); + return QSizeF(-1, -1); +} + +qreal PolarChartAxisAngular::preferredAxisRadius(const QSizeF &maxSize) +{ + qreal radius = maxSize.height() / 2.0; + if (maxSize.width() < maxSize.height()) + radius = maxSize.width() / 2.0; + + int labelHeight = 0; + + if (axis()->labelsVisible()) { + QVector layout = calculateLayout(); + if (layout.isEmpty()) + return radius; + + createAxisLabels(layout); + QStringList labelList = labels(); + QFont font = axis()->labelsFont(); + + QRectF maxRect; + maxRect.setSize(maxSize); + maxRect.moveCenter(QPointF(0.0, 0.0)); + + // This is a horrible way to find out the maximum radius for angular axis and its labels. + // It just increments the radius down until everyhing fits the constraint size. + // Proper way would be to actually calculate it but this seems to work reasonably fast as it is. + QFontMetrics fm(font); + bool nextTickVisible = false; + for (int i = 0; i < layout.size(); ) { + if ((i == layout.size() - 1) + || layout.at(i + 1) < 0.0 + || layout.at(i + 1) > 360.0) { + nextTickVisible = false; + } else { + nextTickVisible = true; + } + + qreal labelCoordinate = layout.at(i); + qreal labelVisible; + + if (intervalAxis()) { + qreal farEdge; + if (i == (layout.size() - 1)) + farEdge = 360.0; + else + farEdge = qMin(360.0, layout.at(i + 1)); + + // Adjust the labelCoordinate to show it if next tick is visible + if (nextTickVisible) + labelCoordinate = qMax(0.0, labelCoordinate); + + labelCoordinate = (labelCoordinate + farEdge) / 2.0; + } + + if (labelCoordinate < 0.0 || labelCoordinate > 360.0) + labelVisible = false; + else + labelVisible = true; + + if (!labelVisible) { + i++; + continue; + } + + QRectF boundingRect = labelBoundingRect(fm, labelList.at(i)); + labelHeight = boundingRect.height(); + QPointF labelPoint = QLineF::fromPolar(radius + tickWidth(), 90.0 - labelCoordinate).p2(); + + boundingRect = moveLabelToPosition(labelCoordinate, labelPoint, boundingRect); + if (boundingRect.isEmpty() || maxRect.intersected(boundingRect) == boundingRect) { + i++; + } else { + radius -= 1.0; + if (radius < 1.0) // safeguard + return 1.0; + } + } + } + + if (!axis()->titleText().isEmpty() && axis()->isTitleVisible()) { + QFontMetrics titleMetrics(axis()->titleFont()); + int titleHeight = titleMetrics.boundingRect(axis()->titleText()).height(); + radius -= titlePadding() + (titleHeight / 2); + if (radius < 1.0) // safeguard + return 1.0; + } + + return radius; +} + +QRectF PolarChartAxisAngular::moveLabelToPosition(qreal angularCoordinate, QPointF labelPoint, QRectF labelRect) const +{ + // TODO use fuzzy compare for "==" cases? + // TODO Adjust the rect position near 0, 90, 180, and 270 angles for smoother animation? + if (angularCoordinate == 0.0) + labelRect.moveCenter(labelPoint + QPointF(0, -labelRect.height() / 2.0)); + else if (angularCoordinate < 90.0) + labelRect.moveBottomLeft(labelPoint); + else if (angularCoordinate == 90.0) + labelRect.moveCenter(labelPoint + QPointF(labelRect.width() / 2.0 + 2.0, 0)); // +2 so that it does not hit the radial axis + else if (angularCoordinate < 180.0) + labelRect.moveTopLeft(labelPoint); + else if (angularCoordinate == 180.0) + labelRect.moveCenter(labelPoint + QPointF(0, labelRect.height() / 2.0)); + else if (angularCoordinate < 270.0) + labelRect.moveTopRight(labelPoint); + else if (angularCoordinate == 270.0) + labelRect.moveCenter(labelPoint + QPointF(-labelRect.width() / 2.0 - 2.0, 0)); // -2 so that it does not hit the radial axis + else if (angularCoordinate < 360.0) + labelRect.moveBottomRight(labelPoint); + else + labelRect.moveCenter(labelPoint + QPointF(0, -labelRect.height() / 2.0)); + return labelRect; +} + +#include "moc_polarchartaxisangular_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/polarchartaxisangular_p.h b/src/axis/polarchartaxisangular_p.h new file mode 100644 index 0000000..7e7b292 --- /dev/null +++ b/src/axis/polarchartaxisangular_p.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTAXISANGULAR_P_H +#define POLARCHARTAXISANGULAR_P_H + +#include "polarchartaxis_p.h" +#include "qvalueaxis.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class PolarChartAxisAngular : public PolarChartAxis +{ + Q_OBJECT +public: + PolarChartAxisAngular(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis = false); + ~PolarChartAxisAngular(); + + Qt::Orientation orientation() const; + QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const; + + virtual void updateGeometry(); + virtual void createItems(int count); + + qreal preferredAxisRadius(const QSizeF &maxSize); + +public Q_SLOTS: + virtual void handleArrowPenChanged(const QPen &pen); + virtual void handleGridPenChanged(const QPen &pen); + +private: + QRectF moveLabelToPosition(qreal angularCoordinate, QPointF labelPoint, QRectF labelRect) const; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTAXISANGULAR_P_H diff --git a/src/axis/polarchartaxisradial.cpp b/src/axis/polarchartaxisradial.cpp new file mode 100644 index 0000000..5592398 --- /dev/null +++ b/src/axis/polarchartaxisradial.cpp @@ -0,0 +1,301 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartaxisradial_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" +#include "qabstractaxis_p.h" +#include "linearrowitem_p.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartAxisRadial::PolarChartAxisRadial(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis) + : PolarChartAxis(axis, item, intervalAxis) +{ +} + +PolarChartAxisRadial::~PolarChartAxisRadial() +{ +} + +void PolarChartAxisRadial::updateGeometry() +{ + const QVector &layout = this->layout(); + if (layout.isEmpty()) + return; + + createAxisLabels(layout); + QStringList labelList = labels(); + QPointF center = axisGeometry().center(); + QList arrowItemList = arrowItems(); + QList gridItemList = gridItems(); + QList labelItemList = labelItems(); + QList shadeItemList = shadeItems(); + QGraphicsSimpleTextItem* title = titleItem(); + qreal radius = axisGeometry().height() / 2.0; + + QLineF line(center, center + QPointF(0, -radius)); + QGraphicsLineItem *axisLine = static_cast(arrowItemList.at(0)); + axisLine->setLine(line); + + QFontMetrics fn(axis()->labelsFont()); + QRectF previousLabelRect; + bool firstShade = true; + bool nextTickVisible = false; + if (layout.size()) + nextTickVisible = !(layout.at(0) < 0.0 || layout.at(0) > radius); + + for (int i = 0; i < layout.size(); ++i) { + qreal radialCoordinate = layout.at(i); + + QGraphicsEllipseItem *gridItem = static_cast(gridItemList.at(i)); + QGraphicsLineItem *tickItem = static_cast(arrowItemList.at(i + 1)); + QGraphicsSimpleTextItem *labelItem = static_cast(labelItemList.at(i)); + QGraphicsPathItem *shadeItem = 0; + if (i == 0) + shadeItem = static_cast(shadeItemList.at(0)); + else if (i % 2) + shadeItem = static_cast(shadeItemList.at((i / 2) + 1)); + + // Ignore ticks outside valid range + bool currentTickVisible = nextTickVisible; + if ((i == layout.size() - 1) + || layout.at(i + 1) < 0.0 + || layout.at(i + 1) > radius) { + nextTickVisible = false; + } else { + nextTickVisible = true; + } + + qreal labelCoordinate = radialCoordinate; + qreal labelVisible = currentTickVisible; + qreal labelPad = labelPadding() / 2.0; + if (intervalAxis()) { + qreal farEdge; + if (i == (layout.size() - 1)) + farEdge = radius; + else + farEdge = qMin(radius, layout.at(i + 1)); + + // Adjust the labelCoordinate to show it if next tick is visible + if (nextTickVisible) + labelCoordinate = qMax(0.0, labelCoordinate); + + labelCoordinate = (labelCoordinate + farEdge) / 2.0; + if (labelCoordinate > 0.0 && labelCoordinate < radius) + labelVisible = true; + else + labelVisible = false; + } + + // Radial axis label + if (axis()->labelsVisible() && labelVisible) { + labelItem->setText(labelList.at(i)); + QRectF labelRect = labelItem->boundingRect(); + QPointF labelCenter = labelRect.center(); + labelItem->setTransformOriginPoint(labelCenter.x(), labelCenter.y()); + QRectF boundingRect = labelBoundingRect(fn, labelList.at(i)); + boundingRect.moveCenter(labelCenter); + QPointF positionDiff(labelRect.topLeft() - boundingRect.topLeft()); + QPointF labelPoint = center; + if (intervalAxis()) + labelPoint += QPointF(labelPad, -labelCoordinate - (boundingRect.height() / 2.0)); + else + labelPoint += QPointF(labelPad, labelPad - labelCoordinate); + labelRect.moveTopLeft(labelPoint); + labelItem->setPos(labelRect.topLeft() + positionDiff); + + // Label overlap detection + labelRect.setSize(boundingRect.size()); + if ((i && previousLabelRect.intersects(labelRect)) + || !axisGeometry().contains(labelRect)) { + labelVisible = false; + } else { + previousLabelRect = labelRect; + labelVisible = true; + } + } + + labelItem->setVisible(labelVisible); + if (!currentTickVisible) { + gridItem->setVisible(false); + tickItem->setVisible(false); + if (shadeItem) + shadeItem->setVisible(false); + continue; + } + + // Radial grid line + QRectF gridRect; + gridRect.setWidth(radialCoordinate * 2.0); + gridRect.setHeight(radialCoordinate * 2.0); + gridRect.moveCenter(center); + + gridItem->setRect(gridRect); + gridItem->setVisible(true); + + // Tick + QLineF tickLine(-tickWidth(), 0.0, tickWidth(), 0.0); + tickLine.translate(center.rx(), gridRect.top()); + tickItem->setLine(tickLine); + tickItem->setVisible(true); + + // Shades + if (i % 2 || (i == 0 && !nextTickVisible)) { + QPainterPath path; + if (i == 0) { + // If first tick is also the last, we need to custom fill the inner circle + // or it won't get filled. + QRectF innerCircle(0.0, 0.0, layout.at(0) * 2.0, layout.at(0) * 2.0); + innerCircle.moveCenter(center); + path.addEllipse(innerCircle); + } else { + QRectF otherGridRect; + if (!nextTickVisible) { // Last visible tick + otherGridRect = axisGeometry(); + } else { + qreal otherGridRectDimension = layout.at(i + 1) * 2.0; + otherGridRect.setWidth(otherGridRectDimension); + otherGridRect.setHeight(otherGridRectDimension); + otherGridRect.moveCenter(center); + } + path.addEllipse(gridRect); + path.addEllipse(otherGridRect); + + // Add additional shading in first visible shade item if there is a partial tick + // to be filled at the center (log & category axes) + if (firstShade) { + QGraphicsPathItem *specialShadeItem = static_cast(shadeItemList.at(0)); + if (layout.at(i - 1) > 0.0) { + QRectF innerCircle(0.0, 0.0, layout.at(i - 1) * 2.0, layout.at(i - 1) * 2.0); + innerCircle.moveCenter(center); + QPainterPath specialPath; + specialPath.addEllipse(innerCircle); + specialShadeItem->setPath(specialPath); + specialShadeItem->setVisible(true); + } else { + specialShadeItem->setVisible(false); + } + } + } + shadeItem->setPath(path); + shadeItem->setVisible(true); + firstShade = false; + } + } + + // Title, along the 0 axis + QString titleText = axis()->titleText(); + if (!titleText.isEmpty() && axis()->isTitleVisible()) { + QFontMetrics titleMetrics(axis()->titleFont()); + if (titleMetrics.boundingRect(titleText).width() > radius) { + QString string = titleText + "..."; + while (titleMetrics.boundingRect(string).width() > radius && string.length() > 3) + string.remove(string.length() - 4, 1); + title->setText(string); + } else { + title->setText(titleText); + } + + QRectF titleBoundingRect; + titleBoundingRect = title->boundingRect(); + QPointF titleCenter = titleBoundingRect.center(); + QPointF arrowCenter = axisLine->boundingRect().center(); + QPointF titleCenterDiff = arrowCenter - titleCenter; + title->setPos(titleCenterDiff.x() - qreal(titlePadding()) - (titleBoundingRect.height() / 2.0), titleCenterDiff.y()); + title->setTransformOriginPoint(titleCenter); + title->setRotation(270.0); + } + + QGraphicsLayoutItem::updateGeometry(); +} + +Qt::Orientation PolarChartAxisRadial::orientation() const +{ + return Qt::Vertical; +} + +void PolarChartAxisRadial::createItems(int count) +{ + if (arrowItems().count() == 0) { + // radial axis center line + QGraphicsLineItem *arrow = new LineArrowItem(this, presenter()->rootItem()); + arrow->setPen(axis()->linePen()); + arrowGroup()->addToGroup(arrow); + } + + for (int i = 0; i < count; ++i) { + QGraphicsLineItem *arrow = new QGraphicsLineItem(presenter()->rootItem()); + QGraphicsEllipseItem *grid = new QGraphicsEllipseItem(presenter()->rootItem()); + QGraphicsSimpleTextItem *label = new QGraphicsSimpleTextItem(presenter()->rootItem()); + QGraphicsSimpleTextItem *title = titleItem(); + arrow->setPen(axis()->linePen()); + grid->setPen(axis()->gridLinePen()); + label->setFont(axis()->labelsFont()); + label->setPen(axis()->labelsPen()); + label->setBrush(axis()->labelsBrush()); + label->setRotation(axis()->labelsAngle()); + title->setFont(axis()->titleFont()); + title->setPen(axis()->titlePen()); + title->setBrush(axis()->titleBrush()); + title->setText(axis()->titleText()); + arrowGroup()->addToGroup(arrow); + gridGroup()->addToGroup(grid); + labelGroup()->addToGroup(label); + if (gridItems().size() == 1 || (((gridItems().size() + 1) % 2) && gridItems().size() > 0)) { + QGraphicsPathItem *shade = new QGraphicsPathItem(presenter()->rootItem()); + shade->setPen(axis()->shadesPen()); + shade->setBrush(axis()->shadesBrush()); + shadeGroup()->addToGroup(shade); + } + } +} + +void PolarChartAxisRadial::handleArrowPenChanged(const QPen &pen) +{ + foreach (QGraphicsItem *item, arrowItems()) + static_cast(item)->setPen(pen); +} + +void PolarChartAxisRadial::handleGridPenChanged(const QPen &pen) +{ + foreach (QGraphicsItem *item, gridItems()) + static_cast(item)->setPen(pen); +} + +QSizeF PolarChartAxisRadial::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const +{ + Q_UNUSED(which); + Q_UNUSED(constraint); + return QSizeF(-1.0, -1.0); +} + +qreal PolarChartAxisRadial::preferredAxisRadius(const QSizeF &maxSize) +{ + qreal radius = maxSize.height() / 2.0; + if (maxSize.width() < maxSize.height()) + radius = maxSize.width() / 2.0; + return radius; +} + +#include "moc_polarchartaxisradial_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/polarchartaxisradial_p.h b/src/axis/polarchartaxisradial_p.h new file mode 100644 index 0000000..9874f28 --- /dev/null +++ b/src/axis/polarchartaxisradial_p.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTAXISRADIAL_P_H +#define POLARCHARTAXISRADIAL_P_H + +#include "polarchartaxis_p.h" +#include "qvalueaxis.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class PolarChartAxisRadial : public PolarChartAxis +{ + Q_OBJECT +public: + PolarChartAxisRadial(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis = false); + ~PolarChartAxisRadial(); + + Qt::Orientation orientation() const; + QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const; + + virtual void updateGeometry(); + virtual void createItems(int count); + + qreal preferredAxisRadius(const QSizeF &maxSize); + +public Q_SLOTS: + virtual void handleArrowPenChanged(const QPen &pen); + virtual void handleGridPenChanged(const QPen &pen); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTAXISRADIAL_P_H diff --git a/src/axis/qabstractaxis.cpp b/src/axis/qabstractaxis.cpp index 60e071d..77c89b5 100644 --- a/src/axis/qabstractaxis.cpp +++ b/src/axis/qabstractaxis.cpp @@ -254,11 +254,11 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE /*! \property QAbstractAxis::alignment - The alignment of the axis. Either Qt::AlignLeft or Qt::AlignBottom. + The alignment of the axis. Can be Qt::AlignLeft, Qt::AlignRight, Qt::AlignBottom, or Qt::AlignTop. */ /*! \qmlproperty alignment AbstractAxis::alignment - The alignment of the axis. Either Qt.AlignLeft or Qt.AlignBottom. + The alignment of the axis. Can be Qt.AlignLeft, Qt.AlignRight, Qt.AlignBottom, or Qt.AlignTop. */ /*! @@ -990,11 +990,11 @@ void QAbstractAxisPrivate::initializeGraphics(QGraphicsItem* parent) void QAbstractAxisPrivate::initializeAnimations(QChart::AnimationOptions options) { - ChartAxis* axis = m_item.data(); + ChartAxisElement *axis = m_item.data(); Q_ASSERT(axis); - if(options.testFlag(QChart::GridAxisAnimations)) { + if (options.testFlag(QChart::GridAxisAnimations)) { axis->setAnimation(new AxisAnimation(axis)); - }else{ + } else { axis->setAnimation(0); } } diff --git a/src/axis/qabstractaxis.h b/src/axis/qabstractaxis.h index 5ebd797..e29325d 100644 --- a/src/axis/qabstractaxis.h +++ b/src/axis/qabstractaxis.h @@ -155,36 +155,37 @@ public: Q_SIGNALS: void visibleChanged(bool visible); - void linePenChanged(const QPen& pen); + void linePenChanged(const QPen &pen); void lineVisibleChanged(bool visible); void labelsVisibleChanged(bool visible); - void labelsPenChanged(const QPen& pen); - void labelsBrushChanged(const QBrush& brush); - void labelsFontChanged(const QFont& pen); + void labelsPenChanged(const QPen &pen); + void labelsBrushChanged(const QBrush &brush); + void labelsFontChanged(const QFont &pen); void labelsAngleChanged(int angle); - void gridLinePenChanged(const QPen& pen); + void gridLinePenChanged(const QPen &pen); void gridVisibleChanged(bool visible); void colorChanged(QColor color); void labelsColorChanged(QColor color); - void titleTextChanged(const QString& title); - void titlePenChanged(const QPen& pen); - void titleBrushChanged(const QBrush& brush); + void titleTextChanged(const QString &title); + void titlePenChanged(const QPen &pen); + void titleBrushChanged(const QBrush &brush); void titleVisibleChanged(bool visible); - void titleFontChanged(const QFont& font); + void titleFontChanged(const QFont &font); void shadesVisibleChanged(bool visible); void shadesColorChanged(QColor color); void shadesBorderColorChanged(QColor color); - void shadesPenChanged(const QPen& pen); - void shadesBrushChanged(const QBrush& brush); + void shadesPenChanged(const QPen &pen); + void shadesBrushChanged(const QBrush &brush); protected: QScopedPointer d_ptr; Q_DISABLE_COPY(QAbstractAxis) friend class ChartDataSet; - friend class ChartAxis; friend class ChartPresenter; friend class ChartThemeManager; friend class AbstractDomain; + friend class ChartAxisElement; + friend class XYChart; }; QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/qabstractaxis_p.h b/src/axis/qabstractaxis_p.h index a284d46..9ab993d 100644 --- a/src/axis/qabstractaxis_p.h +++ b/src/axis/qabstractaxis_p.h @@ -31,7 +31,7 @@ #define QABSTRACTAXIS_P_H #include "qabstractaxis.h" -#include "chartaxis_p.h" +#include "chartaxiselement_p.h" #include "qchart.h" #include @@ -59,7 +59,7 @@ public: void setAlignment( Qt::Alignment alignment); virtual void initializeDomain(AbstractDomain *domain) = 0; - virtual void initializeGraphics(QGraphicsItem* parent) = 0; + virtual void initializeGraphics(QGraphicsItem *parent) = 0; virtual void initializeTheme(ChartTheme* theme, bool forced = false); virtual void initializeAnimations(QChart::AnimationOptions options); @@ -73,7 +73,7 @@ public: virtual qreal min() = 0; virtual qreal max() = 0; - ChartAxis* axisItem() { return m_item.data(); } + ChartAxisElement *axisItem() { return m_item.data(); } public Q_SLOTS: void handleRangeChanged(qreal min, qreal max); @@ -84,7 +84,7 @@ Q_SIGNALS: protected: QAbstractAxis *q_ptr; QChart *m_chart; - QScopedPointer m_item; + QScopedPointer m_item; private: QList m_series; diff --git a/src/axis/valueaxis/chartvalueaxisx.cpp b/src/axis/valueaxis/chartvalueaxisx.cpp index a084ac3..da6055f 100644 --- a/src/axis/valueaxis/chartvalueaxisx.cpp +++ b/src/axis/valueaxis/chartvalueaxisx.cpp @@ -22,7 +22,7 @@ #include "qabstractaxis.h" #include "chartpresenter_p.h" #include "qvalueaxis.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -31,12 +31,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -ChartValueAxisX::ChartValueAxisX(QValueAxis *axis, QGraphicsItem* item ) +ChartValueAxisX::ChartValueAxisX(QValueAxis *axis, QGraphicsItem *item ) : HorizontalAxis(axis, item), m_axis(axis) { - QObject::connect(m_axis,SIGNAL(tickCountChanged(int)),this, SLOT(handleTickCountChanged(int))); - QObject::connect(m_axis,SIGNAL(labelFormatChanged(QString)),this, SLOT(handleLabelFormatChanged(QString))); + QObject::connect(m_axis, SIGNAL(tickCountChanged(int)), this, SLOT(handleTickCountChanged(int))); + QObject::connect(m_axis, SIGNAL(labelFormatChanged(QString)), this, SLOT(handleLabelFormatChanged(QString))); } ChartValueAxisX::~ChartValueAxisX() @@ -61,10 +61,10 @@ QVector ChartValueAxisX::calculateLayout() const void ChartValueAxisX::updateGeometry() { - const QVector& layout = ChartAxis::layout(); + const QVector& layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; - setLabels(createValueLabels(min(),max(),layout.size(),m_axis->labelFormat())); + setLabels(createValueLabels(min(), max(), layout.size(), m_axis->labelFormat())); HorizontalAxis::updateGeometry(); } @@ -86,7 +86,7 @@ QSizeF ChartValueAxisX::sizeHint(Qt::SizeHint which, const QSizeF &constraint) c { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = HorizontalAxis::sizeHint(which, constraint); diff --git a/src/axis/valueaxis/chartvalueaxisx_p.h b/src/axis/valueaxis/chartvalueaxisx_p.h index e0556b6..bebfdb3 100644 --- a/src/axis/valueaxis/chartvalueaxisx_p.h +++ b/src/axis/valueaxis/chartvalueaxisx_p.h @@ -35,13 +35,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE class QValueAxis; -class ChartPresenter; class ChartValueAxisX : public HorizontalAxis { Q_OBJECT public: - ChartValueAxisX(QValueAxis *axis, QGraphicsItem* item = 0); + ChartValueAxisX(QValueAxis *axis, QGraphicsItem *item = 0); ~ChartValueAxisX(); QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint) const; diff --git a/src/axis/valueaxis/chartvalueaxisy.cpp b/src/axis/valueaxis/chartvalueaxisy.cpp index 14b2d20..54b5824 100644 --- a/src/axis/valueaxis/chartvalueaxisy.cpp +++ b/src/axis/valueaxis/chartvalueaxisy.cpp @@ -22,7 +22,7 @@ #include "qabstractaxis.h" #include "chartpresenter_p.h" #include "qvalueaxis.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include #include #include @@ -30,12 +30,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -ChartValueAxisY::ChartValueAxisY(QValueAxis *axis, QGraphicsItem* item) +ChartValueAxisY::ChartValueAxisY(QValueAxis *axis, QGraphicsItem *item) : VerticalAxis(axis, item), m_axis(axis) { - QObject::connect(m_axis,SIGNAL(tickCountChanged(int)),this, SLOT(handleTickCountChanged(int))); - QObject::connect(m_axis,SIGNAL(labelFormatChanged(QString)),this, SLOT(handleLabelFormatChanged(QString))); + QObject::connect(m_axis, SIGNAL(tickCountChanged(int)), this, SLOT(handleTickCountChanged(int))); + QObject::connect(m_axis, SIGNAL(labelFormatChanged(QString)), this, SLOT(handleLabelFormatChanged(QString))); } ChartValueAxisY::~ChartValueAxisY() @@ -62,7 +62,7 @@ QVector ChartValueAxisY::calculateLayout() const void ChartValueAxisY::updateGeometry() { - const QVector &layout = ChartAxis::layout(); + const QVector &layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; setLabels(createValueLabels(min(),max(),layout.size(),m_axis->labelFormat())); @@ -87,7 +87,7 @@ QSizeF ChartValueAxisY::sizeHint(Qt::SizeHint which, const QSizeF &constraint) c { Q_UNUSED(constraint) - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); QSizeF sh; QSizeF base = VerticalAxis::sizeHint(which, constraint); QStringList ticksList = createValueLabels(min(),max(),m_axis->tickCount(),m_axis->labelFormat()); diff --git a/src/axis/valueaxis/chartvalueaxisy_p.h b/src/axis/valueaxis/chartvalueaxisy_p.h index 5a01b9b..3b58669 100644 --- a/src/axis/valueaxis/chartvalueaxisy_p.h +++ b/src/axis/valueaxis/chartvalueaxisy_p.h @@ -35,13 +35,12 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE class QValueAxis; -class ChartPresenter; class ChartValueAxisY : public VerticalAxis { Q_OBJECT public: - ChartValueAxisY(QValueAxis *axis, QGraphicsItem* item = 0); + ChartValueAxisY(QValueAxis *axis, QGraphicsItem *item = 0); ~ChartValueAxisY(); QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint) const; diff --git a/src/axis/valueaxis/polarchartvalueaxisangular.cpp b/src/axis/valueaxis/polarchartvalueaxisangular.cpp new file mode 100644 index 0000000..5fdbcb4 --- /dev/null +++ b/src/axis/valueaxis/polarchartvalueaxisangular.cpp @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartvalueaxisangular_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartValueAxisAngular::PolarChartValueAxisAngular(QValueAxis *axis, QGraphicsItem *item) + : PolarChartAxisAngular(axis, item) +{ + QObject::connect(axis, SIGNAL(tickCountChanged(int)), this, SLOT(handleTickCountChanged(int))); + QObject::connect(axis, SIGNAL(labelFormatChanged(QString)), this, SLOT(handleLabelFormatChanged(QString))); +} + +PolarChartValueAxisAngular::~PolarChartValueAxisAngular() +{ +} + +QVector PolarChartValueAxisAngular::calculateLayout() const +{ + int tickCount = static_cast(axis())->tickCount(); + Q_ASSERT(tickCount >= 2); + + QVector points; + points.resize(tickCount); + + const qreal d = 360.0 / qreal(tickCount - 1); + + for (int i = 0; i < tickCount; ++i) { + qreal angularCoordinate = qreal(i) * d; + points[i] = angularCoordinate; + } + + return points; +} + +void PolarChartValueAxisAngular::createAxisLabels(const QVector &layout) +{ + QStringList labelList = createValueLabels(min(), max(), layout.size(), static_cast(axis())->labelFormat()); + setLabels(labelList); +} + +void PolarChartValueAxisAngular::handleTickCountChanged(int tick) +{ + Q_UNUSED(tick); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +void PolarChartValueAxisAngular::handleLabelFormatChanged(const QString &format) +{ + Q_UNUSED(format); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +#include "moc_polarchartvalueaxisangular_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/valueaxis/polarchartvalueaxisangular_p.h b/src/axis/valueaxis/polarchartvalueaxisangular_p.h new file mode 100644 index 0000000..afe5192 --- /dev/null +++ b/src/axis/valueaxis/polarchartvalueaxisangular_p.h @@ -0,0 +1,56 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTVALUEAXISANGULAR_P_H +#define POLARCHARTVALUEAXISANGULAR_P_H + +#include "polarchartaxisangular_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QValueAxis; + +class PolarChartValueAxisAngular : public PolarChartAxisAngular +{ + Q_OBJECT +public: + PolarChartValueAxisAngular(QValueAxis *axis, QGraphicsItem *item); + ~PolarChartValueAxisAngular(); + + virtual QVector calculateLayout() const; + virtual void createAxisLabels(const QVector &layout); + +private Q_SLOTS: + void handleTickCountChanged(int tick); + void handleLabelFormatChanged(const QString &format); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTVALUEAXISANGULAR_P_H diff --git a/src/axis/valueaxis/polarchartvalueaxisradial.cpp b/src/axis/valueaxis/polarchartvalueaxisradial.cpp new file mode 100644 index 0000000..f11e926 --- /dev/null +++ b/src/axis/valueaxis/polarchartvalueaxisradial.cpp @@ -0,0 +1,79 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartvalueaxisradial_p.h" +#include "chartpresenter_p.h" +#include "abstractchartlayout_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarChartValueAxisRadial::PolarChartValueAxisRadial(QValueAxis *axis, QGraphicsItem *item) + : PolarChartAxisRadial(axis, item) +{ + QObject::connect(axis, SIGNAL(tickCountChanged(int)), this, SLOT(handleTickCountChanged(int))); + QObject::connect(axis, SIGNAL(labelFormatChanged(QString)), this, SLOT(handleLabelFormatChanged(QString))); +} + +PolarChartValueAxisRadial::~PolarChartValueAxisRadial() +{ +} + +QVector PolarChartValueAxisRadial::calculateLayout() const +{ + int tickCount = static_cast(axis())->tickCount(); + Q_ASSERT(tickCount >= 2); + + QVector points; + points.resize(tickCount); + + const qreal d = (axisGeometry().width() / 2) / qreal(tickCount - 1); + + for (int i = 0; i < tickCount; ++i) { + qreal radialCoordinate = qreal(i) * d; + points[i] = radialCoordinate; + } + + return points; +} + +void PolarChartValueAxisRadial::createAxisLabels(const QVector &layout) +{ + setLabels(createValueLabels(min(), max(), layout.size(), static_cast(axis())->labelFormat())); +} + +void PolarChartValueAxisRadial::handleTickCountChanged(int tick) +{ + Q_UNUSED(tick); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +void PolarChartValueAxisRadial::handleLabelFormatChanged(const QString &format) +{ + Q_UNUSED(format); + QGraphicsLayoutItem::updateGeometry(); + if (presenter()) + presenter()->layout()->invalidate(); +} + +#include "moc_polarchartvalueaxisradial_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/axis/valueaxis/polarchartvalueaxisradial_p.h b/src/axis/valueaxis/polarchartvalueaxisradial_p.h new file mode 100644 index 0000000..f443d1d --- /dev/null +++ b/src/axis/valueaxis/polarchartvalueaxisradial_p.h @@ -0,0 +1,56 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTVALUEAXISRADIAL_P_H +#define POLARCHARTVALUEAXISRADIAL_P_H + +#include "polarchartaxisradial_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QValueAxis; + +class PolarChartValueAxisRadial : public PolarChartAxisRadial +{ + Q_OBJECT +public: + PolarChartValueAxisRadial(QValueAxis *axis, QGraphicsItem *item); + ~PolarChartValueAxisRadial(); + + virtual QVector calculateLayout() const; + virtual void createAxisLabels(const QVector &layout); + +private Q_SLOTS: + void handleTickCountChanged(int tick); + void handleLabelFormatChanged(const QString &format); +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTVALUEAXISRADIAL_P_H diff --git a/src/axis/valueaxis/qvalueaxis.cpp b/src/axis/valueaxis/qvalueaxis.cpp index 032fb4e..80aa25c 100644 --- a/src/axis/valueaxis/qvalueaxis.cpp +++ b/src/axis/valueaxis/qvalueaxis.cpp @@ -23,6 +23,8 @@ #include "chartvalueaxisx_p.h" #include "chartvalueaxisy_p.h" #include "abstractdomain_p.h" +#include "polarchartvalueaxisangular_p.h" +#include "polarchartvalueaxisradial_p.h" #include "chartdataset_p.h" #include "chartpresenter_p.h" #include "charttheme_p.h" @@ -387,14 +389,24 @@ void QValueAxisPrivate::setRange(qreal min, qreal max) } } -void QValueAxisPrivate::initializeGraphics(QGraphicsItem* parent) +void QValueAxisPrivate::initializeGraphics(QGraphicsItem *parent) { Q_Q(QValueAxis); - ChartAxis* axis(0); - if (orientation() == Qt::Vertical) - axis = new ChartValueAxisY(q,parent); - if (orientation() == Qt::Horizontal) - axis = new ChartValueAxisX(q,parent); + ChartAxisElement *axis(0); + + if (m_chart->chartType() == QChart::ChartTypeCartesian) { + if (orientation() == Qt::Vertical) + axis = new ChartValueAxisY(q,parent); + if (orientation() == Qt::Horizontal) + axis = new ChartValueAxisX(q,parent); + } + + if (m_chart->chartType() == QChart::ChartTypePolar) { + if (orientation() == Qt::Vertical) + axis = new PolarChartValueAxisRadial(q, parent); + if (orientation() == Qt::Horizontal) + axis = new PolarChartValueAxisAngular(q, parent); + } m_item.reset(axis); QAbstractAxisPrivate::initializeGraphics(parent); @@ -404,20 +416,16 @@ void QValueAxisPrivate::initializeGraphics(QGraphicsItem* parent) void QValueAxisPrivate::initializeDomain(AbstractDomain *domain) { if (orientation() == Qt::Vertical) { - if(!qFuzzyIsNull(m_max - m_min)) { + if (!qFuzzyIsNull(m_max - m_min)) domain->setRangeY(m_min, m_max); - } - else { + else setRange(domain->minY(), domain->maxY()); - } } if (orientation() == Qt::Horizontal) { - if(!qFuzzyIsNull(m_max - m_min)) { + if (!qFuzzyIsNull(m_max - m_min)) domain->setRangeX(m_min, m_max); - } - else { + else setRange(domain->minX(), domain->maxX()); - } } } diff --git a/src/axis/verticalaxis.cpp b/src/axis/verticalaxis.cpp index d3c5efa..3726359 100644 --- a/src/axis/verticalaxis.cpp +++ b/src/axis/verticalaxis.cpp @@ -25,31 +25,29 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -VerticalAxis::VerticalAxis(QAbstractAxis *axis, QGraphicsItem* item, bool intervalAxis) - : ChartAxis(axis, item, intervalAxis) +VerticalAxis::VerticalAxis(QAbstractAxis *axis, QGraphicsItem *item, bool intervalAxis) + : CartesianChartAxis(axis, item, intervalAxis) { - } VerticalAxis::~VerticalAxis() { - } void VerticalAxis::updateGeometry() { - const QVector &layout = ChartAxis::layout(); + const QVector &layout = ChartAxisElement::layout(); if (layout.isEmpty()) return; QStringList labelList = labels(); - QList lines = lineItems(); + QList lines = gridItems(); QList labels = labelItems(); QList shades = shadeItems(); - QList axis = arrowItems(); - QGraphicsSimpleTextItem* title = titleItem(); + QList arrow = arrowItems(); + QGraphicsSimpleTextItem *title = titleItem(); Q_ASSERT(labels.size() == labelList.size()); Q_ASSERT(layout.size() == labelList.size()); @@ -61,32 +59,31 @@ void VerticalAxis::updateGeometry() //arrow - QGraphicsLineItem *arrowItem = static_cast(axis.at(0)); + QGraphicsLineItem *arrowItem = static_cast(arrow.at(0)); //arrow position - if (alignment()==Qt::AlignLeft) - arrowItem->setLine( axisRect.right() , gridRect.top(), axisRect.right(), gridRect.bottom()); - else if(alignment()==Qt::AlignRight) - arrowItem->setLine( axisRect.left() , gridRect.top(), axisRect.left(), gridRect.bottom()); + if (axis()->alignment() == Qt::AlignLeft) + arrowItem->setLine(axisRect.right(), gridRect.top(), axisRect.right(), gridRect.bottom()); + else if (axis()->alignment() == Qt::AlignRight) + arrowItem->setLine(axisRect.left(), gridRect.top(), axisRect.left(), gridRect.bottom()); - QFontMetrics fn(font()); + QFontMetrics fn(axis()->labelsFont()); //title int titlePad = 0; QRectF titleBoundingRect; - if (!titleText().isEmpty() && titleItem()->isVisible()) { + QString titleText = axis()->titleText(); + if (!titleText.isEmpty() && titleItem()->isVisible()) { QFontMetrics fn(title->font()); int size(0); size = gridRect.height(); - QString titleText = this->titleText(); if (fn.boundingRect(titleText).width() > size) { QString string = titleText + "..."; while (fn.boundingRect(string).width() > size && string.length() > 3) - string.remove(string.length() - 4, 1); + string.remove(string.length() - 4, 1); title->setText(string); - } - else { + } else { title->setText(titleText); } @@ -94,10 +91,10 @@ void VerticalAxis::updateGeometry() titleBoundingRect = title->boundingRect(); QPointF center = gridRect.center() - titleBoundingRect.center(); - if (alignment() == Qt::AlignLeft) { + if (axis()->alignment() == Qt::AlignLeft) { title->setPos(axisRect.left() - titleBoundingRect.width() / 2 + titleBoundingRect.height() / 2 + titlePad, center.y()); } - else if (alignment() == Qt::AlignRight) { + else if (axis()->alignment() == Qt::AlignRight) { title->setPos(axisRect.right() - titleBoundingRect.width() / 2 - titleBoundingRect.height() / 2 - titlePad, center.y()); } title->setTransformOriginPoint(titleBoundingRect.center()); @@ -105,14 +102,13 @@ void VerticalAxis::updateGeometry() } for (int i = 0; i < layout.size(); ++i) { - //items QGraphicsLineItem *gridItem = static_cast(lines.at(i)); - QGraphicsLineItem *tickItem = static_cast(axis.at(i + 1)); + QGraphicsLineItem *tickItem = static_cast(arrow.at(i + 1)); QGraphicsSimpleTextItem *labelItem = static_cast(labels.at(i)); //grid line - gridItem->setLine(gridRect.left() , layout[i], gridRect.right(), layout[i]); + gridItem->setLine(gridRect.left(), layout[i], gridRect.right(), layout[i]); //label text wrapping QString text = labelList.at(i); @@ -138,10 +134,10 @@ void VerticalAxis::updateGeometry() int widthDiff = rect.width() - boundingRect.width(); //ticks and label position - if (alignment() == Qt::AlignLeft) { + if (axis()->alignment() == Qt::AlignLeft) { labelItem->setPos(axisRect.right() - rect.width() + (widthDiff / 2) - labelPadding(), layout[i] - center.y()); tickItem->setLine(axisRect.right() - labelPadding(), layout[i], axisRect.right(), layout[i]); - } else if (alignment() == Qt::AlignRight) { + } else if (axis()->alignment() == Qt::AlignRight) { labelItem->setPos(axisRect.left() + labelPadding() - (widthDiff / 2), layout[i] - center.y()); tickItem->setLine(axisRect.left(), layout[i], axisRect.left() + labelPadding(), layout[i]); } @@ -202,7 +198,7 @@ void VerticalAxis::updateGeometry() gridLine = static_cast(lines.at(layout.size())); gridLine->setLine(gridRect.left(), gridRect.top(), gridRect.right(), gridRect.top()); gridLine->setVisible(true); - gridLine = static_cast(lines.at(layout.size()+1)); + gridLine = static_cast(lines.at(layout.size() + 1)); gridLine->setLine(gridRect.left(), gridRect.bottom(), gridRect.right(), gridRect.bottom()); gridLine->setVisible(true); } @@ -212,10 +208,10 @@ QSizeF VerticalAxis::sizeHint(Qt::SizeHint which, const QSizeF &constraint) cons { Q_UNUSED(constraint); - QFontMetrics fn(titleFont()); - QSizeF sh(0,0); + QFontMetrics fn(axis()->titleFont()); + QSizeF sh(0, 0); - if (titleText().isEmpty() || !titleItem()->isVisible()) + if (axis()->titleText().isEmpty() || !titleItem()->isVisible()) return sh; switch (which) { diff --git a/src/axis/verticalaxis_p.h b/src/axis/verticalaxis_p.h index 4d186c4..159bba6 100644 --- a/src/axis/verticalaxis_p.h +++ b/src/axis/verticalaxis_p.h @@ -30,14 +30,14 @@ #ifndef VERTICALAXIS_P_H_ #define VERTICALAXIS_P_H_ -#include "chartaxis_p.h" +#include "cartesianchartaxis_p.h" QTCOMMERCIALCHART_BEGIN_NAMESPACE -class VerticalAxis : public ChartAxis +class VerticalAxis : public CartesianChartAxis { public: - VerticalAxis(QAbstractAxis *axis, QGraphicsItem* item = 0, bool intervalAxis = false); + VerticalAxis(QAbstractAxis *axis, QGraphicsItem *item = 0, bool intervalAxis = false); ~VerticalAxis(); QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const; protected: diff --git a/src/chartdataset.cpp b/src/chartdataset.cpp index d540166..4d7e1a0 100644 --- a/src/chartdataset.cpp +++ b/src/chartdataset.cpp @@ -33,9 +33,13 @@ #include "qpieseries.h" #include "chartitem_p.h" #include "xydomain_p.h" +#include "xypolardomain_p.h" #include "xlogydomain_p.h" #include "logxydomain_p.h" #include "logxlogydomain_p.h" +#include "xlogypolardomain_p.h" +#include "logxypolardomain_p.h" +#include "logxlogypolardomain_p.h" #ifndef QT_ON_ARM #include "qdatetimeaxis.h" @@ -66,6 +70,20 @@ void ChartDataSet::addSeries(QAbstractSeries *series) return; } + // Ignore unsupported series added to polar chart + if (m_chart && m_chart->chartType() == QChart::ChartTypePolar) { + if (!(series->type() == QAbstractSeries::SeriesTypeArea + || series->type() == QAbstractSeries::SeriesTypeLine + || series->type() == QAbstractSeries::SeriesTypeScatter + || series->type() == QAbstractSeries::SeriesTypeSpline)) { + qWarning() << QObject::tr("Can not add series. Series type is not supported by a polar chart."); + return; + } + series->d_ptr->setDomain(new XYPolarDomain()); + } else { + series->d_ptr->setDomain(new XYDomain()); + } + series->d_ptr->initializeDomain(); m_seriesList.append(series); @@ -78,7 +96,7 @@ void ChartDataSet::addSeries(QAbstractSeries *series) /* * This method adds axis to chartdataset, axis ownership is taken from caller. */ -void ChartDataSet::addAxis(QAbstractAxis *axis,Qt::Alignment aligment) +void ChartDataSet::addAxis(QAbstractAxis *axis, Qt::Alignment aligment) { if (m_axisList.contains(axis)) { qWarning() << QObject::tr("Can not add axis. Axis already on the chart."); @@ -87,12 +105,25 @@ void ChartDataSet::addAxis(QAbstractAxis *axis,Qt::Alignment aligment) axis->d_ptr->setAlignment(aligment); - if(!axis->alignment()) { - qWarning()<< QObject::tr("No alignment specified !"); + if (!axis->alignment()) { + qWarning() << QObject::tr("No alignment specified !"); return; }; - QSharedPointer domain(new XYDomain()); + AbstractDomain *newDomain; + if (m_chart && m_chart->chartType() == QChart::ChartTypePolar) { + foreach (QAbstractAxis *existingAxis, axes()) { + if (existingAxis->orientation() == axis->orientation()) { + qWarning() << QObject::tr("Cannot add multiple axes of same orientation to a polar chart!"); + return; + } + } + newDomain = new XYPolarDomain(); + } else { + newDomain = new XYDomain(); + } + + QSharedPointer domain(newDomain); axis->d_ptr->initializeDomain(domain.data()); axis->setParent(this); @@ -122,6 +153,8 @@ void ChartDataSet::removeSeries(QAbstractSeries *series) emit seriesRemoved(series); m_seriesList.removeAll(series); + // Reset domain to default + series->d_ptr->setDomain(new XYDomain()); series->setParent(0); series->d_ptr->m_chart = 0; } @@ -152,13 +185,13 @@ void ChartDataSet::removeAxis(QAbstractAxis *axis) /* * This method attaches axis to series, return true if success. */ -bool ChartDataSet::attachAxis(QAbstractSeries* series,QAbstractAxis *axis) +bool ChartDataSet::attachAxis(QAbstractSeries *series,QAbstractAxis *axis) { Q_ASSERT(series); Q_ASSERT(axis); - QList attachedSeriesList = axis->d_ptr->m_series; - QList attachedAxisList = series->d_ptr->m_axes; + QList attachedSeriesList = axis->d_ptr->m_series; + QList attachedAxisList = series->d_ptr->m_axes; if (!m_seriesList.contains(series)) { qWarning() << QObject::tr("Can not find series on the chart."); @@ -180,25 +213,42 @@ bool ChartDataSet::attachAxis(QAbstractSeries* series,QAbstractAxis *axis) return false; } - AbstractDomain* domain = series->d_ptr->domain(); + AbstractDomain *domain = series->d_ptr->domain(); AbstractDomain::DomainType type = selectDomain(attachedAxisList<type()!=type){ + if (domain->type() != type) { AbstractDomain *old = domain; - domain = createDomain(type); + domain = createDomain(type); domain->setRange(old->minX(), old->maxX(), old->minY(), old->maxY()); + // Initialize domain size to old domain size, as it won't get updated + // unless geometry changes. + domain->setSize(old->size()); } - if(!domain) return false; + if (!domain) + return false; + + if (!domain->attachAxis(axis)) + return false; - if(!domain->attachAxis(axis)) return false; + QList blockedDomains; + domain->blockRangeSignals(true); + blockedDomains << domain; - if(domain!=series->d_ptr->domain()){ - foreach(QAbstractAxis* axis,series->d_ptr->m_axes){ + if (domain != series->d_ptr->domain()) { + foreach (QAbstractAxis *axis, series->d_ptr->m_axes) { series->d_ptr->domain()->detachAxis(axis); domain->attachAxis(axis); + foreach (QAbstractSeries *otherSeries, axis->d_ptr->m_series) { + if (otherSeries != series && otherSeries->d_ptr->domain()) { + if (!otherSeries->d_ptr->domain()->rangeSignalsBlocked()) { + otherSeries->d_ptr->domain()->blockRangeSignals(true); + blockedDomains << otherSeries->d_ptr->domain(); + } + } + } } series->d_ptr->setDomain(domain); series->d_ptr->initializeDomain(); @@ -210,6 +260,9 @@ bool ChartDataSet::attachAxis(QAbstractSeries* series,QAbstractAxis *axis) series->d_ptr->initializeAxes(); axis->d_ptr->initializeDomain(domain); + foreach (AbstractDomain *blockedDomain, blockedDomains) + blockedDomain->blockRangeSignals(false); + return true; } @@ -327,12 +380,12 @@ void ChartDataSet::findMinMaxForSeries(QList series,Qt::Orien { Q_ASSERT(!series.isEmpty()); - AbstractDomain* domain = series.first()->d_ptr->domain(); + AbstractDomain *domain = series.first()->d_ptr->domain(); min = (orientation == Qt::Vertical) ? domain->minY() : domain->minX(); max = (orientation == Qt::Vertical) ? domain->maxY() : domain->maxX(); - for(int i = 1; i< series.size(); i++) { - AbstractDomain* domain = series[i]->d_ptr->domain(); + for (int i = 1; i< series.size(); i++) { + AbstractDomain *domain = series[i]->d_ptr->domain(); min = qMin((orientation == Qt::Vertical) ? domain->minY() : domain->minX(), min); max = qMax((orientation == Qt::Vertical) ? domain->maxY() : domain->maxX(), max); } @@ -437,7 +490,7 @@ QPointF ChartDataSet::mapToPosition(const QPointF &value, QAbstractSeries *serie return point; } -QList ChartDataSet::axes() const +QList ChartDataSet::axes() const { return m_axisList; } @@ -447,7 +500,7 @@ QList ChartDataSet::series() const return m_seriesList; } -AbstractDomain::DomainType ChartDataSet::selectDomain(QList axes) +AbstractDomain::DomainType ChartDataSet::selectDomain(QList axes) { enum Type { Undefined = 0, @@ -458,75 +511,95 @@ AbstractDomain::DomainType ChartDataSet::selectDomain(QList axes int horizontal(Undefined); int vertical(Undefined); - foreach(QAbstractAxis* axis, axes) - { - switch(axis->type()) { - case QAbstractAxis::AxisTypeLogValue: - - if(axis->orientation()==Qt::Horizontal) { - horizontal|=LogType; - } - if(axis->orientation()==Qt::Vertical) { - vertical|=LogType; - } + // Assume cartesian chart type, unless chart is set + QChart::ChartType chartType(QChart::ChartTypeCartesian); + if (m_chart) + chartType = m_chart->chartType(); + foreach (QAbstractAxis *axis, axes) + { + switch (axis->type()) { + case QAbstractAxis::AxisTypeLogValue: + if (axis->orientation() == Qt::Horizontal) + horizontal |= LogType; + if (axis->orientation() == Qt::Vertical) + vertical |= LogType; break; - case QAbstractAxis::AxisTypeValue: - case QAbstractAxis::AxisTypeBarCategory: - case QAbstractAxis::AxisTypeCategory: - case QAbstractAxis::AxisTypeDateTime: - if(axis->orientation()==Qt::Horizontal) { - horizontal|=ValueType; - } - if(axis->orientation()==Qt::Vertical) { - vertical|=ValueType; - } + case QAbstractAxis::AxisTypeValue: + case QAbstractAxis::AxisTypeBarCategory: + case QAbstractAxis::AxisTypeCategory: + case QAbstractAxis::AxisTypeDateTime: + if (axis->orientation() == Qt::Horizontal) + horizontal |= ValueType; + if (axis->orientation() == Qt::Vertical) + vertical |= ValueType; break; - default: - qWarning()<<"Undefined type"; + default: + qWarning() << "Undefined type"; break; } } - if(vertical==Undefined) vertical=ValueType; - if(horizontal==Undefined) horizontal=ValueType; + if (vertical == Undefined) + vertical = ValueType; + if (horizontal == Undefined) + horizontal = ValueType; - if(vertical==ValueType && horizontal== ValueType) { - return AbstractDomain::XYDomain; + if (vertical == ValueType && horizontal == ValueType) { + if (chartType == QChart::ChartTypeCartesian) + return AbstractDomain::XYDomain; + else if (chartType == QChart::ChartTypePolar) + return AbstractDomain::XYPolarDomain; } - if(vertical==LogType && horizontal== ValueType) { - return AbstractDomain::XLogYDomain; + if (vertical == LogType && horizontal == ValueType) { + if (chartType == QChart::ChartTypeCartesian) + return AbstractDomain::XLogYDomain; + if (chartType == QChart::ChartTypePolar) + return AbstractDomain::XLogYPolarDomain; } - if(vertical==ValueType && horizontal== LogType) { - return AbstractDomain::LogXYDomain; + if (vertical == ValueType && horizontal == LogType) { + if (chartType == QChart::ChartTypeCartesian) + return AbstractDomain::LogXYDomain; + else if (chartType == QChart::ChartTypePolar) + return AbstractDomain::LogXYPolarDomain; } - if(vertical==LogType && horizontal== LogType) { - return AbstractDomain::LogXLogYDomain; + if (vertical == LogType && horizontal == LogType) { + if (chartType == QChart::ChartTypeCartesian) + return AbstractDomain::LogXLogYDomain; + else if (chartType == QChart::ChartTypePolar) + return AbstractDomain::LogXLogYPolarDomain; } return AbstractDomain::UndefinedDomain; } - //refactor create factory AbstractDomain* ChartDataSet::createDomain(AbstractDomain::DomainType type) { - switch(type) - { - case AbstractDomain::LogXLogYDomain: - return new LogXLogYDomain(); - case AbstractDomain::XYDomain: - return new XYDomain(); - case AbstractDomain::XLogYDomain: - return new XLogYDomain(); - case AbstractDomain::LogXYDomain: - return new LogXYDomain(); - default: - return 0; - } + switch (type) + { + case AbstractDomain::LogXLogYDomain: + return new LogXLogYDomain(); + case AbstractDomain::XYDomain: + return new XYDomain(); + case AbstractDomain::XLogYDomain: + return new XLogYDomain(); + case AbstractDomain::LogXYDomain: + return new LogXYDomain(); + case AbstractDomain::XYPolarDomain: + return new XYPolarDomain(); + case AbstractDomain::XLogYPolarDomain: + return new XLogYPolarDomain(); + case AbstractDomain::LogXYPolarDomain: + return new LogXYPolarDomain(); + case AbstractDomain::LogXLogYPolarDomain: + return new LogXLogYPolarDomain(); + default: + return 0; + } } #include "moc_chartdataset_p.cpp" diff --git a/src/chartpresenter.cpp b/src/chartpresenter.cpp index 8194f08..f8242aa 100644 --- a/src/chartpresenter.cpp +++ b/src/chartpresenter.cpp @@ -27,24 +27,28 @@ #include "chartanimation_p.h" #include "qabstractseries_p.h" #include "qareaseries.h" -#include "chartaxis_p.h" +#include "chartaxiselement_p.h" #include "chartbackground_p.h" -#include "chartlayout_p.h" +#include "cartesianchartlayout_p.h" +#include "polarchartlayout_p.h" #include "charttitle_p.h" #include QTCOMMERCIALCHART_BEGIN_NAMESPACE -ChartPresenter::ChartPresenter(QChart *chart) +ChartPresenter::ChartPresenter(QChart *chart, QChart::ChartType type) : QObject(chart), m_chart(chart), m_options(QChart::NoAnimation), m_state(ShowState), - m_layout(new ChartLayout(this)), m_background(0), m_title(0) { - + if (type == QChart::ChartTypeCartesian) + m_layout = new CartesianChartLayout(this); + else if (type == QChart::ChartTypePolar) + m_layout = new PolarChartLayout(this); + Q_ASSERT(m_layout); } ChartPresenter::~ChartPresenter() @@ -54,25 +58,25 @@ ChartPresenter::~ChartPresenter() void ChartPresenter::setGeometry(const QRectF rect) { - if(m_rect != rect) { - m_rect=rect; - foreach (ChartItem *chart, m_chartItems){ - chart->domain()->setSize(rect.size()); - chart->setPos(rect.topLeft()); - } - } + if (m_rect != rect) { + m_rect = rect; + foreach (ChartItem *chart, m_chartItems) { + chart->domain()->setSize(rect.size()); + chart->setPos(rect.topLeft()); + } + } } QRectF ChartPresenter::geometry() const { - return m_rect; + return m_rect; } void ChartPresenter::handleAxisAdded(QAbstractAxis *axis) { axis->d_ptr->initializeGraphics(rootItem()); axis->d_ptr->initializeAnimations(m_options); - ChartAxis *item = axis->d_ptr->axisItem(); + ChartAxisElement *item = axis->d_ptr->axisItem(); item->setPresenter(this); item->setThemeManager(m_chart->d_ptr->m_themeManager); m_axisItems<d_ptr->m_item.take(); + ChartAxisElement *item = axis->d_ptr->m_item.take(); item->hide(); item->disconnect(); item->deleteLater(); @@ -275,7 +279,7 @@ bool ChartPresenter::isBackgroundDropShadowEnabled() const } -ChartLayout *ChartPresenter::layout() +AbstractChartLayout *ChartPresenter::layout() { return m_layout; } @@ -295,7 +299,7 @@ ChartBackground *ChartPresenter::backgroundElement() return m_background; } -QList ChartPresenter::axisItems() const +QList ChartPresenter::axisItems() const { return m_axisItems; } diff --git a/src/chartpresenter_p.h b/src/chartpresenter_p.h index 9f1c8b8..19bc5c4 100644 --- a/src/chartpresenter_p.h +++ b/src/chartpresenter_p.h @@ -42,12 +42,12 @@ class AxisItem; class QAbstractSeries; class ChartDataSet; class AbstractDomain; -class ChartAxis; +class ChartAxisElement; class ChartAnimator; class ChartBackground; class ChartTitle; class ChartAnimation; -class ChartLayout; +class AbstractChartLayout; class ChartPresenter: public QObject { @@ -78,7 +78,7 @@ public: ZoomOutState }; - ChartPresenter(QChart *chart); + ChartPresenter(QChart *chart, QChart::ChartType type); virtual ~ChartPresenter(); @@ -88,12 +88,9 @@ public: QGraphicsItem *rootItem(){ return m_chart; } ChartBackground *backgroundElement(); ChartTitle *titleElement(); - QList axisItems() const; + QList axisItems() const; QList chartItems() const; - ChartItem* chartElement(QAbstractSeries* series) const; - ChartAxis* chartElement(QAbstractAxis* axis) const; - QLegend *legend(); void setBackgroundBrush(const QBrush &brush); @@ -128,7 +125,9 @@ public: void setState(State state,QPointF point); State state() const { return m_state; } QPointF statePoint() const { return m_statePoint; } - ChartLayout *layout(); + AbstractChartLayout *layout(); + + QChart::ChartType chartType() const { return m_chart->chartType(); } private: void createBackgroundItem(); @@ -149,14 +148,14 @@ Q_SIGNALS: private: QChart *m_chart; QList m_chartItems; - QList m_axisItems; + QList m_axisItems; QList m_series; QList m_axes; QChart::AnimationOptions m_options; State m_state; QPointF m_statePoint; QList m_animations; - ChartLayout *m_layout; + AbstractChartLayout *m_layout; ChartBackground *m_background; ChartTitle *m_title; QRectF m_rect; diff --git a/src/domain/abstractdomain.cpp b/src/domain/abstractdomain.cpp index bcaa516..277472d 100644 --- a/src/domain/abstractdomain.cpp +++ b/src/domain/abstractdomain.cpp @@ -38,7 +38,7 @@ AbstractDomain::~AbstractDomain() { } -void AbstractDomain::setSize(const QSizeF& size) +void AbstractDomain::setSize(const QSizeF &size) { if(m_size!=size) { @@ -122,9 +122,9 @@ void AbstractDomain::handleHorizontalAxisRangeChanged(qreal min, qreal max) void AbstractDomain::blockRangeSignals(bool block) { - if(m_signalsBlocked!=block){ + if (m_signalsBlocked!=block) { m_signalsBlocked=block; - if(!block) { + if (!block) { emit rangeHorizontalChanged(m_minX,m_maxX); emit rangeVerticalChanged(m_minY,m_maxY); } @@ -165,14 +165,14 @@ qreal AbstractDomain::niceNumber(qreal x, bool ceiling) return q * z; } -bool AbstractDomain::attachAxis(QAbstractAxis* axis) +bool AbstractDomain::attachAxis(QAbstractAxis *axis) { - if(axis->orientation()==Qt::Vertical) { + if (axis->orientation() == Qt::Vertical) { QObject::connect(axis->d_ptr.data(), SIGNAL(rangeChanged(qreal,qreal)), this, SLOT(handleVerticalAxisRangeChanged(qreal,qreal))); QObject::connect(this, SIGNAL(rangeVerticalChanged(qreal,qreal)), axis->d_ptr.data(), SLOT(handleRangeChanged(qreal,qreal))); } - if(axis->orientation()==Qt::Horizontal) { + if (axis->orientation() == Qt::Horizontal) { QObject::connect(axis->d_ptr.data(), SIGNAL(rangeChanged(qreal,qreal)), this, SLOT(handleHorizontalAxisRangeChanged(qreal,qreal))); QObject::connect(this, SIGNAL(rangeHorizontalChanged(qreal,qreal)), axis->d_ptr.data(), SLOT(handleRangeChanged(qreal,qreal))); } @@ -180,14 +180,14 @@ bool AbstractDomain::attachAxis(QAbstractAxis* axis) return true; } -bool AbstractDomain::detachAxis(QAbstractAxis* axis) +bool AbstractDomain::detachAxis(QAbstractAxis *axis) { - if(axis->orientation()==Qt::Vertical) { + if (axis->orientation() == Qt::Vertical) { QObject::disconnect(axis->d_ptr.data(), SIGNAL(rangeChanged(qreal,qreal)), this, SLOT(handleVerticalAxisRangeChanged(qreal,qreal))); QObject::disconnect(this, SIGNAL(rangeVerticalChanged(qreal,qreal)), axis->d_ptr.data(), SLOT(handleRangeChanged(qreal,qreal))); } - if(axis->orientation()==Qt::Horizontal) { + if (axis->orientation() == Qt::Horizontal) { QObject::disconnect(axis->d_ptr.data(), SIGNAL(rangeChanged(qreal,qreal)), this, SLOT(handleHorizontalAxisRangeChanged(qreal,qreal))); QObject::disconnect(this, SIGNAL(rangeHorizontalChanged(qreal,qreal)), axis->d_ptr.data(), SLOT(handleRangeChanged(qreal,qreal))); } @@ -199,10 +199,10 @@ bool AbstractDomain::detachAxis(QAbstractAxis* axis) bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const AbstractDomain &domain1, const AbstractDomain &domain2) { - return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) && - qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) && - qFuzzyIsNull(domain1.m_minX - domain2.m_minX) && - qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); + return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) + && qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) + && qFuzzyIsNull(domain1.m_minX - domain2.m_minX) + && qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); } @@ -218,6 +218,17 @@ QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const AbstractDo return dbg.maybeSpace(); } +// This function adjusts min/max ranges to failsafe values if negative/zero values are attempted. +void AbstractDomain::adjustLogDomainRanges(qreal &min, qreal &max) +{ + if (min <= 0) { + min = 1.0; + if (max <= min) + max = min + 1.0; + } +} + + #include "moc_abstractdomain_p.cpp" QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/domain/abstractdomain_p.h b/src/domain/abstractdomain_p.h index 907cccf..cf7c2ba 100644 --- a/src/domain/abstractdomain_p.h +++ b/src/domain/abstractdomain_p.h @@ -42,12 +42,20 @@ class QTCOMMERCIALCHART_AUTOTEST_EXPORT AbstractDomain: public QObject { Q_OBJECT public: - enum DomainType { UndefinedDomain, XYDomain, XLogYDomain, LogXYDomain, LogXLogYDomain }; + enum DomainType { UndefinedDomain, + XYDomain, + XLogYDomain, + LogXYDomain, + LogXLogYDomain, + XYPolarDomain, + XLogYPolarDomain, + LogXYPolarDomain, + LogXLogYPolarDomain }; public: explicit AbstractDomain(QObject *object = 0); virtual ~AbstractDomain(); - void setSize(const QSizeF& size); + virtual void setSize(const QSizeF &size); QSizeF size() const; virtual DomainType type() = 0; @@ -82,10 +90,10 @@ public: virtual QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const = 0; virtual QPointF calculateDomainPoint(const QPointF &point) const = 0; - virtual QVector calculateGeometryPoints(const QList& vector) const = 0; + virtual QVector calculateGeometryPoints(const QList &vector) const = 0; - virtual bool attachAxis(QAbstractAxis* axis); - virtual bool detachAxis(QAbstractAxis* axis); + virtual bool attachAxis(QAbstractAxis *axis); + virtual bool detachAxis(QAbstractAxis *axis); static void looseNiceNumbers(qreal &min, qreal &max, int &ticksCount); static qreal niceNumber(qreal x, bool ceiling); @@ -100,6 +108,8 @@ public Q_SLOTS: void handleHorizontalAxisRangeChanged(qreal min,qreal max); protected: + void adjustLogDomainRanges(qreal &min, qreal &max); + qreal m_minX; qreal m_maxX; qreal m_minY; diff --git a/src/domain/domain.pri b/src/domain/domain.pri index 1d5823d..ab4fcfc 100644 --- a/src/domain/domain.pri +++ b/src/domain/domain.pri @@ -5,14 +5,24 @@ DEPENDPATH += $$PWD SOURCES += \ $$PWD/abstractdomain.cpp \ + $$PWD/polardomain.cpp \ $$PWD/xydomain.cpp \ + $$PWD/xypolardomain.cpp \ $$PWD/xlogydomain.cpp \ + $$PWD/xlogypolardomain.cpp \ $$PWD/logxydomain.cpp \ - $$PWD/logxlogydomain.cpp + $$PWD/logxypolardomain.cpp \ + $$PWD/logxlogydomain.cpp \ + $$PWD/logxlogypolardomain.cpp PRIVATE_HEADERS += \ $$PWD/abstractdomain_p.h \ + $$PWD/polardomain_p.h \ $$PWD/xydomain_p.h \ + $$PWD/xypolardomain_p.h \ $$PWD/xlogydomain_p.h \ + $$PWD/xlogypolardomain_p.h \ $$PWD/logxydomain_p.h \ - $$PWD/logxlogydomain_p.h + $$PWD/logxypolardomain_p.h \ + $$PWD/logxlogydomain_p.h \ + $$PWD/logxlogypolardomain_p.h diff --git a/src/domain/logxlogydomain.cpp b/src/domain/logxlogydomain.cpp index 7032d13..adb9bc1 100644 --- a/src/domain/logxlogydomain.cpp +++ b/src/domain/logxlogydomain.cpp @@ -45,6 +45,9 @@ void LogXLogYDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) bool axisXChanged = false; bool axisYChanged = false; + adjustLogDomainRanges(minX, maxX); + adjustLogDomainRanges(minY, maxY); + if (!qFuzzyIsNull(m_minX - minX) || !qFuzzyIsNull(m_maxX - maxX)) { m_minX = minX; m_maxX = maxX; @@ -65,7 +68,7 @@ void LogXLogYDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) qreal logMaxY = log10(m_maxY) / log10(m_logBaseY); m_logLeftY = logMinY < logMaxY ? logMinY : logMaxY; m_logRightY = logMinY > logMaxY ? logMinY : logMaxY; - if(!m_signalsBlocked) + if (!m_signalsBlocked) emit rangeVerticalChanged(m_minY, m_maxY); } @@ -141,13 +144,13 @@ QPointF LogXLogYDomain::calculateGeometryPoint(const QPointF &point, bool &ok) c ok = true; return QPointF(x, y); } else { - qWarning() << "Logarithm of negative value is undefined. Empty layout returned"; + qWarning() << "Logarithm of negative value is undefined. Empty layout returned."; ok = false; return QPointF(); } } -QVector LogXLogYDomain::calculateGeometryPoints(const QList& vector) const +QVector LogXLogYDomain::calculateGeometryPoints(const QList &vector) const { const qreal deltaX = m_size.width() / qAbs(m_logRightX - m_logLeftX); const qreal deltaY = m_size.height() / qAbs(m_logRightY - m_logLeftY); @@ -162,7 +165,7 @@ QVector LogXLogYDomain::calculateGeometryPoints(const QList& v result[i].setX(x); result[i].setY(y); } else { - qWarning() << "Logarithm of negative value is undefined. Empty layout returned"; + qWarning() << "Logarithm of negative value is undefined. Empty layout returned."; return QVector(); } } @@ -178,17 +181,17 @@ QPointF LogXLogYDomain::calculateDomainPoint(const QPointF &point) const return QPointF(x, y); } -bool LogXLogYDomain::attachAxis(QAbstractAxis* axis) +bool LogXLogYDomain::attachAxis(QAbstractAxis *axis) { AbstractDomain::attachAxis(axis); QLogValueAxis *logAxis = qobject_cast(axis); - if(logAxis && logAxis->orientation()==Qt::Vertical) { + if (logAxis && logAxis->orientation() == Qt::Vertical) { QObject::connect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleVerticalAxisBaseChanged(qreal))); handleVerticalAxisBaseChanged(logAxis->base()); } - if(logAxis && logAxis->orientation()==Qt::Horizontal) { + if (logAxis && logAxis->orientation() == Qt::Horizontal) { QObject::connect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleHorizontalAxisBaseChanged(qreal))); handleHorizontalAxisBaseChanged(logAxis->base()); } @@ -196,15 +199,15 @@ bool LogXLogYDomain::attachAxis(QAbstractAxis* axis) return true; } -bool LogXLogYDomain::detachAxis(QAbstractAxis* axis) +bool LogXLogYDomain::detachAxis(QAbstractAxis *axis) { AbstractDomain::detachAxis(axis); QLogValueAxis *logAxis = qobject_cast(axis); - if(logAxis && logAxis->orientation()==Qt::Vertical) + if (logAxis && logAxis->orientation() == Qt::Vertical) QObject::disconnect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleVerticalAxisBaseChanged(qreal))); - if(logAxis && logAxis->orientation()==Qt::Horizontal) + if (logAxis && logAxis->orientation() == Qt::Horizontal) QObject::disconnect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleHorizontalAxisBaseChanged(qreal))); return true; @@ -234,10 +237,10 @@ void LogXLogYDomain::handleHorizontalAxisBaseChanged(qreal baseX) bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const LogXLogYDomain &domain1, const LogXLogYDomain &domain2) { - return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) && - qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) && - qFuzzyIsNull(domain1.m_minX - domain2.m_minX) && - qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); + return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) + && qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) + && qFuzzyIsNull(domain1.m_minX - domain2.m_minX) + && qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); } diff --git a/src/domain/logxlogydomain_p.h b/src/domain/logxlogydomain_p.h index 2d60416..014bf80 100644 --- a/src/domain/logxlogydomain_p.h +++ b/src/domain/logxlogydomain_p.h @@ -56,10 +56,10 @@ public: QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; QPointF calculateDomainPoint(const QPointF &point) const; - QVector calculateGeometryPoints(const QList& vector) const; + QVector calculateGeometryPoints(const QList &vector) const; - bool attachAxis(QAbstractAxis* axis); - bool detachAxis(QAbstractAxis* axis); + bool attachAxis(QAbstractAxis *axis); + bool detachAxis(QAbstractAxis *axis); public Q_SLOTS: void handleVerticalAxisBaseChanged(qreal baseY); diff --git a/src/domain/logxlogypolardomain.cpp b/src/domain/logxlogypolardomain.cpp new file mode 100644 index 0000000..a193ce1 --- /dev/null +++ b/src/domain/logxlogypolardomain.cpp @@ -0,0 +1,267 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "logxlogypolardomain_p.h" +#include "qabstractaxis_p.h" +#include "qlogvalueaxis.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +LogXLogYPolarDomain::LogXLogYPolarDomain(QObject *parent) + : PolarDomain(parent), + m_logLeftX(0), + m_logRightX(1), + m_logBaseX(10), + m_logInnerY(0), + m_logOuterY(1), + m_logBaseY(10) +{ +} + +LogXLogYPolarDomain::~LogXLogYPolarDomain() +{ +} + +void LogXLogYPolarDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) +{ + bool axisXChanged = false; + bool axisYChanged = false; + + adjustLogDomainRanges(minX, maxX); + adjustLogDomainRanges(minY, maxY); + + if (!qFuzzyCompare(m_minX, minX) || !qFuzzyCompare(m_maxX, maxX)) { + m_minX = minX; + m_maxX = maxX; + axisXChanged = true; + qreal logMinX = log10(m_minX) / log10(m_logBaseX); + qreal logMaxX = log10(m_maxX) / log10(m_logBaseX); + m_logLeftX = logMinX < logMaxX ? logMinX : logMaxX; + m_logRightX = logMinX > logMaxX ? logMinX : logMaxX; + if (!m_signalsBlocked) + emit rangeHorizontalChanged(m_minX, m_maxX); + } + + if (!qFuzzyIsNull(m_minY - minY) || !qFuzzyIsNull(m_maxY - maxY)) { + m_minY = minY; + m_maxY = maxY; + axisYChanged = true; + qreal logMinY = log10(m_minY) / log10(m_logBaseY); + qreal logMaxY = log10(m_maxY) / log10(m_logBaseY); + m_logInnerY = logMinY < logMaxY ? logMinY : logMaxY; + m_logOuterY = logMinY > logMaxY ? logMinY : logMaxY; + if (!m_signalsBlocked) + emit rangeVerticalChanged(m_minY, m_maxY); + } + + if (axisXChanged || axisYChanged) + emit updated(); +} + +void LogXLogYPolarDomain::zoomIn(const QRectF &rect) +{ + qreal logLeftX = rect.left() * (m_logRightX - m_logLeftX) / m_size.width() + m_logLeftX; + qreal logRightX = rect.right() * (m_logRightX - m_logLeftX) / m_size.width() + m_logLeftX; + qreal leftX = qPow(m_logBaseX, logLeftX); + qreal rightX = qPow(m_logBaseX, logRightX); + qreal minX = leftX < rightX ? leftX : rightX; + qreal maxX = leftX > rightX ? leftX : rightX; + + qreal logLeftY = m_logOuterY - rect.bottom() * (m_logOuterY - m_logInnerY) / m_size.height(); + qreal logRightY = m_logOuterY - rect.top() * (m_logOuterY - m_logInnerY) / m_size.height(); + qreal leftY = qPow(m_logBaseY, logLeftY); + qreal rightY = qPow(m_logBaseY, logRightY); + qreal minY = leftY < rightY ? leftY : rightY; + qreal maxY = leftY > rightY ? leftY : rightY; + + setRange(minX, maxX, minY, maxY); +} + +void LogXLogYPolarDomain::zoomOut(const QRectF &rect) +{ + const qreal factorX = m_size.width() / rect.width(); + + qreal logLeftX = m_logLeftX + (m_logRightX - m_logLeftX) / 2.0 * (1.0 - factorX); + qreal logRIghtX = m_logLeftX + (m_logRightX - m_logLeftX) / 2.0 * (1.0 + factorX); + qreal leftX = qPow(m_logBaseX, logLeftX); + qreal rightX = qPow(m_logBaseX, logRIghtX); + qreal minX = leftX < rightX ? leftX : rightX; + qreal maxX = leftX > rightX ? leftX : rightX; + + const qreal factorY = m_size.height() / rect.height(); + qreal newLogMinY = m_logInnerY + (m_logOuterY - m_logInnerY) / 2.0 * (1.0 - factorY); + qreal newLogMaxY = m_logInnerY + (m_logOuterY - m_logInnerY) / 2.0 * (1.0 + factorY); + qreal leftY = qPow(m_logBaseY, newLogMinY); + qreal rightY = qPow(m_logBaseY, newLogMaxY); + qreal minY = leftY < rightY ? leftY : rightY; + qreal maxY = leftY > rightY ? leftY : rightY; + + setRange(minX, maxX, minY, maxY); +} + +void LogXLogYPolarDomain::move(qreal dx, qreal dy) +{ + qreal stepX = dx * (m_logRightX - m_logLeftX) / m_size.width(); + qreal leftX = qPow(m_logBaseX, m_logLeftX + stepX); + qreal rightX = qPow(m_logBaseX, m_logRightX + stepX); + qreal minX = leftX < rightX ? leftX : rightX; + qreal maxX = leftX > rightX ? leftX : rightX; + + qreal stepY = dy * (m_logOuterY - m_logInnerY) / m_radius; + qreal leftY = qPow(m_logBaseY, m_logInnerY + stepY); + qreal rightY = qPow(m_logBaseY, m_logOuterY + stepY); + qreal minY = leftY < rightY ? leftY : rightY; + qreal maxY = leftY > rightY ? leftY : rightY; + + setRange(minX, maxX, minY, maxY); +} + +qreal LogXLogYPolarDomain::toAngularCoordinate(qreal value, bool &ok) const +{ + qreal retVal; + if (value <= 0) { + ok = false; + retVal = 0.0; + } else { + ok = true; + const qreal tickSpan = 360.0 / qAbs(m_logRightX - m_logLeftX); + const qreal logValue = log10(value) / log10(m_logBaseX); + const qreal valueDelta = logValue - m_logLeftX; + + retVal = valueDelta * tickSpan; + } + return retVal; +} + +qreal LogXLogYPolarDomain::toRadialCoordinate(qreal value, bool &ok) const +{ + qreal retVal; + if (value <= 0) { + ok = false; + retVal = 0.0; + } else { + ok = true; + const qreal tickSpan = m_radius / qAbs(m_logOuterY - m_logInnerY); + const qreal logValue = log10(value) / log10(m_logBaseY); + const qreal valueDelta = logValue - m_logInnerY; + + retVal = valueDelta * tickSpan; + + if (retVal < 0.0) + retVal = 0.0; + } + return retVal; +} + +QPointF LogXLogYPolarDomain::calculateDomainPoint(const QPointF &point) const +{ + if (point == m_center) + return QPointF(0.0, m_minY); + + QLineF line(m_center, point); + qreal a = 90.0 - line.angle(); + if (a < 0.0) + a += 360.0; + + const qreal deltaX = 360.0 / qAbs(m_logRightX - m_logLeftX); + a = qPow(m_logBaseX, m_logLeftX + (a / deltaX)); + + const qreal deltaY = m_radius / qAbs(m_logOuterY - m_logInnerY); + qreal r = qPow(m_logBaseY, m_logInnerY + (line.length() / deltaY)); + + return QPointF(a, r); +} + +bool LogXLogYPolarDomain::attachAxis(QAbstractAxis *axis) +{ + AbstractDomain::attachAxis(axis); + QLogValueAxis *logAxis = qobject_cast(axis); + + if (logAxis && logAxis->orientation() == Qt::Horizontal) { + QObject::connect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleHorizontalAxisBaseChanged(qreal))); + handleHorizontalAxisBaseChanged(logAxis->base()); + } else if (logAxis && logAxis->orientation() == Qt::Vertical){ + QObject::connect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleVerticalAxisBaseChanged(qreal))); + handleVerticalAxisBaseChanged(logAxis->base()); + } + + return true; +} + +bool LogXLogYPolarDomain::detachAxis(QAbstractAxis *axis) +{ + AbstractDomain::detachAxis(axis); + QLogValueAxis *logAxis = qobject_cast(axis); + + if (logAxis && logAxis->orientation() == Qt::Horizontal) + QObject::disconnect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleHorizontalAxisBaseChanged(qreal))); + else if (logAxis && logAxis->orientation() == Qt::Vertical) + QObject::disconnect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleVerticalAxisBaseChanged(qreal))); + + return true; +} + +void LogXLogYPolarDomain::handleHorizontalAxisBaseChanged(qreal baseX) +{ + m_logBaseX = baseX; + qreal logMinX = log10(m_minX) / log10(m_logBaseX); + qreal logMaxX = log10(m_maxX) / log10(m_logBaseX); + m_logLeftX = logMinX < logMaxX ? logMinX : logMaxX; + m_logRightX = logMinX > logMaxX ? logMinX : logMaxX; + emit updated(); +} + +void LogXLogYPolarDomain::handleVerticalAxisBaseChanged(qreal baseY) +{ + m_logBaseY = baseY; + qreal logMinY = log10(m_minY) / log10(m_logBaseY); + qreal logMaxY = log10(m_maxY) / log10(m_logBaseY); + m_logInnerY = logMinY < logMaxY ? logMinY : logMaxY; + m_logOuterY = logMinY > logMaxY ? logMinY : logMaxY; + emit updated(); +} + +// operators + +bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const LogXLogYPolarDomain &domain1, const LogXLogYPolarDomain &domain2) +{ + return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) + && qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) + && qFuzzyIsNull(domain1.m_minX - domain2.m_minX) + && qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); +} + + +bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator!= (const LogXLogYPolarDomain &domain1, const LogXLogYPolarDomain &domain2) +{ + return !(domain1 == domain2); +} + + +QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const LogXLogYPolarDomain &domain) +{ + dbg.nospace() << "AbstractDomain(" << domain.m_minX << ',' << domain.m_maxX << ',' << domain.m_minY << ',' << domain.m_maxY << ')' << domain.m_size; + return dbg.maybeSpace(); +} + +#include "moc_logxlogypolardomain_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/domain/logxlogypolardomain_p.h b/src/domain/logxlogypolardomain_p.h new file mode 100644 index 0000000..106f58a --- /dev/null +++ b/src/domain/logxlogypolardomain_p.h @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef LOGXLOGYPOLARDOMAIN_H +#define LOGXLOGYPOLARDOMAIN_H +#include "polardomain_p.h" +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QTCOMMERCIALCHART_AUTOTEST_EXPORT LogXLogYPolarDomain: public PolarDomain +{ + Q_OBJECT +public: + explicit LogXLogYPolarDomain(QObject *object = 0); + virtual ~LogXLogYPolarDomain(); + + DomainType type() { return AbstractDomain::LogXLogYPolarDomain; } + + void setRange(qreal minX, qreal maxX, qreal minY, qreal maxY); + + friend bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const LogXLogYPolarDomain &domain1, const LogXLogYPolarDomain &domain2); + friend bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator!= (const LogXLogYPolarDomain &domain1, const LogXLogYPolarDomain &domain2); + friend QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const LogXLogYPolarDomain &domain); + + void zoomIn(const QRectF &rect); + void zoomOut(const QRectF &rect); + void move(qreal dx, qreal dy); + + QPointF calculateDomainPoint(const QPointF &point) const; + + bool attachAxis(QAbstractAxis *axis); + bool detachAxis(QAbstractAxis *axis); + +public Q_SLOTS: + void handleVerticalAxisBaseChanged(qreal baseY); + void handleHorizontalAxisBaseChanged(qreal baseX); + +protected: + qreal toAngularCoordinate(qreal value, bool &ok) const; + qreal toRadialCoordinate(qreal value, bool &ok) const; + +private: + qreal m_logLeftX; + qreal m_logRightX; + qreal m_logBaseX; + qreal m_logInnerY; + qreal m_logOuterY; + qreal m_logBaseY; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // LOGXLOGYPOLARDOMAIN_H diff --git a/src/domain/logxydomain.cpp b/src/domain/logxydomain.cpp index ffa739b..da6e210 100644 --- a/src/domain/logxydomain.cpp +++ b/src/domain/logxydomain.cpp @@ -42,6 +42,8 @@ void LogXYDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) bool axisXChanged = false; bool axisYChanged = false; + adjustLogDomainRanges(minX, maxX); + if (!qFuzzyCompare(m_minX, minX) || !qFuzzyCompare(m_maxX, maxX)) { m_minX = minX; m_maxX = maxX; @@ -58,7 +60,7 @@ void LogXYDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) m_minY = minY; m_maxY = maxY; axisYChanged = true; - if(!m_signalsBlocked) + if (!m_signalsBlocked) emit rangeVerticalChanged(m_minY, m_maxY); } @@ -137,13 +139,13 @@ QPointF LogXYDomain::calculateGeometryPoint(const QPointF &point, bool &ok) cons ok = true; return QPointF(x, y); } else { - qWarning() << "Logarithm of negative value is undefined. Empty layout returned"; + qWarning() << "Logarithm of negative value is undefined. Empty layout returned."; ok = false; return QPointF(); } } -QVector LogXYDomain::calculateGeometryPoints(const QList& vector) const +QVector LogXYDomain::calculateGeometryPoints(const QList &vector) const { const qreal deltaX = m_size.width() / (m_logRightX - m_logLeftX); const qreal deltaY = m_size.height() / (m_maxY - m_minY); @@ -158,7 +160,7 @@ QVector LogXYDomain::calculateGeometryPoints(const QList& vect result[i].setX(x); result[i].setY(y); } else { - qWarning() << "Logarithm of negative value is undefined. Empty layout returned"; + qWarning() << "Logarithm of negative value is undefined. Empty layout returned."; return QVector(); } } @@ -174,12 +176,12 @@ QPointF LogXYDomain::calculateDomainPoint(const QPointF &point) const return QPointF(x, y); } -bool LogXYDomain::attachAxis(QAbstractAxis* axis) +bool LogXYDomain::attachAxis(QAbstractAxis *axis) { AbstractDomain::attachAxis(axis); QLogValueAxis *logAxis = qobject_cast(axis); - if(logAxis && logAxis->orientation()==Qt::Horizontal) { + if (logAxis && logAxis->orientation() == Qt::Horizontal) { QObject::connect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleHorizontalAxisBaseChanged(qreal))); handleHorizontalAxisBaseChanged(logAxis->base()); } @@ -187,12 +189,12 @@ bool LogXYDomain::attachAxis(QAbstractAxis* axis) return true; } -bool LogXYDomain::detachAxis(QAbstractAxis* axis) +bool LogXYDomain::detachAxis(QAbstractAxis *axis) { AbstractDomain::detachAxis(axis); QLogValueAxis *logAxis = qobject_cast(axis); - if(logAxis && logAxis->orientation()==Qt::Horizontal) + if (logAxis && logAxis->orientation() == Qt::Horizontal) QObject::disconnect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleHorizontalAxisBaseChanged(qreal))); return true; @@ -212,10 +214,10 @@ void LogXYDomain::handleHorizontalAxisBaseChanged(qreal baseX) bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const LogXYDomain &domain1, const LogXYDomain &domain2) { - return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) && - qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) && - qFuzzyIsNull(domain1.m_minX - domain2.m_minX) && - qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); + return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) + && qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) + && qFuzzyIsNull(domain1.m_minX - domain2.m_minX) + && qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); } diff --git a/src/domain/logxydomain_p.h b/src/domain/logxydomain_p.h index cbc59e1..2f0e49e 100644 --- a/src/domain/logxydomain_p.h +++ b/src/domain/logxydomain_p.h @@ -56,10 +56,10 @@ public: QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; QPointF calculateDomainPoint(const QPointF &point) const; - QVector calculateGeometryPoints(const QList& vector) const; + QVector calculateGeometryPoints(const QList &vector) const; - bool attachAxis(QAbstractAxis* axis); - bool detachAxis(QAbstractAxis* axis); + bool attachAxis(QAbstractAxis *axis); + bool detachAxis(QAbstractAxis *axis); public Q_SLOTS: void handleHorizontalAxisBaseChanged(qreal baseX); diff --git a/src/domain/logxypolardomain.cpp b/src/domain/logxypolardomain.cpp new file mode 100644 index 0000000..b08f6a0 --- /dev/null +++ b/src/domain/logxypolardomain.cpp @@ -0,0 +1,236 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "logxypolardomain_p.h" +#include "qabstractaxis_p.h" +#include "qlogvalueaxis.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +LogXYPolarDomain::LogXYPolarDomain(QObject *parent) + : PolarDomain(parent), + m_logLeftX(0), + m_logRightX(1), + m_logBaseX(10) +{ +} + +LogXYPolarDomain::~LogXYPolarDomain() +{ +} + +void LogXYPolarDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) +{ + bool axisXChanged = false; + bool axisYChanged = false; + + adjustLogDomainRanges(minX, maxX); + + if (!qFuzzyCompare(m_minX, minX) || !qFuzzyCompare(m_maxX, maxX)) { + m_minX = minX; + m_maxX = maxX; + axisXChanged = true; + qreal logMinX = log10(m_minX) / log10(m_logBaseX); + qreal logMaxX = log10(m_maxX) / log10(m_logBaseX); + m_logLeftX = logMinX < logMaxX ? logMinX : logMaxX; + m_logRightX = logMinX > logMaxX ? logMinX : logMaxX; + if (!m_signalsBlocked) + emit rangeHorizontalChanged(m_minX, m_maxX); + } + + if (!qFuzzyIsNull(m_minY - minY) || !qFuzzyIsNull(m_maxY - maxY)) { + m_minY = minY; + m_maxY = maxY; + axisYChanged = true; + if (!m_signalsBlocked) + emit rangeVerticalChanged(m_minY, m_maxY); + } + + if (axisXChanged || axisYChanged) + emit updated(); +} + +void LogXYPolarDomain::zoomIn(const QRectF &rect) +{ + qreal logLeftX = rect.left() * (m_logRightX - m_logLeftX) / m_size.width() + m_logLeftX; + qreal logRightX = rect.right() * (m_logRightX - m_logLeftX) / m_size.width() + m_logLeftX; + qreal leftX = qPow(m_logBaseX, logLeftX); + qreal rightX = qPow(m_logBaseX, logRightX); + qreal minX = leftX < rightX ? leftX : rightX; + qreal maxX = leftX > rightX ? leftX : rightX; + + qreal dy = spanY() / m_size.height(); + qreal minY = m_minY; + qreal maxY = m_maxY; + + minY = maxY - dy * rect.bottom(); + maxY = maxY - dy * rect.top(); + + setRange(minX, maxX, minY, maxY); +} + +void LogXYPolarDomain::zoomOut(const QRectF &rect) +{ + const qreal factorX = m_size.width() / rect.width(); + + qreal logLeftX = m_logLeftX + (m_logRightX - m_logLeftX) / 2.0 * (1.0 - factorX); + qreal logRIghtX = m_logLeftX + (m_logRightX - m_logLeftX) / 2.0 * (1.0 + factorX); + qreal leftX = qPow(m_logBaseX, logLeftX); + qreal rightX = qPow(m_logBaseX, logRIghtX); + qreal minX = leftX < rightX ? leftX : rightX; + qreal maxX = leftX > rightX ? leftX : rightX; + + qreal dy = spanY() / rect.height(); + qreal minY = m_minY; + qreal maxY = m_maxY; + + maxY = minY + dy * rect.bottom(); + minY = maxY - dy * m_size.height(); + + setRange(minX, maxX, minY, maxY); +} + +void LogXYPolarDomain::move(qreal dx, qreal dy) +{ + qreal stepX = dx * (m_logRightX - m_logLeftX) / m_size.width(); + qreal leftX = qPow(m_logBaseX, m_logLeftX + stepX); + qreal rightX = qPow(m_logBaseX, m_logRightX + stepX); + qreal minX = leftX < rightX ? leftX : rightX; + qreal maxX = leftX > rightX ? leftX : rightX; + + qreal y = spanY() / m_radius; + qreal minY = m_minY; + qreal maxY = m_maxY; + + if (dy != 0) { + minY = minY + y * dy; + maxY = maxY + y * dy; + } + setRange(minX, maxX, minY, maxY); +} + +qreal LogXYPolarDomain::toAngularCoordinate(qreal value, bool &ok) const +{ + qreal retVal; + if (value <= 0) { + ok = false; + retVal = 0.0; + } else { + ok = true; + const qreal tickSpan = 360.0 / qAbs(m_logRightX - m_logLeftX); + const qreal logValue = log10(value) / log10(m_logBaseX); + const qreal valueDelta = logValue - m_logLeftX; + + retVal = valueDelta * tickSpan; + } + return retVal; +} + +qreal LogXYPolarDomain::toRadialCoordinate(qreal value, bool &ok) const +{ + ok = true; + if (value < m_minY) + value = m_minY; + + // Dont limit the max. The drawing should clip the stuff that goes out of the grid + qreal f = (value - m_minY) / (m_maxY - m_minY); + + return f * m_radius; +} + +QPointF LogXYPolarDomain::calculateDomainPoint(const QPointF &point) const +{ + if (point == m_center) + return QPointF(0.0, m_minY); + + QLineF line(m_center, point); + qreal a = 90.0 - line.angle(); + if (a < 0.0) + a += 360.0; + + const qreal deltaX = 360.0 / qAbs(m_logRightX - m_logLeftX); + a = qPow(m_logBaseX, m_logLeftX + (a / deltaX)); + + qreal r = m_minY + ((m_maxY - m_minY) * (line.length() / m_radius)); + + return QPointF(a, r); +} + +bool LogXYPolarDomain::attachAxis(QAbstractAxis *axis) +{ + AbstractDomain::attachAxis(axis); + QLogValueAxis *logAxis = qobject_cast(axis); + + if (logAxis && logAxis->orientation() == Qt::Horizontal) { + QObject::connect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleHorizontalAxisBaseChanged(qreal))); + handleHorizontalAxisBaseChanged(logAxis->base()); + } + + return true; +} + +bool LogXYPolarDomain::detachAxis(QAbstractAxis *axis) +{ + AbstractDomain::detachAxis(axis); + QLogValueAxis *logAxis = qobject_cast(axis); + + if (logAxis && logAxis->orientation() == Qt::Horizontal) + QObject::disconnect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleHorizontalAxisBaseChanged(qreal))); + + return true; +} + +void LogXYPolarDomain::handleHorizontalAxisBaseChanged(qreal baseX) +{ + m_logBaseX = baseX; + qreal logMinX = log10(m_minX) / log10(m_logBaseX); + qreal logMaxX = log10(m_maxX) / log10(m_logBaseX); + m_logLeftX = logMinX < logMaxX ? logMinX : logMaxX; + m_logRightX = logMinX > logMaxX ? logMinX : logMaxX; + emit updated(); +} + +// operators + +bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const LogXYPolarDomain &domain1, const LogXYPolarDomain &domain2) +{ + return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) + && qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) + && qFuzzyIsNull(domain1.m_minX - domain2.m_minX) + && qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); +} + + +bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator!= (const LogXYPolarDomain &domain1, const LogXYPolarDomain &domain2) +{ + return !(domain1 == domain2); +} + + +QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const LogXYPolarDomain &domain) +{ + dbg.nospace() << "AbstractDomain(" << domain.m_minX << ',' << domain.m_maxX << ',' << domain.m_minY << ',' << domain.m_maxY << ')' << domain.m_size; + return dbg.maybeSpace(); +} + +#include "moc_logxypolardomain_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/domain/logxypolardomain_p.h b/src/domain/logxypolardomain_p.h new file mode 100644 index 0000000..c7468ab --- /dev/null +++ b/src/domain/logxypolardomain_p.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef LOGXYPOLARDOMAIN_H +#define LOGXYPOLARDOMAIN_H +#include "polardomain_p.h" +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QTCOMMERCIALCHART_AUTOTEST_EXPORT LogXYPolarDomain: public PolarDomain +{ + Q_OBJECT +public: + explicit LogXYPolarDomain(QObject *object = 0); + virtual ~LogXYPolarDomain(); + + DomainType type() { return AbstractDomain::LogXYPolarDomain; } + + void setRange(qreal minX, qreal maxX, qreal minY, qreal maxY); + + friend bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const LogXYPolarDomain &domain1, const LogXYPolarDomain &domain2); + friend bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator!= (const LogXYPolarDomain &domain1, const LogXYPolarDomain &domain2); + friend QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const LogXYPolarDomain &domain); + + void zoomIn(const QRectF &rect); + void zoomOut(const QRectF &rect); + void move(qreal dx, qreal dy); + + QPointF calculateDomainPoint(const QPointF &point) const; + + bool attachAxis(QAbstractAxis *axis); + bool detachAxis(QAbstractAxis *axis); + +public Q_SLOTS: + void handleHorizontalAxisBaseChanged(qreal baseX); + +protected: + qreal toAngularCoordinate(qreal value, bool &ok) const; + qreal toRadialCoordinate(qreal value, bool &ok) const; + +private: + qreal m_logLeftX; + qreal m_logRightX; + qreal m_logBaseX; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // LOGXYPOLARDOMAIN_H diff --git a/src/domain/polardomain.cpp b/src/domain/polardomain.cpp new file mode 100644 index 0000000..d9a9d48 --- /dev/null +++ b/src/domain/polardomain.cpp @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polardomain_p.h" +#include "qabstractaxis_p.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +PolarDomain::PolarDomain(QObject *parent) + : AbstractDomain(parent) +{ +} + +PolarDomain::~PolarDomain() +{ +} + +void PolarDomain::setSize(const QSizeF &size) +{ + Q_ASSERT(size.width() == size.height()); + m_radius = size.height() / 2; + m_center = QPointF(m_radius, m_radius); + AbstractDomain::setSize(size); +} + +QPointF PolarDomain::calculateGeometryPoint(const QPointF &point, bool &ok) const +{ + qreal r; + qreal a = toAngularCoordinate(point.x(), ok); + if (ok) + r = toRadialCoordinate(point.y(), ok); + if (ok) { + return m_center + polarCoordinateToPoint(a, r); + } else { + qWarning() << "Logarithm of negative value is undefined. Empty layout returned."; + return QPointF(); + } +} + +QVector PolarDomain::calculateGeometryPoints(const QList &vector) const +{ + QVector result; + result.resize(vector.count()); + bool ok; + qreal r; + qreal a; + + for (int i = 0; i < vector.count(); ++i) { + a = toAngularCoordinate(vector[i].x(), ok); + if (ok) + r = toRadialCoordinate(vector[i].y(), ok); + if (ok) { + result[i] = m_center + polarCoordinateToPoint(a, r); + } else { + qWarning() << "Logarithm of negative value is undefined. Empty layout returned."; + return QVector(); + } + } + + return result; +} + +QPointF PolarDomain::polarCoordinateToPoint(qreal angularCoordinate, qreal radialCoordinate) const +{ + qreal dx = qSin(angularCoordinate * (M_PI / 180)) * radialCoordinate; + qreal dy = qCos(angularCoordinate * (M_PI / 180)) * radialCoordinate; + + return QPointF(dx, -dy); +} + +#include "moc_polardomain_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/domain/polardomain_p.h b/src/domain/polardomain_p.h new file mode 100644 index 0000000..81e921d --- /dev/null +++ b/src/domain/polardomain_p.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARDOMAIN_H +#define POLARDOMAIN_H +#include "abstractdomain_p.h" +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QTCOMMERCIALCHART_AUTOTEST_EXPORT PolarDomain: public AbstractDomain +{ + Q_OBJECT +public: + explicit PolarDomain(QObject *object = 0); + virtual ~PolarDomain(); + + void setSize(const QSizeF &size); + + QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; + QVector calculateGeometryPoints(const QList &vector) const; + + virtual qreal toAngularCoordinate(qreal value, bool &ok) const = 0; + virtual qreal toRadialCoordinate(qreal value, bool &ok) const = 0; + +protected: + QPointF polarCoordinateToPoint(qreal angularCoordinate, qreal radialCoordinate) const; + + QPointF m_center; + qreal m_radius; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARDOMAIN_H diff --git a/src/domain/xlogydomain.cpp b/src/domain/xlogydomain.cpp index 42e7eda..850eb99 100644 --- a/src/domain/xlogydomain.cpp +++ b/src/domain/xlogydomain.cpp @@ -42,6 +42,8 @@ void XLogYDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) bool axisXChanged = false; bool axisYChanged = false; + adjustLogDomainRanges(minY, maxY); + if (!qFuzzyIsNull(m_minX - minX) || !qFuzzyIsNull(m_maxX - maxX)) { m_minX = minX; m_maxX = maxX; @@ -58,7 +60,7 @@ void XLogYDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) qreal logMaxY = log10(m_maxY) / log10(m_logBaseY); m_logLeftY = logMinY < logMaxY ? logMinY : logMaxY; m_logRightY = logMinY > logMaxY ? logMinY : logMaxY; - if(!m_signalsBlocked) + if (!m_signalsBlocked) emit rangeVerticalChanged(m_minY, m_maxY); } @@ -136,13 +138,13 @@ QPointF XLogYDomain::calculateGeometryPoint(const QPointF &point, bool &ok) cons ok = true; return QPointF(x, y); } else { - qWarning() << "Logarithm of negative value is undefined. Empty layout returned"; + qWarning() << "Logarithm of negative value is undefined. Empty layout returned."; ok = false; return QPointF(); } } -QVector XLogYDomain::calculateGeometryPoints(const QList& vector) const +QVector XLogYDomain::calculateGeometryPoints(const QList &vector) const { const qreal deltaX = m_size.width() / (m_maxX - m_minX); const qreal deltaY = m_size.height() / qAbs(m_logRightY - m_logLeftY); @@ -157,7 +159,7 @@ QVector XLogYDomain::calculateGeometryPoints(const QList& vect result[i].setX(x); result[i].setY(y); } else { - qWarning() << "Logarithm of negative value is undefined. Empty layout returned"; + qWarning() << "Logarithm of negative value is undefined. Empty layout returned."; return QVector(); } } @@ -173,22 +175,22 @@ QPointF XLogYDomain::calculateDomainPoint(const QPointF &point) const return QPointF(x, y); } -bool XLogYDomain::attachAxis(QAbstractAxis* axis) +bool XLogYDomain::attachAxis(QAbstractAxis *axis) { QLogValueAxis *logAxis = qobject_cast(axis); - if(logAxis && logAxis->orientation()==Qt::Vertical){ + if (logAxis && logAxis->orientation() == Qt::Vertical) { QObject::connect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleVerticalAxisBaseChanged(qreal))); handleVerticalAxisBaseChanged(logAxis->base()); } return AbstractDomain::attachAxis(axis); } -bool XLogYDomain::detachAxis(QAbstractAxis* axis) +bool XLogYDomain::detachAxis(QAbstractAxis *axis) { QLogValueAxis *logAxis = qobject_cast(axis); - if(logAxis && logAxis->orientation()==Qt::Vertical) + if (logAxis && logAxis->orientation() == Qt::Vertical) QObject::disconnect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleVerticalAxisBaseChanged(qreal))); return AbstractDomain::detachAxis(axis); @@ -208,10 +210,10 @@ void XLogYDomain::handleVerticalAxisBaseChanged(qreal baseY) bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const XLogYDomain &domain1, const XLogYDomain &domain2) { - return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) && - qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) && - qFuzzyIsNull(domain1.m_minX - domain2.m_minX) && - qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); + return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) + && qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) + && qFuzzyIsNull(domain1.m_minX - domain2.m_minX) + && qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); } diff --git a/src/domain/xlogydomain_p.h b/src/domain/xlogydomain_p.h index 88dd989..68ac6f8 100644 --- a/src/domain/xlogydomain_p.h +++ b/src/domain/xlogydomain_p.h @@ -56,10 +56,10 @@ public: QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; QPointF calculateDomainPoint(const QPointF &point) const; - QVector calculateGeometryPoints(const QList& vector) const; + QVector calculateGeometryPoints(const QList &vector) const; - bool attachAxis(QAbstractAxis* axis); - bool detachAxis(QAbstractAxis* axis); + bool attachAxis(QAbstractAxis *axis); + bool detachAxis(QAbstractAxis *axis); public Q_SLOTS: void handleVerticalAxisBaseChanged(qreal baseY); diff --git a/src/domain/xlogypolardomain.cpp b/src/domain/xlogypolardomain.cpp new file mode 100644 index 0000000..730a9dc --- /dev/null +++ b/src/domain/xlogypolardomain.cpp @@ -0,0 +1,231 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "xlogypolardomain_p.h" +#include "qabstractaxis_p.h" +#include "qlogvalueaxis.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +XLogYPolarDomain::XLogYPolarDomain(QObject *parent) + : PolarDomain(parent), + m_logInnerY(0), + m_logOuterY(1), + m_logBaseY(10) +{ +} + +XLogYPolarDomain::~XLogYPolarDomain() +{ +} + +void XLogYPolarDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) +{ + bool axisXChanged = false; + bool axisYChanged = false; + + adjustLogDomainRanges(minY, maxY); + + if (!qFuzzyIsNull(m_minX - minX) || !qFuzzyIsNull(m_maxX - maxX)) { + m_minX = minX; + m_maxX = maxX; + axisXChanged = true; + if (!m_signalsBlocked) + emit rangeHorizontalChanged(m_minX, m_maxX); + } + + if (!qFuzzyIsNull(m_minY - minY) || !qFuzzyIsNull(m_maxY - maxY)) { + m_minY = minY; + m_maxY = maxY; + axisYChanged = true; + qreal logMinY = log10(m_minY) / log10(m_logBaseY); + qreal logMaxY = log10(m_maxY) / log10(m_logBaseY); + m_logInnerY = logMinY < logMaxY ? logMinY : logMaxY; + m_logOuterY = logMinY > logMaxY ? logMinY : logMaxY; + if (!m_signalsBlocked) + emit rangeVerticalChanged(m_minY, m_maxY); + } + + if (axisXChanged || axisYChanged) + emit updated(); +} + +void XLogYPolarDomain::zoomIn(const QRectF &rect) +{ + qreal dx = spanX() / m_size.width(); + qreal maxX = m_maxX; + qreal minX = m_minX; + + maxX = minX + dx * rect.right(); + minX = minX + dx * rect.left(); + + qreal logLeftY = m_logOuterY - rect.bottom() * (m_logOuterY - m_logInnerY) / m_size.height(); + qreal logRightY = m_logOuterY - rect.top() * (m_logOuterY - m_logInnerY) / m_size.height(); + qreal leftY = qPow(m_logBaseY, logLeftY); + qreal rightY = qPow(m_logBaseY, logRightY); + qreal minY = leftY < rightY ? leftY : rightY; + qreal maxY = leftY > rightY ? leftY : rightY; + + setRange(minX, maxX, minY, maxY); +} + +void XLogYPolarDomain::zoomOut(const QRectF &rect) +{ + qreal dx = spanX() / rect.width(); + qreal maxX = m_maxX; + qreal minX = m_minX; + + minX = maxX - dx * rect.right(); + maxX = minX + dx * m_size.width(); + + const qreal factorY = m_size.height() / rect.height(); + qreal newLogMinY = m_logInnerY + (m_logOuterY - m_logInnerY) / 2.0 * (1.0 - factorY); + qreal newLogMaxY = m_logInnerY + (m_logOuterY - m_logInnerY) / 2.0 * (1.0 + factorY); + qreal leftY = qPow(m_logBaseY, newLogMinY); + qreal rightY = qPow(m_logBaseY, newLogMaxY); + qreal minY = leftY < rightY ? leftY : rightY; + qreal maxY = leftY > rightY ? leftY : rightY; + + setRange(minX, maxX, minY, maxY); +} + +void XLogYPolarDomain::move(qreal dx, qreal dy) +{ + qreal x = spanX() / 360.0; + + qreal maxX = m_maxX; + qreal minX = m_minX; + + if (dx != 0) { + minX = minX + x * dx; + maxX = maxX + x * dx; + } + + qreal stepY = dy * (m_logOuterY - m_logInnerY) / m_radius; + qreal leftY = qPow(m_logBaseY, m_logInnerY + stepY); + qreal rightY = qPow(m_logBaseY, m_logOuterY + stepY); + qreal minY = leftY < rightY ? leftY : rightY; + qreal maxY = leftY > rightY ? leftY : rightY; + + setRange(minX, maxX, minY, maxY); +} + +qreal XLogYPolarDomain::toAngularCoordinate(qreal value, bool &ok) const +{ + ok = true; + qreal f = (value - m_minX) / (m_maxX - m_minX); + return f * 360.0; +} + +qreal XLogYPolarDomain::toRadialCoordinate(qreal value, bool &ok) const +{ + qreal retVal; + if (value <= 0) { + ok = false; + retVal = 0.0; + } else { + ok = true; + const qreal tickSpan = m_radius / qAbs(m_logOuterY - m_logInnerY); + const qreal logValue = log10(value) / log10(m_logBaseY); + const qreal valueDelta = logValue - m_logInnerY; + + retVal = valueDelta * tickSpan; + + if (retVal < 0.0) + retVal = 0.0; + } + return retVal; +} + +QPointF XLogYPolarDomain::calculateDomainPoint(const QPointF &point) const +{ + if (point == m_center) + return QPointF(0.0, m_minY); + + QLineF line(m_center, point); + qreal a = 90.0 - line.angle(); + if (a < 0.0) + a += 360.0; + a = ((a / 360.0) * (m_maxX - m_minX)) + m_minX; + + const qreal deltaY = m_radius / qAbs(m_logOuterY - m_logInnerY); + qreal r = qPow(m_logBaseY, m_logInnerY + (line.length() / deltaY)); + + return QPointF(a, r); +} + +bool XLogYPolarDomain::attachAxis(QAbstractAxis *axis) +{ + QLogValueAxis *logAxis = qobject_cast(axis); + + if (logAxis && logAxis->orientation() == Qt::Vertical) { + QObject::connect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleVerticalAxisBaseChanged(qreal))); + handleVerticalAxisBaseChanged(logAxis->base()); + } + return AbstractDomain::attachAxis(axis); +} + +bool XLogYPolarDomain::detachAxis(QAbstractAxis *axis) +{ + QLogValueAxis *logAxis = qobject_cast(axis); + + if (logAxis && logAxis->orientation() == Qt::Vertical) + QObject::disconnect(logAxis, SIGNAL(baseChanged(qreal)), this, SLOT(handleVerticalAxisBaseChanged(qreal))); + + return AbstractDomain::detachAxis(axis); +} + +void XLogYPolarDomain::handleVerticalAxisBaseChanged(qreal baseY) +{ + m_logBaseY = baseY; + qreal logMinY = log10(m_minY) / log10(m_logBaseY); + qreal logMaxY = log10(m_maxY) / log10(m_logBaseY); + m_logInnerY = logMinY < logMaxY ? logMinY : logMaxY; + m_logOuterY = logMinY > logMaxY ? logMinY : logMaxY; + emit updated(); +} + +// operators + +bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const XLogYPolarDomain &domain1, const XLogYPolarDomain &domain2) +{ + return (qFuzzyIsNull(domain1.m_maxX - domain2.m_maxX) + && qFuzzyIsNull(domain1.m_maxY - domain2.m_maxY) + && qFuzzyIsNull(domain1.m_minX - domain2.m_minX) + && qFuzzyIsNull(domain1.m_minY - domain2.m_minY)); +} + + +bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator!= (const XLogYPolarDomain &domain1, const XLogYPolarDomain &domain2) +{ + return !(domain1 == domain2); +} + + +QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const XLogYPolarDomain &domain) +{ + dbg.nospace() << "AbstractDomain(" << domain.m_minX << ',' << domain.m_maxX << ',' << domain.m_minY << ',' << domain.m_maxY << ')' << domain.m_size; + return dbg.maybeSpace(); +} + +#include "moc_xlogypolardomain_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/domain/xlogypolardomain_p.h b/src/domain/xlogypolardomain_p.h new file mode 100644 index 0000000..3c3a45b --- /dev/null +++ b/src/domain/xlogypolardomain_p.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef XLOGYPOLARDOMAIN_H +#define XLOGYPOLARDOMAIN_H +#include "polardomain_p.h" +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QTCOMMERCIALCHART_AUTOTEST_EXPORT XLogYPolarDomain: public PolarDomain +{ + Q_OBJECT +public: + explicit XLogYPolarDomain(QObject *object = 0); + virtual ~XLogYPolarDomain(); + + DomainType type() { return AbstractDomain::XLogYPolarDomain; } + + void setRange(qreal minX, qreal maxX, qreal minY, qreal maxY); + + friend bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const XLogYPolarDomain &domain1, const XLogYPolarDomain &domain2); + friend bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator!= (const XLogYPolarDomain &domain1, const XLogYPolarDomain &domain2); + friend QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const XLogYPolarDomain &domain); + + void zoomIn(const QRectF &rect); + void zoomOut(const QRectF &rect); + void move(qreal dx, qreal dy); + + QPointF calculateDomainPoint(const QPointF &point) const; + + bool attachAxis(QAbstractAxis *axis); + bool detachAxis(QAbstractAxis *axis); + +public Q_SLOTS: + void handleVerticalAxisBaseChanged(qreal baseY); + +protected: + qreal toAngularCoordinate(qreal value, bool &ok) const; + qreal toRadialCoordinate(qreal value, bool &ok) const; + +private: + qreal m_logInnerY; + qreal m_logOuterY; + qreal m_logBaseY; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // XLOGYPOLARDOMAIN_H diff --git a/src/domain/xydomain.cpp b/src/domain/xydomain.cpp index 26f82a1..8ac1f0a 100644 --- a/src/domain/xydomain.cpp +++ b/src/domain/xydomain.cpp @@ -126,7 +126,7 @@ QPointF XYDomain::calculateGeometryPoint(const QPointF &point, bool &ok) const return QPointF(x, y); } -QVector XYDomain::calculateGeometryPoints(const QList& vector) const +QVector XYDomain::calculateGeometryPoints(const QList &vector) const { const qreal deltaX = m_size.width() / (m_maxX - m_minX); const qreal deltaY = m_size.height() / (m_maxY - m_minY); @@ -156,10 +156,10 @@ QPointF XYDomain::calculateDomainPoint(const QPointF &point) const bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const XYDomain &domain1, const XYDomain &domain2) { - return (qFuzzyCompare(domain1.m_maxX, domain2.m_maxX) && - qFuzzyCompare(domain1.m_maxY, domain2.m_maxY) && - qFuzzyCompare(domain1.m_minX, domain2.m_minX) && - qFuzzyCompare(domain1.m_minY, domain2.m_minY)); + return (qFuzzyCompare(domain1.m_maxX, domain2.m_maxX) + && qFuzzyCompare(domain1.m_maxY, domain2.m_maxY) + && qFuzzyCompare(domain1.m_minX, domain2.m_minX) + && qFuzzyCompare(domain1.m_minY, domain2.m_minY)); } diff --git a/src/domain/xydomain_p.h b/src/domain/xydomain_p.h index 0a99038..657377b 100644 --- a/src/domain/xydomain_p.h +++ b/src/domain/xydomain_p.h @@ -56,7 +56,7 @@ public: QPointF calculateGeometryPoint(const QPointF &point, bool &ok) const; QPointF calculateDomainPoint(const QPointF &point) const; - QVector calculateGeometryPoints(const QList& vector) const; + QVector calculateGeometryPoints(const QList &vector) const; }; QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/domain/xypolardomain.cpp b/src/domain/xypolardomain.cpp new file mode 100644 index 0000000..e947129 --- /dev/null +++ b/src/domain/xypolardomain.cpp @@ -0,0 +1,178 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "xypolardomain_p.h" +#include "qabstractaxis_p.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +XYPolarDomain::XYPolarDomain(QObject *parent) + : PolarDomain(parent) +{ +} + +XYPolarDomain::~XYPolarDomain() +{ +} + +void XYPolarDomain::setRange(qreal minX, qreal maxX, qreal minY, qreal maxY) +{ + bool axisXChanged = false; + bool axisYChanged = false; + + if (!qFuzzyCompare(m_minX, minX) || !qFuzzyCompare(m_maxX, maxX)) { + m_minX = minX; + m_maxX = maxX; + axisXChanged = true; + if (!m_signalsBlocked) + emit rangeHorizontalChanged(m_minX, m_maxX); + } + + if (!qFuzzyCompare(m_minY, minY) || !qFuzzyCompare(m_maxY, maxY)) { + m_minY = minY; + m_maxY = maxY; + axisYChanged = true; + if (!m_signalsBlocked) + emit rangeVerticalChanged(m_minY, m_maxY); + } + + if (axisXChanged || axisYChanged) + emit updated(); +} + + +void XYPolarDomain::zoomIn(const QRectF &rect) +{ + qreal dx = spanX() / m_size.width(); + qreal dy = spanY() / m_size.height(); + + qreal maxX = m_maxX; + qreal minX = m_minX; + qreal minY = m_minY; + qreal maxY = m_maxY; + + maxX = minX + dx * rect.right(); + minX = minX + dx * rect.left(); + minY = maxY - dy * rect.bottom(); + maxY = maxY - dy * rect.top(); + + setRange(minX, maxX, minY, maxY); +} + +void XYPolarDomain::zoomOut(const QRectF &rect) +{ + qreal dx = spanX() / rect.width(); + qreal dy = spanY() / rect.height(); + + qreal maxX = m_maxX; + qreal minX = m_minX; + qreal minY = m_minY; + qreal maxY = m_maxY; + + minX = maxX - dx * rect.right(); + maxX = minX + dx * m_size.width(); + maxY = minY + dy * rect.bottom(); + minY = maxY - dy * m_size.height(); + + setRange(minX, maxX, minY, maxY); +} + +void XYPolarDomain::move(qreal dx, qreal dy) +{ + // One unit scrolls one degree angular and one pixel radial + qreal x = spanX() / 360.0; + qreal y = spanY() / m_radius; + + qreal maxX = m_maxX; + qreal minX = m_minX; + qreal minY = m_minY; + qreal maxY = m_maxY; + + if (dx != 0) { + minX = minX + x * dx; + maxX = maxX + x * dx; + } + if (dy != 0) { + minY = minY + y * dy; + maxY = maxY + y * dy; + } + setRange(minX, maxX, minY, maxY); +} + +QPointF XYPolarDomain::calculateDomainPoint(const QPointF &point) const +{ + if (point == m_center) + return QPointF(0.0, m_minX); + + QLineF line(m_center, point); + qreal a = 90.0 - line.angle(); + if (a < 0.0) + a += 360.0; + a = ((a / 360.0) * (m_maxX - m_minX)) + m_minX; + qreal r = m_minY + ((m_maxY - m_minY) * (line.length() / m_radius)); + return QPointF(a, r); +} + +qreal XYPolarDomain::toAngularCoordinate(qreal value, bool &ok) const +{ + ok = true; + qreal f = (value - m_minX) / (m_maxX - m_minX); + return f * 360.0; +} + +qreal XYPolarDomain::toRadialCoordinate(qreal value, bool &ok) const +{ + ok = true; + if (value < m_minY) + value = m_minY; + + // Dont limit the max. The drawing should clip the stuff that goes out of the grid + qreal f = (value - m_minY) / (m_maxY - m_minY); + + return f * m_radius; +} + +// operators + +bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const XYPolarDomain &domain1, const XYPolarDomain &domain2) +{ + return (qFuzzyCompare(domain1.m_maxX, domain2.m_maxX) + && qFuzzyCompare(domain1.m_maxY, domain2.m_maxY) + && qFuzzyCompare(domain1.m_minX, domain2.m_minX) + && qFuzzyCompare(domain1.m_minY, domain2.m_minY)); +} + + +bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator!= (const XYPolarDomain &domain1, const XYPolarDomain &domain2) +{ + return !(domain1 == domain2); +} + + +QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const XYPolarDomain &domain) +{ + dbg.nospace() << "AbstractDomain(" << domain.m_minX << ',' << domain.m_maxX << ',' << domain.m_minY << ',' << domain.m_maxY << ')' << domain.m_size; + return dbg.maybeSpace(); +} + +#include "moc_xypolardomain_p.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/domain/xypolardomain_p.h b/src/domain/xypolardomain_p.h new file mode 100644 index 0000000..662cc5e --- /dev/null +++ b/src/domain/xypolardomain_p.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef XYPOLARDOMAIN_H +#define XYPOLARDOMAIN_H +#include "polardomain_p.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QTCOMMERCIALCHART_AUTOTEST_EXPORT XYPolarDomain: public PolarDomain +{ + Q_OBJECT +public: + explicit XYPolarDomain(QObject *object = 0); + virtual ~XYPolarDomain(); + + DomainType type(){ return AbstractDomain::XYPolarDomain;} + + void setRange(qreal minX, qreal maxX, qreal minY, qreal maxY); + + friend bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator== (const XYPolarDomain &Domain1, const XYPolarDomain &Domain2); + friend bool QTCOMMERCIALCHART_AUTOTEST_EXPORT operator!= (const XYPolarDomain &Domain1, const XYPolarDomain &Domain2); + friend QDebug QTCOMMERCIALCHART_AUTOTEST_EXPORT operator<<(QDebug dbg, const XYPolarDomain &AbstractDomain); + + void zoomIn(const QRectF &rect); + void zoomOut(const QRectF &rect); + void move(qreal dx, qreal dy); + + QPointF calculateDomainPoint(const QPointF &point) const; + +protected: + qreal toAngularCoordinate(qreal value, bool &ok) const; + qreal toRadialCoordinate(qreal value, bool &ok) const; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // XYPOLARDOMAIN_H diff --git a/src/layout/abstractchartlayout.cpp b/src/layout/abstractchartlayout.cpp new file mode 100644 index 0000000..092e8c0 --- /dev/null +++ b/src/layout/abstractchartlayout.cpp @@ -0,0 +1,197 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "abstractchartlayout_p.h" +#include "chartpresenter_p.h" +#include "qlegend_p.h" +#include "chartaxiselement_p.h" +#include "charttitle_p.h" +#include "chartbackground_p.h" +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +static const qreal golden_ratio = 0.4; + +AbstractChartLayout::AbstractChartLayout(ChartPresenter *presenter) + : m_presenter(presenter), + m_margins(20, 20, 20, 20), + m_minChartRect(0, 0, 200, 200) +{ +} + +AbstractChartLayout::~AbstractChartLayout() +{ +} + +void AbstractChartLayout::setGeometry(const QRectF &rect) +{ + if (!rect.isValid()) + return; + + QList axes = m_presenter->axisItems(); + ChartTitle *title = m_presenter->titleElement(); + QLegend *legend = m_presenter->legend(); + ChartBackground *background = m_presenter->backgroundElement(); + + QRectF contentGeometry = calculateBackgroundGeometry(rect, background); + + contentGeometry = calculateContentGeometry(contentGeometry); + + if (title && title->isVisible()) + contentGeometry = calculateTitleGeometry(contentGeometry, title); + + if (legend->isAttachedToChart() && legend->isVisible()) + contentGeometry = calculateLegendGeometry(contentGeometry, legend); + + contentGeometry = calculateAxisGeometry(contentGeometry, axes); + + m_presenter->setGeometry(contentGeometry); + + QGraphicsLayout::setGeometry(rect); +} + +QRectF AbstractChartLayout::calculateContentGeometry(const QRectF &geometry) const +{ + return geometry.adjusted(m_margins.left(), m_margins.top(), -m_margins.right(), -m_margins.bottom()); +} + +QRectF AbstractChartLayout::calculateContentMinimum(const QRectF &minimum) const +{ + return minimum.adjusted(0, 0, m_margins.left() + m_margins.right(), m_margins.top() + m_margins.bottom()); +} + + +QRectF AbstractChartLayout::calculateBackgroundGeometry(const QRectF &geometry, ChartBackground *background) const +{ + qreal left; + qreal top; + qreal right; + qreal bottom; + getContentsMargins(&left, &top, &right, &bottom); + QRectF backgroundGeometry = geometry.adjusted(left, top, -right, -bottom); + if (background) + background->setRect(backgroundGeometry); + return backgroundGeometry; +} + +QRectF AbstractChartLayout::calculateBackgroundMinimum(const QRectF &minimum) const +{ + qreal left; + qreal top; + qreal right; + qreal bottom; + getContentsMargins(&left, &top, &right, &bottom); + return minimum.adjusted(0, 0, left + right, top + bottom); +} + +QRectF AbstractChartLayout::calculateLegendGeometry(const QRectF &geometry, QLegend *legend) const +{ + QSizeF size = legend->effectiveSizeHint(Qt::PreferredSize, QSizeF(-1, -1)); + QRectF legendRect; + QRectF result; + + switch (legend->alignment()) { + case Qt::AlignTop: { + legendRect = QRectF(geometry.topLeft(), QSizeF(geometry.width(), size.height())); + result = geometry.adjusted(0, legendRect.height(), 0, 0); + break; + } + case Qt::AlignBottom: { + legendRect = QRectF(QPointF(geometry.left(), geometry.bottom() - size.height()), QSizeF(geometry.width(), size.height())); + result = geometry.adjusted(0, 0, 0, -legendRect.height()); + break; + } + case Qt::AlignLeft: { + qreal width = qMin(size.width(), geometry.width() * golden_ratio); + legendRect = QRectF(geometry.topLeft(), QSizeF(width, geometry.height())); + result = geometry.adjusted(width, 0, 0, 0); + break; + } + case Qt::AlignRight: { + qreal width = qMin(size.width(), geometry.width() * golden_ratio); + legendRect = QRectF(QPointF(geometry.right() - width, geometry.top()), QSizeF(width, geometry.height())); + result = geometry.adjusted(0, 0, -width, 0); + break; + } + default: { + legendRect = QRectF(0, 0, 0, 0); + result = geometry; + break; + } + } + + legend->setGeometry(legendRect); + + return result; +} + +QRectF AbstractChartLayout::calculateLegendMinimum(const QRectF &geometry, QLegend *legend) const +{ + QSizeF minSize = legend->effectiveSizeHint(Qt::MinimumSize, QSizeF(-1, -1)); + return geometry.adjusted(0, 0, minSize.width(), minSize.height()); +} + +QRectF AbstractChartLayout::calculateTitleGeometry(const QRectF &geometry, ChartTitle *title) const +{ + title->setGeometry(geometry); + QPointF center = geometry.center() - title->boundingRect().center(); + title->setPos(center.x(), title->pos().y()); + return geometry.adjusted(0, title->boundingRect().height()+1, 0, 0); +} + +QRectF AbstractChartLayout::calculateTitleMinimum(const QRectF &minimum, ChartTitle *title) const +{ + QSizeF min = title->sizeHint(Qt::MinimumSize); + return minimum.adjusted(0, 0, min.width(), min.height()); +} + +QSizeF AbstractChartLayout::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const +{ + Q_UNUSED(constraint); + if (which == Qt::MinimumSize) { + QList axes = m_presenter->axisItems(); + ChartTitle *title = m_presenter->titleElement(); + QLegend *legend = m_presenter->legend(); + QRectF minimumRect(0, 0, 0, 0); + minimumRect = calculateBackgroundMinimum(minimumRect); + minimumRect = calculateContentMinimum(minimumRect); + minimumRect = calculateTitleMinimum(minimumRect, title); + minimumRect = calculateLegendMinimum(minimumRect, legend); + minimumRect = calculateAxisMinimum(minimumRect, axes); + return minimumRect.united(m_minChartRect).size().toSize(); + } + return QSize(-1, -1); +} + +void AbstractChartLayout::setMargins(const QMargins &margins) +{ + if (m_margins != margins) { + m_margins = margins; + updateGeometry(); + } +} + +QMargins AbstractChartLayout::margins() const +{ + return m_margins; +} + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/chartlayout_p.h b/src/layout/abstractchartlayout_p.h similarity index 64% rename from src/chartlayout_p.h rename to src/layout/abstractchartlayout_p.h index 5b8d38a..d28af3d 100644 --- a/src/chartlayout_p.h +++ b/src/layout/abstractchartlayout_p.h @@ -27,51 +27,50 @@ // // We mean it. -#ifndef CHARTLAYOUT_H -#define CHARTLAYOUT_H +#ifndef ABSTRACTCHARTLAYOUT_H +#define ABSTRACTCHARTLAYOUT_H + #include #include #include "qchartglobal.h" QTCOMMERCIALCHART_BEGIN_NAMESPACE -class ChartPresenter; class ChartTitle; +class ChartAxisElement; +class ChartPresenter; class QLegend; -class ChartAxis; class ChartBackground; -class ChartLayout : public QGraphicsLayout +class AbstractChartLayout : public QGraphicsLayout { public: + AbstractChartLayout(ChartPresenter *presenter); + virtual ~AbstractChartLayout(); - ChartLayout(ChartPresenter *presenter); - virtual ~ChartLayout(); + virtual void setMargins(const QMargins &margins); + virtual QMargins margins() const; + virtual void setGeometry(const QRectF &rect); - void setMargins(const QMargins &margins); - QMargins margins() const; +protected: + virtual QRectF calculateBackgroundGeometry(const QRectF &geometry, ChartBackground *background) const; + virtual QRectF calculateBackgroundMinimum(const QRectF &minimum) const; + virtual QRectF calculateContentGeometry(const QRectF &geometry) const; + virtual QRectF calculateContentMinimum(const QRectF &minimum) const; + virtual QRectF calculateTitleGeometry(const QRectF &geometry, ChartTitle *title) const; + virtual QRectF calculateTitleMinimum(const QRectF &minimum, ChartTitle *title) const; + virtual QRectF calculateLegendGeometry(const QRectF &geometry, QLegend *legend) const; + virtual QRectF calculateLegendMinimum(const QRectF &minimum, QLegend *legend) const; - void setGeometry(const QRectF &rect); + virtual QRectF calculateAxisGeometry(const QRectF &geometry, const QList& axes) const = 0; + virtual QRectF calculateAxisMinimum(const QRectF &minimum, const QList& axes) const = 0; -protected: + // from QGraphicsLayout QSizeF sizeHint(Qt::SizeHint which, const QSizeF &constraint = QSizeF()) const; int count() const { return 0; } QGraphicsLayoutItem *itemAt(int) const { return 0; }; void removeAt(int) {}; -private: - QRectF calculateBackgroundGeometry(const QRectF &geometry, ChartBackground *background) const; - QRectF calculateContentGeometry(const QRectF &geometry) const; - QRectF calculateTitleGeometry(const QRectF &geometry, ChartTitle *title) const; - QRectF calculateLegendGeometry(const QRectF &geometry, QLegend *legend) const; - QRectF calculateAxisGeometry(const QRectF &geometry, const QList& axes) const; - QRectF calculateBackgroundMinimum(const QRectF &minimum) const; - QRectF calculateContentMinimum(const QRectF &minimum) const; - QRectF calculateTitleMinimum(const QRectF &minimum, ChartTitle *title) const; - QRectF calculateAxisMinimum(const QRectF &minimum, const QList& axes) const; - QRectF calculateLegendMinimum(const QRectF &minimum, QLegend *legend) const; - -private: ChartPresenter *m_presenter; QMargins m_margins; QRectF m_minChartRect; @@ -80,4 +79,4 @@ private: QTCOMMERCIALCHART_END_NAMESPACE -#endif +#endif // ABSTRACTCHARTLAYOUT_H diff --git a/src/chartlayout.cpp b/src/layout/cartesianchartlayout.cpp similarity index 61% rename from src/chartlayout.cpp rename to src/layout/cartesianchartlayout.cpp index bd885ba..f531775 100644 --- a/src/chartlayout.cpp +++ b/src/layout/cartesianchartlayout.cpp @@ -18,88 +18,25 @@ ** ****************************************************************************/ -#include "chartlayout_p.h" +#include "cartesianchartlayout_p.h" #include "chartpresenter_p.h" -#include "qlegend_p.h" -#include "chartaxis_p.h" -#include "charttitle_p.h" -#include "chartbackground_p.h" +#include "chartaxiselement_p.h" #include QTCOMMERCIALCHART_BEGIN_NAMESPACE static const qreal maxAxisPortion = 0.4; -ChartLayout::ChartLayout(ChartPresenter *presenter) - : m_presenter(presenter), - m_margins(20, 20, 20, 20), - m_minChartRect(0, 0, 200, 200) +CartesianChartLayout::CartesianChartLayout(ChartPresenter *presenter) + : AbstractChartLayout(presenter) { - -} - -ChartLayout::~ChartLayout() -{ - -} - -void ChartLayout::setGeometry(const QRectF &rect) -{ - if (!rect.isValid()) - return; - - QList axes = m_presenter->axisItems(); - ChartTitle *title = m_presenter->titleElement(); - QLegend *legend = m_presenter->legend(); - ChartBackground *background = m_presenter->backgroundElement(); - - QRectF contentGeometry = calculateBackgroundGeometry(rect, background); - - contentGeometry = calculateContentGeometry(contentGeometry); - - if (title && title->isVisible()) - contentGeometry = calculateTitleGeometry(contentGeometry, title); - - if (legend->isAttachedToChart() && legend->isVisible()) - contentGeometry = calculateLegendGeometry(contentGeometry, legend); - - contentGeometry = calculateAxisGeometry(contentGeometry, axes); - - m_presenter->setGeometry(contentGeometry); - - QGraphicsLayout::setGeometry(rect); } -QRectF ChartLayout::calculateContentGeometry(const QRectF &geometry) const +CartesianChartLayout::~CartesianChartLayout() { - return geometry.adjusted(m_margins.left(), m_margins.top(), -m_margins.right(), -m_margins.bottom()); } -QRectF ChartLayout::calculateContentMinimum(const QRectF &minimum) const -{ - return minimum.adjusted(0, 0, m_margins.left() + m_margins.right(), m_margins.top() + m_margins.bottom()); -} - - -QRectF ChartLayout::calculateBackgroundGeometry(const QRectF &geometry, ChartBackground *background) const -{ - qreal left, top, right, bottom; - getContentsMargins(&left, &top, &right, &bottom); - QRectF backgroundGeometry = geometry.adjusted(left, top, -right, -bottom); - if (background) - background->setRect(backgroundGeometry); - return backgroundGeometry; -} - -QRectF ChartLayout::calculateBackgroundMinimum(const QRectF &minimum) const -{ - qreal left, top, right, bottom; - getContentsMargins(&left, &top, &right, &bottom); - return minimum.adjusted(0, 0, left + right, top + bottom); -} - - -QRectF ChartLayout::calculateAxisGeometry(const QRectF &geometry, const QList& axes) const +QRectF CartesianChartLayout::calculateAxisGeometry(const QRectF &geometry, const QList &axes) const { QSizeF left(0,0); QSizeF minLeft(0,0); @@ -115,16 +52,17 @@ QRectF ChartLayout::calculateAxisGeometry(const QRectF &geometry, const QListisVisible()) continue; + QSizeF size = axis->effectiveSizeHint(Qt::PreferredSize); //this is used to get single thick font size QSizeF minSize = axis->effectiveSizeHint(Qt::MinimumSize); - switch (axis->alignment()) { + switch (axis->axis()->alignment()) { case Qt::AlignLeft: left.setWidth(left.width()+size.width()); left.setHeight(qMax(left.height(),size.height())); @@ -163,7 +101,7 @@ QRectF ChartLayout::calculateAxisGeometry(const QRectF &geometry, const QList(axisElement); + foreach (ChartAxisElement *axis , axes) { if (!axis->isVisible()) continue; QSizeF size = axis->effectiveSizeHint(Qt::PreferredSize); - switch(axis->alignment()){ + switch (axis->axis()->alignment()){ case Qt::AlignLeft:{ qreal width = size.width(); if (leftSqueezeRatio < 1.0) @@ -271,21 +208,20 @@ QRectF ChartLayout::calculateAxisGeometry(const QRectF &geometry, const QList& axes) const +QRectF CartesianChartLayout::calculateAxisMinimum(const QRectF &minimum, const QList &axes) const { QSizeF left; QSizeF right; QSizeF bottom; QSizeF top; - foreach (ChartAxis *axis, axes) { - + foreach (ChartAxisElement *axis, axes) { QSizeF size = axis->effectiveSizeHint(Qt::MinimumSize); if (!axis->isVisible()) continue; - switch (axis->alignment()) { + switch (axis->axis()->alignment()) { case Qt::AlignLeft: left.setWidth(left.width() + size.width()); left.setHeight(qMax(left.height() * 2, size.height())); @@ -307,96 +243,4 @@ QRectF ChartLayout::calculateAxisMinimum(const QRectF &minimum, const QListeffectiveSizeHint(Qt::PreferredSize, QSizeF(-1, -1)); - QRectF legendRect; - QRectF result; - - switch (legend->alignment()) { - case Qt::AlignTop: { - legendRect = QRectF(geometry.topLeft(), QSizeF(geometry.width(), size.height())); - result = geometry.adjusted(0, legendRect.height(), 0, 0); - break; - } - case Qt::AlignBottom: { - legendRect = QRectF(QPointF(geometry.left(), geometry.bottom() - size.height()), QSizeF(geometry.width(), size.height())); - result = geometry.adjusted(0, 0, 0, -legendRect.height()); - break; - } - case Qt::AlignLeft: { - qreal width = qMin(size.width(), geometry.width() * maxAxisPortion); - legendRect = QRectF(geometry.topLeft(), QSizeF(width, geometry.height())); - result = geometry.adjusted(width, 0, 0, 0); - break; - } - case Qt::AlignRight: { - qreal width = qMin(size.width(), geometry.width() * maxAxisPortion); - legendRect = QRectF(QPointF(geometry.right() - width, geometry.top()), QSizeF(width, geometry.height())); - result = geometry.adjusted(0, 0, -width, 0); - break; - } - default: { - legendRect = QRectF(0, 0, 0, 0); - result = geometry; - break; - } - } - - legend->setGeometry(legendRect); - - return result; -} - -QRectF ChartLayout::calculateLegendMinimum(const QRectF &geometry, QLegend *legend) const -{ - QSizeF minSize = legend->effectiveSizeHint(Qt::MinimumSize, QSizeF(-1, -1)); - return geometry.adjusted(0, 0, minSize.width(), minSize.height()); -} - -QRectF ChartLayout::calculateTitleGeometry(const QRectF &geometry, ChartTitle *title) const -{ - title->setGeometry(geometry); - QPointF center = geometry.center() - title->boundingRect().center(); - title->setPos(center.x(),title->pos().y()); - return geometry.adjusted(0,title->boundingRect().height()+1,0,0); -} - -QRectF ChartLayout::calculateTitleMinimum(const QRectF &minimum, ChartTitle *title) const -{ - QSizeF min = title->sizeHint(Qt::MinimumSize); - return minimum.adjusted(0, 0, min.width(), min.height()); -} - -QSizeF ChartLayout::sizeHint(Qt::SizeHint which, const QSizeF &constraint) const -{ - Q_UNUSED(constraint); - if (which == Qt::MinimumSize) { - QList axes = m_presenter->axisItems(); - ChartTitle *title = m_presenter->titleElement(); - QLegend *legend = m_presenter->legend(); - QRectF minimumRect(0, 0, 0, 0); - minimumRect = calculateBackgroundMinimum(minimumRect); - minimumRect = calculateContentMinimum(minimumRect); - minimumRect = calculateTitleMinimum(minimumRect, title); - minimumRect = calculateLegendMinimum(minimumRect, legend); - minimumRect = calculateAxisMinimum(minimumRect, axes); - return minimumRect.united(m_minChartRect).size().toSize(); - } - return QSize(-1, -1); -} - -void ChartLayout::setMargins(const QMargins &margins) -{ - if (m_margins != margins) { - m_margins = margins; - updateGeometry(); - } -} - -QMargins ChartLayout::margins() const -{ - return m_margins; -} - QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/layout/cartesianchartlayout_p.h b/src/layout/cartesianchartlayout_p.h new file mode 100644 index 0000000..88d540b --- /dev/null +++ b/src/layout/cartesianchartlayout_p.h @@ -0,0 +1,50 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef CARTESIANCHARTLAYOUT_H +#define CARTESIANCHARTLAYOUT_H + +#include "abstractchartlayout_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class CartesianChartLayout : public AbstractChartLayout +{ +public: + CartesianChartLayout(ChartPresenter *presenter); + virtual ~CartesianChartLayout(); + + // from AbstractChartLayout + QRectF calculateAxisMinimum(const QRectF &minimum, const QList &axes) const; + QRectF calculateAxisGeometry(const QRectF &geometry, const QList &axes) const; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // CARTESIANCHARTLAYOUT_H diff --git a/src/layout/layout.pri b/src/layout/layout.pri new file mode 100644 index 0000000..159eab1 --- /dev/null +++ b/src/layout/layout.pri @@ -0,0 +1,12 @@ +INCLUDEPATH += $$PWD +DEPENDPATH += $$PWD + +SOURCES += \ + $$PWD/abstractchartlayout.cpp \ + $$PWD/cartesianchartlayout.cpp \ + $$PWD/polarchartlayout.cpp + +PRIVATE_HEADERS += \ + $$PWD/abstractchartlayout_p.h \ + $$PWD/cartesianchartlayout_p.h \ + $$PWD/polarchartlayout_p.h diff --git a/src/layout/polarchartlayout.cpp b/src/layout/polarchartlayout.cpp new file mode 100644 index 0000000..ad48244 --- /dev/null +++ b/src/layout/polarchartlayout.cpp @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "polarchartlayout_p.h" +#include "chartpresenter_p.h" +#include "polarchartaxis_p.h" +#include +#include + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +static const qreal golden_ratio = 0.4; + +PolarChartLayout::PolarChartLayout(ChartPresenter *presenter) + : AbstractChartLayout(presenter) +{ +} + +PolarChartLayout::~PolarChartLayout() +{ +} + +QRectF PolarChartLayout::calculateAxisGeometry(const QRectF &geometry, const QList &axes) const +{ + // How to handle multiple angular/radial axes? + qreal axisRadius = geometry.height() / 2.0; + if (geometry.width() < geometry.height()) + axisRadius = geometry.width() / 2.0; + + int titleHeight = 0; + foreach (ChartAxisElement *chartAxis, axes) { + if (!chartAxis->isVisible()) + continue; + + PolarChartAxis *polarChartAxis = static_cast(chartAxis); + qreal radius = polarChartAxis->preferredAxisRadius(geometry.size()); + if (radius < axisRadius) + axisRadius = radius; + + if (chartAxis->axis()->orientation() == Qt::Horizontal + && chartAxis->axis()->isTitleVisible() + && !chartAxis->axis()->titleText().isEmpty()) { + // If axis has angular title, adjust geometry down by the space title takes + QFontMetrics titleMetrics(chartAxis->axis()->titleFont()); + titleHeight = (titleMetrics.boundingRect(chartAxis->axis()->titleText()).height() / 2) + chartAxis->titlePadding(); + } + } + + QRectF axisRect; + axisRect.setSize(QSizeF(axisRadius * 2.0, axisRadius * 2.0)); + axisRect.moveCenter(geometry.center()); + axisRect.adjust(0, titleHeight, 0, titleHeight); + + foreach (ChartAxisElement *chartAxis, axes) + chartAxis->setGeometry(axisRect, QRectF()); + + return axisRect; +} + +QRectF PolarChartLayout::calculateAxisMinimum(const QRectF &minimum, const QList &axes) const +{ + Q_UNUSED(axes); + return minimum; +} + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/layout/polarchartlayout_p.h b/src/layout/polarchartlayout_p.h new file mode 100644 index 0000000..e8d5db4 --- /dev/null +++ b/src/layout/polarchartlayout_p.h @@ -0,0 +1,50 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// W A R N I N G +// ------------- +// +// This file is not part of the QtCommercial Chart API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#ifndef POLARCHARTLAYOUT_H +#define POLARCHARTLAYOUT_H + +#include "abstractchartlayout_p.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class PolarChartLayout : public AbstractChartLayout +{ +public: + PolarChartLayout(ChartPresenter *presenter); + virtual ~PolarChartLayout(); + + // from AbstractChartLayout + QRectF calculateAxisMinimum(const QRectF &minimum, const QList &axes) const; + QRectF calculateAxisGeometry(const QRectF &geometry, const QList &axes) const; +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // POLARCHARTLAYOUT_H diff --git a/src/legend/legendlayout.cpp b/src/legend/legendlayout.cpp index e405c79..6d5f832 100644 --- a/src/legend/legendlayout.cpp +++ b/src/legend/legendlayout.cpp @@ -21,7 +21,7 @@ #include "legendlayout_p.h" #include "chartpresenter_p.h" #include "qlegend_p.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include "qlegendmarker_p.h" #include "legendmarkeritem_p.h" diff --git a/src/legend/qlegend.cpp b/src/legend/qlegend.cpp index 8a34afe..1920d25 100644 --- a/src/legend/qlegend.cpp +++ b/src/legend/qlegend.cpp @@ -25,7 +25,7 @@ #include "qchart_p.h" #include "legendlayout_p.h" #include "chartpresenter_p.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include "qlegendmarker.h" #include "qlegendmarker_p.h" #include "legendmarkeritem_p.h" diff --git a/src/linechart/linechartitem.cpp b/src/linechart/linechartitem.cpp index 1f10d53..dd5f540 100644 --- a/src/linechart/linechartitem.cpp +++ b/src/linechart/linechartitem.cpp @@ -22,7 +22,7 @@ #include "qlineseries.h" #include "qlineseries_p.h" #include "chartpresenter_p.h" -#include "abstractdomain_p.h" +#include "polardomain_p.h" #include #include @@ -30,10 +30,11 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE const qreal mouseEventMinWidth(12); -LineChartItem::LineChartItem(QLineSeries *series,QGraphicsItem* item) +LineChartItem::LineChartItem(QLineSeries *series, QGraphicsItem *item) : XYChart(series,item), m_series(series), - m_pointsVisible(false) + m_pointsVisible(false), + m_chartType(QChart::ChartTypeUndefined) { setAcceptHoverEvents(true); setZValue(ChartPresenter::LineChartZValue); @@ -50,54 +51,235 @@ QRectF LineChartItem::boundingRect() const QPainterPath LineChartItem::shape() const { - return m_path; + return m_shapePath; } void LineChartItem::updateGeometry() { m_points = geometryPoints(); + const QVector &points = m_points; - if (m_points.size() == 0) { + if (points.size() == 0) { prepareGeometryChange(); - m_path = QPainterPath(); + m_fullPath = QPainterPath(); m_linePath = QPainterPath(); m_rect = QRect(); return; } - QPainterPath linePath(m_points.at(0)); + QPainterPath linePath; + QPainterPath fullPath; + // Use worst case scenario to determine required margin. + qreal margin = m_linePen.width() * 1.42; - if (m_pointsVisible) { + // Area series use component line series that aren't necessarily added to the chart themselves, + // so check if chart type is forced before trying to obtain it from the chart. + QChart::ChartType chartType = m_chartType; + if (chartType == QChart::ChartTypeUndefined) + chartType = m_series->chart()->chartType(); + // For polar charts, we need special handling for angular (horizontal) + // points that are off-grid. + if (chartType == QChart::ChartTypePolar) { + QPainterPath linePathLeft; + QPainterPath linePathRight; + QPainterPath *currentSegmentPath = 0; + QPainterPath *previousSegmentPath = 0; + qreal minX = domain()->minX(); + qreal maxX = domain()->maxX(); + qreal minY = domain()->minY(); + QPointF currentSeriesPoint = m_series->pointAt(0); + QPointF currentGeometryPoint = points.at(0); + QPointF previousGeometryPoint = points.at(0); int size = m_linePen.width(); - linePath.addEllipse(m_points.at(0), size, size); - linePath.moveTo(m_points.at(0)); - for (int i = 1; i < m_points.size(); i++) { - linePath.lineTo(m_points.at(i)); - linePath.addEllipse(m_points.at(i), size, size); - linePath.moveTo(m_points.at(i)); + bool pointOffGrid = false; + bool previousPointWasOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); + + qreal domainRadius = domain()->size().height() / 2.0; + const QPointF centerPoint(domainRadius, domainRadius); + + if (!previousPointWasOffGrid) { + fullPath.moveTo(points.at(0)); + if (m_pointsVisible && currentSeriesPoint.y() >= minY) { + // Do not draw ellipses for points below minimum Y. + linePath.addEllipse(points.at(0), size, size); + fullPath.addEllipse(points.at(0), size, size); + linePath.moveTo(points.at(0)); + fullPath.moveTo(points.at(0)); + } } - } else { - for (int i = 1; i < m_points.size(); i++) - linePath.lineTo(m_points.at(i)); - } + qreal leftMarginLine = centerPoint.x() - margin; + qreal rightMarginLine = centerPoint.x() + margin; + qreal horizontal = centerPoint.y(); - m_linePath = linePath; + for (int i = 1; i < points.size(); i++) { + // Interpolating line fragments would be ugly when thick pen is used, + // so we work around it by utilizing three separate + // paths for line segments and clip those with custom regions at paint time. + // "Right" path contains segments that cross the axis line with visible point on the + // right side of the axis line, as well as segments that have one point within the margin + // on the right side of the axis line and another point on the right side of the chart. + // "Left" path contains points with similarly on the left side. + // "Full" path contains rest of the points. + // This doesn't yield perfect results always. E.g. when segment covers more than 90 + // degrees and both of the points are within the margin, one in the top half and one in the + // bottom half of the chart, the bottom one gets clipped incorrectly. + // However, this should be rare occurrence in any sensible chart. + currentSeriesPoint = m_series->pointAt(i); + currentGeometryPoint = points.at(i); + pointOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); + + // Draw something unless both off-grid + if (!pointOffGrid || !previousPointWasOffGrid) { + QPointF intersectionPoint; + qreal y; + if (pointOffGrid != previousPointWasOffGrid) { + if (currentGeometryPoint.x() == previousGeometryPoint.x()) { + y = currentGeometryPoint.y() + (currentGeometryPoint.y() - previousGeometryPoint.y()) / 2.0; + } else { + qreal ratio = (centerPoint.x() - currentGeometryPoint.x()) / (currentGeometryPoint.x() - previousGeometryPoint.x()); + y = currentGeometryPoint.y() + (currentGeometryPoint.y() - previousGeometryPoint.y()) * ratio; + } + intersectionPoint = QPointF(centerPoint.x(), y); + } + + bool dummyOk; // We know points are ok, but this is needed + qreal currentAngle = static_cast(domain())->toAngularCoordinate(currentSeriesPoint.x(), dummyOk); + qreal previousAngle = static_cast(domain())->toAngularCoordinate(m_series->pointAt(i - 1).x(), dummyOk); + + if ((qAbs(currentAngle - previousAngle) > 180.0)) { + // If the angle between two points is over 180 degrees (half X range), + // any direct segment between them becomes meaningless. + // In this case two line segments are drawn instead, from previous + // point to the center and from center to current point. + if ((previousAngle < 0.0 || (previousAngle <= 180.0 && previousGeometryPoint.x() < rightMarginLine)) + && previousGeometryPoint.y() < horizontal) { + currentSegmentPath = &linePathRight; + } else if ((previousAngle > 360.0 || (previousAngle > 180.0 && previousGeometryPoint.x() > leftMarginLine)) + && previousGeometryPoint.y() < horizontal) { + currentSegmentPath = &linePathLeft; + } else if (previousAngle > 0.0 && previousAngle < 360.0) { + currentSegmentPath = &linePath; + } else { + currentSegmentPath = 0; + } + + if (currentSegmentPath) { + if (previousSegmentPath != currentSegmentPath) + currentSegmentPath->moveTo(previousGeometryPoint); + if (previousPointWasOffGrid) + fullPath.moveTo(intersectionPoint); + + currentSegmentPath->lineTo(centerPoint); + fullPath.lineTo(centerPoint); + } + + previousSegmentPath = currentSegmentPath; + + if ((currentAngle < 0.0 || (currentAngle <= 180.0 && currentGeometryPoint.x() < rightMarginLine)) + && currentGeometryPoint.y() < horizontal) { + currentSegmentPath = &linePathRight; + } else if ((currentAngle > 360.0 || (currentAngle > 180.0 &¤tGeometryPoint.x() > leftMarginLine)) + && currentGeometryPoint.y() < horizontal) { + currentSegmentPath = &linePathLeft; + } else if (currentAngle > 0.0 && currentAngle < 360.0) { + currentSegmentPath = &linePath; + } else { + currentSegmentPath = 0; + } + + if (currentSegmentPath) { + if (previousSegmentPath != currentSegmentPath) + currentSegmentPath->moveTo(centerPoint); + if (!previousSegmentPath) + fullPath.moveTo(centerPoint); + + currentSegmentPath->lineTo(currentGeometryPoint); + if (pointOffGrid) + fullPath.lineTo(intersectionPoint); + else + fullPath.lineTo(currentGeometryPoint); + } + } else { + if (previousAngle < 0.0 || currentAngle < 0.0 + || ((previousAngle <= 180.0 && currentAngle <= 180.0) + && ((previousGeometryPoint.x() < rightMarginLine && previousGeometryPoint.y() < horizontal) + || (currentGeometryPoint.x() < rightMarginLine && currentGeometryPoint.y() < horizontal)))) { + currentSegmentPath = &linePathRight; + } else if (previousAngle > 360.0 || currentAngle > 360.0 + || ((previousAngle > 180.0 && currentAngle > 180.0) + && ((previousGeometryPoint.x() > leftMarginLine && previousGeometryPoint.y() < horizontal) + || (currentGeometryPoint.x() > leftMarginLine && currentGeometryPoint.y() < horizontal)))) { + currentSegmentPath = &linePathLeft; + } else { + currentSegmentPath = &linePath; + } + + if (currentSegmentPath != previousSegmentPath) + currentSegmentPath->moveTo(previousGeometryPoint); + if (previousPointWasOffGrid) + fullPath.moveTo(intersectionPoint); + + if (pointOffGrid) + fullPath.lineTo(intersectionPoint); + else + fullPath.lineTo(currentGeometryPoint); + currentSegmentPath->lineTo(currentGeometryPoint); + } + } else { + currentSegmentPath = 0; + } + + previousPointWasOffGrid = pointOffGrid; + if (m_pointsVisible && !pointOffGrid && currentSeriesPoint.y() >= minY) { + linePath.addEllipse(points.at(i), size, size); + fullPath.addEllipse(points.at(i), size, size); + linePath.moveTo(points.at(i)); + fullPath.moveTo(points.at(i)); + } + previousSegmentPath = currentSegmentPath; + previousGeometryPoint = currentGeometryPoint; + } + m_linePathPolarRight = linePathRight; + m_linePathPolarLeft = linePathLeft; + // Note: This construction of m_fullpath is not perfect. The partial segments that are + // outside left/right clip regions at axis boundary still generate hover/click events, + // because shape doesn't get clipped. It doesn't seem possible to do sensibly. + } else { // not polar + linePath.moveTo(points.at(0)); + if (m_pointsVisible) { + int size = m_linePen.width(); + linePath.addEllipse(points.at(0), size, size); + linePath.moveTo(points.at(0)); + for (int i = 1; i < points.size(); i++) { + linePath.lineTo(points.at(i)); + linePath.addEllipse(points.at(i), size, size); + linePath.moveTo(points.at(i)); + } + } else { + for (int i = 1; i < points.size(); i++) + linePath.lineTo(points.at(i)); + } + fullPath = linePath; + } QPainterPathStroker stroker; // QPainter::drawLine does not respect join styles, for example BevelJoin becomes MiterJoin. // This is why we are prepared for the "worst case" scenario, i.e. use always MiterJoin and // multiply line width with square root of two when defining shape and bounding rectangle. - stroker.setWidth(m_linePen.width() * 1.42); + stroker.setWidth(margin); stroker.setJoinStyle(Qt::MiterJoin); stroker.setCapStyle(Qt::SquareCap); stroker.setMiterLimit(m_linePen.miterLimit()); prepareGeometryChange(); - m_path = stroker.createStroke(linePath); - m_rect = m_path.boundingRect(); + m_linePath = linePath; + m_fullPath = fullPath; + m_shapePath = stroker.createStroke(fullPath); + + m_rect = m_shapePath.boundingRect(); } void LineChartItem::handleUpdated() @@ -121,16 +303,35 @@ void LineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opt Q_UNUSED(widget) Q_UNUSED(option) + QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); + painter->save(); painter->setPen(m_linePen); - painter->setClipRect(QRectF(QPointF(0,0),domain()->size())); + bool alwaysUsePath = false; + + if (m_series->chart()->chartType() == QChart::ChartTypePolar) { + qreal halfWidth = domain()->size().width() / 2.0; + QRectF clipRectLeft = QRectF(0, 0, halfWidth, domain()->size().height()); + QRectF clipRectRight = QRectF(halfWidth, 0, halfWidth, domain()->size().height()); + QRegion fullPolarClipRegion(clipRect.toRect(), QRegion::Ellipse); + QRegion clipRegionLeft(fullPolarClipRegion.intersected(clipRectLeft.toRect())); + QRegion clipRegionRight(fullPolarClipRegion.intersected(clipRectRight.toRect())); + painter->setClipRegion(clipRegionLeft); + painter->drawPath(m_linePathPolarLeft); + painter->setClipRegion(clipRegionRight); + painter->drawPath(m_linePathPolarRight); + painter->setClipRegion(fullPolarClipRegion); + alwaysUsePath = true; // required for proper clipping + } else { + painter->setClipRect(clipRect); + } if (m_pointsVisible) { painter->setBrush(m_linePen.color()); painter->drawPath(m_linePath); } else { painter->setBrush(QBrush(Qt::NoBrush)); - if (m_linePen.style() != Qt::SolidLine) { + if (m_linePen.style() != Qt::SolidLine || alwaysUsePath) { // If pen style is not solid line, always fall back to path painting // to ensure proper continuity of the pattern painter->drawPath(m_linePath); diff --git a/src/linechart/linechartitem_p.h b/src/linechart/linechartitem_p.h index 6db4ccf..2a728f3 100644 --- a/src/linechart/linechartitem_p.h +++ b/src/linechart/linechartitem_p.h @@ -32,6 +32,7 @@ #include "qchartglobal.h" #include "xychart_p.h" +#include "qchart.h" #include QTCOMMERCIALCHART_BEGIN_NAMESPACE @@ -44,7 +45,7 @@ class LineChartItem : public XYChart Q_OBJECT Q_INTERFACES(QGraphicsItem) public: - explicit LineChartItem(QLineSeries *series, QGraphicsItem* item = 0); + explicit LineChartItem(QLineSeries *series, QGraphicsItem *item = 0); ~LineChartItem() {} //from QGraphicsItem @@ -52,7 +53,7 @@ public: void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); QPainterPath shape() const; - QPainterPath path() const { return m_linePath; } + QPainterPath path() const { return m_fullPath; } public Q_SLOTS: void handleUpdated(); @@ -63,15 +64,21 @@ protected: void hoverEnterEvent(QGraphicsSceneHoverEvent *event); void hoverLeaveEvent(QGraphicsSceneHoverEvent *event); void suppressPoints() { m_pointsVisible = false; } + void forceChartType(QChart::ChartType chartType) { m_chartType = chartType; } private: QLineSeries *m_series; - QPainterPath m_path; QPainterPath m_linePath; + QPainterPath m_linePathPolarRight; + QPainterPath m_linePathPolarLeft; + QPainterPath m_fullPath; + QPainterPath m_shapePath; + QVector m_points; QRectF m_rect; QPen m_linePen; bool m_pointsVisible; + QChart::ChartType m_chartType; }; QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/qchart.cpp b/src/qchart.cpp index 8ab736e..f2fd1b6 100644 --- a/src/qchart.cpp +++ b/src/qchart.cpp @@ -24,7 +24,7 @@ #include "qlegend_p.h" #include "chartbackground_p.h" #include "qabstractaxis.h" -#include "chartlayout_p.h" +#include "abstractchartlayout_p.h" #include "charttheme_p.h" #include "chartpresenter_p.h" #include "chartdataset_p.h" @@ -59,6 +59,16 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE */ /*! + \enum QChart::ChartType + + This enum describes the chart type. + + \value ChartTypeUndefined + \value ChartTypeCartesian + \value ChartTypePolar + */ + +/*! \class QChart \brief QtCommercial chart API. @@ -110,15 +120,34 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE */ /*! - Constructs a chart object which is a child of a\a parent. Parameter \a wFlags is passed to the QGraphicsWidget constructor. + \property QChart::chartType + Chart type indicates if the chart is a cartesian chart or a polar chart. + This property is set internally and is read only. + \sa QPolarChart + */ + +/*! + \internal + Constructs a chart object of \a type which is a child of a \a parent. + Parameter \a wFlags is passed to the QGraphicsWidget constructor. + This constructor is called only by subclasses. +*/ +QChart::QChart(QChart::ChartType type, QGraphicsItem *parent, Qt::WindowFlags wFlags) + : QGraphicsWidget(parent, wFlags), + d_ptr(new QChartPrivate(this, type)) +{ + d_ptr->init(); +} + +/*! + Constructs a chart object which is a child of a \a parent. + Parameter \a wFlags is passed to the QGraphicsWidget constructor. */ QChart::QChart(QGraphicsItem *parent, Qt::WindowFlags wFlags) : QGraphicsWidget(parent, wFlags), - d_ptr(new QChartPrivate(this)) + d_ptr(new QChartPrivate(this, ChartTypeCartesian)) { - d_ptr->m_legend = new LegendScroller(this); - setTheme(QChart::ChartThemeLight); - setLayout(d_ptr->m_presenter->layout()); + d_ptr->init(); } /*! @@ -269,9 +298,12 @@ void QChart::zoomIn() /*! Zooms in the view to a maximum level at which \a rect is still fully visible. + \note This is not supported for polar charts. */ void QChart::zoomIn(const QRectF &rect) { + if (d_ptr->m_type == QChart::ChartTypePolar) + return; d_ptr->zoomIn(rect); } @@ -306,8 +338,8 @@ void QChart::zoom(qreal factor) } /*! - Returns the pointer to the x axis object of the chart asociated with the specified \a series - If no series is provided then pointer to currently visible axis is provided + Returns the pointer to the x axis object of the chart associated with the specified \a series. + If no series is provided then pointer to currently visible axis is provided. */ QAbstractAxis *QChart::axisX(QAbstractSeries *series) const { @@ -318,8 +350,8 @@ QAbstractAxis *QChart::axisX(QAbstractSeries *series) const } /*! - Returns the pointer to the y axis object of the chart asociated with the specified \a series - If no series is provided then pointer to currently visible axis is provided + Returns the pointer to the y axis object of the chart associated with the specified \a series. + If no series is provided then pointer to currently visible axis is provided. */ QAbstractAxis *QChart::axisY(QAbstractSeries *series) const { @@ -438,6 +470,11 @@ QMargins QChart::margins() const return d_ptr->m_presenter->layout()->margins(); } +QChart::ChartType QChart::chartType() const +{ + return d_ptr->m_type; +} + /*! Returns the the rect within which the drawing of the chart is done. It does not include the area defines by margins. @@ -471,6 +508,8 @@ QChart::AnimationOptions QChart::animationOptions() const /*! Scrolls the visible area of the chart by the distance defined in the \a dx and \a dy. + + For polar charts, \a dx indicates the angle along angular axis instead of distance. */ void QChart::scroll(qreal dx, qreal dy) { @@ -581,11 +620,12 @@ QPointF QChart::mapToPosition(const QPointF &value, QAbstractSeries *series) ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -QChartPrivate::QChartPrivate(QChart *q): +QChartPrivate::QChartPrivate(QChart *q, QChart::ChartType type): q_ptr(q), + m_type(type), m_legend(0), m_dataset(new ChartDataSet(q)), - m_presenter(new ChartPresenter(q)), + m_presenter(new ChartPresenter(q, type)), m_themeManager(new ChartThemeManager(q)) { QObject::connect(m_dataset, SIGNAL(seriesAdded(QAbstractSeries*)), m_presenter, SLOT(handleSeriesAdded(QAbstractSeries*))); @@ -603,6 +643,13 @@ QChartPrivate::~QChartPrivate() } +void QChartPrivate::init() +{ + m_legend = new LegendScroller(q_ptr); + q_ptr->setTheme(QChart::ChartThemeLight); + q_ptr->setLayout(m_presenter->layout()); +} + void QChartPrivate::zoomIn(qreal factor) { QRectF rect = m_presenter->geometry(); diff --git a/src/qchart.h b/src/qchart.h index e18b127..3f02673 100644 --- a/src/qchart.h +++ b/src/qchart.h @@ -45,10 +45,18 @@ class QTCOMMERCIALCHART_EXPORT QChart : public QGraphicsWidget Q_PROPERTY(QChart::AnimationOptions animationOptions READ animationOptions WRITE setAnimationOptions) Q_PROPERTY(QMargins minimumMargins READ minimumMargins WRITE setMinimumMargins) Q_PROPERTY(QMargins margins READ margins WRITE setMargins) + Q_PROPERTY(QChart::ChartType chartType READ chartType) Q_ENUMS(ChartTheme) Q_ENUMS(AnimationOption) + Q_ENUMS(ChartType) public: + enum ChartType { + ChartTypeUndefined = 0, + ChartTypeCartesian, + ChartTypePolar + }; + enum ChartTheme { ChartThemeLight = 0, ChartThemeBlueCerulean, @@ -84,7 +92,7 @@ public: QAbstractAxis *axisY(QAbstractSeries *series = 0) const; // ****************** - void addAxis(QAbstractAxis *axis,Qt::Alignment alignment); + void addAxis(QAbstractAxis *axis, Qt::Alignment alignment); void removeAxis(QAbstractAxis *axis); QList axes(Qt::Orientations orientation = Qt::Horizontal|Qt::Vertical, QAbstractSeries *series = 0) const; @@ -133,7 +141,10 @@ public: QPointF mapToValue(const QPointF &position, QAbstractSeries *series = 0); QPointF mapToPosition(const QPointF &value, QAbstractSeries *series = 0); + ChartType chartType() const; + protected: + explicit QChart(QChart::ChartType type, QGraphicsItem *parent, Qt::WindowFlags wFlags); QScopedPointer d_ptr; friend class QLegend; friend class DeclarativeChart; diff --git a/src/qchart_p.h b/src/qchart_p.h index f26a360..e715bc6 100644 --- a/src/qchart_p.h +++ b/src/qchart_p.h @@ -31,10 +31,10 @@ #define QCHART_P_H #include "qchartglobal.h" +#include "qchart.h" QTCOMMERCIALCHART_BEGIN_NAMESPACE -class QChart; class ChartThemeManager; class ChartPresenter; class QLegend; @@ -44,14 +44,16 @@ class QChartPrivate { public: - QChartPrivate(QChart *q); + QChartPrivate(QChart *q, QChart::ChartType type); ~QChartPrivate(); QChart *q_ptr; QLegend *m_legend; ChartDataSet *m_dataset; ChartPresenter *m_presenter; ChartThemeManager *m_themeManager; + QChart::ChartType m_type; + void init(); void zoomIn(qreal factor); void zoomOut(qreal factor); void zoomIn(const QRectF &rect); diff --git a/src/qpolarchart.cpp b/src/qpolarchart.cpp new file mode 100644 index 0000000..1d93d35 --- /dev/null +++ b/src/qpolarchart.cpp @@ -0,0 +1,126 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qpolarchart.h" +#include "qabstractaxis.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +/*! + \enum QPolarChart::PolarOrientation + + This type is used to signify the polar orientation of an axis. + + \value PolarOrientationRadial + \value PolarOrientationAngular +*/ + +/*! + \class QPolarChart + \brief QtCommercial chart API. + + QPolarChart is a specialization of QChart to show a polar chart. + + Polar charts support line, spline, area, and scatter series, and all axis types + supported by those series. + + \note When setting ticks to an angular QValueAxis, keep in mind that the first and last tick + are co-located at 0/360 degree angle. + + \note If the angular distance between two consecutive points in a series is more than 180 degrees, + any line connecting the two points becomes meaningless, so choose the axis ranges accordingly + when displaying line, spline, or area series. + + \note Polar charts do not support multiple axes of same orientation. + + \sa QChart + */ + +/*! + Constructs a polar chart as a child of a \a parent. + Parameter \a wFlags is passed to the QChart constructor. + */ +QPolarChart::QPolarChart(QGraphicsItem *parent, Qt::WindowFlags wFlags) + : QChart(QChart::ChartTypePolar, parent, wFlags) +{ +} + +/*! + Destroys the object and it's children, like series and axis objects added to it. + */ +QPolarChart::~QPolarChart() +{ +} + +/*! + Returns the axes added for the \a series with \a polarOrientation. If no series is provided, then any axis with the + specified polar orientation is returned. + \sa addAxis() + */ +QList QPolarChart::axes(PolarOrientations polarOrientation, QAbstractSeries *series) const +{ + Qt::Orientations orientation(0); + if (polarOrientation.testFlag(PolarOrientationAngular)) + orientation |= Qt::Horizontal; + if (polarOrientation.testFlag(PolarOrientationRadial)) + orientation |= Qt::Vertical; + + return QChart::axes(orientation, series); +} + +/*! + This convenience method adds \a axis to the polar chart with \a polarOrientation. + The chart takes the ownership of the axis. + + \note Axes can be added to a polar chart also with QChart::addAxis() instead of this method. + The specified alignment determines the polar orientation: horizontal alignments indicate angular + axis and vertical alignments indicate radial axis. + \sa QChart::removeAxis(), QChart::createDefaultAxes(), QAbstractSeries::attachAxis(), QChart::addAxis() +*/ +void QPolarChart::addAxis(QAbstractAxis *axis, PolarOrientation polarOrientation) +{ + if (!axis || axis->type() == QAbstractAxis::AxisTypeBarCategory) { + qWarning("QAbstractAxis::AxisTypeBarCategory is not a supported axis type for polar charts."); + } else { + Qt::Alignment alignment = Qt::AlignLeft; + if (polarOrientation == PolarOrientationAngular) + alignment = Qt::AlignBottom; + QChart::addAxis(axis, alignment); + } +} + +/*! + Angular axes of a polar chart report horizontal orientation and radial axes report + vertical orientation. + This function is a convenience function for converting the orientation of an \a axis to + corresponding polar orientation. If the \a axis is NULL or not added to a polar chart, + the return value is meaningless. +*/ +QPolarChart::PolarOrientation QPolarChart::axisPolarOrientation(QAbstractAxis *axis) +{ + if (axis && axis->orientation() == Qt::Horizontal) + return PolarOrientationAngular; + else + return PolarOrientationRadial; +} + +#include "moc_qpolarchart.cpp" + +QTCOMMERCIALCHART_END_NAMESPACE diff --git a/src/qpolarchart.h b/src/qpolarchart.h new file mode 100644 index 0000000..123eb57 --- /dev/null +++ b/src/qpolarchart.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QPOLARCHART_H +#define QPOLARCHART_H + +#include "qchart.h" + +QTCOMMERCIALCHART_BEGIN_NAMESPACE + +class QAbstractSeries; +class QAbstractAxis; + +class QTCOMMERCIALCHART_EXPORT QPolarChart : public QChart +{ + Q_OBJECT + Q_ENUMS(PolarOrientation) + Q_FLAGS(PolarOrientations) + +public: + enum PolarOrientation { + PolarOrientationRadial = 0x1, + PolarOrientationAngular = 0x2 + }; + Q_DECLARE_FLAGS(PolarOrientations, PolarOrientation) + +public: + explicit QPolarChart(QGraphicsItem *parent = 0, Qt::WindowFlags wFlags = 0); + ~QPolarChart(); + + void addAxis(QAbstractAxis *axis, PolarOrientation polarOrientation); + + QList axes(PolarOrientations polarOrientation = PolarOrientations(PolarOrientationRadial | PolarOrientationAngular), QAbstractSeries *series = 0) const; + + static PolarOrientation axisPolarOrientation(QAbstractAxis *axis); + +protected: + Q_DISABLE_COPY(QPolarChart) +}; + +QTCOMMERCIALCHART_END_NAMESPACE + +#endif // QCHART_H diff --git a/src/scatterchart/scatterchartitem.cpp b/src/scatterchart/scatterchartitem.cpp index 93761ed..fecbeb1 100644 --- a/src/scatterchart/scatterchartitem.cpp +++ b/src/scatterchart/scatterchartitem.cpp @@ -23,6 +23,7 @@ #include "qscatterseries_p.h" #include "chartpresenter_p.h" #include "abstractdomain_p.h" +#include "qchart.h" #include #include #include @@ -30,7 +31,7 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE -ScatterChartItem::ScatterChartItem(QScatterSeries *series, QGraphicsItem* item) +ScatterChartItem::ScatterChartItem(QScatterSeries *series, QGraphicsItem *item) : XYChart(series,item), m_series(series), m_items(this), @@ -93,12 +94,12 @@ void ScatterChartItem::deletePoints(int count) void ScatterChartItem::markerSelected(QGraphicsItem *marker) { - emit XYChart::clicked(domain()->calculateDomainPoint(m_markerMap[marker])); + emit XYChart::clicked(m_markerMap[marker]); } void ScatterChartItem::markerHovered(QGraphicsItem *marker, bool state) { - emit XYChart::hovered(domain()->calculateDomainPoint(m_markerMap[marker]), state); + emit XYChart::hovered(m_markerMap[marker], state); } void ScatterChartItem::updateGeometry() @@ -125,13 +126,16 @@ void ScatterChartItem::updateGeometry() QRectF clipRect(QPointF(0,0),domain()->size()); + QVector offGridStatus = offGridStatusVector(); + for (int i = 0; i < points.size(); i++) { QGraphicsItem *item = items.at(i); const QPointF &point = points.at(i); const QRectF &rect = item->boundingRect(); - m_markerMap[item] = point; + m_markerMap[item] = m_series->pointAt(i); item->setPos(point.x() - rect.width() / 2, point.y() - rect.height() / 2); - if (!m_visible || !clipRect.contains(point)) + + if (!m_visible || offGridStatus.at(i)) item->setVisible(false); else item->setVisible(true); diff --git a/src/scatterchart/scatterchartitem_p.h b/src/scatterchart/scatterchartitem_p.h index 6e8010b..e00404a 100644 --- a/src/scatterchart/scatterchartitem_p.h +++ b/src/scatterchart/scatterchartitem_p.h @@ -44,7 +44,7 @@ class ScatterChartItem : public XYChart Q_OBJECT Q_INTERFACES(QGraphicsItem) public: - explicit ScatterChartItem(QScatterSeries *series, QGraphicsItem* item = 0); + explicit ScatterChartItem(QScatterSeries *series, QGraphicsItem *item = 0); public: //from QGraphicsItem diff --git a/src/splinechart/splinechartitem.cpp b/src/splinechart/splinechartitem.cpp index 261a753..4a46a72 100644 --- a/src/splinechart/splinechartitem.cpp +++ b/src/splinechart/splinechartitem.cpp @@ -22,13 +22,13 @@ #include "qsplineseries_p.h" #include "chartpresenter_p.h" #include "splineanimation_p.h" -#include "abstractdomain_p.h" +#include "polardomain_p.h" #include #include QTCOMMERCIALCHART_BEGIN_NAMESPACE -SplineChartItem::SplineChartItem(QSplineSeries *series, QGraphicsItem* item) +SplineChartItem::SplineChartItem(QSplineSeries *series, QGraphicsItem *item) : XYChart(series,item), m_series(series), m_pointsVisible(false), @@ -49,8 +49,7 @@ QRectF SplineChartItem::boundingRect() const QPainterPath SplineChartItem::shape() const { - QPainterPathStroker stroker; - return stroker.createStroke(m_path); + return m_fullPath; } void SplineChartItem::setAnimation(SplineAnimation *animation) @@ -107,20 +106,179 @@ void SplineChartItem::updateGeometry() Q_ASSERT(points.count() * 2 - 2 == controlPoints.count()); - QPainterPath splinePath(points.at(0)); + QPainterPath splinePath; + QPainterPath fullPath; + // Use worst case scenario to determine required margin. + qreal margin = m_linePen.width() * 1.42; + + if (m_series->chart()->chartType() == QChart::ChartTypePolar) { + QPainterPath splinePathLeft; + QPainterPath splinePathRight; + QPainterPath *currentSegmentPath = 0; + QPainterPath *previousSegmentPath = 0; + qreal minX = domain()->minX(); + qreal maxX = domain()->maxX(); + qreal minY = domain()->minY(); + QPointF currentSeriesPoint = m_series->pointAt(0); + QPointF currentGeometryPoint = points.at(0); + QPointF previousGeometryPoint = points.at(0); + bool pointOffGrid = false; + bool previousPointWasOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); + m_visiblePoints.clear(); + m_visiblePoints.reserve(points.size()); + + qreal domainRadius = domain()->size().height() / 2.0; + const QPointF centerPoint(domainRadius, domainRadius); + + if (!previousPointWasOffGrid) { + fullPath.moveTo(points.at(0)); + // Do not draw points for points below minimum Y. + if (m_pointsVisible && currentSeriesPoint.y() >= minY) + m_visiblePoints.append(currentGeometryPoint); + } + + qreal leftMarginLine = centerPoint.x() - margin; + qreal rightMarginLine = centerPoint.x() + margin; + qreal horizontal = centerPoint.y(); + + for (int i = 1; i < points.size(); i++) { + // Interpolating spline fragments accurately is not trivial, and would anyway be ugly + // when thick pen is used, so we work around it by utilizing three separate + // paths for spline segments and clip those with custom regions at paint time. + // "Right" path contains segments that cross the axis line with visible point on the + // right side of the axis line, as well as segments that have one point within the margin + // on the right side of the axis line and another point on the right side of the chart. + // "Left" path contains points with similarly on the left side. + // "Full" path contains rest of the points. + // This doesn't yield perfect results always. E.g. when segment covers more than 90 + // degrees and both of the points are within the margin, one in the top half and one in the + // bottom half of the chart, the bottom one gets clipped incorrectly. + // However, this should be rare occurrence in any sensible chart. + currentSeriesPoint = m_series->pointAt(i); + currentGeometryPoint = points.at(i); + pointOffGrid = (currentSeriesPoint.x() < minX || currentSeriesPoint.x() > maxX); + + // Draw something unless both off-grid + if (!pointOffGrid || !previousPointWasOffGrid) { + bool dummyOk; // We know points are ok, but this is needed + qreal currentAngle = static_cast(domain())->toAngularCoordinate(currentSeriesPoint.x(), dummyOk); + qreal previousAngle = static_cast(domain())->toAngularCoordinate(m_series->pointAt(i - 1).x(), dummyOk); + + if ((qAbs(currentAngle - previousAngle) > 180.0)) { + // If the angle between two points is over 180 degrees (half X range), + // any direct segment between them becomes meaningless. + // In this case two line segments are drawn instead, from previous + // point to the center and from center to current point. + if ((previousAngle < 0.0 || (previousAngle <= 180.0 && previousGeometryPoint.x() < rightMarginLine)) + && previousGeometryPoint.y() < horizontal) { + currentSegmentPath = &splinePathRight; + } else if ((previousAngle > 360.0 || (previousAngle > 180.0 && previousGeometryPoint.x() > leftMarginLine)) + && previousGeometryPoint.y() < horizontal) { + currentSegmentPath = &splinePathLeft; + } else if (previousAngle > 0.0 && previousAngle < 360.0) { + currentSegmentPath = &splinePath; + } else { + currentSegmentPath = 0; + } + + if (currentSegmentPath) { + if (previousSegmentPath != currentSegmentPath) + currentSegmentPath->moveTo(previousGeometryPoint); + if (!previousSegmentPath) + fullPath.moveTo(previousGeometryPoint); + + currentSegmentPath->lineTo(centerPoint); + fullPath.lineTo(centerPoint); + } + + previousSegmentPath = currentSegmentPath; + + if ((currentAngle < 0.0 || (currentAngle <= 180.0 && currentGeometryPoint.x() < rightMarginLine)) + && currentGeometryPoint.y() < horizontal) { + currentSegmentPath = &splinePathRight; + } else if ((currentAngle > 360.0 || (currentAngle > 180.0 &¤tGeometryPoint.x() > leftMarginLine)) + && currentGeometryPoint.y() < horizontal) { + currentSegmentPath = &splinePathLeft; + } else if (currentAngle > 0.0 && currentAngle < 360.0) { + currentSegmentPath = &splinePath; + } else { + currentSegmentPath = 0; + } + + if (currentSegmentPath) { + if (previousSegmentPath != currentSegmentPath) + currentSegmentPath->moveTo(centerPoint); + if (!previousSegmentPath) + fullPath.moveTo(centerPoint); + + currentSegmentPath->lineTo(currentGeometryPoint); + fullPath.lineTo(currentGeometryPoint); + } + } else { + QPointF cp1 = controlPoints[2 * (i - 1)]; + QPointF cp2 = controlPoints[(2 * i) - 1]; + + if (previousAngle < 0.0 || currentAngle < 0.0 + || ((previousAngle <= 180.0 && currentAngle <= 180.0) + && ((previousGeometryPoint.x() < rightMarginLine && previousGeometryPoint.y() < horizontal) + || (currentGeometryPoint.x() < rightMarginLine && currentGeometryPoint.y() < horizontal)))) { + currentSegmentPath = &splinePathRight; + } else if (previousAngle > 360.0 || currentAngle > 360.0 + || ((previousAngle > 180.0 && currentAngle > 180.0) + && ((previousGeometryPoint.x() > leftMarginLine && previousGeometryPoint.y() < horizontal) + || (currentGeometryPoint.x() > leftMarginLine && currentGeometryPoint.y() < horizontal)))) { + currentSegmentPath = &splinePathLeft; + } else { + currentSegmentPath = &splinePath; + } + + if (currentSegmentPath != previousSegmentPath) + currentSegmentPath->moveTo(previousGeometryPoint); + if (!previousSegmentPath) + fullPath.moveTo(previousGeometryPoint); + + fullPath.cubicTo(cp1, cp2, currentGeometryPoint); + currentSegmentPath->cubicTo(cp1, cp2, currentGeometryPoint); + } + } else { + currentSegmentPath = 0; + } + + previousPointWasOffGrid = pointOffGrid; + if (!pointOffGrid && m_pointsVisible && currentSeriesPoint.y() >= minY) + m_visiblePoints.append(currentGeometryPoint); + previousSegmentPath = currentSegmentPath; + previousGeometryPoint = currentGeometryPoint; + } - for (int i = 0; i < points.size() - 1; i++) { - const QPointF &point = points.at(i + 1); - splinePath.cubicTo(controlPoints[2 * i], controlPoints[2 * i + 1], point); + m_pathPolarRight = splinePathRight; + m_pathPolarLeft = splinePathLeft; + // Note: This construction of m_fullpath is not perfect. The partial segments that are + // outside left/right clip regions at axis boundary still generate hover/click events, + // because shape doesn't get clipped. It doesn't seem possible to do sensibly. + } else { // not polar + splinePath.moveTo(points.at(0)); + for (int i = 0; i < points.size() - 1; i++) { + const QPointF &point = points.at(i + 1); + splinePath.cubicTo(controlPoints[2 * i], controlPoints[2 * i + 1], point); + } + fullPath = splinePath; } + m_path = splinePath; + + QPainterPathStroker stroker; + // The full path is comprised of three separate paths. + // This is why we are prepared for the "worst case" scenario, i.e. use always MiterJoin and + // multiply line width with square root of two when defining shape and bounding rectangle. + stroker.setWidth(margin); + stroker.setJoinStyle(Qt::MiterJoin); + stroker.setCapStyle(Qt::SquareCap); + stroker.setMiterLimit(m_linePen.miterLimit()); prepareGeometryChange(); - // QPainterPathStroker stroker; - // stroker.setWidth(m_linePen.width() / 2.0); - // m_path = stroker.createStroke(splinePath); - m_path = splinePath; - m_rect = splinePath.boundingRect(); + m_fullPath = stroker.createStroke(fullPath); + m_rect = m_fullPath.boundingRect(); } /*! @@ -240,16 +398,38 @@ void SplineChartItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *o Q_UNUSED(widget) Q_UNUSED(option) + QRectF clipRect = QRectF(QPointF(0, 0), domain()->size()); + painter->save(); - painter->setClipRect(QRectF(QPointF(0,0),domain()->size())); painter->setPen(m_linePen); - // painter->setBrush(m_linePen.color()); + painter->setBrush(Qt::NoBrush); + + if (m_series->chart()->chartType() == QChart::ChartTypePolar) { + qreal halfWidth = domain()->size().width() / 2.0; + QRectF clipRectLeft = QRectF(0, 0, halfWidth, domain()->size().height()); + QRectF clipRectRight = QRectF(halfWidth, 0, halfWidth, domain()->size().height()); + QRegion fullPolarClipRegion(clipRect.toRect(), QRegion::Ellipse); + QRegion clipRegionLeft(fullPolarClipRegion.intersected(clipRectLeft.toRect())); + QRegion clipRegionRight(fullPolarClipRegion.intersected(clipRectRight.toRect())); + painter->setClipRegion(clipRegionLeft); + painter->drawPath(m_pathPolarLeft); + painter->setClipRegion(clipRegionRight); + painter->drawPath(m_pathPolarRight); + painter->setClipRegion(fullPolarClipRegion); + } else { + painter->setClipRect(clipRect); + } painter->drawPath(m_path); + if (m_pointsVisible) { painter->setPen(m_pointPen); - painter->drawPoints(geometryPoints()); + if (m_series->chart()->chartType() == QChart::ChartTypePolar) + painter->drawPoints(m_visiblePoints); + else + painter->drawPoints(geometryPoints()); } + painter->restore(); } diff --git a/src/splinechart/splinechartitem_p.h b/src/splinechart/splinechartitem_p.h index a15b6c6..37e1b52 100644 --- a/src/splinechart/splinechartitem_p.h +++ b/src/splinechart/splinechartitem_p.h @@ -42,7 +42,7 @@ class SplineChartItem : public XYChart Q_OBJECT Q_INTERFACES(QGraphicsItem) public: - SplineChartItem(QSplineSeries *series, QGraphicsItem* item = 0); + SplineChartItem(QSplineSeries *series, QGraphicsItem *item = 0); //from QGraphicsItem QRectF boundingRect() const; @@ -70,11 +70,15 @@ protected: private: QSplineSeries *m_series; QPainterPath m_path; + QPainterPath m_pathPolarRight; + QPainterPath m_pathPolarLeft; + QPainterPath m_fullPath; QRectF m_rect; QPen m_linePen; QPen m_pointPen; bool m_pointsVisible; QVector m_controlPoints; + QVector m_visiblePoints; SplineAnimation *m_animation; friend class SplineAnimation; diff --git a/src/src.pro b/src/src.pro index bfb90ba..a60a6cf 100644 --- a/src/src.pro +++ b/src/src.pro @@ -39,8 +39,8 @@ SOURCES += \ $$PWD/chartelement.cpp \ $$PWD/chartitem.cpp \ $$PWD/scroller.cpp \ - $$PWD/chartlayout.cpp \ - $$PWD/charttitle.cpp + $$PWD/charttitle.cpp \ + $$PWD/qpolarchart.cpp PRIVATE_HEADERS += \ $$PWD/chartdataset_p.h \ $$PWD/chartitem_p.h \ @@ -53,7 +53,6 @@ PRIVATE_HEADERS += \ $$PWD/qchartview_p.h \ $$PWD/scroller_p.h \ $$PWD/qabstractseries_p.h \ - $$PWD/chartlayout_p.h \ $$PWD/charttitle_p.h \ $$PWD/charthelpers_p.h PUBLIC_HEADERS += \ @@ -61,7 +60,8 @@ PUBLIC_HEADERS += \ $$PWD/qchartglobal.h \ $$PWD/qabstractseries.h \ $$PWD/qchartview.h \ - $$PWD/chartsnamespace.h + $$PWD/chartsnamespace.h \ + $$PWD/qpolarchart.h include(animations/animations.pri) include(areachart/areachart.pri) @@ -75,6 +75,7 @@ include(scatterchart/scatter.pri) include(splinechart/splinechart.pri) include(themes/themes.pri) include(xychart/xychart.pri) +include(layout/layout.pri) HEADERS += $$PUBLIC_HEADERS HEADERS += $$PRIVATE_HEADERS diff --git a/src/xychart/qxyseries.cpp b/src/xychart/qxyseries.cpp index 3d5f7ee..285c078 100644 --- a/src/xychart/qxyseries.cpp +++ b/src/xychart/qxyseries.cpp @@ -69,6 +69,18 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE */ /*! + \qmlproperty AbstractAxis XYSeries::axisAngular + The angular axis used for the series, drawn around the polar chart view. + \sa axisX +*/ + +/*! + \qmlproperty AbstractAxis XYSeries::axisRadial + The radial axis used for the series, drawn inside the polar chart view. + \sa axisY +*/ + +/*! \property QXYSeries::pointsVisible Controls if the data points are visible and should be drawn. */ @@ -354,6 +366,15 @@ QList QXYSeries::points() const } /*! + Returns point at \a index in internal points vector. +*/ +const QPointF &QXYSeries::pointAt(int index) const +{ + Q_D(const QXYSeries); + return d->m_points.at(index); +} + +/*! Returns number of data points within series. */ int QXYSeries::count() const diff --git a/src/xychart/qxyseries.h b/src/xychart/qxyseries.h index d0e6bb4..8eff380 100644 --- a/src/xychart/qxyseries.h +++ b/src/xychart/qxyseries.h @@ -56,6 +56,7 @@ public: int count() const; QList points() const; + const QPointF &pointAt(int index) const; QXYSeries &operator << (const QPointF &point); QXYSeries &operator << (const QList &points); diff --git a/src/xychart/xychart.cpp b/src/xychart/xychart.cpp index ecadf67..3d442c5 100644 --- a/src/xychart/xychart.cpp +++ b/src/xychart/xychart.cpp @@ -24,6 +24,7 @@ #include "chartpresenter_p.h" #include "abstractdomain_p.h" #include "qxymodelmapper.h" +#include "qabstractaxis_p.h" #include #include @@ -32,7 +33,7 @@ QTCOMMERCIALCHART_BEGIN_NAMESPACE //TODO: optimize : remove points which are not visible -XYChart::XYChart(QXYSeries *series,QGraphicsItem* item): +XYChart::XYChart(QXYSeries *series, QGraphicsItem *item): ChartItem(series->d_func(),item), m_series(series), m_animation(0), @@ -46,7 +47,7 @@ XYChart::XYChart(QXYSeries *series,QGraphicsItem* item): QObject::connect(this, SIGNAL(hovered(QPointF,bool)), series, SIGNAL(hovered(QPointF,bool))); } -void XYChart::setGeometryPoints(const QVector& points) +void XYChart::setGeometryPoints(const QVector &points) { m_points = points; } @@ -61,6 +62,32 @@ void XYChart::setDirty(bool dirty) m_dirty = dirty; } +// Returns a vector with same size as geometryPoints vector, indicating +// the off grid status of points. +QVector XYChart::offGridStatusVector() +{ + qreal minX = domain()->minX(); + qreal maxX = domain()->maxX(); + qreal minY = domain()->minY(); + qreal maxY = domain()->maxY(); + + QVector returnVector; + returnVector.resize(m_points.size()); + + for (int i = 0; i < m_points.size(); i++) { + const QPointF &seriesPoint = m_series->pointAt(i); + if (seriesPoint.x() < minX + || seriesPoint.x() > maxX + || seriesPoint.y() < minY + || seriesPoint.y() > maxY) { + returnVector[i] = true; + } else { + returnVector[i] = false; + } + } + return returnVector; +} + void XYChart::updateChart(QVector &oldPoints, QVector &newPoints, int index) { diff --git a/src/xychart/xychart_p.h b/src/xychart/xychart_p.h index cfdb296..49c57bc 100644 --- a/src/xychart/xychart_p.h +++ b/src/xychart/xychart_p.h @@ -45,10 +45,10 @@ class XYChart : public ChartItem { Q_OBJECT public: - explicit XYChart(QXYSeries *series,QGraphicsItem* item = 0); + explicit XYChart(QXYSeries *series,QGraphicsItem *item = 0); ~XYChart() {} - void setGeometryPoints(const QVector& points); + void setGeometryPoints(const QVector &points); QVector geometryPoints() const { return m_points; } void setAnimation(XYAnimation *animation); @@ -58,6 +58,9 @@ public: bool isDirty() const { return m_dirty; } void setDirty(bool dirty); + void getSeriesRanges(qreal &minX, qreal &maxX, qreal &minY, qreal &maxY); + QVector offGridStatusVector(); + public Q_SLOTS: void handlePointAdded(int index); void handlePointRemoved(int index); diff --git a/tests/auto/README b/tests/auto/README new file mode 100644 index 0000000..6ecc179 --- /dev/null +++ b/tests/auto/README @@ -0,0 +1,9 @@ +Testing polar chart: + +Since the tests typically initialize the chart once for the whole test case, +it is difficult to test the components against polar chart unless all tests +are duplicated or massively refactored. Neither option is desirable, so instead +we check environment variable TEST_POLAR_CHART. If it is not empty, we run +the tests that are relevant for polar chart against polar chart. Unfortunately +This means two runs of the tests with different environment are required for +full coverage. diff --git a/tests/auto/inc/tst_definitions.h b/tests/auto/inc/tst_definitions.h index d580223..32ddf10 100644 --- a/tests/auto/inc/tst_definitions.h +++ b/tests/auto/inc/tst_definitions.h @@ -21,6 +21,7 @@ #ifndef TST_DEFINITIONS_H #define TST_DEFINITIONS_H +#include "qpolarchart.h" #include #include @@ -63,6 +64,16 @@ namespace QTest QSKIP("Cannot test mouse events in this environment"); \ } while (0); \ } + + #define SKIP_ON_POLAR() { \ + if (isPolarTest()) \ + QSKIP("Test not supported by polar chart"); \ + } + + #define SKIP_ON_CARTESIAN() { \ + if (!isPolarTest()) \ + QSKIP("Test not supported by cartesian chart"); \ + } #else #define SKIP_IF_CANNOT_TEST_MOUSE_EVENTS() { \ do { \ @@ -76,6 +87,39 @@ namespace QTest QSKIP("Cannot test mouse events in this environment", SkipAll); \ } while (0); \ } + + #define SKIP_ON_POLAR() { \ + if (isPolarTest()) \ + QSKIP("Test not supported by polar chart", SkipAll); \ + } + + #define SKIP_ON_CARTESIAN() { \ + if (!isPolarTest()) \ + QSKIP("Test not supported by cartesian chart", SkipAll); \ + } #endif +static inline bool isPolarTest() +{ + static bool isPolar = false; + static bool polarEnvChecked = false; + if (!polarEnvChecked) { + isPolar = !(qgetenv("TEST_POLAR_CHART").isEmpty()); + polarEnvChecked = true; + if (isPolar) + qDebug() << "TEST_POLAR_CHART found -> Testing polar chart!"; + } + return isPolar; +} + +static inline QtCommercialChart::QChart *newQChartOrQPolarChart() +{ + if (isPolarTest()) + return new QtCommercialChart::QPolarChart(); + else + return new QtCommercialChart::QChart(); +} + + + #endif // TST_DEFINITIONS_H diff --git a/tests/auto/qabstractaxis/tst_qabstractaxis.cpp b/tests/auto/qabstractaxis/tst_qabstractaxis.cpp index a66c21d..99f39d8 100644 --- a/tests/auto/qabstractaxis/tst_qabstractaxis.cpp +++ b/tests/auto/qabstractaxis/tst_qabstractaxis.cpp @@ -35,7 +35,7 @@ void tst_QAbstractAxis::init(QAbstractAxis* axis, QAbstractSeries* series) { m_axis = axis; m_series = series; - m_view = new QChartView(new QChart()); + m_view = new QChartView(newQChartOrQPolarChart()); m_chart = m_view->chart(); } diff --git a/tests/auto/qchart/tst_qchart.cpp b/tests/auto/qchart/tst_qchart.cpp index 0558ad6..f92be64 100644 --- a/tests/auto/qchart/tst_qchart.cpp +++ b/tests/auto/qchart/tst_qchart.cpp @@ -101,6 +101,7 @@ private slots: void zoomOut(); void createDefaultAxesForLineSeries_data(); void createDefaultAxesForLineSeries(); + void axisPolarOrientation(); private: void createTestData(); @@ -121,7 +122,7 @@ void tst_QChart::cleanupTestCase() void tst_QChart::init() { - m_view = new QChartView(new QChart()); + m_view = new QChartView(newQChartOrQPolarChart()); m_chart = m_view->chart(); } @@ -191,20 +192,22 @@ void tst_QChart::addSeries_data() QAbstractSeries* area = new QAreaSeries(static_cast(line)); QAbstractSeries* scatter = new QScatterSeries(this); QAbstractSeries* spline = new QSplineSeries(this); - QAbstractSeries* pie = new QPieSeries(this); - QAbstractSeries* bar = new QBarSeries(this); - QAbstractSeries* percent = new QPercentBarSeries(this); - QAbstractSeries* stacked = new QStackedBarSeries(this); QTest::newRow("lineSeries") << line; QTest::newRow("areaSeries") << area; QTest::newRow("scatterSeries") << scatter; QTest::newRow("splineSeries") << spline; - QTest::newRow("pieSeries") << pie; - QTest::newRow("barSeries") << bar; - QTest::newRow("percentBarSeries") << percent; - QTest::newRow("stackedBarSeries") << stacked; + if (!isPolarTest()) { + QAbstractSeries* pie = new QPieSeries(this); + QAbstractSeries* bar = new QBarSeries(this); + QAbstractSeries* percent = new QPercentBarSeries(this); + QAbstractSeries* stacked = new QStackedBarSeries(this); + QTest::newRow("pieSeries") << pie; + QTest::newRow("barSeries") << bar; + QTest::newRow("percentBarSeries") << percent; + QTest::newRow("stackedBarSeries") << stacked; + } } void tst_QChart::addSeries() @@ -258,20 +261,23 @@ void tst_QChart::axisX_data() QTest::newRow("categories,areaSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QAreaSeries(new QLineSeries(this)); QTest::newRow("categories,scatterSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QScatterSeries(this); QTest::newRow("categories,splineSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QSplineSeries(this); - QTest::newRow("categories,pieSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QPieSeries(this); - QTest::newRow("categories,barSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QBarSeries(this); - QTest::newRow("categories,percentBarSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QPercentBarSeries(this); - QTest::newRow("categories,stackedBarSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QStackedBarSeries(this); + if (!isPolarTest()) { + QTest::newRow("categories,pieSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QPieSeries(this); + QTest::newRow("categories,barSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QBarSeries(this); + QTest::newRow("categories,percentBarSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QPercentBarSeries(this); + QTest::newRow("categories,stackedBarSeries") << (QAbstractAxis*) new QBarCategoryAxis() << (QAbstractSeries*) new QStackedBarSeries(this); + } QTest::newRow("value,lineSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QLineSeries(this); QTest::newRow("value,areaSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QAreaSeries(new QLineSeries(this)); QTest::newRow("value,scatterSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QScatterSeries(this); QTest::newRow("value,splineSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QSplineSeries(this); - QTest::newRow("value,pieSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QPieSeries(this); - QTest::newRow("value,barSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QBarSeries(this); - QTest::newRow("value,percentBarSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QPercentBarSeries(this); - QTest::newRow("value,stackedBarSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QStackedBarSeries(this); - + if (!isPolarTest()) { + QTest::newRow("value,pieSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QPieSeries(this); + QTest::newRow("value,barSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QBarSeries(this); + QTest::newRow("value,percentBarSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QPercentBarSeries(this); + QTest::newRow("value,stackedBarSeries") << (QAbstractAxis*) new QValueAxis() << (QAbstractSeries*) new QStackedBarSeries(this); + } } void tst_QChart::axisX() @@ -850,14 +856,14 @@ void tst_QChart::createDefaultAxesForLineSeries() series2->append(series2minX, series2minY); series2->append(series2midX, series2midY); series2->append(series2maxX, series2maxY); - QChart chart; - chart.addSeries(series1); - chart.addSeries(series2); - chart.createDefaultAxes(); - QValueAxis *xAxis = (QValueAxis *)chart.axisX(); + QChart *chart = newQChartOrQPolarChart(); + chart->addSeries(series1); + chart->addSeries(series2); + chart->createDefaultAxes(); + QValueAxis *xAxis = (QValueAxis *)chart->axisX(); QCOMPARE(xAxis->min(), overallminX); QCOMPARE(xAxis->max(), overallmaxX); - QValueAxis *yAxis = (QValueAxis *)chart.axisY(); + QValueAxis *yAxis = (QValueAxis *)chart->axisY(); QCOMPARE(yAxis->min(), overallminY); QCOMPARE(yAxis->max(), overallmaxY); QLineSeries *series3 = new QLineSeries(this); @@ -865,12 +871,37 @@ void tst_QChart::createDefaultAxesForLineSeries() series3->append(0, 0); series3->append(100, 100); // Adding a new series should not change the axes as they have not been told to update - chart.addSeries(series3); + chart->addSeries(series3); QCOMPARE(xAxis->min(), overallminX); QCOMPARE(xAxis->max(), overallmaxX); QCOMPARE(yAxis->min(), overallminY); QCOMPARE(yAxis->max(), overallmaxY); +} + +void tst_QChart::axisPolarOrientation() +{ + QLineSeries* series1 = new QLineSeries(this); + series1->append(1, 2); + series1->append(2, 4); + series1->append(3, 8); + QPolarChart chart; + chart.addSeries(series1); + + QValueAxis *xAxis = new QValueAxis(); + QValueAxis *yAxis = new QValueAxis(); + chart.addAxis(xAxis, QPolarChart::PolarOrientationAngular); + chart.addAxis(yAxis, QPolarChart::PolarOrientationRadial); + + QList xAxes = chart.axes(QPolarChart::PolarOrientationAngular); + QList yAxes = chart.axes(QPolarChart::PolarOrientationRadial); + + QCOMPARE(xAxes.size(), 1); + QCOMPARE(yAxes.size(), 1); + QCOMPARE(xAxes[0], xAxis); + QCOMPARE(yAxes[0], yAxis); + QCOMPARE(chart.axisPolarOrientation(xAxes[0]), QPolarChart::PolarOrientationAngular); + QCOMPARE(chart.axisPolarOrientation(yAxes[0]), QPolarChart::PolarOrientationRadial); } QTEST_MAIN(tst_QChart) diff --git a/tests/auto/qchartview/tst_qchartview.cpp b/tests/auto/qchartview/tst_qchartview.cpp index d801971..878b275 100644 --- a/tests/auto/qchartview/tst_qchartview.cpp +++ b/tests/auto/qchartview/tst_qchartview.cpp @@ -67,7 +67,7 @@ void tst_QChartView::cleanupTestCase() void tst_QChartView::init() { - m_view = new QChartView(new QChart()); + m_view = new QChartView(newQChartOrQPolarChart()); m_view->chart()->legend()->setVisible(false); } @@ -120,9 +120,15 @@ void tst_QChartView::rubberBand_data() QTest::addColumn("min"); QTest::addColumn("max"); - QTest::newRow("HorizonalRubberBand") << QChartView::RubberBands(QChartView::HorizonalRubberBand) << 0 << 1 << QPoint(5,5) << QPoint(5,5); - QTest::newRow("VerticalRubberBand") << QChartView::RubberBands(QChartView::VerticalRubberBand) << 1 << 0 << QPoint(5,5) << QPoint(5,5); - QTest::newRow("RectangleRubberBand") << QChartView::RubberBands(QChartView::RectangleRubberBand) << 1 << 1 << QPoint(5,5) << QPoint(5,5); + if (isPolarTest()) { + QTest::newRow("HorizonalRubberBand") << QChartView::RubberBands(QChartView::HorizonalRubberBand) << 0 << 0 << QPoint(5,5) << QPoint(5,5); + QTest::newRow("VerticalRubberBand") << QChartView::RubberBands(QChartView::VerticalRubberBand) << 0 << 0 << QPoint(5,5) << QPoint(5,5); + QTest::newRow("RectangleRubberBand") << QChartView::RubberBands(QChartView::RectangleRubberBand) << 0 << 0 << QPoint(5,5) << QPoint(5,5); + } else { + QTest::newRow("HorizonalRubberBand") << QChartView::RubberBands(QChartView::HorizonalRubberBand) << 0 << 1 << QPoint(5,5) << QPoint(5,5); + QTest::newRow("VerticalRubberBand") << QChartView::RubberBands(QChartView::VerticalRubberBand) << 1 << 0 << QPoint(5,5) << QPoint(5,5); + QTest::newRow("RectangleRubberBand") << QChartView::RubberBands(QChartView::RectangleRubberBand) << 1 << 1 << QPoint(5,5) << QPoint(5,5); + } } void tst_QChartView::rubberBand() @@ -194,7 +200,8 @@ void tst_QChartView::setChart() series1->append(1,1); oldChart->addSeries(series1); - QPointer newChart = new QChart(); + QPointer newChart = newQChartOrQPolarChart(); + QLineSeries *series2 = new QLineSeries(); series2->append(0,1); series2->append(1,0); diff --git a/tests/auto/qlegend/tst_qlegend.cpp b/tests/auto/qlegend/tst_qlegend.cpp index 5eb6e24..de49356 100644 --- a/tests/auto/qlegend/tst_qlegend.cpp +++ b/tests/auto/qlegend/tst_qlegend.cpp @@ -63,7 +63,6 @@ private slots: private: QChart *m_chart; - }; void tst_QLegend::initTestCase() @@ -76,7 +75,7 @@ void tst_QLegend::cleanupTestCase() void tst_QLegend::init() { - m_chart = new QChart(); + m_chart = newQChartOrQPolarChart(); } void tst_QLegend::cleanup() @@ -98,6 +97,8 @@ void tst_QLegend::qlegend() void tst_QLegend::qpieLegendMarker() { + SKIP_ON_POLAR(); + QVERIFY(m_chart); QLegend *legend = m_chart->legend(); @@ -190,6 +191,8 @@ void tst_QLegend::qxyLegendMarker() void tst_QLegend::qbarLegendMarker() { + SKIP_ON_POLAR(); + QVERIFY(m_chart); QLegend *legend = m_chart->legend(); @@ -230,6 +233,8 @@ void tst_QLegend::qbarLegendMarker() void tst_QLegend::markers() { + SKIP_ON_POLAR(); + QVERIFY(m_chart); QLegend *legend = m_chart->legend(); @@ -270,6 +275,8 @@ void tst_QLegend::markers() void tst_QLegend::addAndRemoveSeries() { + SKIP_ON_POLAR(); + QVERIFY(m_chart); QLegend *legend = m_chart->legend(); @@ -291,6 +298,8 @@ void tst_QLegend::addAndRemoveSeries() void tst_QLegend::pieMarkerProperties() { + SKIP_ON_POLAR(); + QVERIFY(m_chart); QLegend *legend = m_chart->legend(); @@ -327,6 +336,8 @@ void tst_QLegend::pieMarkerProperties() void tst_QLegend::barMarkerProperties() { + SKIP_ON_POLAR(); + QVERIFY(m_chart); QLegend *legend = m_chart->legend(); @@ -482,8 +493,10 @@ void tst_QLegend::xyMarkerPropertiesScatter() void tst_QLegend::markerSignals() { + SKIP_ON_POLAR(); + SKIP_IF_CANNOT_TEST_MOUSE_EVENTS(); - QChart *chart = new QChart(); + QChart *chart = newQChartOrQPolarChart(); QLegend *legend = chart->legend(); QBarSeries *bar = new QBarSeries(); diff --git a/tests/auto/qml/tst_qml.cpp b/tests/auto/qml/tst_qml.cpp index 2c57036..98880af 100644 --- a/tests/auto/qml/tst_qml.cpp +++ b/tests/auto/qml/tst_qml.cpp @@ -36,7 +36,8 @@ private slots: void checkPlugin(); private: QString componentErrors(const QDeclarativeComponent* component) const; - QString imports(); + QString imports_1_1(); + QString imports_1_3(); }; @@ -53,12 +54,18 @@ QString tst_QML::componentErrors(const QDeclarativeComponent* component) const return errors.join("\n"); } -QString tst_QML::imports() +QString tst_QML::imports_1_1() { return "import QtQuick 1.0 \n" "import QtCommercial.Chart 1.1 \n"; } +QString tst_QML::imports_1_3() +{ + return "import QtQuick 1.0 \n" + "import QtCommercial.Chart 1.3 \n"; +} + void tst_QML::initTestCase() { @@ -82,34 +89,37 @@ void tst_QML::checkPlugin_data() { QTest::addColumn("source"); - QTest::newRow("createChartView") << imports() + "ChartView{}"; - QTest::newRow("XYPoint") << imports() + "XYPoint{}"; - QTest::newRow("scatterSeries") << imports() + "ScatterSeries{}"; - QTest::newRow("lineSeries") << imports() + "LineSeries{}"; - QTest::newRow("splineSeries") << imports() + "SplineSeries{}"; - QTest::newRow("areaSeries") << imports() + "AreaSeries{}"; - QTest::newRow("barSeries") << imports() + "BarSeries{}"; - QTest::newRow("stackedBarSeries") << imports() + "StackedBarSeries{}"; - QTest::newRow("precentBarSeries") << imports() + "PercentBarSeries{}"; - QTest::newRow("horizonatlBarSeries") << imports() + "HorizontalBarSeries{}"; - QTest::newRow("horizonatlStackedBarSeries") << imports() + "HorizontalStackedBarSeries{}"; - QTest::newRow("horizonatlstackedBarSeries") << imports() + "HorizontalPercentBarSeries{}"; - QTest::newRow("pieSeries") << imports() + "PieSeries{}"; - QTest::newRow("PieSlice") << imports() + "PieSlice{}"; - QTest::newRow("BarSet") << imports() + "BarSet{}"; - QTest::newRow("HXYModelMapper") << imports() + "HXYModelMapper{}"; - QTest::newRow("VXYModelMapper") << imports() + "VXYModelMapper{}"; - QTest::newRow("HPieModelMapper") << imports() + "HPieModelMapper{}"; - QTest::newRow("HPieModelMapper") << imports() + "HPieModelMapper{}"; - QTest::newRow("HBarModelMapper") << imports() + "HBarModelMapper{}"; - QTest::newRow("VBarModelMapper") << imports() + "VBarModelMapper{}"; - QTest::newRow("ValueAxis") << imports() + "ValueAxis{}"; + QTest::newRow("createChartView") << imports_1_1() + "ChartView{}"; + QTest::newRow("XYPoint") << imports_1_1() + "XYPoint{}"; + QTest::newRow("scatterSeries") << imports_1_1() + "ScatterSeries{}"; + QTest::newRow("lineSeries") << imports_1_1() + "LineSeries{}"; + QTest::newRow("splineSeries") << imports_1_1() + "SplineSeries{}"; + QTest::newRow("areaSeries") << imports_1_1() + "AreaSeries{}"; + QTest::newRow("barSeries") << imports_1_1() + "BarSeries{}"; + QTest::newRow("stackedBarSeries") << imports_1_1() + "StackedBarSeries{}"; + QTest::newRow("precentBarSeries") << imports_1_1() + "PercentBarSeries{}"; + QTest::newRow("horizonatlBarSeries") << imports_1_1() + "HorizontalBarSeries{}"; + QTest::newRow("horizonatlStackedBarSeries") << imports_1_1() + "HorizontalStackedBarSeries{}"; + QTest::newRow("horizonatlstackedBarSeries") << imports_1_1() + "HorizontalPercentBarSeries{}"; + QTest::newRow("pieSeries") << imports_1_1() + "PieSeries{}"; + QTest::newRow("PieSlice") << imports_1_1() + "PieSlice{}"; + QTest::newRow("BarSet") << imports_1_1() + "BarSet{}"; + QTest::newRow("HXYModelMapper") << imports_1_1() + "HXYModelMapper{}"; + QTest::newRow("VXYModelMapper") << imports_1_1() + "VXYModelMapper{}"; + QTest::newRow("HPieModelMapper") << imports_1_1() + "HPieModelMapper{}"; + QTest::newRow("HPieModelMapper") << imports_1_1() + "HPieModelMapper{}"; + QTest::newRow("HBarModelMapper") << imports_1_1() + "HBarModelMapper{}"; + QTest::newRow("VBarModelMapper") << imports_1_1() + "VBarModelMapper{}"; + QTest::newRow("ValueAxis") << imports_1_1() + "ValueAxis{}"; #ifndef QT_ON_ARM - QTest::newRow("DateTimeAxis") << imports() + "DateTimeAxis{}"; + QTest::newRow("DateTimeAxis") << imports_1_1() + "DateTimeAxis{}"; #endif - QTest::newRow("CategoryAxis") << imports() + "CategoryAxis{}"; - QTest::newRow("CategoryRange") << imports() + "CategoryRange{}"; - QTest::newRow("BarCategoryAxis") << imports() + "BarCategoryAxis{}"; + QTest::newRow("CategoryAxis") << imports_1_1() + "CategoryAxis{}"; + QTest::newRow("CategoryRange") << imports_1_1() + "CategoryRange{}"; + QTest::newRow("BarCategoryAxis") << imports_1_1() + "BarCategoryAxis{}"; + + QTest::newRow("createPolarChartView") << imports_1_3() + "PolarChartView{}"; + //QTest::newRow("LogValueAxis") << imports_1_3() + "LogValueAxis{}"; } void tst_QML::checkPlugin() diff --git a/tests/auto/qxymodelmapper/tst_qxymodelmapper.cpp b/tests/auto/qxymodelmapper/tst_qxymodelmapper.cpp index e553f5e..f4c3620 100644 --- a/tests/auto/qxymodelmapper/tst_qxymodelmapper.cpp +++ b/tests/auto/qxymodelmapper/tst_qxymodelmapper.cpp @@ -28,6 +28,7 @@ #include #include #include +#include "tst_definitions.h" QTCOMMERCIALCHART_USE_NAMESPACE @@ -145,7 +146,7 @@ void tst_qxymodelmapper::cleanup() void tst_qxymodelmapper::initTestCase() { - m_chart = new QChart; + m_chart = newQChartOrQPolarChart(); QChartView *chartView = new QChartView(m_chart); chartView->show(); } diff --git a/tests/auto/qxyseries/tst_qxyseries.cpp b/tests/auto/qxyseries/tst_qxyseries.cpp index 9a357b7..7988e9b 100644 --- a/tests/auto/qxyseries/tst_qxyseries.cpp +++ b/tests/auto/qxyseries/tst_qxyseries.cpp @@ -32,7 +32,7 @@ void tst_QXYSeries::cleanupTestCase() void tst_QXYSeries::init() { - m_view = new QChartView(new QChart()); + m_view = new QChartView(newQChartOrQPolarChart()); m_chart = m_view->chart(); } diff --git a/tests/polarcharttest/chartview.cpp b/tests/polarcharttest/chartview.cpp new file mode 100644 index 0000000..4c8c577 --- /dev/null +++ b/tests/polarcharttest/chartview.cpp @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "chartview.h" +#include +#include +#include + +QTCOMMERCIALCHART_USE_NAMESPACE + +ChartView::ChartView(QWidget *parent) : + QChartView(parent) +{ +} + +void ChartView::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Plus: + chart()->zoomIn(); + break; + case Qt::Key_Minus: + chart()->zoomOut(); + break; + case Qt::Key_Left: + chart()->scroll(-1.0, 0); + break; + case Qt::Key_Right: + chart()->scroll(1.0, 0); + break; + case Qt::Key_Up: + chart()->scroll(0, 1.0); + break; + case Qt::Key_Down: + chart()->scroll(0, -1.0); + break; + default: + QGraphicsView::keyPressEvent(event); + break; + } +} diff --git a/tests/polarcharttest/chartview.h b/tests/polarcharttest/chartview.h new file mode 100644 index 0000000..82528c2 --- /dev/null +++ b/tests/polarcharttest/chartview.h @@ -0,0 +1,38 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef CHARTVIEW_H +#define CHARTVIEW_H + +#include +#include + +QTCOMMERCIALCHART_USE_NAMESPACE + +class ChartView : public QChartView +{ +public: + ChartView(QWidget *parent = 0); + +protected: + void keyPressEvent(QKeyEvent *event); +}; + +#endif diff --git a/tests/polarcharttest/main.cpp b/tests/polarcharttest/main.cpp new file mode 100644 index 0000000..30db2e2 --- /dev/null +++ b/tests/polarcharttest/main.cpp @@ -0,0 +1,31 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "mainwindow.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + + return a.exec(); +} diff --git a/tests/polarcharttest/mainwindow.cpp b/tests/polarcharttest/mainwindow.cpp new file mode 100644 index 0000000..7e6cef0 --- /dev/null +++ b/tests/polarcharttest/mainwindow.cpp @@ -0,0 +1,1136 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "mainwindow.h" +#include "chartview.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QTCOMMERCIALCHART_USE_NAMESPACE +#include "ui_mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + m_angularTickCount(9), + m_radialTickCount(11), + m_labelsAngle(0), + m_angularMin(0.0), + m_angularMax(40000.0), + m_radialMin(0.0), + m_radialMax(30000.0), + m_angularShadesVisible(false), + m_radialShadesVisible(false), + m_labelsVisible(true), + m_titleVisible(true), + m_gridVisible(true), + m_arrowVisible(true), + m_angularShadesBrush(new QBrush(Qt::NoBrush)), + m_radialShadesBrush(new QBrush(Qt::NoBrush)), + m_labelBrush(new QBrush(Qt::black)), + m_titleBrush(new QBrush(Qt::black)), + m_angularShadesPen(new QPen(Qt::NoPen)), + m_radialShadesPen(new QPen(Qt::NoPen)), + m_labelPen(new QPen(Qt::NoPen)), + m_titlePen(new QPen(Qt::NoPen)), + m_gridPen(new QPen(QRgb(0x010101))), // Note: Pure black is default color, so it gets overridden by + m_arrowPen(new QPen(QRgb(0x010101))), // default theme if set to that initially. This is an example of workaround. + m_labelFormat(QString("%.2f")), + m_animationOptions(QChart::NoAnimation), + m_angularTitle(QString("Angular Title")), + m_radialTitle(QString("Radial Title")), + m_base(2.0), + m_chart(0), + m_angularAxis(0), + m_radialAxis(0), + m_angularAxisMode(AxisModeNone), + m_radialAxisMode(AxisModeNone), + m_series1(0), + m_series2(0), + m_series3(0), + m_series4(0), + m_series5(0), + m_series6(0), + m_series7(0), + m_dateFormat(QString("mm-ss-zzz")), + m_moreCategories(false) +{ + ui->setupUi(this); + + ui->angularTicksSpin->setValue(m_angularTickCount); + ui->radialTicksSpin->setValue(m_radialTickCount); + ui->anglesSpin->setValue(m_labelsAngle); + ui->radialMinSpin->setValue(m_radialMin); + ui->radialMaxSpin->setValue(m_radialMax); + ui->angularMinSpin->setValue(m_angularMin); + ui->angularMaxSpin->setValue(m_angularMax); + ui->angularShadesComboBox->setCurrentIndex(0); + ui->radialShadesComboBox->setCurrentIndex(0); + ui->labelFormatEdit->setText(m_labelFormat); + ui->dateFormatEdit->setText(m_dateFormat); + ui->moreCategoriesCheckBox->setChecked(m_moreCategories); + + ui->series1checkBox->setChecked(true); + ui->series2checkBox->setChecked(true); + ui->series3checkBox->setChecked(true); + ui->series4checkBox->setChecked(true); + ui->series5checkBox->setChecked(true); + ui->series6checkBox->setChecked(true); + ui->series7checkBox->setChecked(true); + + m_currentLabelFont.setFamily(ui->labelFontComboBox->currentFont().family()); + m_currentLabelFont.setPixelSize(15); + m_currentTitleFont.setFamily(ui->titleFontComboBox->currentFont().family()); + m_currentTitleFont.setPixelSize(30); + + ui->labelFontSizeSpin->setValue(m_currentLabelFont.pixelSize()); + ui->titleFontSizeSpin->setValue(m_currentTitleFont.pixelSize()); + + ui->logBaseSpin->setValue(m_base); + + initXYValueChart(); + setAngularAxis(AxisModeValue); + setRadialAxis(AxisModeValue); + + ui->angularAxisComboBox->setCurrentIndex(int(m_angularAxisMode)); + ui->radialAxisComboBox->setCurrentIndex(int(m_radialAxisMode)); + + connect(ui->angularTicksSpin, SIGNAL(valueChanged(int)), this, SLOT(angularTicksChanged(int))); + connect(ui->radialTicksSpin, SIGNAL(valueChanged(int)), this, SLOT(radialTicksChanged(int))); + connect(ui->anglesSpin, SIGNAL(valueChanged(int)), this, SLOT(anglesChanged(int))); + connect(ui->radialMinSpin, SIGNAL(valueChanged(double)), this, SLOT(radialMinChanged(double))); + connect(ui->radialMaxSpin, SIGNAL(valueChanged(double)), this, SLOT(radialMaxChanged(double))); + connect(ui->angularMinSpin, SIGNAL(valueChanged(double)), this, SLOT(angularMinChanged(double))); + connect(ui->angularMaxSpin, SIGNAL(valueChanged(double)), this, SLOT(angularMaxChanged(double))); + connect(ui->angularShadesComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(angularShadesIndexChanged(int))); + connect(ui->radialShadesComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(radialShadesIndexChanged(int))); + connect(ui->animationsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(animationIndexChanged(int))); + connect(ui->labelFormatEdit, SIGNAL(textEdited(QString)), this, SLOT(labelFormatEdited(QString))); + connect(ui->labelFontComboBox, SIGNAL(currentFontChanged(QFont)), this, SLOT(labelFontChanged(QFont))); + connect(ui->labelFontSizeSpin, SIGNAL(valueChanged(int)), this, SLOT(labelFontSizeChanged(int))); + connect(ui->labelComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(labelsIndexChanged(int))); + connect(ui->titleFontComboBox, SIGNAL(currentFontChanged(QFont)), this, SLOT(titleFontChanged(QFont))); + connect(ui->titleFontSizeSpin, SIGNAL(valueChanged(int)), this, SLOT(titleFontSizeChanged(int))); + connect(ui->titleComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(titleIndexChanged(int))); + connect(ui->gridComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(gridIndexChanged(int))); + connect(ui->arrowComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(arrowIndexChanged(int))); + connect(ui->logBaseSpin, SIGNAL(valueChanged(double)), this, SLOT(logBaseChanged(double))); + connect(ui->angularAxisComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(angularAxisIndexChanged(int))); + connect(ui->radialAxisComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(radialAxisIndexChanged(int))); + connect(ui->niceNumbersCheckBox, SIGNAL(clicked()), this, SLOT(niceNumbersChecked())); + connect(ui->dateFormatEdit, SIGNAL(textEdited(QString)), this, SLOT(dateFormatEdited(QString))); + connect(ui->moreCategoriesCheckBox, SIGNAL(clicked()), this, SLOT(moreCategoriesChecked())); + connect(ui->series1checkBox, SIGNAL(clicked()), this, SLOT(series1CheckBoxChecked())); + connect(ui->series2checkBox, SIGNAL(clicked()), this, SLOT(series2CheckBoxChecked())); + connect(ui->series3checkBox, SIGNAL(clicked()), this, SLOT(series3CheckBoxChecked())); + connect(ui->series4checkBox, SIGNAL(clicked()), this, SLOT(series4CheckBoxChecked())); + connect(ui->series5checkBox, SIGNAL(clicked()), this, SLOT(series5CheckBoxChecked())); + connect(ui->series6checkBox, SIGNAL(clicked()), this, SLOT(series6CheckBoxChecked())); + connect(ui->series7checkBox, SIGNAL(clicked()), this, SLOT(series7CheckBoxChecked())); + connect(ui->themeComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(themeIndexChanged(int))); + + ui->chartView->setChart(m_chart); + ui->chartView->setRenderHint(QPainter::Antialiasing); +} + +MainWindow::~MainWindow() +{ + delete ui; + delete m_angularShadesBrush; + delete m_radialShadesBrush; + delete m_angularShadesPen; + delete m_radialShadesPen; +} + +void MainWindow::initXYValueChart() +{ + qreal seriesAngularMin = 1; + qreal seriesAngularMax = 46000; + qreal seriesRadialMin = 1; + qreal seriesRadialMax = 23500; + qreal radialDimension = seriesRadialMax - seriesRadialMin; + qreal angularDimension = seriesAngularMax - seriesAngularMin; + + // Scatter series, points outside min-max ranges should not be drawn + m_series1 = new QScatterSeries(); + m_series1->setName("scatter"); + qreal scatterCount = 10; + qreal scatterAngularStep = angularDimension / scatterCount; + qreal scatterRadialStep = radialDimension / scatterCount; + for (qreal i = 0.0; i < scatterCount; i++) { + m_series1->append((i * scatterAngularStep) + seriesAngularMin, (i * scatterRadialStep) + seriesRadialMin); + //qDebug() << m_series1->points().last(); + } + m_series1->setMarkerSize(10); + *m_series1 << QPointF(50, 50) << QPointF(150, 150) << QPointF(250, 250) << QPointF(350, 350) << QPointF(450, 450); + *m_series1 << QPointF(1050, 0.50) << QPointF(1150, 0.25) << QPointF(1250, 0.12) << QPointF(1350, 0.075) << QPointF(1450, 0.036); + *m_series1 << QPointF(0.50, 2000) << QPointF(0.25, 3500) << QPointF(0.12, 5000) << QPointF(0.075, 6500) << QPointF(0.036, 8000); + + // Line series, points outside min-max ranges should not be drawn, + // but lines should be properly interpolated at chart edges + m_series2 = new QLineSeries(); + m_series2->setName("line 1"); + qreal lineCount = 100; + qreal lineAngularStep = angularDimension / lineCount; + qreal lineRadialStep = radialDimension / lineCount; + for (qreal i = 0.0; i < lineCount; i++) { + m_series2->append((i * lineAngularStep) + seriesAngularMin, (i * lineRadialStep) + seriesRadialMin); + //qDebug() << m_series2->points().last(); + } + QPen series2Pen = QPen(Qt::blue, 10); + //series2Pen.setStyle(Qt::DashDotDotLine); + m_series2->setPen(series2Pen); + + m_series3 = new QLineSeries(); + m_series3->setName("Area upper"); + lineCount = 87; + lineAngularStep = angularDimension / lineCount; + lineRadialStep = radialDimension / lineCount; + for (qreal i = 1.0; i <= lineCount; i++) { + m_series3->append((i * lineAngularStep) + seriesAngularMin, (i * lineRadialStep) + seriesRadialMin + 200.0); + //qDebug() << m_series3->points().last(); + } + + m_series4 = new QLineSeries(); + m_series4->setName("Area lower"); + lineCount = 89; + lineAngularStep = angularDimension / lineCount; + lineRadialStep = radialDimension / lineCount; + for (qreal i = 1.0; i <= lineCount; i++) { + m_series4->append((i * lineAngularStep) + seriesAngularMin + 100.0, (i * lineRadialStep) + seriesRadialMin + i * 300.0); + //qDebug() << m_series4->points().last(); + } + + m_series5 = new QAreaSeries(); + m_series5->setName("area"); + m_series5->setUpperSeries(m_series3); + m_series5->setLowerSeries(m_series4); + m_series5->setOpacity(0.5); + + m_series6 = new QSplineSeries(); + m_series6->setName("spline"); + qreal ad = angularDimension / 20; + qreal rd = radialDimension / 10; + m_series6->append(seriesAngularMin, seriesRadialMin + rd * 2); + m_series6->append(seriesAngularMin + ad, seriesRadialMin + rd * 5); + m_series6->append(seriesAngularMin + ad * 2, seriesRadialMin + rd * 4); + m_series6->append(seriesAngularMin + ad * 3, seriesRadialMin + rd * 9); + m_series6->append(seriesAngularMin + ad * 4, seriesRadialMin + rd * 11); + m_series6->append(seriesAngularMin + ad * 5, seriesRadialMin + rd * 12); + m_series6->append(seriesAngularMin + ad * 6, seriesRadialMin + rd * 9); + m_series6->append(seriesAngularMin + ad * 7, seriesRadialMin + rd * 11); + m_series6->append(seriesAngularMin + ad * 8, seriesRadialMin + rd * 12); + m_series6->append(seriesAngularMin + ad * 9, seriesRadialMin + rd * 6); + m_series6->append(seriesAngularMin + ad * 10, seriesRadialMin + rd * 4); + m_series6->append(seriesAngularMin + ad * 10, seriesRadialMin + rd * 8); + m_series6->append(seriesAngularMin + ad * 11, seriesRadialMin + rd * 9); + m_series6->append(seriesAngularMin + ad * 12, seriesRadialMin + rd * 11); + m_series6->append(seriesAngularMin + ad * 13, seriesRadialMin + rd * 12); + m_series6->append(seriesAngularMin + ad * 14, seriesRadialMin + rd * 6); + m_series6->append(seriesAngularMin + ad * 15, seriesRadialMin + rd * 3); + m_series6->append(seriesAngularMin + ad * 16, seriesRadialMin + rd * 2); + m_series6->append(seriesAngularMin + ad * 17, seriesRadialMin + rd * 6); + m_series6->append(seriesAngularMin + ad * 18, seriesRadialMin + rd * 6); + m_series6->append(seriesAngularMin + ad * 19, seriesRadialMin + rd * 6); + m_series6->append(seriesAngularMin + ad * 20, seriesRadialMin + rd * 6); + m_series6->append(seriesAngularMin + ad * 19, seriesRadialMin + rd * 2); + m_series6->append(seriesAngularMin + ad * 18, seriesRadialMin + rd * 9); + m_series6->append(seriesAngularMin + ad * 17, seriesRadialMin + rd * 7); + m_series6->append(seriesAngularMin + ad * 16, seriesRadialMin + rd * 3); + m_series6->append(seriesAngularMin + ad * 15, seriesRadialMin + rd * 1); + m_series6->append(seriesAngularMin + ad * 14, seriesRadialMin + rd * 7); + m_series6->append(seriesAngularMin + ad * 13, seriesRadialMin + rd * 5); + m_series6->append(seriesAngularMin + ad * 12, seriesRadialMin + rd * 9); + m_series6->append(seriesAngularMin + ad * 11, seriesRadialMin + rd * 1); + m_series6->append(seriesAngularMin + ad * 10, seriesRadialMin + rd * 4); + m_series6->append(seriesAngularMin + ad * 9, seriesRadialMin + rd * 1); + m_series6->append(seriesAngularMin + ad * 8, seriesRadialMin + rd * 2); + m_series6->append(seriesAngularMin + ad * 7, seriesRadialMin + rd * 4); + m_series6->append(seriesAngularMin + ad * 6, seriesRadialMin + rd * 8); + m_series6->append(seriesAngularMin + ad * 5, seriesRadialMin + rd * 12); + m_series6->append(seriesAngularMin + ad * 4, seriesRadialMin + rd * 9); + m_series6->append(seriesAngularMin + ad * 3, seriesRadialMin + rd * 8); + m_series6->append(seriesAngularMin + ad * 2, seriesRadialMin + rd * 7); + m_series6->append(seriesAngularMin + ad, seriesRadialMin + rd * 4); + m_series6->append(seriesAngularMin, seriesRadialMin + rd * 10); + + m_series6->setPointsVisible(true); + QPen series6Pen = QPen(Qt::red, 10); + //series6Pen.setStyle(Qt::DashDotDotLine); + m_series6->setPen(series6Pen); + + // m_series7 shows points at category intersections + m_series7 = new QScatterSeries(); + m_series7->setName("Category check"); + m_series7->setMarkerSize(7); + m_series7->setBrush(QColor(Qt::red)); + m_series7->setMarkerShape(QScatterSeries::MarkerShapeRectangle); + *m_series7 << QPointF(1000, 1000) + << QPointF(1000, 2000) + << QPointF(1000, 4000) + << QPointF(1000, 9000) + << QPointF(1000, 14000) + << QPointF(1000, 16500) + << QPointF(1000, 19000) + + << QPointF(4000, 1000) + << QPointF(4000, 2000) + << QPointF(4000, 4000) + << QPointF(4000, 9000) + << QPointF(4000, 14000) + << QPointF(4000, 16500) + << QPointF(4000, 19000) + + << QPointF(7000, 1000) + << QPointF(7000, 2000) + << QPointF(7000, 4000) + << QPointF(7000, 9000) + << QPointF(7000, 14000) + << QPointF(7000, 16500) + << QPointF(7000, 19000) + + << QPointF(12000, 1000) + << QPointF(12000, 2000) + << QPointF(12000, 4000) + << QPointF(12000, 9000) + << QPointF(12000, 14000) + << QPointF(12000, 16500) + << QPointF(12000, 19000) + + << QPointF(17000, 1000) + << QPointF(17000, 2000) + << QPointF(17000, 4000) + << QPointF(17000, 9000) + << QPointF(17000, 14000) + << QPointF(17000, 16500) + << QPointF(17000, 19000) + + << QPointF(22000, 1000) + << QPointF(22000, 2000) + << QPointF(22000, 4000) + << QPointF(22000, 9000) + << QPointF(22000, 14000) + << QPointF(22000, 16500) + << QPointF(22000, 19000) + + << QPointF(28000, 1000) + << QPointF(28000, 2000) + << QPointF(28000, 4000) + << QPointF(28000, 9000) + << QPointF(28000, 14000) + << QPointF(28000, 16500) + << QPointF(28000, 19000); + + m_chart = new QPolarChart(); + + m_chart->addSeries(m_series1); + m_chart->addSeries(m_series2); + m_chart->addSeries(m_series3); + m_chart->addSeries(m_series4); + m_chart->addSeries(m_series5); + m_chart->addSeries(m_series6); + m_chart->addSeries(m_series7); + + connect(m_series1, SIGNAL(clicked(QPointF)), this, SLOT(seriesClicked(QPointF))); + connect(m_series2, SIGNAL(clicked(QPointF)), this, SLOT(seriesClicked(QPointF))); + connect(m_series3, SIGNAL(clicked(QPointF)), this, SLOT(seriesClicked(QPointF))); + connect(m_series4, SIGNAL(clicked(QPointF)), this, SLOT(seriesClicked(QPointF))); + connect(m_series5, SIGNAL(clicked(QPointF)), this, SLOT(seriesClicked(QPointF))); + connect(m_series6, SIGNAL(clicked(QPointF)), this, SLOT(seriesClicked(QPointF))); + connect(m_series7, SIGNAL(clicked(QPointF)), this, SLOT(seriesClicked(QPointF))); + connect(m_series1, SIGNAL(hovered(QPointF, bool)), this, SLOT(seriesHovered(QPointF, bool))); + connect(m_series2, SIGNAL(hovered(QPointF, bool)), this, SLOT(seriesHovered(QPointF, bool))); + connect(m_series3, SIGNAL(hovered(QPointF, bool)), this, SLOT(seriesHovered(QPointF, bool))); + connect(m_series4, SIGNAL(hovered(QPointF, bool)), this, SLOT(seriesHovered(QPointF, bool))); + connect(m_series5, SIGNAL(hovered(QPointF, bool)), this, SLOT(seriesHovered(QPointF, bool))); + connect(m_series6, SIGNAL(hovered(QPointF, bool)), this, SLOT(seriesHovered(QPointF, bool))); + connect(m_series7, SIGNAL(hovered(QPointF, bool)), this, SLOT(seriesHovered(QPointF, bool))); + + m_chart->setTitle("Use arrow keys to scroll and +/- to zoom"); + m_chart->setAnimationOptions(m_animationOptions); + //m_chart->legend()->setVisible(false); + m_chart->setAcceptHoverEvents(true); +} + +void MainWindow::setAngularAxis(MainWindow::AxisMode mode) +{ + if (m_angularAxis) { + m_chart->removeAxis(m_angularAxis); + delete m_angularAxis; + m_angularAxis = 0; + } + + m_angularAxisMode = mode; + + switch (m_angularAxisMode) { + case AxisModeNone: + return; + case AxisModeValue: + m_angularAxis = new QValueAxis(); + static_cast(m_angularAxis)->setTickCount(m_angularTickCount); + static_cast(m_angularAxis)->setLabelFormat(m_labelFormat); + break; + case AxisModeLogValue: + m_angularAxis = new QLogValueAxis(); + static_cast(m_angularAxis)->setBase(m_base); + static_cast(m_angularAxis)->setLabelFormat(m_labelFormat); + break; + case AxisModeDateTime: + m_angularAxis = new QDateTimeAxis(); + static_cast(m_angularAxis)->setTickCount(m_angularTickCount); + static_cast(m_angularAxis)->setFormat(m_dateFormat); + break; + case AxisModeCategory: + m_angularAxis = new QCategoryAxis(); + applyCategories(); + break; + default: + qWarning() << "Unsupported AxisMode"; + break; + } + + m_angularAxis->setLabelsAngle(m_labelsAngle); + m_angularAxis->setLabelsFont(m_currentLabelFont); + m_angularAxis->setLabelsBrush(*m_labelBrush); + m_angularAxis->setLabelsPen(*m_labelPen); + m_angularAxis->setLabelsVisible(m_labelsVisible); + m_angularAxis->setShadesBrush(*m_angularShadesBrush); + m_angularAxis->setShadesPen(*m_angularShadesPen); + m_angularAxis->setShadesVisible(m_angularShadesVisible); + m_angularAxis->setTitleFont(m_currentTitleFont); + m_angularAxis->setTitleBrush(*m_titleBrush); + m_angularAxis->setTitlePen(*m_titlePen); + m_angularAxis->setTitleVisible(m_titleVisible); + m_angularAxis->setTitleText(m_angularTitle); + m_angularAxis->setGridLinePen(*m_gridPen); + m_angularAxis->setGridLineVisible(m_gridVisible); + m_angularAxis->setLinePen(*m_arrowPen); + m_angularAxis->setLineVisible(m_arrowVisible); + + m_chart->addAxis(m_angularAxis, QPolarChart::PolarOrientationAngular); + + m_series1->attachAxis(m_angularAxis); + m_series2->attachAxis(m_angularAxis); + m_series3->attachAxis(m_angularAxis); + m_series4->attachAxis(m_angularAxis); + m_series5->attachAxis(m_angularAxis); + m_series6->attachAxis(m_angularAxis); + m_series7->attachAxis(m_angularAxis); + + applyRanges(); + + //connect(m_angularAxis, SIGNAL(rangeChanged(qreal, qreal)), this, SLOT(angularRangeChanged(qreal, qreal))); +} + +void MainWindow::setRadialAxis(MainWindow::AxisMode mode) +{ + if (m_radialAxis) { + m_chart->removeAxis(m_radialAxis); + delete m_radialAxis; + m_radialAxis = 0; + } + + m_radialAxisMode = mode; + + switch (m_radialAxisMode) { + case AxisModeNone: + return; + case AxisModeValue: + m_radialAxis = new QValueAxis(); + static_cast(m_radialAxis)->setTickCount(m_radialTickCount); + static_cast(m_radialAxis)->setLabelFormat(m_labelFormat); + break; + case AxisModeLogValue: + m_radialAxis = new QLogValueAxis(); + static_cast(m_radialAxis)->setBase(m_base); + static_cast(m_radialAxis)->setLabelFormat(m_labelFormat); + break; + case AxisModeDateTime: + m_radialAxis = new QDateTimeAxis(); + static_cast(m_radialAxis)->setTickCount(m_radialTickCount); + static_cast(m_radialAxis)->setFormat(m_dateFormat); + break; + case AxisModeCategory: + m_radialAxis = new QCategoryAxis(); + applyCategories(); + break; + default: + qWarning() << "Unsupported AxisMode"; + break; + } + + m_radialAxis->setLabelsAngle(m_labelsAngle); + m_radialAxis->setLabelsFont(m_currentLabelFont); + m_radialAxis->setLabelsBrush(*m_labelBrush); + m_radialAxis->setLabelsPen(*m_labelPen); + m_radialAxis->setLabelsVisible(m_labelsVisible); + m_radialAxis->setShadesBrush(*m_radialShadesBrush); + m_radialAxis->setShadesPen(*m_radialShadesPen); + m_radialAxis->setShadesVisible(m_radialShadesVisible); + m_radialAxis->setTitleFont(m_currentTitleFont); + m_radialAxis->setTitleBrush(*m_titleBrush); + m_radialAxis->setTitlePen(*m_titlePen); + m_radialAxis->setTitleVisible(m_titleVisible); + m_radialAxis->setTitleText(m_radialTitle); + m_radialAxis->setGridLinePen(*m_gridPen); + m_radialAxis->setGridLineVisible(m_gridVisible); + m_radialAxis->setLinePen(*m_arrowPen); + m_radialAxis->setLineVisible(m_arrowVisible); + + m_chart->addAxis(m_radialAxis, QPolarChart::PolarOrientationRadial); + + m_series1->attachAxis(m_radialAxis); + m_series2->attachAxis(m_radialAxis); + m_series3->attachAxis(m_radialAxis); + m_series4->attachAxis(m_radialAxis); + m_series5->attachAxis(m_radialAxis); + m_series6->attachAxis(m_radialAxis); + m_series7->attachAxis(m_radialAxis); + + applyRanges(); + + series1CheckBoxChecked(); + series2CheckBoxChecked(); + series3CheckBoxChecked(); + series4CheckBoxChecked(); + series5CheckBoxChecked(); + series6CheckBoxChecked(); + series7CheckBoxChecked(); + + //connect(m_radialAxis, SIGNAL(rangeChanged(qreal, qreal)), this, SLOT(radialRangeChanged(qreal, qreal))); +} + +void MainWindow::applyRanges() +{ + if (ui->niceNumbersCheckBox->isChecked()) { + if (m_angularAxisMode == AxisModeValue) { + static_cast(m_angularAxis)->applyNiceNumbers(); + m_angularMin = static_cast(m_angularAxis)->min(); + m_angularMax = static_cast(m_angularAxis)->max(); + m_angularTickCount = static_cast(m_angularAxis)->tickCount(); + } + if (m_radialAxisMode == AxisModeValue) { + static_cast(m_radialAxis)->applyNiceNumbers(); + m_radialMin = static_cast(m_radialAxis)->min(); + m_radialMax = static_cast(m_radialAxis)->max(); + m_radialTickCount = static_cast(m_radialAxis)->tickCount(); + } + } + + if (m_angularAxis) + m_angularAxis->setRange(m_angularMin, m_angularMax); + if (m_radialAxis) + m_radialAxis->setRange(m_radialMin, m_radialMax); +} + +void MainWindow::angularTicksChanged(int value) +{ + m_angularTickCount = value; + if (m_angularAxisMode == AxisModeValue) + static_cast(m_angularAxis)->setTickCount(m_angularTickCount); + else if (m_angularAxisMode == AxisModeDateTime) + static_cast(m_angularAxis)->setTickCount(m_angularTickCount); +} + +void MainWindow::radialTicksChanged(int value) +{ + m_radialTickCount = value; + if (m_radialAxisMode == AxisModeValue) + static_cast(m_radialAxis)->setTickCount(m_radialTickCount); + else if (m_radialAxisMode == AxisModeDateTime) + static_cast(m_radialAxis)->setTickCount(m_radialTickCount); +} + +void MainWindow::anglesChanged(int value) +{ + m_labelsAngle = value; + m_radialAxis->setLabelsAngle(m_labelsAngle); + m_angularAxis->setLabelsAngle(m_labelsAngle); +} + +void MainWindow::angularMinChanged(double value) +{ + m_angularMin = value; + if (m_angularAxisMode != AxisModeDateTime) { + m_angularAxis->setMin(m_angularMin); + } else { + QDateTime dateTime; + dateTime.setMSecsSinceEpoch(qint64(m_angularMin)); + m_angularAxis->setMin(dateTime); + } +} + +void MainWindow::angularMaxChanged(double value) +{ + m_angularMax = value; + if (m_angularAxisMode != AxisModeDateTime) { + m_angularAxis->setMax(m_angularMax); + } else { + QDateTime dateTime; + dateTime.setMSecsSinceEpoch(qint64(m_angularMax)); + m_angularAxis->setMax(dateTime); + } +} + +void MainWindow::radialMinChanged(double value) +{ + m_radialMin = value; + if (m_radialAxisMode != AxisModeDateTime) { + m_radialAxis->setMin(m_radialMin); + } else { + QDateTime dateTime; + dateTime.setMSecsSinceEpoch(qint64(m_radialMin)); + m_radialAxis->setMin(dateTime); + } +} + +void MainWindow::radialMaxChanged(double value) +{ + m_radialMax = value; + if (m_radialAxisMode != AxisModeDateTime) { + m_radialAxis->setMax(m_radialMax); + } else { + QDateTime dateTime; + dateTime.setMSecsSinceEpoch(qint64(m_radialMax)); + m_radialAxis->setMax(dateTime); + } +} + +void MainWindow::angularShadesIndexChanged(int index) +{ + delete m_angularShadesBrush; + delete m_angularShadesPen; + + switch (index) { + case 0: + m_angularShadesBrush = new QBrush(Qt::NoBrush); + m_angularShadesPen = new QPen(Qt::NoPen); + m_angularShadesVisible = false; + break; + case 1: + m_angularShadesBrush = new QBrush(Qt::lightGray); + m_angularShadesPen = new QPen(Qt::NoPen); + m_angularShadesVisible = true; + break; + case 2: + m_angularShadesBrush = new QBrush(Qt::yellow); + m_angularShadesPen = new QPen(Qt::DotLine); + m_angularShadesPen->setWidth(2); + m_angularShadesVisible = true; + break; + default: + break; + } + + m_angularAxis->setShadesBrush(*m_angularShadesBrush); + m_angularAxis->setShadesPen(*m_angularShadesPen); + m_angularAxis->setShadesVisible(m_angularShadesVisible); +} + +void MainWindow::radialShadesIndexChanged(int index) +{ + delete m_radialShadesBrush; + delete m_radialShadesPen; + + switch (index) { + case 0: + m_radialShadesBrush = new QBrush(Qt::NoBrush); + m_radialShadesPen = new QPen(Qt::NoPen); + m_radialShadesVisible = false; + break; + case 1: + m_radialShadesBrush = new QBrush(Qt::green); + m_radialShadesPen = new QPen(Qt::NoPen); + m_radialShadesVisible = true; + break; + case 2: + m_radialShadesBrush = new QBrush(Qt::blue); + m_radialShadesPen = new QPen(Qt::DotLine); + m_radialShadesPen->setWidth(2); + m_radialShadesVisible = true; + break; + default: + break; + } + + m_radialAxis->setShadesBrush(*m_radialShadesBrush); + m_radialAxis->setShadesPen(*m_radialShadesPen); + m_radialAxis->setShadesVisible(m_radialShadesVisible); +} + +void MainWindow::labelFormatEdited(const QString &text) +{ + m_labelFormat = text; + if (m_angularAxisMode == AxisModeValue) + static_cast(m_angularAxis)->setLabelFormat(m_labelFormat); + else if (m_angularAxisMode == AxisModeLogValue) + static_cast(m_angularAxis)->setLabelFormat(m_labelFormat); + + if (m_radialAxisMode == AxisModeValue) + static_cast(m_radialAxis)->setLabelFormat(m_labelFormat); + else if (m_radialAxisMode == AxisModeLogValue) + static_cast(m_radialAxis)->setLabelFormat(m_labelFormat); +} + +void MainWindow::labelFontChanged(const QFont &font) +{ + m_currentLabelFont = font; + m_currentLabelFont.setPixelSize(ui->labelFontSizeSpin->value()); + m_angularAxis->setLabelsFont(m_currentLabelFont); + m_radialAxis->setLabelsFont(m_currentLabelFont); +} + +void MainWindow::labelFontSizeChanged(int value) +{ + m_currentLabelFont = ui->labelFontComboBox->currentFont(); + m_currentLabelFont.setPixelSize(value); + m_angularAxis->setLabelsFont(m_currentLabelFont); + m_radialAxis->setLabelsFont(m_currentLabelFont); +} + +void MainWindow::animationIndexChanged(int index) +{ + switch (index) { + case 0: + m_animationOptions = QChart::NoAnimation; + break; + case 1: + m_animationOptions = QChart::SeriesAnimations; + break; + case 2: + m_animationOptions = QChart::GridAxisAnimations; + break; + case 3: + m_animationOptions = QChart::AllAnimations; + break; + default: + break; + } + + m_chart->setAnimationOptions(m_animationOptions); +} + +void MainWindow::labelsIndexChanged(int index) +{ + delete m_labelBrush; + delete m_labelPen; + + switch (index) { + case 0: + m_labelBrush = new QBrush(Qt::NoBrush); + m_labelPen = new QPen(Qt::NoPen); + m_labelsVisible = false; + break; + case 1: + m_labelBrush = new QBrush(Qt::black); + m_labelPen = new QPen(Qt::NoPen); + m_labelsVisible = true; + break; + case 2: + m_labelBrush = new QBrush(Qt::white); + m_labelPen = new QPen(Qt::blue); + m_labelsVisible = true; + break; + default: + break; + } + + m_radialAxis->setLabelsBrush(*m_labelBrush); + m_radialAxis->setLabelsPen(*m_labelPen); + m_radialAxis->setLabelsVisible(m_labelsVisible); + m_angularAxis->setLabelsBrush(*m_labelBrush); + m_angularAxis->setLabelsPen(*m_labelPen); + m_angularAxis->setLabelsVisible(m_labelsVisible); +} + +void MainWindow::titleIndexChanged(int index) +{ + delete m_titleBrush; + delete m_titlePen; + + switch (index) { + case 0: + m_titleBrush = new QBrush(Qt::NoBrush); + m_titlePen = new QPen(Qt::NoPen); + m_titleVisible = false; + m_angularTitle = QString(); + m_radialTitle = QString(); + break; + case 1: + m_titleBrush = new QBrush(Qt::NoBrush); + m_titlePen = new QPen(Qt::NoPen); + m_titleVisible = true; + m_angularTitle = QString(); + m_radialTitle = QString(); + break; + case 2: + m_titleBrush = new QBrush(Qt::NoBrush); + m_titlePen = new QPen(Qt::NoPen); + m_titleVisible = false; + m_angularTitle = QString("Invisible Ang. Title!"); + m_radialTitle = QString("Invisible Rad. Title!"); + break; + case 3: + m_titleBrush = new QBrush(Qt::black); + m_titlePen = new QPen(Qt::NoPen); + m_titleVisible = true; + m_angularTitle = QString("Angular Title"); + m_radialTitle = QString("Radial Title"); + break; + case 4: + m_titleBrush = new QBrush(Qt::white); + m_titlePen = new QPen(Qt::blue); + m_titleVisible = true; + m_angularTitle = QString("Angular Blue Title"); + m_radialTitle = QString("Radial Blue Title"); + break; + default: + break; + } + + m_radialAxis->setTitleBrush(*m_titleBrush); + m_radialAxis->setTitlePen(*m_titlePen); + m_radialAxis->setTitleVisible(m_titleVisible); + m_radialAxis->setTitleText(m_radialTitle); + m_angularAxis->setTitleBrush(*m_titleBrush); + m_angularAxis->setTitlePen(*m_titlePen); + m_angularAxis->setTitleVisible(m_titleVisible); + m_angularAxis->setTitleText(m_angularTitle); +} + +void MainWindow::titleFontChanged(const QFont &font) +{ + m_currentTitleFont = font; + m_currentTitleFont.setPixelSize(ui->titleFontSizeSpin->value()); + m_angularAxis->setTitleFont(m_currentTitleFont); + m_radialAxis->setTitleFont(m_currentTitleFont); +} + +void MainWindow::titleFontSizeChanged(int value) +{ + m_currentTitleFont = ui->titleFontComboBox->currentFont(); + m_currentTitleFont.setPixelSize(value); + m_angularAxis->setTitleFont(m_currentTitleFont); + m_radialAxis->setTitleFont(m_currentTitleFont); +} + +void MainWindow::gridIndexChanged(int index) +{ + delete m_gridPen; + + switch (index) { + case 0: + m_gridPen = new QPen(Qt::NoPen); + m_gridVisible = false; + break; + case 1: + m_gridPen = new QPen(Qt::black); + m_gridVisible = true; + break; + case 2: + m_gridPen = new QPen(Qt::red); + m_gridPen->setStyle(Qt::DashDotLine); + m_gridPen->setWidth(3); + m_gridVisible = true; + break; + default: + break; + } + + m_angularAxis->setGridLinePen(*m_gridPen); + m_angularAxis->setGridLineVisible(m_gridVisible); + m_radialAxis->setGridLinePen(*m_gridPen); + m_radialAxis->setGridLineVisible(m_gridVisible); +} + +void MainWindow::arrowIndexChanged(int index) +{ + delete m_arrowPen; + + switch (index) { + case 0: + m_arrowPen = new QPen(Qt::NoPen); + m_arrowVisible = false; + break; + case 1: + m_arrowPen = new QPen(Qt::black); + m_arrowVisible = true; + break; + case 2: + m_arrowPen = new QPen(Qt::red); + m_arrowPen->setStyle(Qt::DashDotLine); + m_arrowPen->setWidth(3); + m_arrowVisible = true; + break; + default: + break; + } + + m_angularAxis->setLinePen(*m_arrowPen); + m_angularAxis->setLineVisible(m_arrowVisible); + m_radialAxis->setLinePen(*m_arrowPen); + m_radialAxis->setLineVisible(m_arrowVisible); +} + +void MainWindow::angularRangeChanged(qreal min, qreal max) +{ + if (!qFuzzyCompare(ui->angularMinSpin->value(), min)) + ui->angularMinSpin->setValue(min); + if (!qFuzzyCompare(ui->angularMaxSpin->value(), max)) + ui->angularMaxSpin->setValue(max); +} + +void MainWindow::radialRangeChanged(qreal min, qreal max) +{ + if (!qFuzzyCompare(ui->radialMinSpin->value(), min)) + ui->radialMinSpin->setValue(min); + if (!qFuzzyCompare(ui->radialMaxSpin->value(), max)) + ui->radialMaxSpin->setValue(max); +} + +void MainWindow::angularAxisIndexChanged(int index) +{ + switch (index) { + case 0: + setAngularAxis(AxisModeNone); + break; + case 1: + setAngularAxis(AxisModeValue); + break; + case 2: + setAngularAxis(AxisModeLogValue); + break; + case 3: + setAngularAxis(AxisModeDateTime); + break; + case 4: + setAngularAxis(AxisModeCategory); + break; + default: + qWarning("Invalid Index!"); + } +} + +void MainWindow::radialAxisIndexChanged(int index) +{ + switch (index) { + case 0: + setRadialAxis(AxisModeNone); + break; + case 1: + setRadialAxis(AxisModeValue); + break; + case 2: + setRadialAxis(AxisModeLogValue); + break; + case 3: + setRadialAxis(AxisModeDateTime); + break; + case 4: + setRadialAxis(AxisModeCategory); + break; + default: + qWarning("Invalid Index!"); + } +} + +void MainWindow::logBaseChanged(double value) +{ + m_base = value; + if (m_angularAxisMode == AxisModeLogValue) + static_cast(m_angularAxis)->setBase(m_base); + if (m_radialAxisMode == AxisModeLogValue) + static_cast(m_radialAxis)->setBase(m_base); +} + +void MainWindow::niceNumbersChecked() +{ + if (ui->niceNumbersCheckBox->isChecked()) + applyRanges(); +} + +void MainWindow::dateFormatEdited(const QString &text) +{ + m_dateFormat = text; + if (m_angularAxisMode == AxisModeDateTime) + static_cast(m_angularAxis)->setFormat(m_dateFormat); + if (m_radialAxisMode == AxisModeDateTime) + static_cast(m_radialAxis)->setFormat(m_dateFormat); +} + +void MainWindow::moreCategoriesChecked() +{ + applyCategories(); + m_moreCategories = ui->moreCategoriesCheckBox->isChecked(); +} + +void MainWindow::series1CheckBoxChecked() +{ + if (ui->series1checkBox->isChecked()) + m_series1->setVisible(true); + else + m_series1->setVisible(false); +} + +void MainWindow::series2CheckBoxChecked() +{ + if (ui->series2checkBox->isChecked()) + m_series2->setVisible(true); + else + m_series2->setVisible(false); +} + +void MainWindow::series3CheckBoxChecked() +{ + if (ui->series3checkBox->isChecked()) + m_series3->setVisible(true); + else + m_series3->setVisible(false); +} + +void MainWindow::series4CheckBoxChecked() +{ + if (ui->series4checkBox->isChecked()) + m_series4->setVisible(true); + else + m_series4->setVisible(false); +} + +void MainWindow::series5CheckBoxChecked() +{ + if (ui->series5checkBox->isChecked()) + m_series5->setVisible(true); + else + m_series5->setVisible(false); +} + +void MainWindow::series6CheckBoxChecked() +{ + if (ui->series6checkBox->isChecked()) + m_series6->setVisible(true); + else + m_series6->setVisible(false); +} + +void MainWindow::series7CheckBoxChecked() +{ + if (ui->series7checkBox->isChecked()) + m_series7->setVisible(true); + else + m_series7->setVisible(false); +} + +void MainWindow::themeIndexChanged(int index) +{ + m_chart->setTheme(QChart::ChartTheme(index)); +} + +void MainWindow::seriesHovered(QPointF point, bool state) +{ + QAbstractSeries *series = qobject_cast(sender()); + if (series) { + if (state) { + QString str("'%3' - %1 x %2"); + ui->hoverLabel->setText(str.arg(point.x()).arg(point.y()).arg(series->name())); + } else { + ui->hoverLabel->setText("No hover"); + } + } else { + qDebug() << "seriesHovered - invalid sender!"; + } +} + +void MainWindow::seriesClicked(const QPointF &point) +{ + QAbstractSeries *series = qobject_cast(sender()); + if (series) { + QString str("'%3' clicked at: %1 x %2"); + m_angularTitle = str.arg(point.x()).arg(point.y()).arg(series->name()); + m_angularAxis->setTitleText(m_angularTitle); + } else { + qDebug() << "seriesClicked - invalid sender!"; + } +} + +void MainWindow::applyCategories() +{ + // Basic layout is three categories, extended has five + if (m_angularAxisMode == AxisModeCategory) { + QCategoryAxis *angCatAxis = static_cast(m_angularAxis); + if (angCatAxis->count() == 0) { + angCatAxis->setStartValue(4000); + angCatAxis->append("Category A", 7000); + angCatAxis->append("Category B", 12000); + angCatAxis->append("Category C", 17000); + } + if (angCatAxis->count() == 3 && ui->moreCategoriesCheckBox->isChecked()) { + angCatAxis->setStartValue(1000); + angCatAxis->replaceLabel("Category A", "Cat A"); + angCatAxis->replaceLabel("Category B", "Cat B"); + angCatAxis->replaceLabel("Category C", "Cat C"); + angCatAxis->append("Cat D", 22000); + angCatAxis->append("Cat E", 28000); + } else if (angCatAxis->count() == 5 && !ui->moreCategoriesCheckBox->isChecked()) { + angCatAxis->setStartValue(4000); + angCatAxis->replaceLabel("Cat A", "Category A"); + angCatAxis->replaceLabel("Cat B", "Category B"); + angCatAxis->replaceLabel("Cat C", "Category C"); + angCatAxis->remove("Cat D"); + angCatAxis->remove("Cat E"); + } + } + + if (m_radialAxisMode == AxisModeCategory) { + QCategoryAxis *radCatAxis = static_cast(m_radialAxis); + if (radCatAxis->count() == 0) { + radCatAxis->setStartValue(2000); + radCatAxis->append("Category 1", 4000); + radCatAxis->append("Category 2", 9000); + radCatAxis->append("Category 3", 14000); + } + if (radCatAxis->count() == 3 && ui->moreCategoriesCheckBox->isChecked()) { + radCatAxis->setStartValue(1000); + radCatAxis->replaceLabel("Category 1", "Cat 1"); + radCatAxis->replaceLabel("Category 2", "Cat 2"); + radCatAxis->replaceLabel("Category 3", "Cat 3"); + radCatAxis->append("Cat 4", 16500); + radCatAxis->append("Cat 5", 19000); + } else if (radCatAxis->count() == 5 && !ui->moreCategoriesCheckBox->isChecked()) { + radCatAxis->setStartValue(2000); + radCatAxis->replaceLabel("Cat 1", "Category 1"); + radCatAxis->replaceLabel("Cat 2", "Category 2"); + radCatAxis->replaceLabel("Cat 3", "Category 3"); + radCatAxis->remove("Cat 4"); + radCatAxis->remove("Cat 5"); + } + } +} diff --git a/tests/polarcharttest/mainwindow.h b/tests/polarcharttest/mainwindow.h new file mode 100644 index 0000000..94aa7d8 --- /dev/null +++ b/tests/polarcharttest/mainwindow.h @@ -0,0 +1,155 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc +** All rights reserved. +** For any questions to Digia, please use contact form at http://qt.digia.com +** +** This file is part of the Qt Commercial Charts Add-on. +** +** $QT_BEGIN_LICENSE$ +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. +** +** If you have questions regarding the use of this file, please use +** contact form at http://qt.digia.com +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Ui { +class MainWindow; +} + +QTCOMMERCIALCHART_USE_NAMESPACE + +class QBrush; +class QPen; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + +public slots: + void angularTicksChanged(int value); + void radialTicksChanged(int value); + void anglesChanged(int value); + void angularMinChanged(double value); + void angularMaxChanged(double value); + void radialMinChanged(double value); + void radialMaxChanged(double value); + void angularShadesIndexChanged(int index); + void radialShadesIndexChanged(int index); + void labelFormatEdited(const QString &text); + void labelFontChanged(const QFont &font); + void labelFontSizeChanged(int value); + void animationIndexChanged(int index); + void labelsIndexChanged(int index); + void titleIndexChanged(int index); + void titleFontChanged(const QFont &font); + void titleFontSizeChanged(int value); + void gridIndexChanged(int index); + void arrowIndexChanged(int index); + void angularRangeChanged(qreal min, qreal max); + void radialRangeChanged(qreal min, qreal max); + void angularAxisIndexChanged(int index); + void radialAxisIndexChanged(int index); + void logBaseChanged(double value); + void niceNumbersChecked(); + void dateFormatEdited(const QString &text); + void moreCategoriesChecked(); + void series1CheckBoxChecked(); + void series2CheckBoxChecked(); + void series3CheckBoxChecked(); + void series4CheckBoxChecked(); + void series5CheckBoxChecked(); + void series6CheckBoxChecked(); + void series7CheckBoxChecked(); + void themeIndexChanged(int index); + void seriesHovered(QPointF point, bool state); + void seriesClicked(const QPointF &point); + +private: + enum AxisMode { + AxisModeNone, + AxisModeValue, + AxisModeLogValue, + AxisModeDateTime, + AxisModeCategory + }; + + void initXYValueChart(); + void setAngularAxis(AxisMode mode); + void setRadialAxis(AxisMode mode); + + void applyRanges(); + void applyCategories(); + + Ui::MainWindow *ui; + + int m_angularTickCount; + int m_radialTickCount; + qreal m_labelsAngle; + qreal m_angularMin; + qreal m_angularMax; + qreal m_radialMin; + qreal m_radialMax; + bool m_angularShadesVisible; + bool m_radialShadesVisible; + bool m_labelsVisible; + bool m_titleVisible; + bool m_gridVisible; + bool m_arrowVisible; + QBrush *m_angularShadesBrush; + QBrush *m_radialShadesBrush; + QBrush *m_labelBrush; + QBrush *m_titleBrush; + QPen *m_angularShadesPen; + QPen *m_radialShadesPen; + QPen *m_labelPen; + QPen *m_titlePen; + QPen *m_gridPen; + QPen *m_arrowPen; + QString m_labelFormat; + QFont m_currentLabelFont; + QFont m_currentTitleFont; + QChart::AnimationOptions m_animationOptions; + QString m_angularTitle; + QString m_radialTitle; + qreal m_base; + QString m_dateFormat; + + QPolarChart *m_chart; + QAbstractAxis *m_angularAxis; + QAbstractAxis *m_radialAxis; + AxisMode m_angularAxisMode; + AxisMode m_radialAxisMode; + bool m_moreCategories; + + QScatterSeries *m_series1; + QLineSeries *m_series2; + QLineSeries *m_series3; + QLineSeries *m_series4; + QAreaSeries *m_series5; + QSplineSeries *m_series6; + QScatterSeries *m_series7; +}; + +#endif // MAINWINDOW_H diff --git a/tests/polarcharttest/mainwindow.ui b/tests/polarcharttest/mainwindow.ui new file mode 100644 index 0000000..79cf379 --- /dev/null +++ b/tests/polarcharttest/mainwindow.ui @@ -0,0 +1,929 @@ + + + MainWindow + + + + 0 + 0 + 1207 + 905 + + + + MainWindow + + + + + + + + + + + 0 + 0 + + + + + 200 + 0 + + + + Settings + + + + + 110 + 90 + 71 + 22 + + + + + + + 10 + 90 + 101 + 16 + + + + Radial Tick count + + + + + + 10 + 120 + 101 + 16 + + + + Angular Tick count + + + + + + 110 + 120 + 71 + 22 + + + + + + + 110 + 150 + 71 + 22 + + + + -9999 + + + 9999 + + + 5 + + + + + + 10 + 150 + 101 + 16 + + + + Label angles + + + + + + 10 + 180 + 101 + 16 + + + + Angular min + + + + + + 10 + 210 + 101 + 16 + + + + Angular max + + + + + + 90 + 180 + 91 + 22 + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + 10.000000000000000 + + + + + + 90 + 210 + 91 + 22 + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + 10.000000000000000 + + + + + + 90 + 270 + 91 + 22 + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + 10.000000000000000 + + + + + + 90 + 240 + 91 + 22 + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + 10.000000000000000 + + + + + + 10 + 270 + 101 + 16 + + + + Radial max + + + + + + 10 + 240 + 101 + 16 + + + + Radial min + + + + + + 10 + 300 + 171 + 22 + + + + + No angular shades + + + + + Gray angular shades + + + + + Yellow ang. shades + custom pen + + + + + + + 10 + 330 + 171 + 22 + + + + + No radial shades + + + + + Green radial shades + + + + + Blue rad. shades + custom pen + + + + + + + 10 + 360 + 101 + 16 + + + + Label format + + + + + + 100 + 360 + 81 + 20 + + + + + + + 10 + 390 + 101 + 16 + + + + Label font size + + + + + + 10 + 420 + 171 + 22 + + + + + + + 100 + 390 + 81 + 22 + + + + -100000 + + + 100000 + + + + + + 10 + 480 + 171 + 22 + + + + + No animations + + + + + Series animation + + + + + Grid animation + + + + + All animations + + + + + + + 10 + 450 + 171 + 22 + + + + 1 + + + + No labels + + + + + Black label + + + + + White label + blue pen + + + + + + + 100 + 510 + 81 + 22 + + + + -100000 + + + 100000 + + + + + + 10 + 570 + 171 + 22 + + + + + 0 + 0 + + + + 3 + + + + Invisible empty title + + + + + Visible empty title + + + + + Invisible title + + + + + Black title + + + + + White title + blue pen + + + + + + + 10 + 540 + 171 + 22 + + + + + + + 10 + 510 + 101 + 16 + + + + Title font size + + + + + + 10 + 600 + 171 + 22 + + + + + 0 + 0 + + + + 1 + + + + Invisible grid + + + + + Black grid + + + + + Custom grid pen + + + + + + + 10 + 630 + 171 + 22 + + + + + 0 + 0 + + + + 1 + + + + Invisible arrow + + + + + Black arrow + + + + + Custom arrow pen + + + + + + + 10 + 20 + 171 + 22 + + + + 1 + + + + No Angular Axis + + + + + Angular Value Axis + + + + + Angular Log Axis + + + + + Angular DateTime Axis + + + + + Angular Category Axis + + + + + + + 10 + 50 + 171 + 22 + + + + 1 + + + + No Radial Axis + + + + + Radial Value Axis + + + + + Radial Log Axis + + + + + Radial DateTime Axis + + + + + Radial Category Axis + + + + + + + 10 + 660 + 101 + 16 + + + + Log Base + + + + + + 90 + 660 + 91 + 22 + + + + 5 + + + -999999999.000000000000000 + + + 999999999.000000000000000 + + + 8.000000000000000 + + + + + + 10 + 690 + 91 + 16 + + + + Nice Numbers + + + + + + 100 + 710 + 81 + 20 + + + + + + + 10 + 710 + 101 + 16 + + + + DateTime format + + + + + + 100 + 690 + 141 + 16 + + + + More Categories + + + + + + 10 + 730 + 31 + 16 + + + + 1 + + + + + + 40 + 730 + 31 + 16 + + + + 2 + + + + + + 70 + 730 + 31 + 16 + + + + 3 + + + + + + 10 + 750 + 31 + 16 + + + + 4 + + + + + + 40 + 750 + 31 + 16 + + + + 5 + + + + + + 70 + 750 + 31 + 16 + + + + 6 + + + + + + 100 + 740 + 31 + 16 + + + + 7 + + + + + + 10 + 770 + 171 + 22 + + + + + 0 + 0 + + + + 0 + + + + Theme: Light + + + + + Theme: Blue Cerulean + + + + + Theme: Dark + + + + + Theme: Brown Sand + + + + + Theme: Blue Ncs + + + + + Theme: High Contrast + + + + + Theme: Blue Icy + + + + + + + 10 + 800 + 171 + 16 + + + + Hover coordinates here! + + + + + + + + + + 0 + 0 + 1207 + 21 + + + + + + TopToolBarArea + + + false + + + + + + + + ChartView + QGraphicsView +
      chartview.h
      +
      +
      + + +
      diff --git a/tests/polarcharttest/polarcharttest.pro b/tests/polarcharttest/polarcharttest.pro new file mode 100644 index 0000000..25f4b4e --- /dev/null +++ b/tests/polarcharttest/polarcharttest.pro @@ -0,0 +1,20 @@ +!include( ../tests.pri ) { + error( "Couldn't find the test.pri file!" ) +} + +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = polarcharttest +TEMPLATE = app + + +SOURCES += main.cpp \ + mainwindow.cpp \ + chartview.cpp + +HEADERS += mainwindow.h \ + chartview.h + +FORMS += mainwindow.ui diff --git a/tests/tests.pro b/tests/tests.pro index 068ad6f..f57bd3e 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -7,7 +7,8 @@ SUBDIRS += \ auto \ qmlchartproperties \ qmlchartaxis \ - presenterchart + presenterchart \ + polarcharttest contains(QT_CONFIG, opengl) { SUBDIRS += chartwidgettest \

4M%R-m8XQtKW4lFuW*+RL-5=Q~N* zXQZT0t07SES*s0*ZZ>y3d6-S#6<9w+i;+bE12$y-y08#@jY|YQIe^a`_k~EYtETLl zOskFBS!}${qo`XO*5M2~X=R|!HQl!gb{Wgw=`r9AyS)*%w#`a-QVg!|3F&JEgvTB- zb)|3B$%3jfhFGpL@G@n^1#&793X13ydtmqa(|zI`xw!K9{Crq;Hfj0?CzwjjqhP!P z*?;S=>wL3c*}wp0Sf$uUT>VY5+`F}bdu)~x6!Py9=lKjT_}b)dYENFQDeODDyD@To z>zB}|?dpy7nWc0j&?kK;7-pB&=s zK8)%8()vyYqqnx}{kz3xE11?A(qO58E8Cw8cJAV`tlc$k^1~OTY5J#SLPqw#kc7}c zj1Z6zPDT$u%YYBI(_epb3-H;JGgD4d1d5whsd93nTJ+m)X$^cXZ@t^~sCUG>DS$0PMUGL0wg6BWa(`S*i{> zRa5bkpsCGw`66#e@mvqOe%ry!XGF95e1Msy>2g197c5TP2@0i{29;NCbRFe z`hMPIpjE8#i>AYpU5~>LwAJ(UDRSnk*VW{bQA#B<+)g`4o12?tsL?_ss3IyPsBrJl zUMpNpjgl(bv7b-_5lQ+$D+yF|E5qm|-RE#xOBh($KbYb>)H^dJB?UI!aBBIqH>OYV z#WqBtP`Ypmlu(6(hevqV7|oS`&LfSA+Q2IOx%`Eo2VoB@JAG&TYIJNnt&My{=hKu_ z5t0!}E^X^O^k7cELv&aHhfUna}l3~+7ExbxZhjR9v8=hre}Q#fvzIWX9*q z>5w}^bYT5hJ$~Lf1)_Y?gtJBeuu+~VyJ%;B4q*#}F1mE5U9)?coUVuKYCYf^F%Y2p z`uak1LIEmI9oczq3X3xnoi^zL@OXv4WB!N3?Sxf!G6E}?> z9tV{{vLl#&cy^RCtilD#A0lA-3^rfb@D_zBMo^yivPn>LOY9%>%}`LH$VIt4yIQ44 zN^L2dVcZdh%4a)2}#w%cgRs#P@)$tOtX9Mg)>mb<~D(M;n85#Lb}!vRfK z?Pc6MS~QR<6QfzvOjAYk$vfrL0;G#FirB{dj#ZPg6&4-N_HpE*XS!19@+I-|(BFGO*kny&*Y z4l3A4sZ_ma#u7@H`E1waYEPM6eiI0x7}$pY;R6Y;yyXJn(`Y&=obgHf%jR4=s(u^V z^F$*jhudX5QM-46%ghaa{8@aE?+D6H8!`y|`KW!fEzCfC!B{DS>b&=aO!_b+cQ#qb+iNv zq;4F1{BCx6%UZnQ;o-YB!SP*oPR_o80XSet*~_*O?EqQ)c7vN!_YyDF?WRn(L-KcwvK zY*J9A{F-yv6#T^GC3QFPdmDJbVomN}c@oL!%?)ll4qO-t-0C~6^8*MFMpdH55F>6w^xMfJ+423M{(qX4qB|F(}LO~ zoJkfH9d}s~iPM_u=J6!6BLJ}`k_5+uJZAZ;S@mBr9 zseL7SELc|9*K=w%Ha6WRXA2#&rkh0$O+blB0h0igwlPk=y3s>!=PP*qyBoYafWZt2 z4lmDWIF}zR>L&_X^-M=cxv~xQ*{+=8JKv68R?+gg>n27Me}B7t8^&PQObH>#W^+EK zZ(+@IH8@6GH*RXdA8m@`1%Bz`(zI?Dk*U4EnvkSPmE^1~x2+d%UM5Lnh?B_|-)+Ku zW(}(kKecp!uD#wC!^#9qkovro9Y|UvuBRsnzx091a`@<*Y{~jaEEEJN{^k z0}}@-ZGQtqiNtNIo=ExtGnzYw2-{@Je2y{!cQQl6$vV|RCztK5;QJB)0q&3fGpkgvE-A4pm* z&+M#{dRr7Pa$&Ju^1bvVdUPBpw;0cXK#vrG0=!BEw-SXzD?RBsEw49s?(M7RPATo5 zLk%Td!6<_%u7#agMIS%XOJ8s8+)IXJb=VFGQVIXQOd?e>a7m?9@RIX+F|7hC=97y{ z0w4*1YBiOiOn>+q8L3>V{=w^pzCVg^F*D<2{eO{B|EuWb@OFK!-Me?{^z|`T$Xom3d2gz(^a7$1(L~0>+KCeRTnBda+|Afm?hW7AbvxGIeUs7 zzot2DE?WOu2+^wG3yS7ZOK=geC?*CJpX8RVl1FwBlriFl6O`2CopVO&66PeFcFuJp zU5ERxc@JmBwGzWvVybXO=1nbmg39QUN`qL#b4a8w3YN*n(mldJ(%e2zcLlSGae>s~ zp_}f1T<)Smhrs^ds`lB*w|cagO6Z5hgaeK6KOcJ-mj{@K)-a}#W{sgky``h0`0!i2YG#8l05}zkbmlyGH1i^^A|7aUvvT zCaYq&Z!L31w=5PMh|#-}%c&Xjlw?cUl^?>AD(XKOXnk&%lNq@{bbH!C7B@w`tI>90 zm;YpHD&w4PMf5JvJyRCd9VmI!yC22D#g?B_{VdGK*$MX}PbXY3TNJ()=p9bt&;Y`m zqk2M2cRF4%dZB~Wl>XLJzYLy0Q0E@$a3@11RuIm*gkQ4UQq`d>AEeujfO3i{4?z@QZJ;yypw9F z)-{^sGae|TGqiPlQqeyZesKO#e}YZ?K-nrzycv(;n^nkE?zTS-KdPX(Je_~Aon(+b zV+q8W{(3breHzMuH#t8(<Jvcsq5$Y{7o_FbmDjGUlQw7OBGmPfHwpCTLrsT-8Wpu!mak@{QZF<*fN@T8&3 z9$~q)9;k`h**@yTSQT7|58dm$4^*EgCfrh>s)DS4_YBtB)*4x(H^3{S#!l;t&AHGI zlVy#4WK2p)PX4Y*Z=6aAh<~YiN>?(}1U)Lbo=^Pfw&U#+h#229PT^lyo z-VgCSycQW-<M|td*OqHg)ag@ z(|z>@ZKso#rN*f#kW3_-+Z; zW3(BOmatls`05I?)rJ4Gyi-oCvx<{hGaybu4|uo*(o zh%S@s?W;sUX^`1C5bpDwUSO`4g9q`!Nvr1`xk-+_kGz5<8eom`yFVG_wJtto>Lq{1 zbc((Sr&Tba5`!=G>Yfag%rZivpS>l1HgoD$FWaI*2?UAU z3)N}Gf$rKOPQC8hWCaJS6^RZ&c(5DspH$@(R{UOHISLgubW;?QDCJ|MV6+P z*8Jo{zRLb#M~M(rPtq!vXOPBEbp3O$md@QfnDQ;ctav&f*_HE+i&Ne6KN+W|PhjLO zGJr)M!Xs_tGCg)csRh@5p@B)XFPU?RFNjrnXXCLPCplp4EF6Lj5RAh5YYW!mq zNZ!u;L1p-hXesK&nO#n%Es2Q9_|v>n!I4Z_1gV^Edr0LYBlYvT;E?FlmWt0txSa6A zQl|u26bavF$fWQG5UG?`uaoLkS=H1AO<94!_T~CMAwwZVyEM4$Fi(Ysewc^Z&kH1- zqsusD&8elWy`YqQO%BN4W>*m6udy*U0Rf-hfJnBqD3aG*=Z#Dz|7RMiw7mdp%(iC< zc;jg$-mk%)E?!o(DnAM^(LdvZKms0jcC5C(YpE{a)qL%Y?m9$G^_z~Gv`uGV?Q+yh zVnFb98K2zUR*l;c+I3AyUd@{wF^sY2@9c6Pk_H`2B?hRrdb5|9+Ejn?(5`T9WC zTyT@L7f6mu@#%e_-BdQHoW07U~?g?@P9_9QOYh?#>!yjHvSvc=EdB_lP} z*wr3dwo*6a4z82j5jCZ|vP z>m`X)(p^I`(BjHBt#npQL4&k22Lu@kh~3P%xG7|53@6E$Ct6zC&t$K?3}u3|lgX_0lh}6W8OJfFSv{>O3onB@Ndigd zW(LbzallB1=P1!eDpP-x&UYp4?VFSqF(BK6fpReKR$CGxSCKOwG_7H+oD$c zk+W2aGc8|&Jh00m{nC+a74^Y@GJSS-O}vri(rre?k4P8!AE1$Owc3 zbaeDLUbK|U6}_9g?NRu3$rY#T&tXiCW+aE|R+#41nOZ>gTJMpo+h5 z)D1Is2y|XpnVCjYxx5~x5iGOgcVuj?Enr9>L{w-pvTZBf-iQY=PqDUr%SP8X2z<3IdZIOWT+p~5(jZz+ zs1lRz2#Z+F+hk3K6zd9g7q!rpgeptYh+=&QSYEJohoE_luDM8(BMTAWR7Zce*- z4-q{^V&e(txxcQ5OXu%2Pa{=Seu2-S&)!7Sdhd9Q zxi$IdTD!s_Po6avKejtZ-d?w810Rvl?9_`!*gE_f2dk&AB*}{F8E@dwYbS!WbJ@2Zc%iN>S@roR7Gx78E!O(2ERG_ zz%t_~RG2nPr1kL9dxEG`J;J;i03GG)jM&NAPOEEb#EMkA*hl#3`T>u_`)Ec$wd6Fm z43x>T@K$UK*7^c1D%=f0zk{Z(ZcBD(pgvT$!OQj7X+zp!$;_#P>$bG&{MwkDuZN!4 ziKNCXHG&1F#*Z0Vz~MLYO$a*i^Rj`mMb~89pRY?}mpgj8MYyJIFk^Xx3S{?apot^u ze7!r-0&&3_q&Ll=jQcj~W}8JhIl1o^cnLB=2?>~8%uCoWK^z)$3WePV2bQbhQeQUv z1;3P8r3?D%BauM(8IJwa+&-c)!f`y`5P$X5@}=!l#*TIOCO+&hll!N;}_RS)ve@V-{b8e z)=wq+L)V~&>hs+(qaW3K2EJvD?J-l<*DJd?Ff*13+NE`NIj!%Ik+Z205@hn305qI> z&W){m081^41OlQAoHP(;&R!{qi-F8dTaL5EBeU%)+Aq*cLU4F}$-Bovth7#wQDXsH zSu3CeNQo&Dxxq%sNXU^!sp_iT86Dzu^`g&)8nADEBrukWum51Q<;FD(qt-m0@X-&yp65-mzxH^Tm^L4uJ6;s67YmfAD}WEWSgU$k zvN;^w6jN7M_5U4X#ohdvPv6zm6$A9iZ(Ejq$#;snca%O`_Mls30p<%?C_lwg^=wr< zakW~%GON|wA9E!@qL7=)QN>%%m?o^};b!0$Uz5JGNuq9S6FR!1e2W#HV_X3HsVy@> zIYtfIpz8B-M=OVd+*6e7o{Six;?40rpgaIyYYAIduOAkz;QXcBZXKP)*WiH$b~p3*iftu&lg}MD^rkbKH(w z)KQz4ld?=yaA3ShJU|zRhm@(2PA%}F87iH_siokW-Wq`eJ_N$QS}!wh_6)oRP9Dc;PzTaS;J`NEPWzz73W$lj{OUUEb%W*O#GdWZHR|%oIn}1H* zLtLAOFUJI#U~v96bAM;O<_kH>>QW0=TS+;_I)n9~pi>Am--EMNv3HYDev;WrM8A1# zIW<1l9%!m3ie!H+!-rJ!DZ1Al!<&z?YtZMCdrxZ-!*S8u8Ii-w{xcxw{SX%6se}z^ zy!?Gp)+;sk-3MAD4U=5jf zUCN%nal^q8!1?3Da$TSyMkBc@5dMA%%=matfymW(4xVAIgI@~dh6_FX1ardYq6Y+E zDETSCpUtZeuh^>4V}<4H)lCK*1p;gJ{3s}lBazPMp@PKyw=60IH8nL!qD&|$DP50n zbl%51pq)?W`v&53;taQBkr%-#V+6@ja@o7!2fwAG!}RX}hF{E-TVab;bVClI(P`5R znv4wjAy$80nVc+xd~~lsB(ok*I%|8hlkt76mLIG^ssGT~k!eR6Z)9nSFm~z67doO4 zJGKivSG#mpgUm6~>(u+rU&-wFZ><94fpT%tdq>Dqv)RA!GZ|cTar&cX%&-`GCF>7e zp31CH!R_Z-bqjXO->yc63`K|Bh#+CuRkgEL{0Jki=`ghjO-`H)979qMZt zy(G|xo7qe;Uj;BjJ3BiMGGb*8Pj|3N7(w^<9xNpAP!OabXo$|WvsoRx<8a=?onF(f z{rS~S_(O~uixb|bMn43pDO)apT7SDn>6Qhu4-_=GkpR59H+V-FeMjl{$zSSiH`zB8 zIYJiL=cQ-@S@a=`akjfyN0YZ1dGJj{|7cXL?}9-bb%G2~d$rp66Q7af<~ig|oSna= zq~PXIAK<@l3^v<_ne^5KjF({X4`353&;dQobZq=;bW~b0vxcg>j6oN z5cCT<-q&xXt~M{fD_g9-DZO3=I2--V6niB-qI6u0*?qxj75MUUf*qcmV$+SD#|Lk@ z#laK4oGzOe!E%#t$P{Vv*@H42U^jgH*F#J}LB(vP4)(_vBflJKIeB>&;2Vv)5>qv8 zsIje9YeqhI-5>d0o-V<6-NaY{@qZ7!ub3_`F99=Bq0sPi^lJE7{<+RSKkjk2$ zXuDrNGP0N+XBu5nDThfK8U^Iwg_D@D|7s-MMq0C5uq8}1()M}@u?jFDjG)&Zp%+Hb z=Tu=Fm_rDZ6)bSvw3h}3bWFuh`u&FAEe_QGfcjU+Af+ zfmbEf)Y#=k)n-Zr3@nrxqxb0bD#jC#>`h2B^|7|SkiD)j1O(XL4)oYsQf11f3_e!DoKfq4QbG0S=2Am z)6)a_1FUs)%hi6#+un`cb_N~Z8{0-ZCn<2jqVIJquJ67M4X6SUi{{^A5f%)~N(QPY zidb+(W82f=Ia>3eB`hENewzP<{J)5Byy)qi{_fZn&;%Uu`Q4=bzC)0a(AvOVXP;ky zrw9vKxJWv6w4EkM#Xm;^fCKGyo#7Fesg}f?i}Rla>KY@QvUZX;@DMa3GCGu{HM8P7 z5*su~HX{90GLHk1T(M543-x-S_`w*N{2!{cTbCY_`_GXTEjyz;tgO8k7Z5=rjpYy9 zUMo(o+D{4x0qiv325haqqN@S#brIE{#!f8kGa@xlbhqyB1>dYG%D=co5&7H}T*#eZ zj_e=^uK}1i^?eU7RZD!eBT)xN`!?LgLI%eNq_o1Ro?Ctr;W=cq+|cOW@d{~9oep-k zJx@!LLm4$Sl_HE3xnPlOArJ1L5M+t>-ON&Prpx~G`_5;4cKpe*WHjzJNgWAcbdQ1> z-;0X@=eo)L4ha3;3}kalS|P!GKW4C`z zxWn$^xqQm1i3;kATgca+gJ|IpYsfknDA7P72Bu zCAbYV;juAE#cIkFiBro{g)tbi|Gco|Hsf&j4&9(is9yP7y?BAm;vC$tZM|$^$$B$u z*c^#N5{g(7O@a(gaA#Ln7sq>*M`^J#WkyA%($Q-*!nR%h(_DRn?fFkx;{PRjuc66F zG(Z$sCk>qZ6wjD^_2+bj?sScMV-oU zM{)}1eJ@@xTTkv}_EhhqgPi0R4p)*1_!-A^EzcobfL#l z_$smIUita5Y&}Wb5KkQ7dn|ZX7X1Q2(oI927K@yz1@8k9Y;yxnhq$qylWpr4-Db-( zI;!Z0wpoEh1AJXi=2&_xldqhZg#A$b?zH^{9R!~);H`~h_@Cp@AREZmFK6T6n9CA%)YISs4f^f$e}Tton7gDI&|<)ShRGB;{Kc~Np}||vX+7q99C?} z^(%5et}~w1)X7U#kqtZLxwQX-!-j)G`W@|5`|ULy)Mnw`LfA?bRpuR5^iocU8)xB$ zG#2Arv_73QT55q&Dy4FAmm6kHg~VQMN?Ka3ddYgzN!y9+pN)g26b$0!S*(-pv|*R4)@wbUu%e(%4LmbNP?)bbkLDDNT|1!mI)~xQ~6HZoyf;E!1r6}MqrgGS)jE#YJ zP?VW`>0~jiJ*lmYKHoH<^s>g2DtMS!K70tXNUhOk0Q6QePK1}$&&1D|B4ja}6!M22Q%C`CXeyveEJ#iWJ8841cy ztI*TS1S?(r`q7?jj9BZK9~S9`PAAPiut2`GXn_X-aRs^xIu(HC>wGWaF%Pw2X+9;Hlnq%^t&sGeo|d< z>pkCyNxP#zZ2N_lC+o)RdD~%Qi!HJ=#IVYF9@4VC6AEru52d|v4hf~U=kxJf{lrR} zw2292nS3;5jLL=vsaoyhh{mAB!PJtHT@T)q15Y}@u{!evfCim;M%s#+A5R{+6BE{{ z1nHUtr zO7u4>g{rg(ZRoL9rmT&_>W{9g(~H@nk}M;Cu;I1-Lg(T2pNOyl5>}gFfwJ$~?4B7? zu693v0dFe3?bmJayGCjrnMJE~G=2o@Hj>T?_3mdGEKT0KK!j`=}sRX0!+m zUhTb1Z~IX#*H@sfvwpvug$gwu%huo~q7c_75%KWOob5i+oQ+Ml~ zpZo+^ht}Q;=;=e*uV3Ym!HbV=vdzmad?8F;PQ#nQyhWt<$Y71`%gMAAdKI=iQyiid zo%&hPXmA92v~|ta>$TRbyX%$%Z~Ynd|FD?8mc2Td&D8SChcV!^2M`dkNdAK=7{fk=yMA z2@w(TYh)xTDjXD-h~{hQ7XUPSoCc2Nmo*5gtYm4a=#Jib=6FNQfAvAt!wahaSd-Fw zHGkpN4j)JUtTp-`IO*As)qcPwdExy*?%Uy-t{9rivybUe)! zM5jPG!q6m;hI4VLQsn_Z<41=+Cg>sC&mkWl8~fgRv0%nhxk%NX;FIB@avTTp#2CQME~T*#t$rO|8dm~v!Gyf+2La50lC83bW8WkxnqDGpTVw9 zIs2W__RipRQc}_!dEc|?Gs<}v=jQw^t6qbh&afMu@uW+$GYt>;BC5b6p(eIK(act4A9h?yz{U#$kgQ^%c+$lb8QS}8CC63pwV!Owj(Wgq#!zbdVGqi%r`_4vNCrmil*o>X~mZZ11sBtu5GZRh~U&~{kcAdAPGo?yuC{Xg*^ z4B@vH{O@Hlc;)p;%i7obd-Z0%@U6_;iX$JM{_I-nTI%6*T$}5vY-BIe29zBl zAtS$sdOKnsFOe(Em_(jI#CLdhcE((*9Z4NtVbg3U2D+jzEU=jPJUy^|FQYACl+NI%dmHxXe5h;`J zUtGN9*l4r3o3B2Qq&OF9>*z$#1Mhp7VrFT38q9$t%9HW#9?TRvV0?&VSNaSmuW+6n zHg@8!wmN=i_(gUeO!;%bs&$1R3%M_`0RngNXAdHwhtsQV= zjRBAV4EG$p*VTmN{nM`C*ze!j{4{Ac*!Jn1Mh&+`nn2(och98K)%|<_#qhAl;n&a* zf@BoI{Oq6Sl@jEo&rXzy1;m#bY^^^M$_`lp1@e&Ya;kno_=58F5A@{130y;DkLf?x zKqoD{zAwz(>J`s;ydJg^wO^C)aG7jFg8}0LZQXnQJBc1l6Oh5%P|fTJjTw!k(2~$N z$m+A$?S{FropmJDC^uyaL{5-&a#mKq%d%hboHb{J5srq|lIvo|wI>FGnYw!7Lrx|z zY5|%2Nd{=z|2i#@zj`CE%Ta9?`@^(8MI7EP=}hfs9%6R{J)me=w)+)rd%Wyrtsb+MrbBRT4+N~l{pYpH#LDjkc`76CM-7w)H1~dGPWV=$IM@uOGAX&ZpvcChx~$k zBu?l1b}$mlUUw7Ig~AF|CU(RNI+o?7a(4OvXv61PZ2&!XvZH;=z~TdhrBZYi8449+ z7Gzx~l&gFg;aNO$(DHOKY%KKm?+5%xR`{-Ljb-wWGfuH7m{;G`*No^S?JAdr4S>S# zC#;#4`(gWiEfy!em%@{u#$N8dcs~s;zU9fFtX+U8uu!J{7HeiE8BL|8xX#P!Gi*k8 zzMQT^r9RmcH#*SZVE7WU`H{wWo|dF!cnG!u9o(VLWPl~Lp6S5nLEN;K&(>lpuSIu6cXT&(S=mZ-fWUU^>Q zKqIkXsW{+eQvIZnX*843%^Cbb|9LuddJKp{-QC?!8%)n#4BIJ7HsDn=Kv_W2)Jas# zUo+}2D10DWc-l(VJal+7v2T~Or&0n4b#>!N;+4MrwJE%6!rgWg419# z>E|g?f2UrwD~+Q_uByl4-u`@Ye7yH=1a{m0zlztPrZ8A!?An+i=5mE23%mlT)xe8a;Wncg>AEJ6r>1?pN% zGM)qu2N!p~(gf^jX*F9Eg3@Xe=3#O#ovI(CYng76uXyD7XZp}ZrD)po;r(~JUQ^V& z*Cys643H-mb1PRgdjr>gt??r0yIJ*LlVeV^$9^-u2=!QdiIIhOQC+Mo7En6&40WqoU)#woebAl{YQydFtpjTc zQhCcoI1crH`>#!#FccLo^@XX0E2Px!m95T$qz!2v-dyy7T zkjlpZ)AKyf`tyi~$&Vw$L55M%n@7cP-;skUaO(D@}84md6n1N~fA;GOp4uM-zYFy8#zh!zChT*)}12KcP_9V)J9+P7?|F za6{1}<>H5*WU$bRr?X)@`X(={!=KvLeZ;t){~_wEOETR*=qf9}B__jO(8yw3A=u5)iaJs2oc($qAE z^4O7z3^ITqUz)ke9*c046%o$lxV$@Gl)!!R$86s)|8Ga_QIv_Ys*Nrs=!{Bths1~r zvP-}xBa>9}l9dr=W*ETm4jGTdm=xf%mV=S)?^d;`1x3)JB2cQ5 zUMW$=CJfkHmS}sGB=<5OrkKjPptp!ao7(n?2_wu_bpD$IYnk&E{**z&s_B)CcvDZ$q;TTJrM2GJ4>zmS!~yw#uPFF>o`}6|oZl#G z+@FbEU4)I*(Rt84Og$X^LH+RXQ|0ilnYkWLER=~tF}J^va$)SD1I!9*l;*1`j^ zlzSA?rgr$bG(1psXbG03GwPVt{25Z|x$YD5`^i^$!GAN9i9uhR*dK`L5eI6!Yz!!% z5@X(D(H9b}7z4rcUf-;~kKOcg6G(@M6&39?)HNOZzKZK3D|RLQz}+@E#~$$WhX;XM ztHh|`TlwfcU1D8Vh@qh&O1+b_vaME5ucN?!We6fVG~FR+P`d zE#Kx)dX`qPy-%6R|88YoT3A?|-B`SgGNnMYSCuvMr#Rll*=$lUPizcjg}CPm$;DLE z)p_cdwV(`lKwG}iJK9WYaiqrWNHWXtQX{_6EKOELmq6kdbz6th!ql4RgT{*%8C=gN zpQ4Le*ob&b6Nkjq#{0#cue*NE(*CwVi6pPD&tM;EFipC$%~UT znH+a`Jy$1P$@$#$w8Qmt)M6Py&Wp9ne6?s__u)o)kxYa$#UC7{qLN0|F79{;qSDyK zD=jy7x~1YwVtCITQVlI5ZzK=9q#U<-^<9q*rQx3+NBkKRqi{+YWKaXBu_vB?rCwJbOJ|$R9iJ;3Ldp+Zx)_=}7yO_mOrxq}C|! z58#b{EigNo3I0(z?@&>to~4)G;Vi%e6@RU7W@ZLew~qVZ7plk*Vy$PD(n3e;(*L-W zHG5Ye!5r4VPC`cZ-pwtpu#g$rMGJ+aj7CywYPNdePb-j<;d#m}K%OiHjwf%8($-z% zITp(6Iv3-9zlIN&)Hm!)Y9jY=VYXN=BEK~WExtkSxd|wItFOv8uRC8rm(5jgx>yN+ z|B~?EgVtJzVx4$u%d+N&y`bRf!5}ho1Sb;pgl1O zRR%DVnl`-#X$VHgQyQzVPtD4rVPIrLiJokXWbwp%3ya{~Tzaxdw2aM%UtSe>s-toA z+Fj~!S>Hs-Y{^MJS2#MJb~-=3%hrdU=RY$Ua%UQ~5~@G4%yzkl^-fHfg9h8G zs%|HeV=!hMmm5*FcUM9o=0R{|h?Fx^11m);Y7yq^s70U%#8h{>1>-}N>mxD7)IoLj z#N?C|+QfbuIs8cJ0F)cTl)!6=qnYHX{qDaALm5UrVjufS8MAM?Af?7V0L?54B~-yL zX4!w*OCmpAr!fUCB$f zu(`YLe#$sYaTAchy?FAe)zX~r!vzM)^~N%)KtF;1h7QFOEp2U5YU;%??ztYaWFS{L z_@3lrCi$hKdlRdIf+7- zi@hGL^_Pc}rnMp{xOZ~vFU%$NlhW${f4Z!Re-&OEc)4ny1r=ZSk z*snJDA1UKKfBrmYY@2Pfa#nd}?{%<=K`j#&qL(WGKpjM8Lo3*?)5J zf^)b`8R8V43AJP|)dm(*!TA=<%biT|-+i4?L5vmApQhp$eiDE9pZ7(Y zdj_Z|t`5~=b(B9PbmM?E!CfFFmwE~)yks-Tu;0XMP=XB5R1>|~)9Jgl&=6cyq^Njv z|Et+6!tBy_>fGyGcf!zjV8V5OXkQ(ku%=GSd;kUX%0`(WK7D!%Bc&mtQHL(w&^$q- z#m$mXmmzs9_FyyfOPtB8&m{6;6y#d0$$3@8^^I{%>VIaeqLb5$qj&aqiaat~Z`9_6 zc?5-peKnEAx_2TE9`3to-y1b&@Axj9>n&ZK?l$%v)zKg4DGrV8LdX|Yf=%9&FgVW{ zFfI@<5^bGdbF(nRWMOf#Ku7nhTfgiPS(+#0a;8#Oq468U^Og@X(@*dL{XCZ*>m|F$wW)yp|+#YPzbe60rujl!1 zDyxRi>sDq^8O?^;+X0Xgs*pOT83t7`89)^!)*jO8W=x$IF=6JbWaiE8Ie3(%KMAAQ zw+W z!1u7HhOVD`6l88=Q&j0&8LOCQtq3?xP8H<8WE?;Kww5eU)II*ANlMdYh3_ltbGzC0 z8bgr%W`Rxa<(lU}dR=rwrBQufZulnu*w15xvd-ECU}q>Y5~S1tnYYiv`n*&lApc1Y zk72D)9`zCuG!Hos=rmyHdu~6f5(P^Kn%28hs4=9(Dm|Y;Q zDCGTh-06IFoxb?C7Zn&h&k(0;5yL0|e5J}j;NL(g=x*0o{}U*?Ml?2dVrK`+T-o76 z{o8AnYDcgB?Eeb|>9TjN%Ec3QG6OzRbWcB1mx%B;F(==vEd0j5luo`G@@VNDAHQ{` z%PSF5N@iX&kxto_9QoK#=46P|?{emr(G{c+9PVJHb7tmGFML#~(k@CFb$@YqV7OE( zdgkdmx@2P&EFUEkx2WF+26s89*=JE;q2(kirY^hy);-*0neKtKy`F=iFU?2|cLPajvR^_=F37zd*QXy?xMW6}lPVVKH? z&#s$8%Y0OF_(}urO7hJ)W#mbDN)+gk_8qeeXR-gcsSFNtbt79pw;IYwvCz)vf~m`d zU$k|6JCHH?9jVQb#eN)_V=YgC7#J9!@Els360W)i7r_~#pFg3*>X`A+_^QXml%SI3 zndbj9o_JfN{_v|r*#%QVH1756ITVgXowLOQkdrf<)a>`?J;JY#ADVF({*uS#m7zRv zc6kTTO#lgu;eDM*Ss1`BAlF^2VnwO`<#A#wD~I~OaOL47!6H*yZ_<{3^;RvvsYZlC z=J4m-b(EPP16wZduDA&FMlvs*EE{y&IRG(s(8}k$F`M0E z+Ihw?9yDl~1{0Nu1@)GrRgp9vp;?%8%Wq_$2_NhLlAqF}Y?<+L>MLt_*z}NGGRL$V zbhiWN{)wxr>oIM-Y>uovFy=ZPMMo5fdRyc`HiMEzATHm~KF=eL^ShX;A-?6-pc=^g zwb_w9cSnMnyuu>r!~{5sTGS6Q%%H{)PHXAwOKN`T3!-6C@97VCBhh!%Lzua3En$>{ zZf2v$s4b+9O*O*S_gd;v3T?p^$G4`Y5BecUxwsxS3rm%syO6?N8`>I(`VPJQY;M_Z zH*HP2vdmC?+}|gA^Y~l>gufJDy{v?ga1|9iw#Vqeovo>~_jw=HkCG?%^i6&_Zqg^h zpTEi!Pqat2%48Io1pwOHsgyAu{&0Z363HLtKc*FaX*rBDrq4AS`Z$3PQ;H5p<5;pQ zyhS2^EDO0ivU^JA@9(ocBp~V!bvWlWwVN}t_=*k&2h*?>> z=Hf6oR+KRFjqq+eT19YS1(3gS_>wm$S$)Q{VmfLzdZIUL>O|JquEA@7q?1dL)Al=T z59z6jDEB)pJ{JGqxx$Rfkyhx<&6sn!3)+QJUE61~G9rR5<#;2`mEXU2R+=Ktgo1OK zld(R(hylKL*awz1PLsM_lJ;rRinlxncb5d+s6aU#APoR=;C(_?hk^c?l*c z^=bd+=k*sp8c9=1`3}xb$h`2gvEtp~$A@hLRv|2%&JjgL1hJzHRaN5oW9^)0+QfXy z$pG<+mX-)qjZs+~<;<2>9h0h(q*KBsmI7#nQ)I^GuPbes@Iz*35!nK3kCTK(devXR z$yMhmY(zyxqv1@f92|6=BheVs<&q9ryJ)48zag@~gXe~WthDX3v3y2)4=YKlXqF56ZJGZhLhBz!R{iM7|`$jCZT>@vj znZ4xh+OCtC$e~&atk2z}+4TRS@<9xcX85GLAx>H?(0t^U^F{t@xXNUb(v4yxkns#eSDwyuPJM+)kr*5e-&s&TsfWaMAb}>L3vs;hW>u0DKnW?rte*eF){NzyEm~XYDR*AF%V(rp>g1w$* z?vDY=F~HlEDG+hyVVhobmp<7Ey$xj^%I3}S6Xj~eyj})s;~|3OEc1*A%ey+eDjt@o zbk5!*rs|ILgIp^$AR$;y!{go?v6!5hDKqvRme&v^PwUv1qoTGJnHTS{>}5X+$}8%V zuFaCGG}71K?2CW(e~I!*XqeyY*vxdrEQO)L?*Pk3x{frSgwP0DJhQH4S|hdfFRNL}WC?7opQVHL3P|##qZB-+eoyu> zs${XNOFCVawbsV(qyV3pcve`jXWpzaUiQd{2)KmJ!N>Te%WFq3j<~R%D~*v6@LP$n zBIti{lveg%7M;7qLOWPS;w0y+!#1)zo=0P5e;+e6 z^EJn*n#^K?`bJ}1t}%-Cmf!Tr@Hwvx!1f3X-#Af3znrqVxd6?3${=5WlpI;-ku}ZI zsjYIrzYm9lU&z$gvvY)9b+=&4^Hlq_)bX#-Q9Yk#smkBCfTpg0y@PU$k@#Qe6YJh*-eNtSkQXc?CT%xC_*p?Q>2&wc|1%e{OR=@CJ zqr`n>rdq+E@r*3N++_B3dGi@rBD>*xYwNI=7#JO2_@d-u0zpd5{Z&WjuFG(xEKlQ0 z4ClHN(P7GCO$i`*=&uF*$!#}PK#-B6qbmV|2Bu%<5MIPWUY(M+GQyg5jaPH2P$g2CgePQowhk!G=!~kiruI=nf zs%mOFkB`CL&Ft1>u|PcC1Xb`;FxVu8p=@{Ai>;^#Uc~ZWR~YpP7_0_(Di?z*h38B$ zw&z~&eaeFJvL$6@C?*eshYAfNw1{6%aH+bbt3#fhbWpzfq=c6Xs2BHL-4<@nn|ClJ zK`SmUCZL1qE6O*oRXkIb)+LH8aU@}YqCCl9#Ol`R>rgmGnB?)s+r-AVv|g?-;Iqx) zOh>al6pfCLFYHp3pN0$yDXTQ6cYvJgy<)jZnM-AKiR-nKZ!Qp`l^qhn=0fGo!c4ad zL-^RM_&Zu`cP$S z0G^ubssovk7$cd5+l!q? z<30yGi|4h14OF6jA83It;icZa)6)czej;|cbwdLbj%L1#js#D9Ei8Ujmfx0n2oJ6I zi^jbhMwA$Qe8ALI1Ei(H8_i*tbs8`qSt4)hHAj+m!K$nuJKDbffg9q>O!Z7s3q1 z>UTlWjk%xSr{v_-@3TC!_|4j3LKwj&OMKXS?y1Siq$t80`tjot4-bz{f>eUJTcaTn zF|qEucZ^Y{MMY%OSC$-DsiY>=e80@1Ff(a~m|Y5pTbnkK{81>VGI4fxehEld$Np#5tljQF}Zk|OF>2h&Jg z%p~iy1{P{IHqzrfDYXw~2*NEXF)(#MyVy_H^?3X29hEd)N5?RxB4(^S1h1&~rm~~g z&rn-(#F$43lFX{Wi4~55kox*q)WW7}!t<;NTl+!UX zvaq+0Y&UkYDim_=6SwN{r6i^crA55w!!%n(zxGbIP8k>;cHNud4aAK^X?CnS$F##R zE@BX(YG0+Ljw}sJI(P;9TSD|DZ9D_G@2+}E1vqCHe2T_hE*{&cac>HT=2ElIR99Ax z=(uL3y71gJ^fdRZDRB6vUS2-cg!rrHmiPxW6fmto;Q}}O@tDzyM@P2-{f04;CFSK6 zH8l~qpS5gLE-y6{l?EO1OBOh`8lq4u85(lg8E2l^do7NUkeImb-WXCucQPxGyb`Mw z;{R1`k_{@UN=SG*^XB}PikOJ#VzRb{7Z1O_D`#9`rbdS3j9hmXWWUp3lcRMqBD44@ z&${XNsKRl${$J!__*7k8-JNl~|Ay?Dlj))11LbP}6;<2kG#DKfM~rjLMvjP-iacEL zuS}TVzVY1bAKXpe-M3x6d7Ta8&n|e^We0mj#YlXnofyB#e0xXM->=OKjnJ}%qLy)S zr+KpCxgLF4jdLeNebIL4-+C5QoGR*>p|W^e!M7zr36$i^_IqgH9-|ndR?r5M6c+`ntU2w}SidO*o zI3;s;>sli+%&r`1YHIrUe*yPEE(SEZWL{j~<7dm`_(TF`U`446#G$RsX81DD2a{DN`P=dUxX?f>N1pRdMU zq;8efrL+6?jg+C3WaLb4=Mt_L~pJ%l)jkvb$Z8RFsm zJ_HK?I-ij>mW-&G?`=BFxKI)~>zwpYTfXTU;aF~9M;TSr&Z`2RBFo<~Vz?(#gEgEC zA}OK^NV;0adb0y*!37CtlWv=lys)C8(QOJ-dwc8m*zbyYl}OI;#6dML24(2;$*r$e z`KX*JPJTS+=;3hsTjH!^^300LY~f5Z9v~O`56OC(1NAt@1!unJEso~u(ouE?v+;x| z{H8QCJ!grfrGC3Whq6{~pw)*g%~HX`l?0RQI6yR%dSn-|pzP+Tg9k&A?vQZ(v5?Bq z!;GmYp}VKlt#s?BOI9h;`D4p#y|J3$u4-E(3D!RQON4UD+C|)zm3qs`0D}szWR-}J z%oz=AR-lOdyWZGu@A)u(JN<0F6mc~G1;UY)+h!KqnF2&RW}TvEP2^~+9v;(UFsrC*^sm6wZSgcJWt=*TSR|A{f+h3EBCn z8VXJHcG&Q#zE!!rQ04Qx!DRw$of6?%vnYO{#%Rop}xJbfXA zV4K;)4iGD^C+7lp)fleu*_rtwj>uB2Nu}`q!0Qcd;$P)v!1(+XmWQhDlm+b(Z!7Ry z9<@2&!ycC}x`A9cNtV18By-+jqdf*lO}lQ+H1@6aX&1PN;SmE)t!SU-Zp5x~3BF0Q z)&bKuFJS5>aEi8m+?+?8td-qxwbsN+ikuH${GMz+ir@6wiGQ9Z?BkX?VpIL+Ej>AW zEiEmkev^~9WUm0MOas!v%9218JmR>ZuSVYcZ}IDccCn*<#s(c8fRs>qC!PIya0miY z1;+~wlU-0Tb1D)1)3b==hO*wO64>|20BJh(ZCWqF<>YU5-R(BdYj?O)G) z;k(8Zh;c~sf&H``L;TXUS(r3gq(zGn8$h!^UsY)6og2pI7bro?c+)jL{;N~w<-s;= zSh%Zy1uSyj?@02{WkV?OZhNEk_PBNFhvPki3dKiH!rVL?QHJi!AaYHre{i| z8kO~0J=sv9kpg6^hjCaFbK4jvO`H7xW&t$8C>DvNR#?oWjSA?tP$()2HlL1^LbIQx zQ$nD=tr_>QSca{MSU@0zvo(m8GThHVIc#9Xs}W8zuK_{NmPKPu zV?V!^%3BfU7w%BFF(--BN@v*P<~uDE_=(=3hgRjtYC>H=u!RDro)^TLk$k${Co=BOU#NiRm4M5Y2^x= zq29NGH?@+Gp_phI*us>Y<@H;WIOjshJz)TEk41)a-C(Q#l_znL0)w|~=-OHC# zFxVT*=zN=EK$=G!{pgcCk;PhAclRrmV;Igo59VpiZXO=jg5O5464jO~f7cJI!xe<_LU?PvRyHEGG;L&<0|7W53^fG1saMa9z+M+@ zlrCNQuVoTNbbwzNAIXgIk>2>!9W#C@rLqy!FCCI}b4mZxY;$y=kd3X5QOc`rC{swu zJBI)B=gwi01lFa0ew4DC(iQU@$c{0&(k}sXqnb}>zcVpdnLs{pOmGxm`b)Fwk};0br{vroX|?R+nU z=|SMO2SFf@9V_@rKYx3J*dz`fvK52FA6x6)gUVJ6tKtb7xgXM!8(Ko;O1eav;EAa# zB`NwB9X2`3(YWFkZ2x)?b)l50xVpL9Zf9XqW%EqP zFtu#jiw>(2@QpF274XrG^qC>CuB4{+UBpV)uqvxxx@R_n&KO35`e$$ua94F+UGxnv zZ%UFmZfmFN2xg5GlP45vuWF~>wwu3dTviA_r;yJ?kCn)melz+%*ex_dvg31SL zaq((Stsqba;IUQCgq5KFgE!qxl_sGz>iGB``^_CiUo-LsB5;DL&)w(A=F>0w(!$Ef zBgbuKYG!34H)a>_*z@x9*SlE$WTH$Ley?xO)s;7$I2g$rwd%yidYO6Gfbeg^SDSa? zu(8IxUwRmQf5#{|Fy)cwfA(6+rF;5bwV4wOXKu+7a3C0EN<>6NCcJ-L7!Ek1ortu= z+s2g2i(w=)SC5fmHlG$}@=ytH1~V+4x12A!mKL!vLn)A_s(|)O*< zPD^&a5BH6)tCwRe&}yDFl%Xs=%3d;qua(0-bnw!$#thIZy;3y&IKjJ|B z83Am>BKp}rWiS|=J&}^P_|DYErl!(&gHOc7Grb|h2B>b4(xLV;kADH3t4JX2McO85 zo^_@*U7rgHH1S;eJ=)QX=1tn{f-sEH!OOZ0Pa6ZHyDW(2gs= zv8`GlTW%1mQ^ze?3-nEd@4Srj5tW#;HLGHMek{~cV+b(6h>saxsL(GicFv53esKLT zQf%Bv#!uzE?eMO@Y&Qn@OiGPHp4PEZi!id}7pIQLHpj-3S5HwPTv>^kc>iR5!vE>* z!QRyyhFqhn>Fzh|x(P1>Nv>xS)q)Yjt6oe^`5dgr}|$)Ys+S1OnYmE<+3O5x&y{q(sn z6kcabkZ~)!X3xYTHP3*Nx6OfkqCmRbApKf)sXLa5!=Fg2<yn_0->XR&S~iEuoY(P~e=S<&81#wEknL_Vv?yWdAao zxYn2o@x!DVmH9Wc{Le=IXf>sj*4pEuet-GP70aH)7Nt4jC&erQ>v6_l`aywl3U(LY zH^~Dh5e;P(d$ad`YPJ|-*_sAJA}`1E7=xbUG{n_0C8+3HJK{1V78UhW*@5sc(O8!8 z=_#*Xk==I187t9mBgf|*;Y~NBal7|aYEJ~AbH)`5TYk3OEY`a0y087R8hM)`X$DOf z7)3=cm&fM+Uk%lM9-NbtFWhVh3D3_!G{*!^y_8Z$X@xIzBSH0C1YI==z9atHE4)QVN%`= zG#S9E-&GO3U25XI>Fi2@PSfcwgH)4SD;-bDxx~5!hu^)I{z)iFPPRT{K92OGX1~Yl zMArKI7jG*PsMX8i%8h;hGx*4YXmQt2>uon~n<&;^fBFhG$Jk>bh*At`fK+6}ZGlat z>HMn0jq&Qn!TA`u1aqaz{`Ku$fehVn8vkRAooYu7k5=V$1fWnpV`t4oK4Vg|dRUDJ z=+L`%-g-}qBjSnIF?DJ3w$NwoVqmCu8W!6<#}zQ~iYjmC@iO+YajZEhN6W=Z5B;&$ z6EZJer3HemfCsasT)yaIx*f9j)PQ-3fjfX3RqlL-iPz;0<@T6TPtIoK{WoKUt?uR; z^ZLMLTF!-olp_4d@p}beOaKivu>$a%)GLDr^>+qQcz7zJ5CoNFT#NmMIp_BS27<13~Z@stjQzxcqV-`SV(=Y@8~>2YQo7!mT3x+#q|Mavq1zFWb(2oNZB2%qG8-nkw?t_jW;m2aTVq@Nl06##u5<@VQbZCm0e(c~$&6pj4o* zC5ourq~-2uT>m{|V0pA}E3Y7CCknf%qgJJN#A-L(q8xGSzwY}5 zc%M2Jga-;uFyY4v$@j*lU{b4o{j_A2u{jR$l}TLEH$V10@DY9Vw1lXBj=N_%zpD5qZTihG7i~ z%MS;a%OQ(DipyI-c!NWKZ^qY@#BiTPj?3g(HwbsAKDghT&D+vpxj#YrJh@D__B)B{ zcD((~>c5#5ZcOruX(z4;`cw0wB8jz1T=|}9g(2@zFHAWrhT9_TEz3&oJQ#C0s4tm3aqdwIBz==V_s`4 z-MQR$7F!Jv0S4BJguL_Kre8S8IPc-`|J%FbXH^5t!;JXYVQHDv(s=Po$v5Rx)<@?Y zo;M*#)0X-at{z(pyU5J6+Kbsu13RVZJKv8?)YydQ5xQqF!CFXO-GoTtNDo_4>WLTr z^ZxPPm;92G`8}XxNycc`2g&P4H5=Tj zdWy9vw|c!NalkOyF%ra)6Ge1k;dG^Rjs0wQ&ED+LI#5pM-8%%tP-uAe&WqstjDP~U z$lehDu}elZYa6hWhg%b}*#tqJKI^!HMLwB+6>Jg%Ly6Ewg%(AFWIcJo5viB6KzBB?f8}@+WL~M@cVK?glscc<$dhxhzxU z=^B&Kj?>MUGi2{8QA`#Sf~y3{mY-9F=p8+^{|5}jV=7q z8eZ@md`!-KVh%bTs@dZvDTy}Ra+tqgRW6;EcNA6x`rltqiy_TLW&Z{VDGGCckk?yn?j|Y)ViZYOSCBcZXS6&3P6c@qEcDpt7 z#z4)b){1&tow*8G)vBz~;vT(mld257>D#2~GVFY5eAZb^PNzHmy$3f#`4<-mQRlT; zJtVIh=%t*vUa7T(rAkz#*l4?xAu$qCSOn#Zs`tAUXgL^js6|xmWnPWvUu>1%VlpbH ziDBrpz?^T3Wc;%w3l$VOI)QKi9_@>feq5RG$_Nvs!o2u3uVCxf=WPyPi_V zFqwtE)7`{1;?{qaKcU_Dr|g>^{ard733WT@8WnAf{tJZ^D|LLSk*ud$z*ikt2(x5b z?H+=7+Ol9uoqi%F!8a%^EIG1Tt8i8fdhdquK6>U5FHgqdMRrusib)d7w-bg<4gtZa z%Rj$4(|T3h>XO6F59krGCR_EgM~U_PPb!}b75_tm8^mB+I$Wry{O->3;^%bgeHoJ| z!cp`xRp=;K+a^W2cmh6QgZJcNczAeSS^xO>IPBRvqv)la$T2M1>Ev2&M@?6tMA3nC(1XA4VR$yn$o6J(FjZ-(ulKuJS4ATR+jV^YxQKFXZhrw36 z**qr9KNrM@TiMZlsSp0CP{dM%K6hVLCuk6cVW*C@Sw4N$EO==DqQy}hzSAI5(KboG zm~V8)0(t#)08}NJAIft~B0pkLdqjx>4DiI1;PHaNJDLG|_z!Vc!SM!#GS9UOKvAT< zZJL0!^BoN_qW-*+Up_ek{!ll;c$Vtq+#FKViba32_*H=vrRCkD}E^7 ze;Z~-a0)}0wlY`Id>TWndt=~+|?DsXQ;Pwv;96f8%%iNsixTQF_o;qwD5;=Ov zE&_>v)i&+ZG8-><_W((3=wnfw-#D^LgaVST zo+bIzFHpVaPc&$>Zn2-?ZCOlYGas-3p;XU0fF@l5T@&B4UEspSTpC}VPOc+J{ zoSAtAb4;mlso)>0;7frxT1X`&#G8oBFAV6{Og)X3DuM+uOD!*`-Fud&=E9k}T{_|j z1x%Jzx7jK_V0O?U=(Ils!bg(1JTE?X^ahzUubPm?E1q5NqWCVKXVFSvedy*X^?lpB z2Zuk)Zxuw&v)-{-#Q*hwaIF)$HG1(Q zxtV+6aaA^SPT1cwqj#n-AIdr4YKz1#XD3l5S0`5$)ole!TWaOc%Xx0WvbQllkR+Gv zLaVFk^-8g8_Fnt38D0J;Kk3i(IVH0GkQOa`Ngv#f*u4CW?w`mZbQCS^wp!-;YpQZD zSO_K0Q|ESLYO-tOY5=}^V`rCdWyjUp4xC$ql_R^ViS)q)rg-Y%7%-UvuaBkDeA4Y| zApU03_&FcCBt0EqvQ$b!(jT$mr1;xddTK%yKp^`3EUfV^(qxs!{#Rs(CRhs0+gJ4# zi9jaxgV?Taol20ig&NPBZjR9s*$r2I1U)~tc>5QU&lCuyZ7#&-KE$;M zpE(OYje@FVVeL|?q*c^8jkKSF8s^zcCr8|SghLfKyyRP3Mby%DhW)m9gif;>TNH-g z&Gm|uyKJv``E7r+Z72&j*JWj6;}R5X++^>Qp@9iHU;V-wv1=0)H~~6$t!oo#Y0o^- z4Km41(vqbYkKqm$YQrCq-DP>vF%u!Lcdgt-3{}>0@0qm467F<=ijo)s+obKxkIOU` zi!a6;=Z{Vk)?>_%rUa)PB3DWBWp^6H47F?B`Y3V8$$ilRJ^}s>rt@PsQ$+g$I}3u? z%g-2MvUl)(zW9ESxiU6X=MYwz_klH(1ZNAX#|lOC^6XmNW{=+ZvA+s(`nG`ApYJli zcNtY}vs?{gXPrv(`6x|C^xu}@lm#&f2K61qoeb6c26D9C0q`3mRn00 z?{t<;pyw;=h!);?EF#5KQf#0*S5ge(R-}W|0*l{Ek{LsBk+MH0Iwrsl5GUb7PfV%Y zf*HU=E3t?@vm`e1F=eikeAR|`(u&Q58`Q0(QKQa64fDG)cP}@Fd>!nEVkr(R*R{NN z`4At+Pv=vZ|4nDDE#SZY0T{b2;qN;lmrBQWyx8aJVC(1J*x2Z1N0}%)86L@n{FWmu z+9r8jPKrfX0ZMzQ#J3eYD+d)KInS9u)Lmyxzf)l{n(2j6?Gl@I%ITE{De84CEki$k zTnJ?DOEI7aKmby?W67B};U@!L?7kP9mu-3Iw|k41qbTe^h;)6UrntLyxQVPnqcfFvVHW^qhB znyTd{d1NS8^(_)i7A!}BPKTFxT)-ls}jlo9x@V5e)@fkQE|04~Dn zAkodAYQ~qsE7f|AXOydV#Z9Z5-J^M|nH~E4xZrDvKE|Ac6!pQEUHDt%sc`E%nwQLq zF@2P;`?-X1I)^MixUS7$oi0C)$FwTS7vS<=YsnwaAbE_C<2b_$Ja_ zI(^1l19;#!7Mr)Nqt5D>_>By*D!5gg$OH|f1@~$M2r2fCd4Ib*vXU%jg8JmKA|JsA z80L#Sz6Wm?^HT+_%$0b2u+llG%d8#)t#^hIM5ZAf<9dr&boKS&WBjit$yculhfg@M z0&J0G&MTeI+zcHRX=|P4iHeI+%;!Hm)R3+lJM`<2524nzTkciDZW-tm>3~Rbm!ub8 z;SOePTUmg_ckqW~JHDm+*;8zNobi@AaLOTQdp5}(hwi#|szD-8^ugGHWsXY9e!Eus z8eci_*!Qi|s>%%Q=g;%KPA3w#+#7ukirD^58JWzi`w8+Yr$|o1#Y0Wf!yEe3YCuYe zH?O|27ZA6KPh3-#%6|c}NWo)eo_Seld?N+(!>`3@o|xYd(qLg1FzCGbZ0`-rxAIC;Dwm3=DHb2n%?Nnx-&3fBqb$ zrbm~<{w-=@bML*q6PU<;Ivm{2xd_|qXkTtw7_R3Au6MoORSUB)(~*{?r#%+PQ+_-k zKsE`sIi;jcHh(4_n#~{mrxS=5{57~kK)!h0c#4#5Q!0+SCD7`ggyNc1HIj?MKb#4Z zPa0oSlQz0~YiW0VF==I{WhIHSM^{V_5}(^kU!_ne=aT8o62Af|FTy2+NabST*TyhSfR=&Zgt;#S%31s~{fS24IBlR1 zt-J&U?+>mj*kJJ-xC|8VvZ~;aD*&UPnd<}j*tV&KKNLR8d7|7oU>d>w&aYV6C5^Ul z7flX7>NL&8khR-RsO3uizb-NZSR|33U+fF zPX#$G4xQ|9`qliE(&#Q_3*HbfBBV_9;n0ML_+27#%U?nM70(1!1w%>-ZOnDy)-!me z^(Tt}aaBN9mhG8jgFIxFG}J*Au!F)I5=^s+M`>?ljUd3th+}p70b%(;^!|LOH-e=s zA_Zu{;KM@+jkM+^?jhDrq2a<^?_cT8i#@0!RbzipN2$($!vNFQNs!Pb3N)c!bY%CP zuA=x+;SV&_Tnx1?GWgKBx4HbWvlx2Ru1ClI%Bt+NF*VfcG zFQuMcRJ3=lzl@uY?X3Pj^`TaOr<3Nu4fa7Ce=$0xIJ+936xRqjcuK1{(!x^ zz6tr`>*ytx5IaCtVD@!gr@$Rx-Xru~6RS5-s-!`_{uuv?3V@`ta5#$n+qW}^jqLE@ z1&?Px>dh-H(98&LKv|{l_V!lCS8RB9?Vp`vqqeX8wwo{by*mDunG1y*dZaQt)#L1| z<~+G(owKv2i}T7VoSt1pZ1_kAn)m1x+V_W*uE;8(Tm3144Wni74{VhVuCcRJVMb6l z(-Pr`M;?5?#g3>_GDB18^n0_!ux1!PIBN-bt7?&Ev<4n@s?ihilEuMqU%s-+6gK>K zYL_V<2cd$0fq76oqY0KUCZ4|t0zS}H<`!x`;TW80&4NTTEp|%;|Jk-F2ufeGhz&%Z zb@tv2$wx*u!AQL<9>XwjY&Q&+E8$E@>VJiahr`5wP4XETllMI>8P*zbR^IkXvBrY` z=9#ARlt&cr|E4z5_Rx0Gc7{hQ?u{ z8*7t;hGS|e`i``!`3#a#8YiBv+Fy#;$td|?^*CENOMdx^Dr|wpuGj*(ipolWuYMRJ zi?V)84oqiXSYB8r=?+`Jn)t#dwE3Q&Gz%*1Muy}YM_$~Bgrwyb|GO1~z_vBrtd`}N zzIxciIv@FNYk?N6w9mBorBX53K>Z`dY*$}YmviMV`r(zuxas0~uv8VA`4=8ipN&HE zxt$0Ue}Tc)6OO5z#_x{9A89-r(EawSgS55(%$+qOGbf~^q<(Y#ec<}_@K@SKVp~%2 z>gfWCwp*M z)qGJBaqE9SrfxU}x+c2@Nj{1&0qw(#jn%Djdn&4~JA`yTaJoC72R5LV+XOxu_D!Qa zKgcCm=h@0i`x$rX%Kn=NG+feBh`EkKcIOwPH4~t8m4GA4;S)lw(y>&#C6ckqJ?GoR*HyzkAyJ2aGjr)*mB33^~Gbb!H@@ z)9UK#_&_8Zw9F-6KJM#6KDL6`wzu!0D3iZIer(;PxQ-${o62yV;&Ao%@wNVQRM23sLcex?R~?>Tc$BUIg)%eft-$bjKH}z}( z)t30l1n=nA!)(|%h{*!~W54^Z`ifdB-FPUDRx4>%Q}ei>w7I2u{?BN{;3DZcp~9Tb zn$Db#iVk{-P2a_DUY!i|R6Wo>Cdvld#f#@g2yLj`ZCQl<5I7W-10xVqCTc?5FA*3@ z39D8IfXlV<_XwW0%GJtF!AWECJN;nsS0p>}WKm|&0362w`K2V!o_~6zkq1RB&8d*e zkV+;}tN0t0C>A8;u*$&6SnE_m7X!!rj48Tbk`bWuJd2f>sJ8aPDy>h>MWSk$)I@j$Ixeldh3a zf19|bqAcekvMM`c+AGkG?!_e&mpl<2q&K0K&Vgiqy13<_FJGz*6IURasEOzV&@rQk z+HuBF`i_bo$4Vk$2vpF(1Cmla2-G9S2R3~$U6X+ikcRy+ej%JFXg&KS*~0_$#f-HOE>d@I=a_T+=}x_thFMKF{)P6dfbp(f zdT2FlRPuhir1Q91>b`tQAz~QbsIkWH4R_KZ+SPjJZZT|SCuKemLZ)b`FYYP6(OZMR zfipC)KuBrwwx#Sgk9?DLW$Zq_10YERjhw!o$AD<}qPeITc0~Rvsbl z2gzzHc7$#>R4206BxG(xMsg7rTnrS41*pc#!(-EN2A<*(DM87XEug{lt<$DG1Ynd< zhNb-eO)FTVT!IReN~FHigcz!$uXWrOAwB*c&Uxnt8xt#gA&lz@PlW)zzw8=VA$Ldy zG!MG5K20*-9AG{hB#E_#7$a9c*s>qLSS)9(o%!)2VA$iE6!%`VlaA{ci9d}>*_>u_ z^#V9~Jh1;p+rN`^g!!KXdYa|uSUN~2RwknGdE@~>L_=h? zAx_T+YFKvHW~6*e4N**jc$~qC{iBaOXKNC*N-4yUVgWs8x!B#TBSXbPWn(nS-UY5G z;?Zn){vD?HPRb(MmF$(kc!5s^>apx2K3cqVd0Yv3PLddY-l)hF5T0gpna0xT0YhzY z@Zqi7JIo$0zR-Lt(U&+nFf6yx2IYLN+bGGnR`YMKeMlj;lk;6B8=D;RkAn)G!MNTJ zkG`!PxrGu2RJztuxgElFCUF(?>cm93^=SPa_OTjXk5uCm z0YU(XOlGFrQ<6H2eF4Yt?TadMwr_ysNAN3WncPe3wL|d1yjc)T>AL9L4Pa6AkVKS@ zwX09Ldv2{2RU|iGau&Q~PLu__Hq5VdK->_CSejpQ-kIxm!z(P>;Y?7G96rE*!h8xQ z+VeA2OUBTul+L2U_EeE>R_ns~YE3pJAS^-&h+#s%p#>D3EQ1M>f#??Z@3!X!|hdX{k#Mhf{zYxdC4;)uy zV_{o7(ki}bR5irkZ}?}cES9z4AK^9Q70HH!6E>V zhyqDeUUyhGZ6K{o@{(uM*!l>u1as8rUD?G8MSqJ}5T(pb zH;JW^3KMCDN3hfJN`8*mVYo=UXdZvmh4JAq`*$LVJoK(a!lA}$5bf9#ej@KRR@@7| zB_C|^IV09;W9>#Y8dZ-VvQz%quZx%42h59Kmh9<4&&(6w;?G>di7t;$R%*AK_-qy4 zM@(^Pe^u(HM!Ic=4B%_Q`y3&#y;s_1@t~@zfcPDB9$=sWHD0TFKEjb3K3*Ogz6a6g z^-BgLMoSXNzkdzB-Nj`2{_u~TFNw`F!MaxD+x2ao-8JO6zdqMtuYsT9u0I-Cv6n{Y zCv;vT3hR#gx!cXED}lOL-($3b7*87zx&tQmiw-A}poa58tz-+?^sH6Sc(ogO<#6~q z*Tc4@XS87J z|52-PFc*;M_&iWCIXK@iI&QpO1hs(cy9aa5%>LjgN-!=%b-@YFEf>;K69$pSw=;RZ zsSTzFp5?$rkzJpGb)TRNPw7&&rp7nz9B#zGY zlXA?>%j!u*{leQI4VS}eKe^)EU|H&MHhheXxK=FrF`ehzqSZ+L5uW&XLHheH8G1Vl z_GM)yw<-QbDSu?k#w-s0?hZkJR5NX)QyfRRA=ng=a{O4U5cPo`7ZB{RjE2lA`Tr_Gy$xk#3h*X(Ey3-3Yv^>KU)O7ZJAF(y{C!I^mFj%NRpC1p@F0jCr zGZ(RbK74Y4+Z?@4##5D^p4VDDN3>k0r>9KP@{|p4hhTxmlH^Z}dRBs^&sP>UkFLJ) zEi(o@GX?*Vs9!?xxWizGbZ@nz4VD(cU}IhCSM)5l`0BM8N2jfTyIXm$l6o)uhRZ6k zNx@KZ8GF()1>6Ee?|1?4kTOdd$|A+nx-NM6$I8D<_*$ zzRq9!6=^Cp9yh;wUAR+`Ldg`8H`}#gNgz$K{$h2!+-8TMQ^unu`wsZa^8GsE0yCiA zcJl?LbXv5`jPhIY7XR)(KVy9y;%acVb2;pExU}ur$;m%<`v0;um1EjX1*09CL7MuC z+BNZcUJ;L1+ zv#+SCq|jF}e7auBRNM2Nd*VIH9#2mh6p<8n@#5s{jLz?>vv6h>-Qb?`x_a=Q@zC*6 zapNOkk3MtORPqJPtahLTTJEwZJ!`jr*{9}o7Ubg?``wf#uw;h$fxlKuSnXlkR4p!a zp}LNHFcS9`Em1dq*9f41d_7T(Lpt(~Mp=~I?}Bc(jw2$&b1|G=nfC`V2Sr)gx#>iTC>n4TWt=Zd8Mhs!Be zg2iaJPmqzOK4L80x=N5nPma6@h*pRlQUR6KM?Fa=>OMl^yzWlLn8zA11&>ALtx1-E z$kZbKpFC{V!-#%Kze+!^zVHn{kCGa2Ttk~K#X(a~VMaj`cjkiI!w8$z0>{?T*?KeX z`dck)=;?4>RH{D)venI9LBcPh%me9aPwhQ3cByD+A5{xd(5Q2{=V#lmw020P^I!6L zs@Ng`BWA_i-hpVX+B|d3E&Jnl=$UmeoOdR?53WtF&8*7;DX!(ljEpr~!NFQ3*xgaz z+X-1nAWdMGt=!{k)0p;xPibkXQl%UQH^pb^8JPyL5Y0<{=7>1jD(mE-dYz_9hgbE^ zDTVNUE2Mm;w7qN-L?&zI4Zp@HWRRKZm5PbKIF>64J99!(xQR${y`KTAe82?B;I~_C zsd-2IgKGtPbV!{G%em82E13V)pgzEW{T4d^L0*6q^_4__yX>-C5Ub?D7sc@xfnaiM zQ-+G%kZ)m)_zGTMcdn3&$zwk$AZeG70?6Y}KKhBIlC$_EP+!0Qd=iN^z^o zwa$u6DfjD4hRe(EgoN))7X=+jVpcj05;#UFE(BccM#UDm6R-R`8vVZyu&jN;WBVI8 z&s%%#56IfNo~h`MyrGgGdbe+H-L4cU2Vo^PGTH z`hUS?x3pUP?Q8S*E@VoWoJEN(juXVBt=v-%&P#1JJl`*J!Ers71g^uIEr4#Eq7-=#@SlKP|s93c4i zfTaK&zw-q^<7Y9JeCgL%SL%du_8%otYGA24q6_o~!KI7Oq6DSnoP7ex#1wG{bs(Fc z;!@fuH${|M_>|j(HfMf7%pSF)AcP8mpMI7GAxvH@DaRQcboz zEpAK1uWLXp0_%ySSQKy+*clJ!<+#GuO?l=`zHg5&gYtn8t^tc|`NZdzwX|RMRpTtz2gEFh!ErZPcGF~v# z!&>i*dd$2MIwvg<6KK{>K=Q;`(Gbn55q)-jCJOs1BH@f=yad!^`Crz3?62?{kVT%r znf|i;o#qB{~TQkrm5}uzn6-K$s*s85FQ;7~9=Z-#?J;|k5n{2z40s>$#ONUBT)*j(oDV`i}9hwbZ z8M5E0`sW81hY81VL73x$LvcA*?XuT6Ehp0rLMU(u@`23jC#}Keloj5X02hQqvpBbF z5Q#dD8^%{9{;C?mBqCXsoaMN|3 zp3)``Ps^Mqe~rIEek#FCe_dB%p?D4IxW5E`+3P0a(c%-;1tPkJfWX2ipQo)`9_H0++n2T3 zc-J>+?csGky})+N+)EdGPYSsE)#yCHU_XEnUl~PTxhd5^e|2AOLQF(80-JdNXqk*f z_s?D&E)UDaK&;HABniP@aZR<#x=|qzz_7FG&7C>~i5n^p3HFhVYcp>MhT+pLff6xQ z_0U7(=yOGbXPu@Zq3aBCkLi>@;D{_I-@f%$Si+Vsr)3Qo*4$<0{|hWCFjeN(52Lsf zZ_(?V7DU?~sMX1+jr{i7WN5PV@YC?Wf2?1q6VJ&N{t@+$s*ti;EUq?8_A{qL3_-V! zsh)?c^@nNRTMH5chW}Y^MAi+DKqQxl6C;CNod8O3qn-uv;xwsCo=E;s1l&BmdCFzN z#@=x6_rDGv3_PZeP_H_JFuj+Vmd`rAkW}Ej`qCCt z7#E*JT2)nUYZ>|uydPCwP4^Z(mRs6vPTapy4y=Sv2ucJ->u&V4xGouC17evd!$>r~ znWqtj^{q-Di@?RSBFlU}R#Pdq@;k~|57}pGB*M#-wBl*lILP+lCZ9xoI66lr6#=<; z(E&IoUI9@hO*3@JIl$DdeQ&@+tVKXmv?8wh!_A|^bf?sR1y7|``9Uskd51Xn6@c2d*N2C1j5-iuF~1E>`nP^&{VtCJ^XV==6Xb0rrZd zXX3`oBm)sUSSl)TLCv!Hb>9RhtEsEQa{nBvbK#SNn4*_??N<*%{<4BV=KUXSl|urp ztV2)_uP}vxs)CbwHow7Fw+X(u1L{^s`jWUV87-e!y2KMVr`wJxGl9NJUH1O!M}h0U z9`avlDCXb37WQg!V86a5>YSESO|tz{H_uQ`5sB3Qm*XXceahl(-G%N+Y|X&9WqjzE@S^ z|LX-no0Fch`Z|~L{f-~OBJt0I%%t-Xq^mXUf5nZ?W!R3}*%qmkct0UU`jQw9C@@nT zYcP;5x>L=J`=9p}GA7vxg^SmqIE9PzW4nta9387J{*^JEQT6V#1}}XM#sj|X1PS8< z2xEU&8foTptE?7os%Ms4H|tjeSrQ^F~J-Qn!AUrsS8 z_`aDc$2JGc_kL45Veq{MEyzq&3#5_+>2l7r)I{;15`}-o#ceTTb?=Xyqlsw0k2>~5 zy*JyJppzZGbcLb9G47pE@0Z(R)}@9*PC#C!1Lab93%5?)$#Ft#Nx9A8+-;CB6B9L9 zFo$TVM0v)bOZH1m_k(>C%20tmfZck`76W{?NLUmxb#$I|n^KT%rFgd-1g)cZy!p~= z9x;nJshm}1k$p&I@o1g@?Ap03%Wj8DIiGf#M+ERhtNFK`E_mbx26T*$jo=g7mqUdz z(Dj>>g3kEKSh5Mh_juzY)&pccYck++^cg_F;klY27qe1#VM>tO0;6f36M`q5K7w{rU!{>)4LW#UR51 z4Q@O3j8C0AezwBjy(7loMZ;4(xxYkBB3!P( zI@=Q24&|tXM6SMIL1Vz+lqOYqizQmOzyplE>8DGLeo*t(=3%FGK{U6Q+YM5q0(nqk zBXW!F-2MXtZ*xn_gl{bG0|5v8v1uQ?utU?IXLt?r%ih~F=uTXY-Ol#~-?xQ-B7U7FsEWTZPBat42m)N;4pdr#k{bh3N z26>%dX-q-JeUwSTr4!kY9}NBm@ptQA>S)yN_*_a&t^Y?wC&PXaMM^~b2GqWTO8~OI zh)=|q@U+#EvRqu4?a^@yb%877l#&0nY>~AU;FJx2f1h*z+wQiwH&e;!vE*^=>4K}}5Cb30^u3y>sgSB?1Y#k44Qe^mY}YGsvF_IR{9LioK5yL6 z@IAMqqxOUgNWAs^gc(Pl!*UA6*6#V!d^bo*7j) zvpuU4|C#e*`R!%)w%l;;K zL)pmPnQ;kbklJH(?irmS!CM*Nm*FX6;f+|YV|?w_U#*ZXN+T*xsyss0Pb|y9!nE^) zn|^H`z7RX_kU;YhxqCOyNGDnNV{GJJfjZ2eGRga&mGmF7c>u|)u}H*s&zQ<0ga-p* z9qTeF%(K#IB}^_znn2s#p0sA24k@?G5!}7hI zu%$Yg@cn@aXDc=QgD>#_%q_+*DG)%OzV(XY)ngx)pB{8<}a1zRC-%cOV0+uA=MQi(Lt&}Ouw{rEEVD`LlMFiwYu zjI|2m5_SwN#;Gn+6|Vrfkrl!85^sXok!hijC{pU|37e*Zzr}u#2k!>x{%dn7ipdLS z;k*!BmT;*6)I9Q0XF2roQr=Y!ZG2(hv*o{a9TUln`=4%l+|x{;U$50)D30PSQ5Ao6 zVgU80V5ChcZXWE+9**K8PogV_xis3Pvh8@>goytAEw<=ngw)#SE>`0G<;>l-+J;om zY2sU6C0xgLA3>~iT>fQ{CEmb^VzIfW%9ihXli$bZ|9Z;f-}xjWmQ3)8go#ipcC*e` zJLN%zYc@(Hw-rwtu@8js3!hmr#p7gNF9&~&ajSha;MYgW#hUZQYU@x8&@7lr;A+o4 zs!7TdD<1y+G*yEZwEI;}IW|A>F1vLx&l#>`gk<7tCA!<$k&+z1)G;Hl2TIz6yv42? zg6qakliosAFrM^}37S-y5}c^Qnp|v28{?&FRXsge9IS$JZAbmT={e`;b}q_Lgwrwh zbV(7+TguUrkLEc+>m$z4V4rii? z;85aNdrf_RFw{_PzVO#rRg7#9g`q0c`n(HDC^s6uh&`^pZ~R#G4zRM6>@S+sQqk}itaY#a`xOO0IWBZ=&8(%-NKhd`6POte- zNv1a_J*25F*)E4E!wOd=cNKu!1VQ@4=@3mbzvruYI~Q#7t*&TNFRP|Ce4Zt~pP1&& zaC(tca!aH2%de;HvW`ADr4QNYn)vl~Epg_@g|8z5^=+IxEKoZ9b|7?xz(l)c-k$&t zU+$SGQh^7+%yBUbq)!q92z{|`!C*l*oHfW$jJIEu4rdIDI9x#(%6VDe$^)%5bCS(F z;eA=ESm&#If}*m#3WBZ117moLZt}l&i;{+`7oA)U9<1rqq86r9PeIz0VD97A1I+1xwKPjpB$%1aO3;c*k%V0`X95G z3+eq8sa&Ck>crH0MrU7uQpam9WTvbLlvIp1&r`cx*`8f%6XDNLVSco0h_|=&1!rxE zIM~qNcj+xpNa;Rd6w&Yaax$+1r>k}dq;wgoDHX?yx<{;(IKZ2Pt#F=69aKDb$3{KA2`sZpg5tDV;a+x(Yy>EybAeM5=$q!oO|2xg#cm zrVs1Y>#0{?VUCUQb0{%vuwd0Xe76Der(-ET$|V<|5UR4GgxD1D`Gw8pl&$)#PD{Mf zi7DCSk6SfY%a0cyM@*J8{lc~Ys|xh5ah1uRd`8inN1?6`I%d{6V`xYsio+qM)#xzT z`PZPAU)wv%kh{+OgLedb`&h~iZ8YKsoyvdfcYAd5ol?JVLmXJ#%z#h+Z9WP?v`WB-5e1O2e<_5Mr`^2Y>}z|cnPMXer^ zwediip6RVN=I@z$&#OL6xz|#zcp@pV*dNIHB*RQR_YhcxU)*L-{?49Qc9=#vo+s4P zY1I?Kf}^Wwqe#10Zo@#!=kk9MEzHW}@e$71SU)^8%h_=s8R=@yen81?CqTQV<#yN| zY;O0(3J%m4mlXMq4~9Zq#bvh%QdZI7h~5{9jK;j-(=~Wyj6Wa2`|Tc4R-5kEh2GX) zxF=5tL7=3hMB{NbJFC9sSXVr8x2t{4@azp?RyTi-H`ze!`$&N2p+PJuO(Z-Ho;Gtu zwJ}y%heq?;2|URR4?I5N6YgZ$ydSZ-9|U0veLS)b;NnQ`@1YEd z?@da5t09?QE2{A>3f0kR5pGy>5swRzG=4%@uRL2w(y@ZtY<_PA1rmv$Vzv4m;C=|v zaMkRWqm9?6IIdx=t-S?}B`qyc!S@KwL0R9Uq#bQ;o63q6v&glIS44bG6Jv1^Q=pKH_^= zvU0Uv81H^ODRiHFyEa^5gs->JdB5UiXBYML6{yi3W$wM<)m|v5tgNhR;^yL#C{Pq! zxD)CR-%0sgR_r#k=Xmm!2M1qPbXy$1|3z`UOk}?+iD49vRYZbJhDa1=JiLgX_&M-T z@UDMwKw*5}dDf}?w`3l)JubI<#ukZ*a@BhB2)I`yXBqQ&3p3cZ(voLz_gV=X6)KQj zr^j@jDuh!+Ye2_|sLAM$xCFd|-L+ERl>^zRWCY_j+dOG#z=sGow79OGZM>l+NZuAx_Z?I z2Z^JfrA5+ui#2f3p&;dJ1J^4s_|~ZA@NGBewpeVx!M9zY@jkY1SAIrG+d5eipOE{&t zar6CpfBlK=_Hr4H_TK}p`dg%#GW;A4zMuG!+cGj$7f0p!=FgeNo82ps{#bb!~O)O z{pEZo)AO9`YISo{^}PAypz#wL2JRRDa+3{_UXZJVo`KrJb(s0v^WEC*aoLMjcyMNo z!;PioDC-cXSLaUkuVb&Lze>C}y@2sv2p3jKTaOssGia?B(MFevn^ilR@HLKgk%_&A z*rAaKFATRfdX?k5Kg=W=rLzuo&OE<-_Rm)N8*F?AE;h2%0$vW=cpb#vy>!XFu7+(m z7#v$@M?!MVqC|~8Rgg2Iq0*Z>yPNr3a;!`WP9Q>h07Cj3gT-NC@8-A2 z9Q<#kF6TirlKMmv-;=f&Y;iY}2{L#6rtafb+AUl9vRdmddQbM}^3F`C-Gb~7K*6OP z+D`Hu9lQ*vr`#%Lc}gflB*YF(DcY5TMik$fWt6^+N@17kObnjRcx6Uu_UVusW0x?K3y^i_|lsUdhm$=)p9%KxV*B*t_;DfIY8s!%6QFolMqP4 z&c%y}-a1NyX;GYZUkxVEi$<%Avut6TKkML~M{-rSbs*V0I_9hImaz8j$F{iu!+i|K ztLO8?wg1NmT&BPqQ^y$k%*;$jT^v$DQBl!v>;mpW@0a@=b8=x}9*m7_Zvc6F6tXnx zGiszNrmTD(zVp@qY|alfSazmwHPllppZTc4&zQkygA$sTccMU5tKC&R?i#97O?27o z8U2?j`Bxl4A({9_pYJ=LLAJ_|Sr=g_8=BPab*fL0^$Pbqy5{wfDL`u^$ic^_Dn2*n z&L#U*5V`7)g}$MzEI~notD%0l+Js|h?oW%lMn{t}ws4?Cib(--$m^-8fK-}E>xqtl z)3QsJHfD-dohDF1s9crilqDlIRd9yYOFzzc?Lps2VaK=SxZ~;rAMmtFoQ!EI^a?c2 zqjH=P&B$tN9WGcqE+uk!cf-TQHN4s1jv5dp6(A^^`}~n77F}a+Xg^@MUQ-D2^|jZ% zEGeO)9qDGon2vj4KJdc5`#yIReRu^Ahy2s@69g%=-ZG4hRlT+4E5JE#qdZ6U+=~2#$P779vztL+ zO=D)_&aqVxuwe3`z2I9?QCe(%n_Z7D3pE-mYLrB6H5}Z<7v9fZG1h25JK~7-ywwTu z@2z!>VZ4$rxT6!F!Dh%(7Z>PMQn3Ok0}7fFmbZZ;uo9@4;IUFiql3GKUatXpue%$s zkJ`^GiesY_YrCyzbSe;n-{XKDcV|{*Yn_*;O0B$4AfM;yhwGVS)p70>RZqhrH1om? zvuLH;6wt{nrCCzfuG`>c7gvX2LiTpbGQXPv2T9TI9GQt5|xpF;dWSeWW~Y)dXhi6$ja>vX~^n zz!Tnba&OOA^+THnrZp{cQkF>V^QfmuHLyk6m=tS|CW0KzZbWiPc z|FvaQ!rg?C(ECGtwI#-NgJaeGp6&rY=QKr&GLuQ<;JsWi{AcDDP<_!ONWB&s7Xu21 z{pF!#P;z*mnBzgEl%_>I3ju4Q5Mdr&+P%{5&z-g)QL35^dY}Q36abWaxLpCD8G*IO zLvN1Gr6iU>RRH8t9&Ak5vmrMjllwlcckH%zG$tGVy_uk&8+~1s84n=(u8`1;Ic$%Y zW!A%Fc46GPJ`(shORb1-Z~Fa01GBhg5pF~K^aU%J>X+^v7GDC2D8eqSNKX!pV=O9W zHwF&0g7QWCl7=&As}x`S+g_hs)@^2mybK z2sAC&QVIiiGzi%ULRB+Qa#W0FO|U)^XzQpp97eCs?{f}%d-XcxNp1X-WD?9@`_Vg1 zu^cnoD2dC^`fQCf=vv1@fkQxv*7Gw?49OXI|4ts;7+%5&(;sy$o6*J#}Uu-y^htJdzBBF%mZ{y zN=VsA(Q!+$iS#MabHic2L21tv0>_oKo|C@J`=!tjBfQEsS2qii-Al0O)T0nqS$P&$ zdh5BMPU4%B#kA-W@{r)@Qn%vzsN9ocQUCn>5Sk36eCLkF7w&d!~aAScq$&0}R2 zZ)Iy}1%LaXw!PyuqoZo#vg$^;LIu=+ z?Ze0Si8F3A;zM)AlZ>ruD@|&j0Agty`nDGje@+fGc^AwNAD?`_|aL@B6LskrBQk~M&C-aE``-ogh)9-5@menEo zK+4l|ceoew_TKlSU+#aOW-R#&3(bSh4^D_lnlN5c3hj&*cLqb`%20$+^R^APvJ2@; zF0xJNX#0d!&@=_(!>F^dnZk^-v2fBdiKwZCE}X@`a&+Qg>{63{26$V&GIx?xqDSN> zMX!S*5cF@y)t`XUzu}o|wxVuBnG;N0{ucD^2LZCm9)@9S17$Ibvhn^rIvsF&n2B$d zRO6QBLzmFI>uie)^UB~7JI?8H1WA7KZq3lI>XB2(5^7Y@)yD7VI92F zFV!}t{L22XI-oXW=gJ^jyuxLaa&+;-e}wd5;xchU_Ild2DAa_!xS!OCqVh)*gJILl zkpv=RarBfmXR$*BE$bnncnKo);#X=HxxguFQrix+r)wk^NE>)cFYx6(}W?_rZrsqe}Mbm?i&rZiXGojB%>ey(qg#$q_ zu|1wE1TcU|z1HV5rhnW;-)d+{;d{CVK80lx5T?1kwbo@o zg0r?`Y*$2lGwYUTo*9bCh7>{ueyVR4yjO`?(NMW0$fArgILsoS(gI@NBwwb1=+OJ5 zIa*)+Ri^BJmfO7UA7Uo~IfSG#y#B&^y^7I!TG)OCXirRne?iD8`G+Ckp8*#smn4Jc z27A^kqErT53Odx1j$Fp$F(4U2UE}*q7)5e?G_$Fa0(|{0CapaYJTHsNO(ttD4WH?8 zze_eLoQjqt3{@iuqd5c}grblTvQu33_FJt$F>fxdpvW|v6d6XUpyi$D<4$S8VZ%FX zH`CL{utgr|t?6jVP|Vg>0v06{``_6n*2;LNqhxS+yNyA_4NM8pUA*4xkHw|GERHCH z`IFnzwj|RT@7vMx4ZM~l;sD&P|7(WfiDNW>iG^=HPnR!S8yqv8GR+j^@Mcw_xP=rd zlHH{0ErEoF20xSxtYH25O@U}jlswFc^pIV_Vk=_B%a!CpP2(tTsTE|pGauhBsgNK( zvkKK*=kb+gU0*a!uAD|(GEUnKH1j0H3Id;hO|hK9F>F!z8kRkjSZznW= znDNbMBu{%~4SAk7C4XL?Lub4b!hpVRzJQpAXkIM{%VHP4iD+NH*#3ql{Smo8Ln#^J zmS?>w=4lv+yhVCEHSkeUj}Zj-=IjRIq_d-4quL3y3`wLEh)Kt}B~-udhX-Tq6b@da zODl4YnQ|?N?$^F6w&3`B6f%bVh8oJXg-!U^+W+UP61@u;3ADkv|8X0!FDKL#d50gy zII0Ft4h3oxofikGG&OCl8jYfmj0G6owsHP7-!0ldQz4qxV^vNy_02KOp`)}Gju@>9{Cb2{%=G>VF23Rgz1Cn*i zkvptO5JPw*0vePe6%geAxIc4aV~XlDlj?RaIF^%1!>MP^dj2D@GaphiLJ(anMsu~A z^35gVpz-)8_a9@wEQF%`{eqrMjp#5H z$?C=;RPghpb6?!SZHwsW9_;+E?KnM7L449`PgTm0y{1jf8Vlt5rEK3P3b zS&rnI7o4IO$$E3PFO}BB=?Km%u{|nbuYFj$9vVM_bpkigmOEsN{My0&t98z79AVXIU`WG(gP10g{X|Qh=3ZgZWQ$U;)1eKJ@YFfSSp_4bpzb8d~!#;-UuasKvNCSrVRbev(h>#%&(oCE-QOzBKS%e5ekA4c{ zlFIDk&&;Y=RSe`nCa7_@bBmKq?%prw#%0g?tU-N0OzYa9)SUe@ZfG#5ROU(+DB!Xh zg7vrDZN$E#cGkC~WMq&)^O~3z>9Qapc81FQ<67@nrad zHa_yjYpZI+mY>~wKA33dEHLYRnDgU3Abrsyt9caKKif{gY>MEM-OjhJi6?bUDP)oU z^2Yhgly86T3Vh%(@qv5g)-eez1Qt<*(>@2s9cCZHgcamrZjxfgy zUVI;guv*?ND8A1YP|yhZ;^kpZfg8MF;T;hj?eOvT&{0Tq?UdqmJw(xTR=>i1@A{c0 zkRfjGGFnuGJL=$m9^EtQ;yFH`v5Xv3FTpGE-|xa_HSQRJ)*R6vn&h+3wKdDkjPOkH zeC7;Ki=$U0kzo{Aep&1j6@12r`~x79oUN@ zZymY#aH&PCZx)+N52oP`P>yA31}3|la7E$0n0iE{SKZPu*1?uoq14lg>eX{ShD8-h z+#;C4pRqEIB?c@FDbH+wvGf@w5mcOU^6z8-0Jn^kr&-UU@OW;H{t!M-GHi1q)?0TJ zMp;id%Bw``w76vaO*`=@U9(|mF)g6hP_7e9>*CZS;O|Umc;&jAn__ns$kCcO@S(_! z&7iDx1vLP3vM0m#9kw@P6$sQVi4h1}(P6-mjDF86X=H=lLRODjj zAi4;IR-NGdo3EXAWC86%|355RHz(G(vQkrXi;5~^m?j2BD2i2T$_1d!BRHQBM)%>2 z?1L@LMSLVDOLshRsP@e-sWTM;g)58wjV~mToA&j}FDNzUt@Zv1OJ-pRpM+&e8c_=m zw>ris_MAU42mf2$`5qlDcdSJzT`w@7z4sB|qXARS6dceudPES*X>OD?%|zw5pC_ddIS@3S*!<`XmDbIypi zZE>u%{VrH(SMbE-i*RYDF))E$EQ6tBSO=qsoU3da1E4kC{ILv|$?Lnu+F%1dvQ8Bg zJTy@#qPs2z;lSByGusjj#UyWJ4EmPDfFIG<^lVR#P)FxadJg7mO+KAJ6y`nLZ*^jk zclutzJbipNg|YDQ@y{Z5TRL6%izoQcQj`we@6@UqztYgCYk%;%Ikd39Ws3O1 zEdr)XM6pG=SsJg4V~Nq_KQd%dPU?olEyByMNE| zA8?Kz57@1AvCHF8^3%)K@_&$(pQMxfqd?K}ymsf6eyE_TysSS-gq5Mge>Xtb`^ApW zclYDV1KEdX(8W=9!7&wgY_`<08NMh2*>p{xJT>XE&$O&d<$W``>LGJulRw8Ae4;YJ z7b&ZDDsjco2eyK@-zhT&^5e}vDDftn_it~3`eMg_rZy3|VF5%yu6a02A9gFf32j^MRcRAMl!nruL!AO-cU(V3wlpKcbSLM>~c#4L5n-YR5C`+vO!fo9G;b#8O#AfZW%h-%BdJPEZhf$7CU&zirQ}zt#prbXpQ23rYz;#6k6Cy<6_5NR z;^Vw=HWSG80imvMFtmQ4`IuAO;OKY#I9SvwZgi&XV52L2#{&GoLLk z(nC5)`sTkSn_AxM{R*71SzoHDn!3Pb=d<|e?rF&u<~ndj06A4?Dt2o1Y<+_wZ3BfI z>wa)|JUDq@?<2-sx9mJ^-?)3>&(6uY;YU>kqe}mjVkMo*SZEiAgH>M-@wuPV8ibnW z1E1xlKkJ+nN_WFdP0lH@54n1o;1=L}#(*JPTvDRwPho8!2xr;dcOUwtr*3*vpc9j? zFn})I8-@{i0g#V!Q1I0a9=-uI-yhI-H+fW~#=0VfY5y=HTIg-%vCeyTcp_IFy)ey0 z#>`6aiZSU`pAp_7m6J(A6?vrDa0w~*8HW2d>27;m?X`%Zv&PZlT(k!6g)nD=OAHh? za(ld>Q&w60h1p%){kv*vW^Ylmf!>}rf#keG=PdYV0V(bwPqHPmv9BKmRd zDSp~vJWmrylSzMn@#T3Ywh>da^)%?{=&-Lo0@FPyAHpRrGB|#}0_RZt>=`x|@^syM zxe#FyXDqIO`{ElO5i##rp8DKw>Oh?w&L-XzxOj!9ziEi_kJ!pbd9})flTNr}n1ki~ zi*H9CsRB@g4jzFAi%fF7<7cwtOz&6-R2T?jn#HSN08e8S; zoFlv<7dk_2afwS@&gyI?Hcs5Af&gN&`d-1iw|TtN(JI);37rnwDhZApYefQi#6mE( zorHM`vAvHZXb#_G=Fa^skRg+<6p)|tc%4^9=XWOk5NyF$je606o3$%@p1Yo+DOSo! z3>c`${_QK?Ln|}mOG^~*v0AHib>vO-7VBSnEqA&ZfBharvjejj`qaF*>=FOd3H7sr zsr)@NGsWZfwD$c0@$5hu3`Hm%@#{b`o(7T z!X{lE0rgTutD z275RSjE~m>TUc<|nbdG)|ni zw}Z(ny}A?SN|g5h1M}mCc*rA0iN>-7FG4*`HtBU731g{4GJ{gNwpJMo@8~o|u&3eT zDw=DBsD8hhF@OAA48T)SpPuAzYW)eWt>RTzOBzy4f1Zh-xI@)G{Cu42);2=fW@KYd zIc?9Nwj4CXM0V8q?R%HPYpcR}HON%Fx4J=|MyjSvfj7uF~1q_MGpk z993m`{=Jz<3jWZXcB{l{aq8Y5&i9fW#^|L(_bE&^Dv^ixb?wmeT*6KLt*ol=O>}bs z$79Bp_Q+w?7Jf+Uw&=`?^^gghJVZ5kEh}#MNx}^>3XZ;K7Ta0Yn-jhY$5Rg9;84un z39}HNiVZdq!m5-Wa-I#G{e5-jGLyHt`e79ssqsiRGn9t2$M|Zz)0q^itR^@x81s4A zyJl|xT{+AJeC=L=2ibovF{H-KF3wK>eG7$PirM7C&yC})JyEbZZ^+X2oEz~2krx7U zvaJ;~4F6gaWGC ztrJtwEkC29q`tw?2xn9|Nx;~V5=|BgO2{H{`AE{QB&9jUuTvoP5AlS5Ecs-hp!I>? z#N&;OFdMLKJ~R1+oWSzN!#8wVIG5qd30}xRjxoTSkeO3d zhgneF$B{LWt}vR8i$v@719L|Q0VS00NG1JNcSd5LmzcS`E?Jmt4Qf+06`1*quJ>7s zOg+3_13kAR+XMyEL)=UF2B)UNP2zifOH`rK1JI2PI!FSwi{oN4gB=5z2t{7u^1J=I zhV2VI%T&&-LgjF+k~u_`|E_y36Kgxq3gYl-JaR77peLP?YBKJfxmQ%p_c@Zyd!iv} z2K*GHO*#%k|0(n19iqnDisnHi_A(5L8K{2s0@T?zng-%$bX=TIMn`$~ETHwydms88 zsjp3!k;2Q>mH{ul*WMM9-I#~+3$hlCk4~9l_Am@5nKX|1 zAbx4!FR`{kiapJno=!HSgX(m~_T!8-CJ;)u=l6X1NX3%wCAWwOWMP3WIoU&nPLU=# zHa2!-bd+*VP-$QNX5P==qGOf%=3szh&$poSGJvr<4`5)z$#OR8uSm+P5tgcGd-LoV z^8yfXGXlQwC5)Dia(%z#TORLn~Ke|+$f1ob_@TYb90 zn?wK3DjhDe=Km$u^6+ZX4KS(7H*vs#b~xIcb6R&}jdmy< zS)zh~-=7GW@wI(sMDbq0QGS`-+9IhW>3i=%%jA}oit+|z_=wGB(l`ReboBfpRc%I? z00H#X$^01j!+2;5(q3GU)Z^y8iL<|7oi4XPrKc7F4ps!EEEjy|N_DCn+3v6v%2 z%@mZc^$)U;-Tbj9HMYZbj|~2G|Kz@NjW)63+br3FPr4L6(yAXjZo&9E1^h*cJ%)jn z{8OQPtk8^>X=(D3nVzva@*-O;>5L9hhV+M)EU#}Iul9fyw`~n%?_fx(AF{;>-3|(U zhCxH2pHw$8@syuIPFaS(vg~i(IWYnwX_6m;si*m|1D!3kN_t{ZgKRJzZyzL+lamX+ z#jCh)H%8T0>k!;LQ{V3KSEQ@(XGzed#EUYT6+23;V|Sq$?L&aMi>04ncWJF$^ftAk z6r4SCd43k8oD|oF+8DiSo>@FHGV&AqdALk;q0EoDmsiU^)Lb3+@LUk~(YJNgpP^Nw$^CRC+kGQQ>2&_ZqQ1zg* zPgw=-`ZiPa_tauwWxsU}^m%NpmeZ_`ZXcf3`p1pX>3m*sIRDy6vx-K@oBmS_>m5gp zYRltHR(^@p@aLI31Ui@ua8j7p8&YC$jQ zu6auxLVUY~_IC9NOuFUMa|xtj)X@>(z1^I#>v{EWCR(;PppCQOQDkg!aS(R+Xl9?0 zK)%Skn>xpsja#}>2PW+CdpB^yNqJz7+>Vd;{2j|y`tmuAtMwV zn!FEPN3|=YkDvKba!Lj&bb`E%tI&&jPcG}9b;gDaAK004c)nxi!xP>GO<4o9n%u9s zN@fOis1sa}5h{PKt!35KoEHq#tNdLk}w0(Z>wD%{Oj$8khoSk2YyKqgSpOJq>S z5XE7Vzesg^emB8PZ6U?yx;h{|GPHD2JMosE7en@st>Qm8$l|R^0fG97EMSMD%1t#< zqK2+uPE#IdT%(6HoO9``j=BDLA_O#3FLu@24@4_kY-mM|(Wiz`FPapbM0ln65qwP4 z(tQDPyCzo1ME#P3n}`b`{%&hIK-3L-uQzXkt`CO($mjtLZThsRjZ!S#(S)g!$Y_1i z*R2n2XJ+2fH!v`_-cC;%Ku#R&>yrmiN%*rkFmAaz#yc#FVtP2NtV(-dq~PhMx{tMk z4UYA(V3baDhKO>oCf(Q#`b3I{LSMC7?rs>YGBHyl{(5ne=MZT$r#n3Wl|wjt?3Y36 zp>6M=kqJXGgI2uve-y`Mwh+OyfUvQQKg-;E9kA$=gzmpjy}%w1j^pCGrw2D(lT)~K zB$zDEUukxhO3CJ2)u!h5#EftHl6k-R9_3NT%`hHh-#c*WW}+A;CNMjr(=?IuIsYaC}@cjp9JCM+)ACKqd}2FRsa4R&`U@$vD|CwRB%u*PIN`(z9bieU6C zU1Sd~b1S7ivyI*}S|ZV3y;vG>eFwkY3;R=)Elxfo_DY876S{nyDTeEL)Z0(CW22Xf zU+gj?efVT%Nc^J?XR^+yo!Cp>HY0;>bJSMPegMHw>H`1p;f}3AD%wi8{*Qk&{>mkS zA8J6Lp^XhB4-ZAfrX#}Adc~IF9`gzLSuxv%#z)=Rp1c6&j#!tf`#1jcNY!3aJrek< zzs!mXj>MJr!PKq$A2~v>fYtB(XKHvfDS#1-^7At8HChpshQG7qvo9RBb*hq?i2S!` zD~K`=lWinn#K=U%fu7Umj)D4mA|gR&uO)i8Yn{@Bsn+}|6&T%HxYPiYV*OK7c+xmO z{oI;q0J2m3AFa3NE;|mb$Dq!8HnGnda(-nwYRB#PiP^{0=^VF!b!moQsVbn$AdyNF zS)?F~^Yz!hYM?YNqR>6eWl~k+>%CNGpCv13{ZXcxCEg5-0r{0a9Xr&J_qZr^2>^E`pF!i?sidtPSfJae%f&}tB%Eh>O% zT;ACjamIG`ftfRiou0-ONo5b01MPk?CgC?qWz*n2PhZz|5?K+6 z(cbFrzA@`O?1hF=!GkFUN=s|pi@6a(ZbT0lzCk=!qekB^Umima=v>z%`N<6W|7 z((fj2EYz}oOwp#MGL!uJSzK!m`tFVN;Bjt(U%S~mWnrpx@qm%7uIM)(lXnDc%qVBX z6tu!w`A}$`NG*Qj79ykIasWRHUx1sq$Ip7Fs+30+&wz% zQx~Na+WDhtj2X9`Y_qtmn<_d3ZPcU^W=IZb}>>x9!|@WWZ4<(`@Ac(2b!=3T6*lRTrzRoTE- z>&_9rfef4PR~!e8Ty*f~;Wn1cqVm7#4*q4+y0`ZwmJlT2%dc(cZ zppmb{kL3caJU8VZ}z`7tkCug||pOg?(RRHVfaJatY8vIV4sFy^2lfwwE z_20y$*+FBJf{Vr8-(HQK4*ZtQ-W4CaSvhNSy~#T{Kh2vjQM#4WIyMBhWsW@zj6EcB zT5tprEt(XbjIN6h3Dc1ie3Fl)B=$7%O77(E`!eSBTLAqaC zKDf)~G(W+D%(I`v&=R)fH4a>qp{s6V#yUGYW9Q`L6%@P#w5O|0r+1l$4i4U!6wy$p zs8#h=Kj%Km-B5gbpffW7Us6cCcMvZo6}Kg)(4;lhmmP@uTzLaPHse4B=wuUes+h6ccMRMy4X6L~|J z>Q~!;B)=HZdIXA1GYifrDpDO8qavLL8QG;pvL;3F#BDvaSGz2IpleoA84Nl5r4zhk zsfZ}CyseKcNW0o z@|^dOr)$y@>kN{E?4aA4Cjtajc|ZqnXJ%(#_9P+<8Kr=SpV+(97opO`JvHgMD~o<1 zIK00oa#7bByWGHa**}o4W<(SAQL%lQrt@V6(6ev~00yuN{rG^k08l59K%_*b7uTdx zJb?vcRHt+cR(It@C0TorP(Kj+>-y2!18XVFA8MEO&kNB8V1Gf)+8`nYZ`MoepP}-r zKC0;aC}$tbUOExS7LOj5<>%jF?`D#o?uS`B`+jKbnYHW(Y?ZVkV;{wtqm*f&7uN0zh=f89oVBBnW|L$Oj zi+cRNA$(%ELQ1Q?-mH!{GPr&H3@#Mu_02<(0r>}UVg{-*ryA( zJNnUPTjvWUzkJ~I0V`Cq7kl1%-b_i0Tr!cR%;x(q8y@Wr{&1NGNW@h7rD)~xf6cdF z{lF5bHMlz=56{F7F`06!Dl1o`WI+K)ec_jYbFXzb2boG8eq2ON<`eW;+S>3JOAZZ9 z8sKnTvY*?C_XyltDghzP0ZLb#e`GlWc)4WwQHK$L;trF|jKtZ)~5MxS8J|b%Msg6GB%$lJ7H?WM*k_SgUZ8nA5sQm`f!nCXv>YizLf;kU7mx zCNPtuS!3}_jQg1?fJ#vE@fWLCdl&xf?CcZH&5A3^@bA(2l3KqZUda6r8>}tk)o8V< zN@#WfhlNw+ceaQ#%7lF?nOqwjEaB$Ktw5L1Z|q>JkkVjN>V3)KGm2?`7h@qd)i9B_ zC`iMUfa979ynFhdTSc2=fe3?C3lar-yBQsw_E93v;qN%zMQr8a z$ma|`%!Oz-B{BV1>xnyi>JZEWV#wTse7%FI-$;(7ricIGv@upk*I8O{Ja+aq*Hs%w z(;?96^l|ECA3^pt{%@U_+WR}-P#5hxJTcdS=_E&PnsIS9zr!5w;X38%pSZv_-QqXj z$HS75$)TlRv~JLs6+Mt+7Hn#E&;ArW^Gw!-Z0x#4{whE?&z_V4r_(=P2n)UEDSCbv z#A({5R5IOl*4C^RISZ<4BHz0X&f426hzzdZjpt=$WH(m&eoYRC?o8H8>%{*ZEXa+{ztN|sQl3}+M73>rCl$|E7Jzs7TXm&bY2R*zI zhMDzTCL?H#Af`q&RiEIZ2H^f7$K569tdmJO|4^xc`%Tz9+HFdPf^kc_|LfJ4KQR&< zCDy)q<)u9i^q=xdo}uN5euxe%cn64&pWO3A?@25wVq_ogK5J9pJok}Fk}I@RGa&Lc z?L0U*1y)Ik1!QXm!;|laRs#TH8sbr$e2%1(sXf*rpO8^#zbN?@JsA~lecqdi^ll%s zC~hQ@|6)O+Cr{59Ay^SY02YgTE6NDk%GZ?mBKg4L>S|36aia&;F~N8hIXv4~-u@%DCAzp`s5j}ymk?SP=QL*ZPqPb*9)~h zZW8{yi3oR&^>*9Z%1X%)i`z5SveIsWZpjT)_M@wGY~94vQ79NFkjnGcS2Wdz))-Tj z!1f5Ge~HhwG8ql*O#x}C5`(`Ftvvo*pqL;?msC)N7Qd-4u%Pu{zPhBnyy~VB-aOS!KK!t3-adb z*}@5g;F=WSNc#g?IT&nS1$LB(gkvwK|f5`50@ z6Eqfmx7VWra(DELdmHc2&CGuxEtYdv;&BKAB%jzbmqk8gIM6M-m!FROd?Bcuq|ooe zpv((5$oL;%Z{-FYS@`++L17C`vZ<5+r5SiVg zy%6KMVJ(mE?e+MHi{_J#sp$j9(XRNQ<4Ki7n`s*Nfz~V`C|G#UFOx);>WSpH$Sr*5 z^p;EePQcrIlS%ZnA60%&TeZ4&Q+*cGagkOzKMU~rWe=6v-NLNz!5}GW*m*$ncwgS} z-D2ryd9B1^RxJYD2wHh9a|5DcJLjig0r7v!cjs;t1e;=c>meD+Ap(xJK~@N@1s z$3;)kum9eGgLXs!(nG8Q-T!ED~ z4QiipCWWdoEsHZLpDf{2Pj|axhG|>se@~E#lDxv$jcl#{=hegtqS4^#{`eFt<<@0| zf9C`QXq`|fgUwj>`X9ZH(Pf`iF&UsIg5qr(v247}biP`5Lylgx_!yrIb9vqz6rE@8 zLYFCH^ZZ+SAjepOT)b8B3eLy$R~iTtC=CXmkKa7J4#|`9UgfgfYwbXHU;fOpPpwu7 zRj-bFF%m=~a>ed{1rv5K>$=1XJJv1~oi`6<=V5`Z;cyPS#9O56r!G+`yGcaf@9TB+ z*N+EB8Cv)1U=UPr?x>9}nO>-;T3Hy(c=MN*l^r^#Aacv_^8L`E^x1nAe)Y{CkyJ?L zem5JJ9hj{LSVevO92h;_O(ts7q77NtXnp(d0nz|dd0#r&dNG%z!3vD%GtLO-kAqlV zIzKX)sKxhrVWffU?nC|LQ^pe7n?Zb7$2mZvD=3U9u_{lel$Mq%$hrT(sAzds52Nj?M-2XPyq@|wQWHgVil1%xfvnWKmJz-kY5u|vWVurUoSG-6azf` zu4*1UL(S*`Fl44f-zG;lIB}$xhYevn&kYj^hiczh-r) zWAAIqfvGS!dhe;BqDyTYI6OKrC%H&g#T%LaRAIe2Tzz!fH-rNd0vmv!meB3)UnsP) zpNR6E!1B_rm1}jd zD<@)*d+XQ{kXBOZ)C%?UwslZ_DgPXV74bZ`=IagrJd4Ns6;6|%LJM1;;l;5^g!gh{k;Qfp$0%a<{_YR2$W0ac zXP&!jOfegcU#QwuZj{fLb|lk8QVBnzja}X=PO~Y;*`04vcVT2I=Nw(#d5Y#J%m5Hp zm$Me3wH35_M@HgqyY*OBkaq2fSqNMxsY34EC?G!;qO~Z0Dv1`_1@6LrU%X>E3%s_` zLTFuHJHxuHH1VVCwV*R8q|U#)s-(JBnA2$+kun}#kt4Ldsg1QR8AZ_MDyihUiHWjVJO+kvsYUpyJoC<3+BA$ks z$`3Fm1&dy{xSjYWjn*oMp*lykh5y#+PU=YvffwcD37+b-6hpivRWM-XF9E9Fx&sO2 z1LSF`PT<-yLMM#p+}#aGFf2RCGiK#4=YFTYw73i%2nZ%S9Efxi`?|eRUr?sa6RsDEML1>K$fl zW5h7og2gP=5Sc7&^G3pOTC~z2Yep`bZ`+*eiF+GaDqisFxuAcj0-d|j`Q1Z5SqJ~O z8PNkt7nIv3nGBccm}o`g{FSD10qa>e!b^%9T80Hu09*x z3b{$eA9Tt%s75A>lL%{r;k&{&^)2zUWb7t$@f#qrjvmPIJ<+-@@^sXJC(`>MCiQBm zEA4^xw|9SvjB_fQNnzzLzT(uss^;RbOA8JrRiAEo>b>(j`n7{)CB|>?k(3-ICu$~+ zCSC9))2oA1<&gkMfZr4I1T)nVQ7jWaXM>&Vdg(PSO~jKv2r_v`RuF%q{787UXDbIoj}+O zrx&;ny?A>FO+4$6(*`O%XDYuOD8sjZ7Pv5?lLBoB{dCLmF9m5;9fsOu2GTqI&-}8j zUc&ZBpY>C0oW9bG`tCVSKmmk45>3CinjyVK17<)P=KtzeKE zF9&r-iyo=N=;2maIVe^Js!gZV*7A6Dv;7N{7Op!vK1e6Guo6X3mDT(Q9iFN4nKyW_ zqrSn8nFR0qZlt z9Mb4xFP;WlGl$imOjAux7(H`wiDuI2gr9JW)r1ie-lg3{_{u+yCW5_}ze*x9nE^?? z4w(1Ce1Y6M91MCs9?XsImcv=eiioIz`Ar)DAn;^bffhox*{j$Eul6_yKi;+dq-|HJ zqob9&HK46lz#=o4pW*eOn*aKjN&CaK}zoZ6@PjHmyxA=A* z1sj+AZcB>oTAW>jfaJKvb`}PLp zrL?L=gAFVxpV6YETH!{_7ORlyM5zyK$P^>`BJ5icg>;x{m`maS4y~hLjK>ErWz~@l z13jqIF)u66#Y)YiTR%fOE(*TLSbBmV#K%(dM4QAz6Ek5MRs#kDHihE>Uq~=eQKTpI zAuB$Hh7KL8dU*d$%_D<1_C=++Z^izo7d6{wM&Mu>G&Q5ARgsDZz&rW{nDsHtHf~g( zW~-<|#N-$yxaKO<+xi}lmlE1w5TMUJ_skqdWpvU^gR;#^z66y;mPums(NF(3(MunN zllmC}n}@cIHgf2sxQc|0L(WdwWl4p~;fb`lB5kzwUhFT|zhb?aeDiDs_1J%U(}y{k zv3nPn{(&P%KLsuLEEsow|H=2pppRY6QiV{C0G9E zJ#)psvswx!P4QEdDi|Qpg!W+6`~1(q>}8v|86GU%GVmJE5mrUo)^>aB>-E6bisu74 zjoPf4ylS7$C|o^0a(oi|XKZ$S|1vJR^+Z(Q&~A4`EKZ%c#hWe zAHb5()+$VSD7>Dnz0r;{SCiZ7m0y@sDuKL4rULr9ISFd{sojt$gjgHp<1J|1?M=pS zk{Ky1`gN>IR>Ti~+%_FSd!*RTp;q)L@u6uaBqt|V)6ftEjW=b2cm4U&l$DeCQ)diy zbm%t~V)AXpzZv|u^1*d=Sm1%F3wMq5&od_tE5=WXyp0|4xP+HS$j#> zu@z@t&@Wf;5v-{;GI3aSaR0pcwPFXc-*}#4mLEl{(?lkFWW1mCd_@7qyh^}wrJO0p zVx?{@g3+f@IH+kp^>*KU$f>~excL}CBgch`aE~T>zNW8DW?5m`Tf0WjMqXfdt1ASz z88B^GKWw5_vc}IWxdujxS%0JoK!SBiCAi%Obk5zVS8r`?Eq){k$}El_(+A=t-HxuRcyBo>WYUkx`Y+?{Kj`3*X*uMpzB96#D8%pl2t2`tlO&jjq6aE4q=>( zF?!w(&*1PDu5u~zY`k|vE)4U1_Yq$P9ARfTcS%@l1OUtH5TksINKJ3wI&&I??zu=L zP-EDRrZ-r#8zLG>|MeN*xF>Kn1i)*YTth^YLdC5c60*o+$JZ`4QIQVHdy^4Ubn&J(`R-j{|OLzaqM)tv5Fl(zc_> zef*$xK4ucIFjh&nNP|wcY^I}wE3aiqr$_L@%0TcvC~-$bM@L|4jYWOBe>&qo`lk#% zzUy@xp?+K^CdbVv-hY8j$5!fqI=*sByeACVW!kxJ_vmQb)TlbV6n+S9KE3Lg&iKT= zd*nh9oBkDFYG7+dv2-70U4@-bH1Uwn6BCX@D+_*jpH^1(TUqSmK4@cP57Uuu0M#X? zh64i(ND7FfyV8@P}dG8V7)};Eeuz@}8q>wlG{zN>56Dr)v*-ezxq?P*?kgU&kAY zA@|JJH+fItK~Autp;yz>)A7gk49a9VminoTez`)a8>A5y^tkJ$)g_?D|5cCQK`q3# zS@G$Gc-tKOQee?disH90;O)o>Q2U9p*!k?RQS^{I!{RsC)AdXPIm$R>9AU~j2wwE z&HG;FEB0grVl^xtD(uKR|Huc9#u*LP9W=E7zyB-yk69cSnPTX2Lmpq#o3#%9L`aiQ z2L3dSG1@E32uk6~w-YrpyWK$`u~GBm+HWuMYI%}NGAUFFNDsFfYG}NKDs=ns`T6Nx zZO$uPk^=_OfHxQ>bkjJr^kZ#g(|exjxw+QdBsEP<5g$x^vEo2gdlxfN`OlM+>rtd5 z&us8eaJ@v|c9ikVy5Hj&h;-I2q;H!9+VN~PkIUbO*w9Och*;3FR|AKf3VUB-glW@? zVLLvZnOeD9S|aywJI6A;F_KyQ;J;O~=(dnPB0v~#Y+zu=j#TkqaK3fyJ#}%w>NjEP zCbRMU%g@oj3R?Jb1M4jhuTIZv^PaD%zU~-uSRP-2^MF^G)Hj1l=BQ9|h2m5or{)QG zziz_0ulS3f4L5Q(hL(P5guY|YOO1p?+hwY0YHor!U%!5hfr{*fcbv>ZR@3B7y6BCl zQia*bJT!#36r|Pl+KSLL^fJJR+?3>G#6C?hi@xsuIMfV!QSC?|-jQ;eRUL7^y_Xnl z>hhZLvt1bacz4a`y^`wrh~**Z%w4cpA%E)Mv+(iGs>zcB`79B*2dLJHf{Zo)ovwP8 z>%U*x&-vMS)`h+Oj~8+s`-)k8MtMuXhQUh6Ipq2EU-y63&b18ZX;$6LS$}aexBdOb zR})Xn3`0w>ws`h=w_4!AE&rV9|vR9OiZ~$5WVM+~WELQR{Rmt#4J7NiOvb zLC=(mOAeW-)j>RrpBFAuJh2A3OAoxB_=t$p>cVKlK1Bj=O=}D`*)gaGYMc) zRLiswqWFBi+|JSy`!>iD!xHngwzhWhqnTjNl;Ny^8-){sS8pGpH2@}KU$pp$$AVf7Xa^A z3q^KaU-O4{^=G{Y_|O5LW!e^qBL*57rZa$9#5Yn%{eRlv&_s zy37xA-Vv^E3e#okopYbN(Pyy7e2tHfUt~l2G3M#xiK?cc0Bg4&G|Zc@vE zJH*F$`f>Pj!+mv(Bf;j12>}Iax;lLbT=($sXdVSG&&_?G-s7j!GUBIS`RKQ6>zTC&Oc1rPJ?RK$cmIhE$eADQ7l5g17YONX5m)wvC3L z4En=g6YCop1t`X04W6Z#y2KY1y(W;4IBq7P&g=K`(S>-QI)0T#^^4)(zRt z^L<#VhaHMcsYH6va}c*#iUlvFsy%jd-6t4>JfrCq0ZR&Z!X;&8{aFwEtVwPZ^kJR+ zqrmF+wzfbcPngcXd)OBfd!8iJE==mXuAi#>G6Laq%+2)4<2es`BK4;?`0?u|Bmn`4 zpM`bF0Do2l6BTx_6ju=Gc}DEf%r{_ykY^_-T_Fo-a=ICaG3}W+4I28q7(A4 z8{Xn|S9^q#Hvu%`fS_{?v0F2?+6jlZUZW!vo5^|iD0#QPOPtFe+Ux4NudlDgbm_88 zL%2zP+7|w6B$hbEPdbs6Sa14;z>@Gr4%P2=x0qRN6oS$dB83q`Jp)i#zP~+|CvxTE zXLaaP{bW%FYgoj0bgWn*g7oMzz=4WX78Vv?>Xu9zKIHim^i0(nQ);h1MPVr>)L6-N z=+(|9>5iZMA6q*ojOp=gKv(;`^R$nqQ zECV<2^X4wH2YWtxd3}qHj_$WsaQ>_R1imUKnP)PkS_#nAWEKm|uri#Jvcq!7=!BMmI2)6v^l$Q5gR3S}dIPEM*BF%!N!H|QA zi77oV58`DKlW%Ti75?{vgTWdE^36K+xn&5fu+^sEnmnv}r}F}F%|%2sOZcL98k=n1 z9(^lPB^4DhBy|(H<$tDVlzI=D#?;a9-~L6k5U?0C)=y0DOGUsv#N5}L`!#m_KuG)jPefiiQ8#EI%_ ze$VBB+5~sW1SV+1WRL0FGYS2h5UDY3=0}R}r{9L3!nimBU$L%c!{?n7@FDA2pkhH< zpdO!;lxFkdvW_fM&?6~3ZLRp1u1B&JTe3t<9A4;sMriB&5144~4v(bd*!{hDeSQ60 zlX|LEMRj$3P0im;IYQ~4-vw<5uTXUcPbjgn4u9p3kIt8DUm=6RRN1-A5a#tg!F;js z7_WeUX1M@h(-)15)Tkwq=93%&LRT{;ujHut`8YYRp7`WSZq(p22D;MOR5)3_GT+Xi zR7|R8r;YJfQy(=uJ60e5l)HnVp6mM|@p0Ce{K@%vP!)aeu2|5lm-@fw(YYMMH=I6~ zdVi5Qz09A5ofdHg#o@P^+ zVO+Z(V&m+JVG10~d%DRZ-@Bd4ys~}`FeWl zR+xgIjAG0e)7%(ud+H)WX8Chyb`QijygDi>A+~kh6==H)cqRRkpNmb%4=Z5nqe&&( zl-uH=QZ3nanWkAtOGKj7l1Ry$Sg9s7Hd1W`wx38Y6YIcRJgZ@9E1X`y(5`0?H?mUo zFbn<4Nbw&dhvvg8)5smLJp6~lmFpx%Gx5#&aEP%-r-7n0ajo1m#6#tSVUD=TX& z1vzU41%=*6pvj*<`(^uOM`c45Q^#kf?FVIJn`s?U15uifV2BNuF(>d@Fv;2KSWDZ5 z_VhKy79~VdPK7#uNxY{-^9iHzGJN=`Bl#vN%jvY1L9ub+kB15)U&5#Di4h!=4shZJ z13+{TvBahCjavONB&Mtkq4}%~81P=cFC}PlN*??)U|kPA^Bc>R7#`-zW6T+|Tr_xJ ziV+>AP6zDf$0vyhuObgh8{o#e-k46X=;c)R$t3r{{ytDXtU_?1%Y<#i55D?2ioKG# zofDbdwi&^&>hpWsY&mm#*EPyn{`*e!pYo9PHDLh;J;rE^T)Y6w{R@9H zS%*85Qo>!;Rh2JqY2@D1;5a7ww`oUZ#|H3-y?2kDRta-p+~Ah(e--h{xBo2HGdYW0aZ0s%~RTKjGg3nlI3Mh zqk1bO{Lb4khe`KNL@nkXiKs9HLciu8ghyd{XX)A96!7kO*D9@Ljn_Xl$Vh+$z zWEfr`b#bme-W@~uN5SpE9eRxFe9{Er` zcL*W4LxAA!Fa!t;PJ$2aHb{cIyMCSXByZJM#rzm3s=D{S@3pSAdv`bEeSwjY-0hO} zo)(;;GpL@~sqH4Uk@5~hd}?#NuA_I-m7LBb!8>K}wPwXI`e!ND1cjon z%uz5H%$=!uV23!t9eXKyJbCZ#BUKUK_!?~x`6`s!`71a?D*Nd*E#qiXqU@M?Ty%TQ z1AnY9Ct+x6Hz%sNCW#w1X!_&S5BKiEuKRmXN-{-4V}o|_K?=;JNvTF}|IWh>3YSu! zn!W!Xa8&Eg!*M*VvB6Y%XlxJ%Qa!2%h~NuZCh~2V`5z${`I!(({Vb@=|lr3Avt-8 zP6bO?c9o#2q-VY%rjNpwk3o=)!t?ehm84WthsfXy!B+(zWJeza@pt1ac*$f+q!&HZ zlZR1mH7Lo#*&W(+E$N;Xc#e*|Uz3!`)bPg^8=JN-dBE0l*QK9L+3~nE8LgOfzz-&N z+-_-WxwyFe%dnU*UVV~7e*V$^t%E~!Uf!cN;{H|ny2kWSMaZ=RmRsi+l#$KCD>ugt zQfd+uM{4MOOLsyOpOi!&ARD(LkC`_Ch1p zYSA1?PwEP+bee*JS*UR`Br3LV(M^`5bXE?d;d*f%oooJ@F*MyA78=HI7Nb7Zn>Rl-`MDMzfT*h4)u#? zl!?!R-a_9W{}dMsMW_LO=(K`41ZNG8v5t6b@Q|Nyi8@&cK+mw9lF%^^$Ty- z%aEV{Z%Ey)a(jB4lH7Tfp=*q}sDj!>6}!f>;b?1v9s>ra9b8y!zKb)B zVB8aKJnHjW|M?VAte07xv#we4#8S8Hy6z%PV(mPcY~(3Hm8ysA9<%nvrOB@>InrF* z1*Z#je0Z;-nbDoZ&8V2x&p8(yFL31jxH&Ys>So2D$9FuFAsSs3$IiDT(Fgc?HA|Jw-s;xdr%w9<`uur^%c>2T zHj|eG;nQY+B|6cnE`s@r&5u2K;JgYKhLxt2?bFr_k$dd7pXeuUl~J+bT=C>?E0$w* zmb|1~c0Q}tR07l)?|LYT{Aa|D7*z(7TGfaX|Gp<3&FQK4;9EE^fTfLKRgKT%vTDPx zSl&@OA7Fh^6Gk;!lu6Z-f|?|AP0QZwD|WnT+50*gz~Ur&D$dNKND{mix?K~-;5~bP z&*|tU6lC>@{j|Sl6fMlo-2*P657nUUVz&fRQts(-`LvxW%Kkt8=oQ9#dbw)!;Rt#Z z7*q@xuRN&Hg^&L!wmm%^wEl0J@~2O4)0r3sk%0;vb=ARz@rMe9q6VCfR9vMe(Oi!` zcq<^iyQ$eT+oQf`lNIegnP^G)ur-jgI%&NemUzmLTJlndDwSP1X?$G$BgI<}kCZns ziakO%E{R!)u@RQf%OJu@H&M!fJphUDtuJs zj{baeVgl0Um0Ys-@hb zTm*rk@Y!j((X&@w@85Pt|6f}HqG(sIuwoL}hWjH6XF;W;%`V&?3dioeA{#yZafed) zZCZ>dDy7q@!}-fmD~B}66&W-S-9Y{qB;hF;N^*l?Yw@|~@_FFH>L@#AKz7p&3IY?L z8XGP+$S>^biH6r5w-qZAPwF?;<+0@2gM;W!L9@ApTGvC09pBHAM=2DM>Co;8(D$Eo z`zL4w(%Q!);*5`pjeMtU@u?m2Gbo4G5BODLl!`w%THlGjBu;>pl`-`8^}V!esW)H| z%u-o-4&K-b5Yv?!+;B0~mHcyFUteVMJAZWGtpn$LKaiSLw$yJwj4YaEla$88$7kKR zH@1{VUL*L;C-_e{q5%hal~iFEL`4+i3m<%H501vmX){UJF{Nz7$~fqf1ryFI&qi6=|5+*pzmdN^ocN*GAi3TJs3JdbV)TU~@x>@hSo5 z)pOqVf=%34V_DG7VLBA_y2GR33K`TCm{cP>5 zqi`oVb9yYZSK{A!wl2zQfB3J7eK(Bt3$JX73l-~19e4@~^g5yb))Etx+@D;i!EpYo z#i#0x2BVpmBb5!Y24>W%K{yF-vK`q@o)r7U*L2WyKfr zL)Qx4eJ(Xo_EJtApT;h6thx&8Jx&OLJZ=}Nde~u4Z6bmITLsLr$%r;%INvsyy6x=j z3^?&Cf0Ajt`fAD|6ciM59t{dF+}uc@Elo&&&#Y#N3TCL8>8XdMK5d%P8h@1k>$FL3 z(yp)Xmu_)iNTpb6B)&;uJHjNC-ou}nXYPV!c;5|ojMSYyWmS**Cn6mU-uIl1z6YJ) zkv&}7EzmGQCk=aL3VGyFA*Ww#9XCF!jVR2lD@VCir$Io=&>@`9(STd}Z#PYtVyQn;ywN4v zhT+uWVd5VWZr|uLBpvx5sm3eO-ZiW?5Jg2r0nF`Ye00apnKl%wHD~-V7!m&awpLOh zT9wvkKgeV&0X(TLMwwO!sSdcXTrm+#_=X7@~$x%G?@y2gVRq9OhjlKm`sjrhqhj!F!n66!(D*tIdcdfw?n zX%ydSL;Qg(-2-RB9^>rn2Y$f)tV`=@$EL9YT@Kssdv0XI*O=b@Y@OT)e7X}D8NS~3 zA~@-V2S}-ZI)d8p8UqwRveTAAUPevv`jO^GqSE6|&Ld%jl=<7rNEE$tb~~N*Du9;~4ECdwQqucS|8( z^`^)>R#ogN;PJ4>v}8G(aPC2#(D(Up_!t&q@iI-8N&!7`_Lid_y`~H{%jLF*ghAD| zwx?1`+oa#^`JI=~;hL0l|FERf=9S4kU#9C-m6#KUo|H|E?0}6){g?_)$Tzt1f+M=V zjvVde7oQI)>|@_vDO9iEp4B%tz7fpY$~yAvE51_#<2#eoef`Rg@xXLPIzbdlnZ5N| z>xJ=O2gFQP2(ivNFohICi0LC2bko{ENfld)hum$M{bH;BV)vNKu6<4}e%HqIi6c!5 zy4Ppcfr)I{PyPl!R(~ES**zOFb7PX=yN`nF_B9;5%X8q(`1EU`%9NXi>Qdy}+}eKH zX2mA|c?+v)_C4w2W>27Om4DDTj!t;oBbIgg%J4VOi?K5y>K7v5)#le)G$xv$uJ`g3 zhNa~)RAvRXZDWd)yKnU<69k?`@*KPBRE?PF%QG`I?T9A9VE`ffWQP^o`oFHitw{JI zqhSKhtIu!zvK5K~58faa9mhh5x0@sOX6~31QjzycYk%ya1)v`Z-V*PtE~=8q3iCY? zKA!K1jjSaFeq3>XX&0!`>syfsgr!I!rp*Sr&*{bEcJ(v33iW%rYaC)NS6zl{{Hq}Y zTi1j5%Y!J%qm+uAQ1p(dqviwZ=mqHMc6YDkZDVon8mIb!|BqvZ1k^6psubChI#7Js zAY;wIw{cCpa?H+tY6LMS6cnyG545gEP-`$n(uEJ{VC`)-{yg@P!&Pe@-4%-lYP0a zZ|U0|YL4K8rl0qzr;R76j0W^9Hbbm)sbD3Tk@yYU(}|tU_k_Zan7*xA)vMKC>L|8< z-mwUhk{k1Vt(+>a5=Jcu0ZYsIAV&WR%BrJ}{F0LcN@ZfLE`${3aL$9FJ8pu7lRNFB znjO9}&y-YElZLG@|Kv=v&F%>7br*dr>$}vzRJp=!H^1hkN2vd{Z2P*5fnRC)zU@SdsWG28r~u8$it%Xh2AH=Kc26EAOCQbpx}TKp_JM2z-exa_xf2TM=gk^mQu zY#!t(NnARMgvnebe)cs9rbkxSem$s=PhWv=mAcB>47fa|^DlPJrdKi zwsS?=mkeyRlRINOZgNE${=~sYci#+Hey@ul>HH7SE(L*RD=RzmC+*6`S>A!Ik8CJ{ zi&YCj^80Wodb<9y+jfa}UyZ4iu>VW(xz(Mtj#bs1n^|FxLr*;i)r%vk#1>rr$uT7) zZI79@>hf~E6lvO|-!CFr>2oGI-Xo|PJpa^T9b03!M-rEP7kGW1cExcZ8Vmt7S?}Ln zTSM<<(cY2iX%joinYJ;_8mE6GYf+lD18E^iCWNnjvE+;~)7w+P{{U&)rj<)aOe;VT z@C1TYl}@o9$%-0rRMzi|EsTMM^nh;r;!9fE&Rt~bn?L9o4ya;0tS~KeKS#+fJ+^p< zJL6&W4qoO581SaG9po=&=CLBbWeY#b|f<*z$8BHkw?kR@pN z2oC8^6GC)(TsWZ3*0r{=ac%MtqWRr~liVDr1xKzM&8(qjxwZ!6P+j7cD~4o+)owZ|glRlz>SzG^=TuSrSmLUT{{76Oe zCOnKgz1IBq0bEE5-~Obm#qt(x@S}#tHqL#3=KO%OtsvC1F z%8pB{=G`HJ$qZRiRr=^sTN+x2lR0mK(C<4_;zqChJK6W|kl#9G^f9xtUQ*%R_TS$N zhe(KLs=fHAOKC3s8*|Qn2BRk`6nPysIi0(c1mmoxt86)E_OOgDwVD8fU}LkL?{%K? zwRV%gTI}57eo!bd%(w691rm#Dn@CqGo8RX+k%unf+tWbH=98EM)1uEfpgW3Y+YT@Gm2o1C3luMLqGqE-||LD@BR50F{ucs<@(!x5LEY?Gp^Mphua%Vr} zJ!(-ZADg;7+!FU-eMoxuSfQ|<_$riAue{r-qef1_@ZsDqwGvQsL*sQg8mOO$)4A0- zfg2k$6jx#wG5&MVt+30I^hXMcq$W{@c}Oyh;#KLi9Rg(nJHZPNq8P;XBep@t?0Onc{aZdxT7DA zi0vw0h7^eR!b{U-ri^<>Mv?hX4bBXb{_XCQqOWrmd2-6%3M1^EclXF5?sAptWY8sil`l#(Jk2z02FO5u;k?`?Mzgl`tk5^13~bov!?iv#=eZ)C*Zmu8 zsa-U-LzbM~&e4D*!yU_~8M9PgYHN>kA4I~dt~vk7Jybn&IXG+8x)TPM=zfuxFgLvg z$D;YaT)Q-ut5|20QI~=eV}vd-fqlI@5n+Wf#e^TEaDK!P#)x^Jy<(e1$j-bf`zE>X zq2yd1GU2G9@G>J&?w_vnah6~`azqvZjJ`C0IJwJ#bW_p5K!}r;nVxR`^!R{q*0f~9 zeM_byC?*zHY&+Kw^M=C9oyzf4bb<(a8o8qT*k5ta+OfJ)@j%Z6Hob9j?>X9y?C3bP z-VudrGbRT@)8q+1f5i=YTSA{Y$jr?Ael4c&Rxnm)kouM1#Lf?X!~*(4jkE$i5@Ldg z$~(-beJ3k^xSrU#2Xon}t`l*Q4MTUg5?}U4&%l;|p^!XXD=7D-TO94((Z<_LlhDL4 zUEI834Bw>ZY!A3#?ILycZFiBy?jGBgPpOR&``7HAOVFlAza?-g#VbhT3rM`8d}2HZ zNk4_H1(?FIj(d3AIOj-SDqdN`2+Na58LmKLAOfEiIb|LmYbepx3|ermYZ~*ViK(Re z6ccz>BE?rAi7D)C3MP1w=AAgqg}QS_jAs2Jz6dSN{8JWsIC?vAndRlO`u8KP%ASyknOLs+eXBHH%leyc+zll@pjvEgPF#ZmukGyqjyV zWQ44er*@5TOoH)jyazrV(9Co=X?${JlN>0!ci&xo?b*8Obix$Sc z9si&!*G-oDU|T(Sr>b*(z=uXn$&uZjb#i`?$$w3)G}X~NSkLBab|POse5RGLX_;ABMS9F=(NaFyvv*VlqgVn00uhdI2`a!21Xi&D9{A^YOcLFBcZvZI zZ>ScT=8?HX0`l%bnBI$gL8k7Q-{#Sua+bHScqGsN^H(3wQtHp`;<#NnvV zI~MF#mReM{K+?b5_3IuXx(G~%tiAf>^#$)DatiVe_fq^<;`|;mq^f#>Cc5r#F(!j? zoo)Ui*$rHu-7DVJ8nFWWdbeO;K?5{^Ou_h7rqs&SiEx}@dadEm-@O3fCh1K6{}KF~ zu>zfnU;HYj(pVHZCf?_%G1H|F40P@V82aj-Z#Pf z2-Wq__T#Y&ha%C-&<2` zlo#l9=3n6Fkih$;843GQwpp)tEa4W;*`X~-AX#6|R?jYe3i&(YuJx+m^~#6!N@vbZ z0$YpcBAzJCPb5AHl<&>wqz;RIUoaaQEv{AvSu7Z*`uK+ST!QLnEL9^jwrmiibQgsj zquEp1V=BFgJDxiE10OPLsTc~8xNoNE>{~BFLF56sAF8f&^gK0a@i+ZYjiw+iOI8+or~>pLu?^ zOt{&GuW!??R3%WuoC@?squ(8a{BqTDtW}X-rf-T)X)cVm+##0{F|shPpD41BQW1F~ z@_4pz0TeD$UImnMK`E9x$wZ8xTLE4GG=jYOTMODjt~irkH%hrz79+SMdTrELM{T?I zMlA`I22E3Gx&QqsZKx(Reyo;Xyu0PeBW1FFgvbs#(0^vFr{{xM;NU9;W!lI&H^^!| z#&eD=m2gsD(6`M^mD9+mlrgm_R_BvEQZXw4vST&jCu%eH$?|>9KBn=gYBUq#u&C!> zGBSw2;L-ozpny9bJXErUVX&k)=cfnsD|hDL6gH%Im95fQCb`5`=0+jPgn#(9&XvVB zZ6KZ}Ce^d@v`MqA`IPdUfNz~f1e36|BYmJpD}!2>*0ZWPDyuoEYC4UoU0K_Nkg>Us zI?oBp@H(DN-1JqS-eX~%_AE+9m_N#@S8+UEj|Zy8i^-G7W)M|F^el%sfCBQi7Zb1- z^WLLqP`9j~9!D27G1jEgJ@u>V3d=4%VM51K!`b$L2Ige&!zH5N-R)rz2QcB3+^6kg z)G|qmgGG+114Mq6*V@x}cmvJ)X%4&@Zh~3u&}lt+g3zfw_uk&#ThWyH`T3wyyuX+9 z{@r}MJKCBm)8vnxymH!n$ztjv|K7gd+fx6xVqiu*Z>+Y|P@o^;;iJXYm)z{uV+$Lv zWd<58Nt%4PXL~QN8kSPEaLy`APi^ z!AmK1&nEL-4W+WJ_HbKL3D2Y4q&qAutjNj&CcNgvZndN@c0`)W7Pk6{KCy*56z@B%4XjV*7+V!+*!KWSKgDF}-?_-4)lTQhiV&d z-_u1%$m^vix}za;6I91(_pI@8b&@_aJG)3DR#~bVW6Cb9Vmk|~!(4lRgf?SqX19B%s$o>vz zTTkuqOLV$P;X|96L!@ED`q>$R@}DNW{Tlx}Sbm)L`Wf^Rh~j+4*q68B))bd@^!^k)ZgK%}d5_is=^Uultv1i-{brojLyvbii&CEmfE1%5zS65fRuiLQ#oPB+4<}88pXYs1HhB->1s8drl zizNKye^)RNs1p=XU%&o!CmJJTA!~{CQ~3`2{z9ntdO~caudm(WQ=GqVMBj2-+-SfS zzf%jP_q_jXC$}=72=SnZot+&PQ~l%*Vq$Bj8A)N|!|}8Gs`;>!^og5o%P5_z3H%qu zQ{Calc=m9^@l83Mme9U)WO7H(fWkGmaSQz5m_T*P9?Ve3+zd=-u&>0Ve}{CFa0o~q z^>s}C-X`SM;j9(Wj39m(jpfNv&C*5fl0vr0VK0B_oStILu!^ z{?rOqopv&(440?HJtV!HZFQoHyxEM@h7~T$#AIN&m$5#FL*wEM2HPn4BxF*(-+!Z1 zfj7M|G2W&K`PXg=ln3Rz=T~;j)#t1&fAbj2LaGTRR;$*vsTW2LQBQ=OL++I9x$U~0 zTkS^NJKojmRG7gp->gf|*zt6-<1{7wv6M!S$Q~+KArFQ{O(l*x&gxwissB=1+S&at z^2hP!5O>Zr)X03bGA)lKULBl}ygW**i08=)At50z1hYomMXcMfPyFg7qr3aIWXG$* zebEf$D%+y@c1#TXKW}_#Jjm~3-7;BdKX5F^5(|*E>biQ0SZkw_fs?)yac&5Sm7Vg4 z=OdXR0ssM(38b>psk4!SC?3|+^J4b`xj2m?gLFaLtWuGgl{MFvsBCHh-eSr*@4y?8 zAFjp7gW<{}CN}!`aMy#RM)AKmJv-9we^;tgA$-!EifAr8zmG9otk{aZ^pj0to%~^( z{OnBZDbVzyy}{8!AMfn!ti{F9Fr}xz{}s$K(V$AE$*A}0!QKbr!epe=yHZV3LLIWs zg(&N4k5W!-)UJFnC*231!G#fAC(K%2k96s>Ih;a>HSfn9ZC42znp80B4L-z1kvR%i zYA1<&`xKgw&^2pnSu%N2Ha!5i+@OL6ee_#`8OS0O@GEAfh-8>*A%M5@h_j5Ne6kr|gLgn;KcZ?TPb-zQw{Qv8Tt2 zHY#cA2&N34SX%9XjUUg*y}h?c%RnFeFz})@@&zzX`NIR9P0dSKVhSCa58?GStof7&)^MsF`fyRbj;= z;9K0|=m|ERGgYc~9JpFEKXWMRYLfZzoJ7HXQDjWreY3@g z6}zvdbmFh89<2R-^otFB+?D_^JzZbN+_C$b9vdhO84{QXiHU#bPw(aIxtH9^1#0xb z1nqdVC@CwemuW%z3rIL|ANQbWY=i6ARU9VgmP&w=nlk3ri4~nXzQn3w4XB)(Gq%@5URV)0 ziJ&#_n4qM7Mf`jkzm*kdj(HU=ZJI2VI;Ry;=D|*9lH%fGJtHHdLs@U@@t<`^Bx}BL z3x)1!aU&}q%t9Ay*nY$_ob~$S9W`@=G^oU(H0|ZCadJ&BpY_e$N5{p6EiP^2^I%#j z9O2HxR_b>}KIR6mF>A1@j@x+?g!UXCi)xe&xrg|p5ncF%P#nXy6ya{DrX5N%d+xwt zq$!~dwPuOnd1-rEnI zP_C69pkddBk+pb%?$?hLf#0p?gu zNp&2+Gi&E5mAGn3Hmg$61!Bzf3MX$I{j2s8L2VZdr` z{K!VnQts3TutlX$?F!?fJT&o3q7r( z(-Oz3!uj2?DaOHkfSqVDx_&Z~qt&FyVM~yk>pxuNtRQ1OKQ{ zRBL^Ot~}XqRfiKLy~ZLC}eo!#>KgF zCkolqpvOV9dn_&~5qSUe1AhY2pJ1~EeEkRTHoBbqi8nD!Ic&zQ{rhN{QYNA!!YS_# zU~03vfM0}2XLEkI1we=gXCTQ z*c}f9zm5>_HG!3!EKB}2gF?{*0JCmOi<5U6P-5n=+69j%ooimIq$O~wUA^G@X|>TD zG1WY^@9e10{~*&g3cBbuMJ==I>AD314KRGj4|ik0rE$&EI2Y*6usd0&Lq@VQk`$VPabD6VgcaRXgj-}- zO%~0&ZF?Ya({v9Njt#wXe1aY{wf5E9@EY%448(@knp7yssCNThP*6~Sz!O$iSBEb~ z9oCJc^oM~SHh_RkmpXVl>=a;;+p7)KH>>LFN-5SFoL!>lKeEx>jUy~^6E3*Z2@{l_ zgRqb$cDPgCQOZx?niGZ?AN&5QvNp9lXu4^94+$aj9rFk;U~OM1o^A%k)-s!=B=wr%G8bZ2I49wcCPztKS4~fWR|xQ$$e8f7}IFHfySUkw&3LSsPPgnqho>pcEgq z-!#pux(WxeclxX`^o2ye-I#6&FXWL^rEB2_`L;ymanb z@YiZMxYFwC+A$uA;*sJp!nLqh(k2~!xp@LMDM*q3UdH&|x)wnhfzF6V^P8ThN&Efw zO@V-W{&GRRRDJ7_Ofx?}qs+=bUcm0P=zq23_Y1}JX9Qh)f7_KPH~X4_-j`q4IoBYu z`!2C>Oq=RJXUoWxPTOf!X#dCb`!Ii*ft8@cKwRoFj4A5)nUojWu|GV=t7u&(LDH;Mpqs$201W7IRWfBWtuh1sR=hGKY+G6){sN?+G2A>XyTT=|w z)s6u}gUjs(Cv5a#@4Ss*A*rP2bJSp8`i1FoiavsdP;h9x??Lthb3{HypT1P0C2~l@ zlB-#f9iWq<=XvO7zptr&kKO}Pf1w7$IGe_wk`5k>z^3y8hQRy&gw18e{~;BY{HszN z1J!bQ!eHG+0BCwwS6AOlDm8{B*>A@<R4%_Q zuHpTt$W{F4)|W5*V(eH=Afh6Th=>TJPWDAyS6h8N3kwS%DW0EmA6$~{70@&E?+~+B zVsu49e69)?y2C~8J>)PPD#OZ{2k9d}7Taz&$J(arrATw88R4BP(MqzWQs<0vadS)6 z=&hgcOad_hW!*&@FtLdp;T^YFEk;SswBA|`Jsqt7I7fL>UHi4Z4li>8saQT7Y5YXa zPM3|!vUnSJUVgdpaQ6TVFcY8@z?Al^&Os9T>5#zJAe�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