--- /dev/null
+#clang-tidy
+be6a7bfff4e1d0fb4da1b7f4738075b92d1a10c5
--- /dev/null
+*kdev*
+*.kate-swp
+/compile_commands.json
+/.clang-format
+.idea
+/build
+.clangd
+/cmake-build*
+.cache
--- /dev/null
+# SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
+# SPDX-License-Identifier: CC0-1.0
+
+include:
+ - project: sysadmin/ci-utilities
+ file:
+ - /gitlab-templates/linux-qt6.yml
+ - /gitlab-templates/linux-qt6-next.yml
+ - /gitlab-templates/linux-qt6-static.yml
+ - /gitlab-templates/android-qt6.yml
+ - /gitlab-templates/freebsd-qt6.yml
+ - /gitlab-templates/windows-qt6.yml
+ - /gitlab-templates/alpine-qt6.yml
+ - /gitlab-templates/xml-lint.yml
+ - /gitlab-templates/yaml-lint.yml
--- /dev/null
+Dependencies:
+ - 'on': ['@all']
+ 'require':
+ 'frameworks/extra-cmake-modules': '@same'
+
+Options:
+ test-before-installing: True
+ require-passing-tests-on: ['Linux', 'FreeBSD', 'Windows']
--- /dev/null
+cmake_minimum_required(VERSION 3.16)
+
+set(KF_VERSION "6.13.0") # handled by release scripts
+set(KF_DEP_VERSION "6.13.0") # handled by release scripts
+
+project(KQuickCharts VERSION ${KF_VERSION})
+
+include(FeatureSummary)
+find_package(ECM 6.13.0 NO_MODULE)
+set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
+feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
+
+set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake )
+
+include(KDEInstallDirs)
+include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE)
+include(KDECMakeSettings)
+include(KDEGitCommitHooks)
+
+include(ECMGenerateExportHeader)
+
+include(ECMAddTests)
+include(ECMSetupVersion)
+include(ECMQtDeclareLoggingCategory)
+include(ECMQmlModule)
+include(ECMDeprecationSettings)
+
+option(BUILD_EXAMPLES "Build example applications" OFF)
+
+set(REQUIRED_QT_VERSION 6.6.0)
+find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Qml Quick QuickControls2 ShaderTools)
+
+set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].")
+
+ecm_set_disabled_deprecation_versions(
+ QT 6.9.0
+)
+
+ecm_setup_version(
+ PROJECT
+ VARIABLE_PREFIX KF6QuickCharts
+ VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/quickcharts_version.h"
+ PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF6QuickChartsConfigVersion.cmake"
+ SOVERSION 1
+)
+
+add_subdirectory(controls)
+add_subdirectory(src)
+
+if(BUILD_EXAMPLES)
+ add_subdirectory(examples)
+endif()
+
+if(BUILD_TESTING)
+ add_subdirectory(autotests)
+endif()
+
+configure_package_config_file(
+ "KF6QuickChartsConfig.cmake.in"
+ "KF6QuickChartsConfig.cmake"
+ INSTALL_DESTINATION ${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6QuickCharts
+)
+
+install(FILES
+ "${CMAKE_CURRENT_BINARY_DIR}/KF6QuickChartsConfig.cmake"
+ "${CMAKE_CURRENT_BINARY_DIR}/KF6QuickChartsConfigVersion.cmake"
+ DESTINATION ${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6QuickCharts
+ COMPONENT Devel
+)
+
+install(EXPORT KF6QuickChartsTargets DESTINATION "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6QuickCharts"
+ FILE KF6QuickChartsTargets.cmake NAMESPACE KF6:: COMPONENT Devel)
+
+include(ECMFeatureSummary)
+ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
+
+kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
--- /dev/null
+@PACKAGE_INIT@
+
+include(CMakeFindDependencyMacro)
+find_dependency(Qt6Core @REQUIRED_QT_VERSION@)
+
+find_dependency(ECM @KF_DEP_VERSION@)
+include(${ECM_MODULE_DIR}/ECMFindQmlModule.cmake)
+
+ecm_find_qmlmodule(org.kde.quickcharts 1.0)
+
+include("${CMAKE_CURRENT_LIST_DIR}/KF6QuickChartsTargets.cmake")
+@PACKAGE_INCLUDE_QCHTARGETS@
--- /dev/null
+Copyright (c) <year> <owner>
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
--- /dev/null
+GNU LESSER GENERAL PUBLIC LICENSE
+
+Version 2.1, February 1999
+
+Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts as the
+successor of the GNU Library Public License, version 2, hence the version
+number 2.1.]
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share
+and change it. By contrast, the GNU General Public Licenses are intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users.
+
+This license, the Lesser General Public License, applies to some specially
+designated software packages--typically libraries--of the Free Software Foundation
+and other authors who decide to use it. You can use it too, but we suggest
+you first think carefully about whether this license or the ordinary General
+Public License is the better strategy to use in any particular case, based
+on the explanations below.
+
+When we speak of free software, we are referring to freedom of use, not price.
+Our General Public Licenses are designed to make sure that you have the freedom
+to distribute copies of free software (and charge for this service if you
+wish); that you receive source code or can get it if you want it; that you
+can change the software and use pieces of it in new free programs; and that
+you are informed that you can do these things.
+
+To protect your rights, we need to make restrictions that forbid distributors
+to deny you these rights or to ask you to surrender these rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of
+the library or if you modify it.
+
+For example, if you distribute copies of the library, whether gratis or for
+a fee, you must give the recipients all the rights that we gave you. You must
+make sure that they, too, receive or can get the source code. If you link
+other code with the library, you must provide complete object files to the
+recipients, so that they can relink them with the library after making changes
+to the library and recompiling it. And you must show them these terms so they
+know their rights.
+
+We protect your rights with a two-step method: (1) we copyright the library,
+and (2) we offer you this license, which gives you legal permission to copy,
+distribute and/or modify the library.
+
+To protect each distributor, we want to make it very clear that there is no
+warranty for the free library. Also, if the library is modified by someone
+else and passed on, the recipients should know that what they have is not
+the original version, so that the original author's reputation will not be
+affected by problems that might be introduced by others.
+
+Finally, software patents pose a constant threat to the existence of any free
+program. We wish to make sure that a company cannot effectively restrict the
+users of a free program by obtaining a restrictive license from a patent holder.
+Therefore, we insist that any patent license obtained for a version of the
+library must be consistent with the full freedom of use specified in this
+license.
+
+Most GNU software, including some libraries, is covered by the ordinary GNU
+General Public License. This license, the GNU Lesser General Public License,
+applies to certain designated libraries, and is quite different from the ordinary
+General Public License. We use this license for certain libraries in order
+to permit linking those libraries into non-free programs.
+
+When a program is linked with a library, whether statically or using a shared
+library, the combination of the two is legally speaking a combined work, a
+derivative of the original library. The ordinary General Public License therefore
+permits such linking only if the entire combination fits its criteria of freedom.
+The Lesser General Public License permits more lax criteria for linking other
+code with the library.
+
+We call this license the "Lesser" General Public License because it does Less
+to protect the user's freedom than the ordinary General Public License. It
+also provides other free software developers Less of an advantage over competing
+non-free programs. These disadvantages are the reason we use the ordinary
+General Public License for many libraries. However, the Lesser license provides
+advantages in certain special circumstances.
+
+For example, on rare occasions, there may be a special need to encourage the
+widest possible use of a certain library, so that it becomes a de-facto standard.
+To achieve this, non-free programs must be allowed to use the library. A more
+frequent case is that a free library does the same job as widely used non-free
+libraries. In this case, there is little to gain by limiting the free library
+to free software only, so we use the Lesser General Public License.
+
+In other cases, permission to use a particular library in non-free programs
+enables a greater number of people to use a large body of free software. For
+example, permission to use the GNU C Library in non-free programs enables
+many more people to use the whole GNU operating system, as well as its variant,
+the GNU/Linux operating system.
+
+Although the Lesser General Public License is Less protective of the users'
+freedom, it does ensure that the user of a program that is linked with the
+Library has the freedom and the wherewithal to run that program using a modified
+version of the Library.
+
+The precise terms and conditions for copying, distribution and modification
+follow. Pay close attention to the difference between a "work based on the
+library" and a "work that uses the library". The former contains code derived
+from the library, whereas the latter must be combined with the library in
+order to run.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License Agreement applies to any software library or other program
+which contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Lesser General
+Public License (also called "this License"). Each licensee is addressed as
+"you".
+
+A "library" means a collection of software functions and/or data prepared
+so as to be conveniently linked with application programs (which use some
+of those functions and data) to form executables.
+
+The "Library", below, refers to any such software library or work which has
+been distributed under these terms. A "work based on the Library" means either
+the Library or any derivative work under copyright law: that is to say, a
+work containing the Library or a portion of it, either verbatim or with modifications
+and/or translated straightforwardly into another language. (Hereinafter, translation
+is included without limitation in the term "modification".)
+
+"Source code" for a work means the preferred form of the work for making modifications
+to it. For a library, complete source code means all the source code for all
+modules it contains, plus any associated interface definition files, plus
+the scripts used to control compilation and installation of the library.
+
+Activities other than copying, distribution and modification are not covered
+by this License; they are outside its scope. The act of running a program
+using the Library is not restricted, and output from such a program is covered
+only if its contents constitute a work based on the Library (independent of
+the use of the Library in a tool for writing it). Whether that is true depends
+on what the Library does and what the program that uses the Library does.
+
+1. You may copy and distribute verbatim copies of the Library's complete source
+code as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and disclaimer
+of warranty; keep intact all the notices that refer to this License and to
+the absence of any warranty; and distribute a copy of this License along with
+the Library.
+
+You may charge a fee for the physical act of transferring a copy, and you
+may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Library or any portion of it,
+thus forming a work based on the Library, and copy and distribute such modifications
+or work under the terms of Section 1 above, provided that you also meet all
+of these conditions:
+
+ a) The modified work must itself be a software library.
+
+b) You must cause the files modified to carry prominent notices stating that
+you changed the files and the date of any change.
+
+c) You must cause the whole of the work to be licensed at no charge to all
+third parties under the terms of this License.
+
+d) If a facility in the modified Library refers to a function or a table of
+data to be supplied by an application program that uses the facility, other
+than as an argument passed when the facility is invoked, then you must make
+a good faith effort to ensure that, in the event an application does not supply
+such function or table, the facility still operates, and performs whatever
+part of its purpose remains meaningful.
+
+(For example, a function in a library to compute square roots has a purpose
+that is entirely well-defined independent of the application. Therefore, Subsection
+2d requires that any application-supplied function or table used by this function
+must be optional: if the application does not supply it, the square root function
+must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Library, and can be reasonably
+considered independent and separate works in themselves, then this License,
+and its terms, do not apply to those sections when you distribute them as
+separate works. But when you distribute the same sections as part of a whole
+which is a work based on the Library, the distribution of the whole must be
+on the terms of this License, whose permissions for other licensees extend
+to the entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest your
+rights to work written entirely by you; rather, the intent is to exercise
+the right to control the distribution of derivative or collective works based
+on the Library.
+
+In addition, mere aggregation of another work not based on the Library with
+the Library (or with a work based on the Library) on a volume of a storage
+or distribution medium does not bring the other work under the scope of this
+License.
+
+3. You may opt to apply the terms of the ordinary GNU General Public License
+instead of this License to a given copy of the Library. To do this, you must
+alter all the notices that refer to this License, so that they refer to the
+ordinary GNU General Public License, version 2, instead of to this License.
+(If a newer version than version 2 of the ordinary GNU General Public License
+has appeared, then you can specify that version instead if you wish.) Do not
+make any other change in these notices.
+
+Once this change is made in a given copy, it is irreversible for that copy,
+so the ordinary GNU General Public License applies to all subsequent copies
+and derivative works made from that copy.
+
+This option is useful when you wish to copy part of the code of the Library
+into a program that is not a library.
+
+4. You may copy and distribute the Library (or a portion or derivative of
+it, under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you accompany it with the complete corresponding
+machine-readable source code, which must be distributed under the terms of
+Sections 1 and 2 above on a medium customarily used for software interchange.
+
+If distribution of object code is made by offering access to copy from a designated
+place, then offering equivalent access to copy the source code from the same
+place satisfies the requirement to distribute the source code, even though
+third parties are not compelled to copy the source along with the object code.
+
+5. A program that contains no derivative of any portion of the Library, but
+is designed to work with the Library by being compiled or linked with it,
+is called a "work that uses the Library". Such a work, in isolation, is not
+a derivative work of the Library, and therefore falls outside the scope of
+this License.
+
+However, linking a "work that uses the Library" with the Library creates an
+executable that is a derivative of the Library (because it contains portions
+of the Library), rather than a "work that uses the library". The executable
+is therefore covered by this License. Section 6 states terms for distribution
+of such executables.
+
+When a "work that uses the Library" uses material from a header file that
+is part of the Library, the object code for the work may be a derivative work
+of the Library even though the source code is not. Whether this is true is
+especially significant if the work can be linked without the Library, or if
+the work is itself a library. The threshold for this to be true is not precisely
+defined by law.
+
+If such an object file uses only numerical parameters, data structure layouts
+and accessors, and small macros and small inline functions (ten lines or less
+in length), then the use of the object file is unrestricted, regardless of
+whether it is legally a derivative work. (Executables containing this object
+code plus portions of the Library will still fall under Section 6.)
+
+Otherwise, if the work is a derivative of the Library, you may distribute
+the object code for the work under the terms of Section 6. Any executables
+containing that work also fall under Section 6, whether or not they are linked
+directly with the Library itself.
+
+6. As an exception to the Sections above, you may also combine or link a "work
+that uses the Library" with the Library to produce a work containing portions
+of the Library, and distribute that work under terms of your choice, provided
+that the terms permit modification of the work for the customer's own use
+and reverse engineering for debugging such modifications.
+
+You must give prominent notice with each copy of the work that the Library
+is used in it and that the Library and its use are covered by this License.
+You must supply a copy of this License. If the work during execution displays
+copyright notices, you must include the copyright notice for the Library among
+them, as well as a reference directing the user to the copy of this License.
+Also, you must do one of these things:
+
+a) Accompany the work with the complete corresponding machine-readable source
+code for the Library including whatever changes were used in the work (which
+must be distributed under Sections 1 and 2 above); and, if the work is an
+executable linked with the Library, with the complete machine-readable "work
+that uses the Library", as object code and/or source code, so that the user
+can modify the Library and then relink to produce a modified executable containing
+the modified Library. (It is understood that the user who changes the contents
+of definitions files in the Library will not necessarily be able to recompile
+the application to use the modified definitions.)
+
+b) Use a suitable shared library mechanism for linking with the Library. A
+suitable mechanism is one that (1) uses at run time a copy of the library
+already present on the user's computer system, rather than copying library
+functions into the executable, and (2) will operate properly with a modified
+version of the library, if the user installs one, as long as the modified
+version is interface-compatible with the version that the work was made with.
+
+c) Accompany the work with a written offer, valid for at least three years,
+to give the same user the materials specified in Subsection 6a, above, for
+a charge no more than the cost of performing this distribution.
+
+d) If distribution of the work is made by offering access to copy from a designated
+place, offer equivalent access to copy the above specified materials from
+the same place.
+
+e) Verify that the user has already received a copy of these materials or
+that you have already sent this user a copy.
+
+For an executable, the required form of the "work that uses the Library" must
+include any data and utility programs needed for reproducing the executable
+from it. However, as a special exception, the materials to be distributed
+need not include anything that is normally distributed (in either source or
+binary form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component itself
+accompanies the executable.
+
+It may happen that this requirement contradicts the license restrictions of
+other proprietary libraries that do not normally accompany the operating system.
+Such a contradiction means you cannot use both them and the Library together
+in an executable that you distribute.
+
+7. You may place library facilities that are a work based on the Library side-by-side
+in a single library together with other library facilities not covered by
+this License, and distribute such a combined library, provided that the separate
+distribution of the work based on the Library and of the other library facilities
+is otherwise permitted, and provided that you do these two things:
+
+a) Accompany the combined library with a copy of the same work based on the
+Library, uncombined with any other library facilities. This must be distributed
+under the terms of the Sections above.
+
+b) Give prominent notice with the combined library of the fact that part of
+it is a work based on the Library, and explaining where to find the accompanying
+uncombined form of the same work.
+
+8. You may not copy, modify, sublicense, link with, or distribute the Library
+except as expressly provided under this License. Any attempt otherwise to
+copy, modify, sublicense, link with, or distribute the Library is void, and
+will automatically terminate your rights under this License. However, parties
+who have received copies, or rights, from you under this License will not
+have their licenses terminated so long as such parties remain in full compliance.
+
+9. You are not required to accept this License, since you have not signed
+it. However, nothing else grants you permission to modify or distribute the
+Library or its derivative works. These actions are prohibited by law if you
+do not accept this License. Therefore, by modifying or distributing the Library
+(or any work based on the Library), you indicate your acceptance of this License
+to do so, and all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+10. Each time you redistribute the Library (or any work based on the Library),
+the recipient automatically receives a license from the original licensor
+to copy, distribute, link with or modify the Library subject to these terms
+and conditions. You may not impose any further restrictions on the recipients'
+exercise of the rights granted herein. You are not responsible for enforcing
+compliance by third parties with this License.
+
+11. If, as a consequence of a court judgment or allegation of patent infringement
+or for any other reason (not limited to patent issues), conditions are imposed
+on you (whether by court order, agreement or otherwise) that contradict the
+conditions of this License, they do not excuse you from the conditions of
+this License. If you cannot distribute so as to satisfy simultaneously your
+obligations under this License and any other pertinent obligations, then as
+a consequence you may not distribute the Library at all. For example, if a
+patent license would not permit royalty-free redistribution of the Library
+by all those who receive copies directly or indirectly through you, then the
+only way you could satisfy both it and this License would be to refrain entirely
+from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents
+or other property right claims or to contest validity of any such claims;
+this section has the sole purpose of protecting the integrity of the free
+software distribution system which is implemented by public license practices.
+Many people have made generous contributions to the wide range of software
+distributed through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing to
+distribute software through any other system and a licensee cannot impose
+that choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+12. If the distribution and/or use of the Library is restricted in certain
+countries either by patents or by copyrighted interfaces, the original copyright
+holder who places the Library under this License may add an explicit geographical
+distribution limitation excluding those countries, so that distribution is
+permitted only in or among countries not thus excluded. In such case, this
+License incorporates the limitation as if written in the body of this License.
+
+13. The Free Software Foundation may publish revised and/or new versions of
+the Lesser General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to address
+new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library specifies
+a version number of this License which applies to it and "any later version",
+you have the option of following the terms and conditions either of that version
+or of any later version published by the Free Software Foundation. If the
+Library does not specify a license version number, you may choose any version
+ever published by the Free Software Foundation.
+
+14. If you wish to incorporate parts of the Library into other free programs
+whose distribution conditions are incompatible with these, write to the author
+to ask for permission. For software which is copyrighted by the Free Software
+Foundation, write to the Free Software Foundation; we sometimes make exceptions
+for this. Our decision will be guided by the two goals of preserving the free
+status of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
+THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
+STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY
+"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
+BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE
+OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
+THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE
+OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
+OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES
+OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
+HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Libraries
+
+If you develop a new library, and you want it to be of the greatest possible
+use to the public, we recommend making it free software that everyone can
+redistribute and change. You can do so by permitting redistribution under
+these terms (or, alternatively, under the terms of the ordinary General Public
+License).
+
+To apply these terms, attach the following notices to the library. It is safest
+to attach them to the start of each source file to most effectively convey
+the exclusion of warranty; and each file should have at least the "copyright"
+line and a pointer to where the full notice is found.
+
+< one line to give the library's name and an idea of what it does. >
+
+Copyright (C) < year > < name of author >
+
+This library is free software; you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the Free
+Software Foundation; either version 2.1 of the License, or (at your option)
+any later version.
+
+This library is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this library; if not, write to the Free Software Foundation, Inc., 51
+Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information
+on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your school,
+if any, to sign a "copyright disclaimer" for the library, if necessary. Here
+is a sample; alter the names:
+
+Yoyodyne, Inc., hereby disclaims all copyright interest in
+
+the library `Frob' (a library for tweaking knobs) written
+
+by James Random Hacker.
+
+< signature of Ty Coon > , 1 April 1990
+
+Ty Coon, President of Vice
+
+That's all there is to it!
--- /dev/null
+GNU LESSER GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+This version of the GNU Lesser General Public License incorporates the terms
+and conditions of version 3 of the GNU General Public License, supplemented
+by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+
+
+As used herein, "this License" refers to version 3 of the GNU Lesser General
+Public License, and the "GNU GPL" refers to version 3 of the GNU General Public
+License.
+
+
+
+"The Library" refers to a covered work governed by this License, other than
+an Application or a Combined Work as defined below.
+
+
+
+An "Application" is any work that makes use of an interface provided by the
+Library, but which is not otherwise based on the Library. Defining a subclass
+of a class defined by the Library is deemed a mode of using an interface provided
+by the Library.
+
+
+
+A "Combined Work" is a work produced by combining or linking an Application
+with the Library. The particular version of the Library with which the Combined
+Work was made is also called the "Linked Version".
+
+
+
+The "Minimal Corresponding Source" for a Combined Work means the Corresponding
+Source for the Combined Work, excluding any source code for portions of the
+Combined Work that, considered in isolation, are based on the Application,
+and not on the Linked Version.
+
+
+
+The "Corresponding Application Code" for a Combined Work means the object
+code and/or source code for the Application, including any data and utility
+programs needed for reproducing the Combined Work from the Application, but
+excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+You may convey a covered work under sections 3 and 4 of this License without
+being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+If you modify a copy of the Library, and, in your modifications, a facility
+refers to a function or data to be supplied by an Application that uses the
+facility (other than as an argument passed when the facility is invoked),
+then you may convey a copy of the modified version:
+
+a) under this License, provided that you make a good faith effort to ensure
+that, in the event an Application does not supply the function or data, the
+facility still operates, and performs whatever part of its purpose remains
+meaningful, or
+
+b) under the GNU GPL, with none of the additional permissions of this License
+applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+The object code form of an Application may incorporate material from a header
+file that is part of the Library. You may convey such object code under terms
+of your choice, provided that, if the incorporated material is not limited
+to numerical parameters, data structure layouts and accessors, or small macros,
+inline functions and templates (ten or fewer lines in length), you do both
+of the following:
+
+a) Give prominent notice with each copy of the object code that the Library
+is used in it and that the Library and its use are covered by this License.
+
+b) Accompany the object code with a copy of the GNU GPL and this license document.
+
+ 4. Combined Works.
+
+You may convey a Combined Work under terms of your choice that, taken together,
+effectively do not restrict modification of the portions of the Library contained
+in the Combined Work and reverse engineering for debugging such modifications,
+if you also do each of the following:
+
+a) Give prominent notice with each copy of the Combined Work that the Library
+is used in it and that the Library and its use are covered by this License.
+
+b) Accompany the Combined Work with a copy of the GNU GPL and this license
+document.
+
+c) For a Combined Work that displays copyright notices during execution, include
+the copyright notice for the Library among these notices, as well as a reference
+directing the user to the copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+0) Convey the Minimal Corresponding Source under the terms of this License,
+and the Corresponding Application Code in a form suitable for, and under terms
+that permit, the user to recombine or relink the Application with a modified
+version of the Linked Version to produce a modified Combined Work, in the
+manner specified by section 6 of the GNU GPL for conveying Corresponding Source.
+
+1) Use a suitable shared library mechanism for linking with the Library. A
+suitable mechanism is one that (a) uses at run time a copy of the Library
+already present on the user's computer system, and (b) will operate properly
+with a modified version of the Library that is interface-compatible with the
+Linked Version.
+
+e) Provide Installation Information, but only if you would otherwise be required
+to provide such information under section 6 of the GNU GPL, and only to the
+extent that such information is necessary to install and execute a modified
+version of the Combined Work produced by recombining or relinking the Application
+with a modified version of the Linked Version. (If you use option 4d0, the
+Installation Information must accompany the Minimal Corresponding Source and
+Corresponding Application Code. If you use option 4d1, you must provide the
+Installation Information in the manner specified by section 6 of the GNU GPL
+for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+You may place library facilities that are a work based on the Library side
+by side in a single library together with other library facilities that are
+not Applications and are not covered by this License, and convey such a combined
+library under terms of your choice, if you do both of the following:
+
+a) Accompany the combined library with a copy of the same work based on the
+Library, uncombined with any other library facilities, conveyed under the
+terms of this License.
+
+b) Give prominent notice with the combined library that part of it is a work
+based on the Library, and explaining where to find the accompanying uncombined
+form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+The Free Software Foundation may publish revised and/or new versions of the
+GNU Lesser General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to address
+new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library as you
+received it specifies that a certain numbered version of the GNU Lesser General
+Public License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that published version or of
+any later version published by the Free Software Foundation. If the Library
+as you received it does not specify a version number of the GNU Lesser General
+Public License, you may choose any version of the GNU Lesser General Public
+License ever published by the Free Software Foundation.
+
+If the Library as you received it specifies that a proxy can decide whether
+future versions of the GNU Lesser General Public License shall apply, that
+proxy's public statement of acceptance of any version is permanent authorization
+for you to choose that version for the Library.
--- /dev/null
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the license or (at your option) any later version
+that is accepted by the membership of KDE e.V. (or its successor
+approved by the membership of KDE e.V.), which shall act as a
+proxy as defined in Section 6 of version 3 of the license.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
--- /dev/null
+MIT License Copyright (c) <year> <copyright holders>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice (including the next
+paragraph) shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
+OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--- /dev/null
+# KQuickCharts
+
+A QtQuick module providing high-performance charts.
+
+## Introduction
+
+The Quick Charts module provides a set of charts that can be used from QtQuick
+applications. They are intended to be used for both simple display of data as
+well as continuous display of high-volume data (often referred to as plotters).
+The charts use a system called distance fields for their accelerated rendering,
+which provides ways of using the GPU for rendering 2D shapes without loss of
+quality.
+
+### Usage Example
+
+The following piece of code will render a simple line chart containing three
+lines:
+
+\snippet snippets/minimal.qml example
+
+## Concepts
+
+There are three main concepts to consider when using this module: charts, data
+sources and decorations.
+
+### Charts
+
+These are the main items of the module. They process data and render it in a
+certain way. Currently there are three main types of charts: pie charts, line
+charts and bar charts. All charts inherit the [Chart] class, which provides the
+most basic chart interface. On top of that is the [XYChart] base class, which
+provides an interface for charts that are based on an X/Y grid.
+
+* [PieChart](\ref PieChart)
+* [LineChart](\ref LineChart)
+* [BarChart](\ref BarChart)
+
+[Chart]: \ref Chart
+[XYChart]: \ref XYChart
+
+### Data Sources
+
+Data sources are objects that provide data to charts. These objects act as
+adapters to other objects, like Qt's models. All data source objects inherit
+[ChartDataSource], which represents the basic data source interface.
+
+* [SingleValueSource](\ref SingleValueSource)
+* [ArraySource](\ref ArraySource)
+* [ModelSource](\ref ModelSource)
+* [ValueHistorySource](\ref ValueHistorySource)
+* [ModelHistorySource](\ref ModelHistorySource)
+* [ColorGradientSource](\ref ColorGradientSource)
+* [ChartAxisSource](\ref ChartAxisSource)
+
+[ChartDataSource]: \ref ChartDataSource
+
+### Decorations
+
+Decorations are items that provide extra information about a chart. These are
+things like a legend or axis labels. They usually work with multiple types of
+charts, though some may be limited to X/Y charts.
+
+* [AxisLabels](\ref AxisLabels)
+* [GridLines](\ref GridLines)
+* [LegendModel](\ref LegendModel)
+
+## Controls
+
+A submodule is provided that contains pre-made controls that are provided as a
+convenience layer. These controls may be more restrictive in their data sources
+or how they display things. They are composed of Charts items along with some
+QtQuick Controls items. Some of the controls may use style-specific theming.
+
+* [Legend](\ref org::kde::quickcharts::controls::Legend)
+* [LegendDelegate](\ref org::kde::quickcharts::controls::LegendDelegate)
+* [LineChartControl](\ref org::kde::quickcharts::controls::LineChartControl)
+* [PieChartControl](\ref org::kde::quickcharts::controls::PieChartControl)
+
+## Supporting Code
+
+There are a number of classes and other pieces of code that are not considered
+public API and thus not part of the public API documentation. Primarily, these
+are all the QtQuick Scene Graph related classes located in `src/scenegraph/`,
+in addition to the shaders that are used for rendering charts.
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include <QTest>
+
+#include "datasource/ArraySource.h"
+
+class ArraySourceTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testCreate()
+ {
+ // Basic creation should create an empty source.
+ auto source = new ArraySource{};
+
+ QCOMPARE(source->itemCount(), 0);
+ QCOMPARE(source->item(0), QVariant{});
+ QCOMPARE(source->minimum(), QVariant{});
+ QCOMPARE(source->maximum(), QVariant{});
+ }
+
+ void testWithArray_data()
+ {
+ QTest::addColumn<QVariantList>("array");
+ QTest::addColumn<int>("itemCount");
+ QTest::addColumn<QVariant>("firstItem");
+ QTest::addColumn<QVariant>("lastItem");
+ QTest::addColumn<QVariant>("minimum");
+ QTest::addColumn<QVariant>("maximum");
+
+ QTest::newRow("simple ints") << QVariantList{0, 1, 2, 3, 4} << 5 << QVariant{0} << QVariant{4} << QVariant{0} << QVariant{4};
+ QTest::newRow("random ints") << QVariantList{-3, 6, 4, 9, 4} << 5 << QVariant{-3} << QVariant{4} << QVariant{-3} << QVariant{9};
+ QTest::newRow("floats") << QVariantList{2.56, 4.45, 2.5, 5.3} << 4 << QVariant{2.56} << QVariant{5.3} << QVariant{2.5} << QVariant{5.3};
+ }
+
+ void testWithArray()
+ {
+ auto source = new ArraySource{};
+ QFETCH(QVariantList, array);
+ source->setArray(array);
+
+ QFETCH(int, itemCount);
+ QCOMPARE(source->itemCount(), itemCount);
+
+ QFETCH(QVariant, firstItem);
+ QCOMPARE(source->item(0), firstItem);
+
+ QFETCH(QVariant, lastItem);
+ QCOMPARE(source->item(itemCount - 1), lastItem);
+
+ QCOMPARE(source->item(itemCount), QVariant{});
+
+ QFETCH(QVariant, minimum);
+ QCOMPARE(source->minimum(), minimum);
+
+ QFETCH(QVariant, maximum);
+ QCOMPARE(source->maximum(), maximum);
+ }
+
+ void testWrap_data()
+ {
+ QTest::addColumn<QVariantList>("array");
+ QTest::addColumn<int>("itemCount");
+ QTest::addColumn<QVariant>("firstItem");
+ QTest::addColumn<QVariant>("lastItem");
+
+ QTest::newRow("simple ints") << QVariantList{0, 1, 2, 3, 4} << 5 << QVariant{0} << QVariant{4};
+ QTest::newRow("random ints") << QVariantList{-3, 6, 4, 9, 4} << 5 << QVariant{-3} << QVariant{4};
+ QTest::newRow("floats") << QVariantList{2.56, 4.45, 2.5, 5.3} << 4 << QVariant{2.56} << QVariant{5.3};
+ }
+
+ void testWrap()
+ {
+ auto source = new ArraySource{};
+ QFETCH(QVariantList, array);
+ source->setArray(array);
+ source->setWrap(true);
+
+ QFETCH(int, itemCount);
+ QFETCH(QVariant, firstItem);
+ QFETCH(QVariant, lastItem);
+
+ QCOMPARE(source->item(0), firstItem);
+ QCOMPARE(source->item(itemCount - 1), lastItem);
+ QCOMPARE(source->item(itemCount), firstItem);
+ QCOMPARE(source->item(itemCount * 2 - 1), lastItem);
+
+ QCOMPARE(source->item(itemCount * 99), firstItem);
+ }
+};
+
+QTEST_GUILESS_MAIN(ArraySourceTest)
+
+#include "ArraySourceTest.moc"
--- /dev/null
+# SPDX-FileCopyrightText: 2019,2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+# SPDX-FileCopyrightText: 2022 Alexander Lohnau <alexander.lohnau@gmx.de>
+# SPDX-License-Identifier: BSD-2-Clause
+find_package(Qt6 COMPONENTS QuickTest)
+
+include(ECMAddTests)
+
+include_directories(${CMAKE_SOURCE_DIR}/src)
+
+ecm_add_tests(
+ ArraySourceTest.cpp
+ MapProxySourceTest.cpp
+ HistoryProxySourceTest.cpp
+ ItemBuilderTest.cpp
+ LINK_LIBRARIES PRIVATE Qt6::Test QuickCharts
+)
+if (NOT BUILD_SHARED_LIBS)
+ target_link_libraries(ItemBuilderTest PRIVATE QuickChartsplugin)
+ qt6_import_qml_plugins(ItemBuilderTest)
+endif()
+
+add_executable(qmltest qmltest.cpp)
+
+target_link_libraries(qmltest PRIVATE Qt6::QuickTest QuickCharts)
+if (NOT BUILD_SHARED_LIBS)
+ target_link_libraries(qmltest PRIVATE QuickChartsplugin)
+ qt6_import_qml_plugins(qmltest)
+endif()
+
+macro(add_qml_tests)
+ if (WIN32)
+ set(_extra_args -platform offscreen)
+ endif()
+
+ foreach(test ${ARGV})
+ add_test(NAME ${test}
+ COMMAND ${CMAKE_BINARY_DIR}/bin/qmltest
+ ${_extra_args}
+ -import ${CMAKE_BINARY_DIR}/bin
+ -input ${test}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+ )
+ endforeach()
+endmacro()
+
+add_qml_tests(
+ tst_BarChart.qml
+ tst_LineChart.qml
+ tst_PieChart.qml
+)
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include <QTest>
+
+#include "datasource/HistoryProxySource.h"
+#include "datasource/ModelSource.h"
+#include "datasource/SingleValueSource.h"
+
+#define qs QStringLiteral
+
+class TestModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(int minimum READ minimum NOTIFY minimumChanged)
+ Q_PROPERTY(int maximum READ maximum NOTIFY maximumChanged)
+
+public:
+ TestModel(QObject *parent = nullptr)
+ : QAbstractListModel(parent)
+ {
+ m_items.fill(1, 10);
+ }
+
+ int rowCount(const QModelIndex &parent) const override
+ {
+ if (parent.isValid()) {
+ return 0;
+ } else {
+ return 10;
+ }
+ }
+
+ QVariant data(const QModelIndex &index, int role) const override
+ {
+ Q_UNUSED(role)
+
+ if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) {
+ qDebug() << "index invalid";
+ return QVariant{};
+ }
+
+ return m_items.at(index.row());
+ }
+
+ void set(int item, int value)
+ {
+ if (item < 0 || item >= 10) {
+ return;
+ }
+
+ m_items[item] = value;
+ Q_EMIT dataChanged(index(item, 0), index(item, 0));
+ Q_EMIT minimumChanged();
+ Q_EMIT maximumChanged();
+ }
+
+ int minimum()
+ {
+ return *std::min_element(m_items.cbegin(), m_items.cend());
+ }
+
+ int maximum()
+ {
+ return *std::max_element(m_items.cbegin(), m_items.cend());
+ }
+
+ Q_SIGNAL void minimumChanged();
+ Q_SIGNAL void maximumChanged();
+
+private:
+ QList<int> m_items;
+};
+
+class HistoryProxySourceTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testCreate()
+ {
+ // Basic creation should create an empty source.
+ auto source = new HistoryProxySource{};
+
+ QCOMPARE(source->itemCount(), 0);
+ QCOMPARE(source->item(0), QVariant{});
+ QCOMPARE(source->minimum(), QVariant{});
+ QCOMPARE(source->maximum(), QVariant{});
+ QCOMPARE(source->source(), nullptr);
+ }
+
+ void testWithValue()
+ {
+ auto valueSource = std::make_unique<SingleValueSource>();
+
+ // Simple case first, using default values for maximum history and fill
+ // mode. This should lead to 10 history items with the most recent value
+ // being the first item.
+ auto historySource = std::make_unique<HistoryProxySource>();
+ historySource->setSource(valueSource.get());
+ historySource->setMaximumHistory(10);
+ historySource->setFillMode(HistoryProxySource::DoNotFill);
+
+ for (int i = 0; i < 15; i++) {
+ valueSource->setValue(i);
+ QCOMPARE(historySource->itemCount(), std::min(i + 1, 10));
+
+ for (int item = 0; item < historySource->itemCount(); ++item) {
+ QCOMPARE(historySource->item(item), i - item);
+ }
+ }
+
+ // Minimum and maximum should be minimum and maximum of current history
+ // items.
+ QCOMPARE(historySource->minimum(), 5);
+ QCOMPARE(historySource->maximum(), 14);
+
+ // Clearing will reset the history.
+ QCOMPARE(historySource->itemCount(), 10);
+ historySource->clear();
+ QCOMPARE(historySource->itemCount(), 0);
+
+ valueSource->setValue(5);
+
+ // With an interval, we should be getting maximumHistory items of the
+ // same value if we simply wait for at least interval * 10 milliseconds.
+ historySource->setInterval(50);
+
+ QTest::qWait(750);
+
+ QCOMPARE(historySource->itemCount(), 10);
+ for (int i = 0; i < historySource->itemCount(); ++i) {
+ QCOMPARE(historySource->item(i), 5);
+ }
+
+ historySource->setInterval(0);
+
+ // Increasing history size does not delete existing history.
+ historySource->setMaximumHistory(20);
+
+ QCOMPARE(historySource->itemCount(), 10);
+
+ // Changing fill mode resets history and should now report
+ // maximumHistory number of items. Empty items should use the same
+ // QVariant type as the value source does.
+
+ historySource->setFillMode(HistoryProxySource::FillFromStart);
+
+ QCOMPARE(historySource->item(0), QVariant{QMetaType(QMetaType::Int)});
+ QCOMPARE(historySource->itemCount(), 20);
+ QCOMPARE(historySource->item(19), QVariant{QMetaType(QMetaType::Int)});
+
+ // Using the FillFromEnd fill mode adds items to the end while itemCount
+ // is less than maximumHistory.
+
+ historySource->setFillMode(HistoryProxySource::FillFromEnd);
+ QCOMPARE(historySource->itemCount(), 20);
+
+ for (int i = 0; i < 19; ++i) {
+ valueSource->setValue(i);
+ QCOMPARE(historySource->item(0), QVariant{QMetaType(QMetaType::Int)});
+ for (int item = 19; item > 1; --item) {
+ if ((item - 19) + i < 0) {
+ QCOMPARE(historySource->item(item), QVariant{QMetaType(QMetaType::Int)});
+ } else {
+ QCOMPARE(historySource->item(item), 19 - item);
+ }
+ }
+ }
+ }
+
+ void testWithModel()
+ {
+ auto model = std::make_unique<TestModel>();
+ auto modelSource = std::make_unique<ModelSource>();
+ modelSource->setModel(model.get());
+ modelSource->setRole(Qt::DisplayRole);
+
+ auto historySource = std::make_unique<HistoryProxySource>();
+ historySource->setSource(modelSource.get());
+ historySource->setMaximumHistory(10);
+ historySource->setFillMode(HistoryProxySource::DoNotFill);
+
+ // When there is a source with an itemCount > 1, we can use setItem to
+ // indicate which item should be watched.
+ historySource->setItem(2);
+
+ // When we then change the model, the history source should record an
+ // item.
+ for (int i = 0; i < 10; ++i) {
+ model->set(2, i);
+ QCOMPARE(historySource->itemCount(), i + 1);
+ for (int item = 0; item < historySource->itemCount(); ++item) {
+ QCOMPARE(historySource->item(item), i - item);
+ }
+ }
+
+ historySource->clear();
+
+ model->set(2, 5);
+ model->set(2, 2);
+
+ // When the model has a minimum and a maximum property, those will be
+ // used for the history source's minimum and maximum rather than
+ // calculating it based on the items in the history.
+
+ QCOMPARE(historySource->minimum(), 1);
+ QCOMPARE(historySource->maximum(), 2);
+ }
+};
+
+QTEST_GUILESS_MAIN(HistoryProxySourceTest)
+
+#include "HistoryProxySourceTest.moc"
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include <QTest>
+#include <QSignalSpy>
+#include <QQmlEngine>
+#include <QQmlContext>
+
+#include "ItemBuilder.h"
+
+class TestIncubationController : public QObject, public QQmlIncubationController
+{
+public:
+ TestIncubationController()
+ {
+ startTimer(16);
+ }
+
+protected:
+ void timerEvent(QTimerEvent *) override
+ {
+ incubateFor(5);
+ }
+};
+
+class ItemBuilderTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void initTestCase()
+ {
+ m_engine = new QQmlEngine{this};
+ m_component = new QQmlComponent{m_engine, this};
+ m_component->setData("import QtQuick 2.15; Item { }", QUrl());
+ m_item = new QQuickItem{};
+ }
+
+ void testBasic()
+ {
+ ItemBuilder builder;
+ builder.setComponent(m_component);
+ builder.setCount(10);
+ builder.setIncubationMode(QQmlIncubator::IncubationMode::Synchronous);
+
+ QSignalSpy beginCreateSpy{&builder, &ItemBuilder::beginCreate};
+ QSignalSpy endCreateSpy{&builder, &ItemBuilder::endCreate};
+ QSignalSpy finishedSpy{&builder, &ItemBuilder::finished};
+
+ builder.build(m_item);
+
+ QCOMPARE(beginCreateSpy.size(), 10);
+ QCOMPARE(endCreateSpy.size(), 10);
+ QCOMPARE(finishedSpy.size(), 1);
+
+ auto items = builder.items();
+ QCOMPARE(items.size(), 10);
+ for (auto item : items) {
+ QVERIFY(item);
+ QCOMPARE(item->parentItem(), m_item);
+ }
+
+
+ beginCreateSpy.clear();
+ endCreateSpy.clear();
+ finishedSpy.clear();
+
+ builder.setCount(20);
+ QCOMPARE(builder.items().size(), 0);
+ QVERIFY(!builder.isFinished());
+
+ builder.build(m_item);
+
+ QVERIFY(builder.isFinished());
+
+ items = builder.items();
+ QCOMPARE(items.size(), 20);
+ QCOMPARE(finishedSpy.size(), 1);
+ QCOMPARE(beginCreateSpy.size(), 20);
+ QCOMPARE(endCreateSpy.size(), 20);
+
+ for (auto item : items) {
+ QVERIFY(item);
+ QCOMPARE(item->parentItem(), m_item);
+ }
+ }
+
+ void testInitialProperties()
+ {
+ ItemBuilder builder;
+ builder.setComponent(m_component);
+ builder.setCount(10);
+ builder.setIncubationMode(QQmlIncubator::IncubationMode::Synchronous);
+
+ builder.setInitialProperties({
+ { QStringLiteral("width"), 100 },
+ { QStringLiteral("height"), 100 }
+ });
+
+ builder.build(m_item);
+
+ QVERIFY(builder.isFinished());
+
+ const auto items = builder.items();
+ QCOMPARE(items.size(), 10);
+ for (auto item : items) {
+ QVERIFY(item);
+ QCOMPARE(item->parentItem(), m_item);
+ QCOMPARE(item->width(), 100);
+ QCOMPARE(item->height(), 100);
+ }
+ }
+
+ void testAsync()
+ {
+ m_engine->setIncubationController(new TestIncubationController);
+
+ ItemBuilder builder;
+ builder.setComponent(m_component);
+ builder.setCount(10);
+ builder.setIncubationMode(QQmlIncubator::IncubationMode::Asynchronous);
+
+ builder.setInitialProperties({
+ { QStringLiteral("width"), 100 },
+ { QStringLiteral("height"), 100 }
+ });
+
+ builder.build(m_item);
+
+ QTRY_VERIFY_WITH_TIMEOUT(builder.isFinished(), 500);
+
+ const auto items = builder.items();
+ QCOMPARE(items.size(), 10);
+ for (auto item : items) {
+ QVERIFY(item);
+ QCOMPARE(item->parentItem(), m_item);
+ QCOMPARE(item->width(), 100);
+ QCOMPARE(item->height(), 100);
+ }
+
+ m_engine->setIncubationController(nullptr);
+ }
+
+ void cleanupTestCase()
+ {
+ delete m_item;
+ delete m_component;
+ delete m_engine;
+ }
+
+private:
+ QQmlEngine *m_engine = nullptr;
+ QQmlComponent *m_component = nullptr;
+ QQuickItem *m_item = nullptr;
+};
+
+QTEST_GUILESS_MAIN(ItemBuilderTest)
+
+#include "ItemBuilderTest.moc"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include <QTest>
+
+#include "datasource/ArraySource.h"
+#include "datasource/MapProxySource.h"
+
+#define qs QStringLiteral
+
+class MapProxySourceTest : public QObject
+{
+ Q_OBJECT
+
+private Q_SLOTS:
+ void testCreate()
+ {
+ // Basic creation should create an empty source.
+ auto source = new MapProxySource{};
+
+ QCOMPARE(source->itemCount(), 0);
+ QCOMPARE(source->item(0), QVariant{});
+ QCOMPARE(source->minimum(), QVariant{});
+ QCOMPARE(source->maximum(), QVariant{});
+ QCOMPARE(source->source(), nullptr);
+ }
+
+ void testWithArray_data()
+ {
+ QTest::addColumn<QVariantList>("array");
+ QTest::addColumn<QVariantMap>("map");
+ QTest::addColumn<QVariantList>("expected");
+ QTest::addColumn<QVariant>("minimum");
+ QTest::addColumn<QVariant>("maximum");
+
+ // clang-format off
+ QTest::newRow("basic mapping")
+ << QVariantList{qs("one"), qs("two"), qs("three"), qs("four")}
+ << QVariantMap{{qs("one"), 4}, {qs("two"), 3}, {qs("three"), 2}, {qs("four"), 1}}
+ << QVariantList{4, 3, 2, 1}
+ << QVariant{1}
+ << QVariant{4};
+
+ QTest::newRow("colors")
+ << QVariantList{qs("red"), qs("green"), qs("blue"), qs("red"), qs("green"), qs("blue")}
+ << QVariantMap{{qs("red"), QColor{Qt::red}.rgba()}, {qs("green"), QColor{Qt::green}.rgba()}, {qs("blue"), QColor{Qt::blue}.rgba()}}
+ << QVariantList{QColor{Qt::red}.rgba(), QColor{Qt::green}.rgba(), QColor{Qt::blue}.rgba(), QColor{Qt::red}.rgba(), QColor{Qt::green}.rgba(), QColor{Qt::blue}.rgba()}
+ << QVariant{QColor{Qt::blue}.rgba()}
+ << QVariant{QColor{Qt::red}.rgba()};
+ // clang-format on
+ }
+
+ void testWithArray()
+ {
+ auto arraySource = new ArraySource{};
+ QFETCH(QVariantList, array);
+ arraySource->setArray(array);
+
+ auto mapSource = new MapProxySource{};
+ mapSource->setSource(arraySource);
+
+ QFETCH(QVariantMap, map);
+ mapSource->setMap(map);
+
+ QFETCH(QVariantList, expected);
+ QCOMPARE(mapSource->itemCount(), expected.size());
+
+ for (int i = 0; i < mapSource->itemCount(); ++i) {
+ QCOMPARE(mapSource->item(i), expected.at(i));
+ }
+
+ QCOMPARE(mapSource->item(expected.size()), QVariant{});
+
+ QFETCH(QVariant, minimum);
+ QCOMPARE(mapSource->minimum(), minimum);
+
+ QFETCH(QVariant, maximum);
+ QCOMPARE(mapSource->maximum(), maximum);
+ }
+};
+
+QTEST_GUILESS_MAIN(MapProxySourceTest)
+
+#include "MapProxySourceTest.moc"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ * SPDX-FileCopyrightText: 2022 Alexander Lohnau <alexander.lohnau@gmx.de>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include <QtQuickTest>
+
+QUICK_TEST_MAIN(Charts)
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtTest
+
+import org.kde.quickcharts as Charts
+
+TestCase {
+ id: testCase
+ name: "Bar Chart Tests"
+
+ width: 400
+ height: 400
+ visible: true
+ when: windowShown
+
+ Component {
+ id: minimal
+ Charts.BarChart { }
+ }
+
+ Component {
+ id: simple
+ Charts.BarChart {
+ width: 200
+ height: 200
+ nameSource: Charts.ArraySource { array: ["Test 1", "Test 2", "Test 3"] }
+ colorSource: Charts.ArraySource { array: ["red", "green", "blue"] }
+ valueSources: Charts.ArraySource { array: [1, 2, 3, 4, 5] }
+ }
+ }
+
+ function test_create_data() {
+ return [
+ { tag: "minimal", component: minimal },
+ { tag: "simple", component: simple }
+ ]
+ }
+
+ function test_create(data) {
+ var item = createTemporaryObject(data.component, testCase)
+ verify(item)
+ verify(waitForRendering(item))
+ }
+}
+
+
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtTest
+
+import org.kde.quickcharts as Charts
+
+TestCase {
+ id: testCase
+ name: "Line Chart Tests"
+
+ width: 400
+ height: 400
+ visible: true
+ when: windowShown
+
+ Component {
+ id: minimal
+ Charts.LineChart { }
+ }
+
+ Component {
+ id: simple
+ Charts.LineChart {
+ width: 200
+ height: 200
+ nameSource: Charts.ArraySource { array: ["Test 1", "Test 2", "Test 3"] }
+ colorSource: Charts.ArraySource { array: ["red", "green", "blue"] }
+ valueSources: Charts.ArraySource { array: [1, 2, 3, 4, 5] }
+ }
+ }
+
+ function test_create_data() {
+ return [
+ { tag: "minimal", component: minimal },
+ { tag: "simple", component: simple }
+ ]
+ }
+
+ function test_create(data) {
+ var item = createTemporaryObject(data.component, testCase)
+ verify(item)
+ verify(waitForRendering(item))
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtTest
+
+import org.kde.quickcharts as Charts
+
+TestCase {
+ id: testCase
+ name: "Pie Chart Tests"
+
+ width: 400
+ height: 400
+ visible: true
+ when: windowShown
+
+ Component {
+ id: minimal
+ Charts.PieChart { }
+ }
+
+ Component {
+ id: simple
+ Charts.PieChart {
+ width: 200
+ height: 200
+ nameSource: Charts.ArraySource { array: ["Test 1", "Test 2", "Test 3"] }
+ colorSource: Charts.ArraySource { array: ["red", "green", "blue"] }
+ valueSources: Charts.ArraySource { array: [1, 2, 3, 4, 5] }
+ }
+ }
+
+ Component {
+ id: multiValue
+ Charts.PieChart {
+ width: 200
+ height: 200
+ nameSource: Charts.ArraySource { array: ["Test 1", "Test 2", "Test 3"] }
+ colorSource: Charts.ArraySource { array: ["red", "green", "blue"] }
+ valueSources: [
+ Charts.ArraySource { array: [1, 2, 3, 4, 5] },
+ Charts.ArraySource { array: [1, 2, 3, 4, 5] },
+ Charts.ArraySource { array: [1, 2, 3, 4, 5] }
+ ]
+ }
+ }
+
+ Component {
+ id: model
+ Charts.PieChart {
+ width: 200
+ height: 200
+
+ valueSources: [
+ Charts.ModelSource {
+ model: ListModel {
+ id: listModel
+ ListElement { name: "Test 1"; color: "red"; value: 1 }
+ ListElement { name: "Test 2"; color: "green"; value: 2 }
+ ListElement { name: "Test 3"; color: "blue"; value: 3 }
+ ListElement { name: "Test 4"; color: "cyan"; value: 4 }
+ ListElement { name: "Test 5"; color: "magenta"; value: 5 }
+ ListElement { name: "Test 6"; color: "yellow"; value: 6 }
+ }
+ }
+ ]
+ nameSource: Charts.ModelSource { model: listModel; roleName: "name" }
+ colorSource: Charts.ModelSource { model: listModel; roleName: "color" }
+ }
+ }
+
+ function test_create_data() {
+ return [
+ { tag: "minimal", component: minimal },
+ { tag: "simple", component: simple },
+ { tag: "multiValue", component: multiValue },
+ { tag: "model", component: model }
+ ]
+ }
+
+ function test_create(data) {
+ var item = createTemporaryObject(data.component, testCase)
+ verify(item)
+ verify(waitForRendering(item))
+ }
+}
+
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "AxisLabels.h"
+
+#include <QDebug>
+#include <QQmlContext>
+
+#include "ItemBuilder.h"
+#include "datasource/ChartDataSource.h"
+
+AxisLabelsAttached::AxisLabelsAttached(QObject *parent)
+ : QObject(parent)
+{
+}
+
+int AxisLabelsAttached::index() const
+{
+ return m_index;
+}
+
+void AxisLabelsAttached::setIndex(int newIndex)
+{
+ if (newIndex == m_index) {
+ return;
+ }
+
+ m_index = newIndex;
+ Q_EMIT indexChanged();
+}
+
+QString AxisLabelsAttached::label() const
+{
+ return m_label;
+}
+
+void AxisLabelsAttached::setLabel(const QString &newLabel)
+{
+ if (newLabel == m_label) {
+ return;
+ }
+
+ m_label = newLabel;
+ Q_EMIT labelChanged();
+}
+
+AxisLabels::AxisLabels(QQuickItem *parent)
+ : QQuickItem(parent)
+{
+ m_itemBuilder = std::make_unique<ItemBuilder>();
+ connect(m_itemBuilder.get(), &ItemBuilder::finished, this, &AxisLabels::polish);
+ connect(m_itemBuilder.get(), &ItemBuilder::beginCreate, this, &AxisLabels::onBeginCreate);
+}
+
+AxisLabels::~AxisLabels() = default;
+
+AxisLabels::Direction AxisLabels::direction() const
+{
+ return m_direction;
+}
+
+void AxisLabels::setDirection(AxisLabels::Direction newDirection)
+{
+ if (newDirection == m_direction) {
+ return;
+ }
+
+ m_direction = newDirection;
+ polish();
+ Q_EMIT directionChanged();
+}
+
+QQmlComponent *AxisLabels::delegate() const
+{
+ return m_itemBuilder->component();
+}
+
+void AxisLabels::setDelegate(QQmlComponent *newDelegate)
+{
+ if (newDelegate == m_itemBuilder->component()) {
+ return;
+ }
+
+ m_itemBuilder->setComponent(newDelegate);
+ updateLabels();
+ Q_EMIT delegateChanged();
+}
+
+ChartDataSource *AxisLabels::source() const
+{
+ return m_source;
+}
+
+void AxisLabels::setSource(ChartDataSource *newSource)
+{
+ if (newSource == m_source) {
+ return;
+ }
+
+ if (m_source) {
+ m_source->disconnect(this);
+ }
+
+ m_source = newSource;
+
+ if (m_source) {
+ connect(m_source, &ChartDataSource::dataChanged, this, [this]() {
+ updateLabels();
+ });
+ }
+
+ updateLabels();
+ Q_EMIT sourceChanged();
+}
+
+Qt::Alignment AxisLabels::alignment() const
+{
+ return m_alignment;
+}
+
+void AxisLabels::setAlignment(Qt::Alignment newAlignment)
+{
+ if (newAlignment == m_alignment) {
+ return;
+ }
+
+ m_alignment = newAlignment;
+ polish();
+ Q_EMIT alignmentChanged();
+}
+
+bool AxisLabels::constrainToBounds() const
+{
+ return m_constrainToBounds;
+}
+
+void AxisLabels::setConstrainToBounds(bool newConstrainToBounds)
+{
+ if (newConstrainToBounds == m_constrainToBounds) {
+ return;
+ }
+
+ m_constrainToBounds = newConstrainToBounds;
+ polish();
+ Q_EMIT constrainToBoundsChanged();
+}
+
+void AxisLabels::updatePolish()
+{
+ if (!m_itemBuilder->isFinished()) {
+ return;
+ }
+
+ auto maxWidth = 0.0;
+ auto totalWidth = 0.0;
+ auto maxHeight = 0.0;
+ auto totalHeight = 0.0;
+
+ auto labels = m_itemBuilder->items();
+ for (auto label : labels) {
+ maxWidth = std::max(maxWidth, label->implicitWidth());
+ maxHeight = std::max(maxHeight, label->implicitHeight());
+ totalWidth += label->implicitWidth();
+ totalHeight += label->implicitHeight();
+ }
+
+ auto impWidth = isHorizontal() ? totalWidth : maxWidth;
+ auto impHeight = isHorizontal() ? maxHeight : totalHeight;
+
+ if (qFuzzyCompare(impWidth, width()) && qFuzzyCompare(impHeight, height())) {
+ return;
+ }
+
+ setImplicitSize(impWidth, impHeight);
+
+ auto spacing = (isHorizontal() ? width() : height()) / (labels.size() - 1);
+ auto i = 0;
+ auto layoutWidth = isHorizontal() ? 0.0 : width();
+ auto layoutHeight = isHorizontal() ? height() : 0.0;
+
+ for (auto label : labels) {
+ auto x = 0.0;
+ auto y = 0.0;
+
+ switch (m_direction) {
+ case Direction::HorizontalLeftRight:
+ x = i * spacing;
+ break;
+ case Direction::HorizontalRightLeft:
+ x = width() - i * spacing;
+ break;
+ case Direction::VerticalTopBottom:
+ y = i * spacing;
+ break;
+ case Direction::VerticalBottomTop:
+ y = height() - i * spacing;
+ break;
+ }
+
+ if (m_alignment & Qt::AlignHCenter) {
+ x += (layoutWidth - label->implicitWidth()) / 2;
+ } else if (m_alignment & Qt::AlignRight) {
+ x += layoutWidth - label->implicitWidth();
+ }
+
+ if (m_alignment & Qt::AlignVCenter) {
+ y += (layoutHeight - label->implicitHeight()) / 2;
+ } else if (m_alignment & Qt::AlignBottom) {
+ y += layoutHeight - label->implicitHeight();
+ }
+
+ if (m_constrainToBounds) {
+ x = std::max(x, 0.0);
+ x = x + label->implicitWidth() > width() ? width() - label->implicitWidth() : x;
+ y = std::max(y, 0.0);
+ y = y + label->implicitHeight() > height() ? height() - label->implicitHeight() : y;
+ }
+
+ label->setX(x);
+ label->setY(y);
+
+ i++;
+ }
+}
+
+void AxisLabels::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
+{
+ QQuickItem::geometryChange(newGeometry, oldGeometry);
+
+ if (newGeometry != oldGeometry) {
+ polish();
+ }
+}
+
+bool AxisLabels::isHorizontal()
+{
+ return m_direction == Direction::HorizontalLeftRight || m_direction == Direction::HorizontalRightLeft;
+}
+
+void AxisLabels::updateLabels()
+{
+ m_itemBuilder->clear();
+
+ if (!m_itemBuilder->component() || !m_source) {
+ return;
+ }
+
+ m_itemBuilder->setCount(m_source->itemCount());
+ m_itemBuilder->build(this);
+}
+
+void AxisLabels::onBeginCreate(int index, QQuickItem *item)
+{
+ QObject::connect(item, &QQuickItem::implicitWidthChanged, this, &AxisLabels::polish);
+ QObject::connect(item, &QQuickItem::implicitHeightChanged, this, &AxisLabels::polish);
+
+ auto attached = static_cast<AxisLabelsAttached *>(qmlAttachedPropertiesObject<AxisLabels>(item, true));
+ attached->setIndex(index);
+ attached->setLabel(m_source->item(index).toString());
+}
+
+#include "moc_AxisLabels.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef AXISLABELS_H
+#define AXISLABELS_H
+
+#include <memory>
+
+#include <QQuickItem>
+#include <Qt>
+#include <qqmlregistration.h>
+
+class ChartDataSource;
+class ItemBuilder;
+
+class AxisLabels;
+
+class AxisLabelsAttached : public QObject
+{
+ Q_OBJECT
+ QML_ANONYMOUS
+
+public:
+ explicit AxisLabelsAttached(QObject *parent = nullptr);
+
+ Q_PROPERTY(int index READ index NOTIFY indexChanged)
+ int index() const;
+ void setIndex(int newIndex);
+ Q_SIGNAL void indexChanged();
+
+ Q_PROPERTY(QString label READ label NOTIFY labelChanged)
+ QString label() const;
+ void setLabel(const QString &newLabel);
+ Q_SIGNAL void labelChanged();
+
+private:
+ int m_index = -1;
+ QString m_label;
+};
+
+/**
+ * An item that uses a delegate to place axis labels on a chart.
+ */
+class AxisLabels : public QQuickItem
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_ATTACHED(AxisLabelsAttached)
+
+public:
+ enum class Direction { HorizontalLeftRight, HorizontalRightLeft, VerticalTopBottom, VerticalBottomTop };
+ Q_ENUM(Direction)
+
+ explicit AxisLabels(QQuickItem *parent = nullptr);
+ ~AxisLabels() override;
+
+ Q_PROPERTY(AxisLabels::Direction direction READ direction WRITE setDirection NOTIFY directionChanged)
+ AxisLabels::Direction direction() const;
+ Q_SLOT void setDirection(AxisLabels::Direction newDirection);
+ Q_SIGNAL void directionChanged();
+
+ Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
+ QQmlComponent *delegate() const;
+ Q_SLOT void setDelegate(QQmlComponent *newDelegate);
+ Q_SIGNAL void delegateChanged();
+
+ Q_PROPERTY(ChartDataSource *source READ source WRITE setSource NOTIFY sourceChanged)
+ ChartDataSource *source() const;
+ Q_SLOT void setSource(ChartDataSource *newSource);
+ Q_SIGNAL void sourceChanged();
+
+ Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged)
+ Qt::Alignment alignment() const;
+ Q_SLOT void setAlignment(Qt::Alignment newAlignment);
+ Q_SIGNAL void alignmentChanged();
+
+ Q_PROPERTY(bool constrainToBounds READ constrainToBounds WRITE setConstrainToBounds NOTIFY constrainToBoundsChanged)
+ bool constrainToBounds() const;
+ Q_SLOT void setConstrainToBounds(bool newConstrainToBounds);
+ Q_SIGNAL void constrainToBoundsChanged();
+
+ static AxisLabelsAttached *qmlAttachedProperties(QObject *object)
+ {
+ return new AxisLabelsAttached(object);
+ }
+
+protected:
+ void updatePolish() override;
+ void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
+
+private:
+ bool isHorizontal();
+ void updateLabels();
+ void onBeginCreate(int index, QQuickItem *item);
+
+ Direction m_direction = Direction::HorizontalLeftRight;
+ ChartDataSource *m_source = nullptr;
+ Qt::Alignment m_alignment = Qt::AlignHCenter | Qt::AlignVCenter;
+ bool m_constrainToBounds = true;
+
+ std::unique_ptr<ItemBuilder> m_itemBuilder;
+ bool m_layoutScheduled = false;
+};
+
+#endif // AXISLABELS_H
--- /dev/null
+
+add_library(QuickChartsControls)
+set_target_properties(QuickChartsControls PROPERTIES
+ SOVERSION ${KF6QuickCharts_SOVERSION}
+ VERSION ${KF6QuickCharts_VERSION}
+)
+
+ecm_add_qml_module(QuickChartsControls URI "org.kde.quickcharts.controls" VERSION 1.0 DEPENDENCIES QtCore QtQuick GENERATE_PLUGIN_SOURCE)
+
+target_sources(QuickChartsControls PRIVATE
+ AxisLabels.cpp
+ AxisLabels.h
+ GridLines.cpp
+ GridLines.h
+ LegendLayout.cpp
+ LegendLayout.h
+ LegendModel.cpp
+ LegendModel.h
+ LineGridNode.cpp
+ LineGridNode.h
+)
+
+set_source_files_properties(Theme.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
+
+ecm_target_qml_sources(QuickChartsControls SOURCES
+ Legend.qml
+ LegendDelegate.qml
+ LineChartControl.qml
+ PieChartControl.qml
+ Theme.qml
+ KirigamiTheme.qml
+)
+
+target_link_libraries(QuickChartsControls PRIVATE
+ Qt6::Core
+ Qt6::Quick
+ Qt6::Qml
+ Qt6::Gui
+ Qt6::QuickControls2
+ QuickCharts
+)
+
+ecm_finalize_qml_module(QuickChartsControls DESTINATION ${KDE_INSTALL_QMLDIR})
+
+install(TARGETS QuickChartsControls ${KDE_INSTALL_DEFAULT_ARGUMENTS})
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "ChartsControlsPlugin.h"
+
+#include <QDebug>
+#include <QFile>
+#include <QQmlEngine>
+#include <QQuickStyle>
+#include <QUrl>
+
+ChartsControlsPlugin::ChartsControlsPlugin(QObject *parent)
+ : QQmlExtensionPlugin(parent)
+{
+}
+
+void ChartsControlsPlugin::registerTypes(const char *uri)
+{
+ Q_ASSERT(QString::fromLatin1(uri) == QLatin1String("org.kde.quickcharts.controls"));
+
+ m_styleName = QQuickStyle::name();
+
+ qmlRegisterSingletonType(componentUrl(QStringLiteral("Theme.qml")), uri, 1, 0, "Theme");
+ qmlRegisterType(componentUrl(QStringLiteral("Legend.qml")), uri, 1, 0, "Legend");
+ qmlRegisterType(componentUrl(QStringLiteral("LegendDelegate.qml")), uri, 1, 0, "LegendDelegate");
+ qmlRegisterType(componentUrl(QStringLiteral("LineChartControl.qml")), uri, 1, 0, "LineChartControl");
+ qmlRegisterType(componentUrl(QStringLiteral("PieChartControl.qml")), uri, 1, 0, "PieChartControl");
+
+ qmlRegisterSingletonType(componentUrl(QStringLiteral("Logging.qml")), uri, 1, 0, "Logging");
+}
+
+QUrl ChartsControlsPlugin::componentUrl(const QString &fileName)
+{
+ auto url = baseUrl();
+ url.setPath(url.path() % QLatin1Char('/'));
+
+ auto styled = url.resolved(QUrl{QStringLiteral("styles/") % m_styleName % QLatin1Char('/') % fileName});
+ if (QFile::exists(styled.toLocalFile())) {
+ return styled;
+ }
+
+ return url.resolved(QUrl{fileName});
+}
+
+#include "moc_ChartsControlsPlugin.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef CHARTSCONTROLSPLUGIN_H
+#define CHARTSCONTROLSPLUGIN_H
+
+#include <QQmlExtensionPlugin>
+
+class ChartsControlsPlugin : public QQmlExtensionPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
+
+public:
+ explicit ChartsControlsPlugin(QObject *parent = nullptr);
+ void registerTypes(const char *uri) override;
+
+private:
+ QUrl componentUrl(const QString &fileName);
+ QString m_styleName;
+};
+
+#endif // CHARTSCONTROLSPLUGIN_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "GridLines.h"
+
+#include "LineGridNode.h"
+#include "XYChart.h"
+
+LinePropertiesGroup::LinePropertiesGroup(GridLines *parent)
+ : QObject(parent)
+{
+ m_parent = parent;
+}
+
+bool LinePropertiesGroup::visible() const
+{
+ return m_visible;
+}
+
+void LinePropertiesGroup::setVisible(bool newVisible)
+{
+ if (newVisible == m_visible) {
+ return;
+ }
+
+ m_visible = newVisible;
+ Q_EMIT propertiesChanged();
+}
+
+QColor LinePropertiesGroup::color() const
+{
+ return m_color;
+}
+
+void LinePropertiesGroup::setColor(const QColor &newColor)
+{
+ if (newColor == m_color) {
+ return;
+ }
+
+ m_color = newColor;
+ Q_EMIT propertiesChanged();
+}
+
+float LinePropertiesGroup::lineWidth() const
+{
+ return m_lineWidth;
+}
+
+void LinePropertiesGroup::setLineWidth(float newLineWidth)
+{
+ if (newLineWidth == m_lineWidth) {
+ return;
+ }
+
+ m_lineWidth = newLineWidth;
+ Q_EMIT propertiesChanged();
+}
+
+int LinePropertiesGroup::frequency() const
+{
+ return m_frequency;
+}
+
+void LinePropertiesGroup::setFrequency(int newFrequency)
+{
+ if (newFrequency == m_frequency) {
+ return;
+ }
+
+ m_frequency = newFrequency;
+ Q_EMIT propertiesChanged();
+}
+
+int LinePropertiesGroup::count() const
+{
+ return m_count;
+}
+
+void LinePropertiesGroup::setCount(int newCount)
+{
+ if (newCount == m_count) {
+ return;
+ }
+
+ m_count = newCount;
+ Q_EMIT propertiesChanged();
+}
+
+GridLines::GridLines(QQuickItem *parent)
+ : QQuickItem(parent)
+{
+ setFlag(QQuickItem::ItemHasContents);
+
+ m_major = std::make_unique<LinePropertiesGroup>(this);
+ connect(m_major.get(), &LinePropertiesGroup::propertiesChanged, this, &GridLines::update);
+ m_minor = std::make_unique<LinePropertiesGroup>(this);
+ connect(m_minor.get(), &LinePropertiesGroup::propertiesChanged, this, &GridLines::update);
+}
+
+GridLines::Direction GridLines::direction() const
+{
+ return m_direction;
+}
+
+void GridLines::setDirection(GridLines::Direction newDirection)
+{
+ if (newDirection == m_direction) {
+ return;
+ }
+
+ m_direction = newDirection;
+ update();
+ Q_EMIT directionChanged();
+}
+
+XYChart *GridLines::chart() const
+{
+ return m_chart;
+}
+
+void GridLines::setChart(XYChart *newChart)
+{
+ if (newChart == m_chart) {
+ return;
+ }
+
+ if (m_chart) {
+ disconnect(m_chart, &XYChart::computedRangeChanged, this, &GridLines::update);
+ }
+
+ m_chart = newChart;
+
+ if (m_chart) {
+ connect(m_chart, &XYChart::computedRangeChanged, this, &GridLines::update);
+ }
+
+ update();
+ Q_EMIT chartChanged();
+}
+
+float GridLines::spacing() const
+{
+ return m_spacing;
+}
+
+void GridLines::setSpacing(float newSpacing)
+{
+ if (newSpacing == m_spacing || m_chart != nullptr) {
+ return;
+ }
+
+ m_spacing = newSpacing;
+ update();
+ Q_EMIT spacingChanged();
+}
+
+LinePropertiesGroup *GridLines::majorGroup() const
+{
+ return m_major.get();
+}
+
+LinePropertiesGroup *GridLines::minorGroup() const
+{
+ return m_minor.get();
+}
+
+QSGNode *GridLines::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *)
+{
+ if (!node) {
+ node = new QSGNode{};
+ node->appendChildNode(new LineGridNode{});
+ node->appendChildNode(new LineGridNode{});
+ }
+
+ if (m_chart) {
+ if (m_direction == Direction::Horizontal) {
+ m_spacing = width() / (m_chart->computedRange().distanceX - 1);
+ } else {
+ m_spacing = height() / (m_chart->computedRange().distanceY);
+ }
+ }
+
+ updateLines(static_cast<LineGridNode *>(node->childAtIndex(0)), m_minor.get());
+ updateLines(static_cast<LineGridNode *>(node->childAtIndex(1)), m_major.get());
+
+ return node;
+}
+
+void GridLines::updateLines(LineGridNode *node, LinePropertiesGroup *properties)
+{
+ node->setVisible(properties->visible());
+ node->setRect(boundingRect());
+ node->setVertical(m_direction == Direction::Vertical);
+ node->setColor(properties->color());
+ node->setLineWidth(properties->lineWidth());
+ if (properties->count() > 0) {
+ node->setSpacing(m_direction == Direction::Horizontal ? width() / (properties->count() + 1) : height() / (properties->count() + 1));
+ } else {
+ node->setSpacing(m_spacing * properties->frequency());
+ }
+ node->update();
+}
+
+#include "moc_GridLines.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef GRIDLINES_H
+#define GRIDLINES_H
+
+#include <memory>
+
+#include <QQuickItem>
+
+class GridLines;
+class LineGridNode;
+class XYChart;
+
+class LinePropertiesGroup : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("Grouped Property")
+
+public:
+ explicit LinePropertiesGroup(GridLines *parent);
+
+ Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY propertiesChanged)
+ bool visible() const;
+ void setVisible(bool newVisible);
+
+ Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY propertiesChanged)
+ QColor color() const;
+ void setColor(const QColor &newColor);
+
+ Q_PROPERTY(float lineWidth READ lineWidth WRITE setLineWidth NOTIFY propertiesChanged)
+ float lineWidth() const;
+ void setLineWidth(float newLineWidth);
+
+ Q_PROPERTY(int frequency READ frequency WRITE setFrequency NOTIFY propertiesChanged)
+ int frequency() const;
+ void setFrequency(int newFrequency);
+
+ Q_PROPERTY(int count READ count WRITE setCount NOTIFY propertiesChanged)
+ int count() const;
+ void setCount(int newCount);
+
+ Q_SIGNAL void propertiesChanged();
+
+private:
+ GridLines *m_parent = nullptr;
+ bool m_visible = true;
+ QColor m_color = Qt::black;
+ float m_lineWidth = 1.0;
+ int m_frequency = 2;
+ int m_count = -1;
+};
+
+/**
+ * An item that renders a set of lines to make a grid for a chart.
+ */
+class GridLines : public QQuickItem
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ enum class Direction { Horizontal, Vertical };
+ Q_ENUM(Direction)
+ /**
+ * Default constructor
+ */
+ explicit GridLines(QQuickItem *parent = nullptr);
+
+ Q_PROPERTY(GridLines::Direction direction READ direction WRITE setDirection NOTIFY directionChanged)
+ Direction direction() const;
+ void setDirection(GridLines::Direction newDirection);
+ Q_SIGNAL void directionChanged();
+
+ Q_PROPERTY(XYChart *chart READ chart WRITE setChart NOTIFY chartChanged)
+ XYChart *chart() const;
+ void setChart(XYChart *newChart);
+ Q_SIGNAL void chartChanged();
+
+ Q_PROPERTY(float spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
+ float spacing() const;
+ void setSpacing(float newSpacing);
+ Q_SIGNAL void spacingChanged();
+
+ Q_PROPERTY(LinePropertiesGroup *major READ majorGroup CONSTANT)
+ LinePropertiesGroup *majorGroup() const;
+
+ Q_PROPERTY(LinePropertiesGroup *minor READ minorGroup CONSTANT)
+ LinePropertiesGroup *minorGroup() const;
+
+private:
+ QSGNode *updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *) override;
+ void updateLines(LineGridNode *node, LinePropertiesGroup *properties);
+
+ GridLines::Direction m_direction = Direction::Horizontal;
+ XYChart *m_chart = nullptr;
+ float m_spacing = 10.0;
+
+ std::unique_ptr<LinePropertiesGroup> m_major;
+ std::unique_ptr<LinePropertiesGroup> m_minor;
+};
+
+#endif // GRIDLINES_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+
+import org.kde.kirigami as Kirigami
+
+QtObject {
+ property real gridUnit: Kirigami.Units.gridUnit
+ property real smallSpacing: Kirigami.Units.smallSpacing
+ property real largeSpacing: Kirigami.Units.largeSpacing
+ property real cornerRadius: Kirigami.Units.cornerRadius
+
+ property real smallIconSize: Kirigami.Units.iconSizes.small
+
+ property color highlightColor: Kirigami.Theme.highlightColor
+ property color backgroundColor: Kirigami.Theme.backgroundColor
+
+ Kirigami.Theme.colorSet: Kirigami.Theme.View
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+
+import org.kde.quickcharts as Charts
+import org.kde.quickcharts.controls
+
+/**
+ * A pre-made legend control that displays a legend for charts.
+ */
+Control {
+ id: control
+
+ /**
+ * The chart to display the legend for.
+ */
+ property Charts.Chart chart
+ /**
+ * The delegate to use to display legend information.
+ *
+ * \sa Legend::delegate
+ */
+ property alias delegate: legendRepeater.delegate
+ /**
+ *
+ */
+ property alias model: legendRepeater.model
+
+ property alias horizontalSpacing: legend.horizontalSpacing
+ property alias verticalSpacing: legend.verticalSpacing
+
+ property real maximumDelegateWidth: Theme.gridUnit * 10
+
+ property var formatValue: function(input, index) { return input }
+ property var maximumValueWidth: function(input, index) { return -1 }
+
+ property alias preferredWidth: legend.preferredWidth
+
+ property string nameRole: "name"
+ property string shortNameRole: "shortName"
+ property string colorRole: "color"
+ property string valueRole: "value"
+
+ property bool highlightEnabled: false
+ property int highlightedIndex: -1
+
+ default property alias _children: legend.children
+
+ leftPadding: 0
+ rightPadding: 0
+ topPadding: 0
+ bottomPadding: 0
+
+ implicitWidth: Math.max(implicitContentWidth, implicitBackgroundWidth) + leftPadding + rightPadding
+ implicitHeight: Math.max(implicitContentHeight, implicitBackgroundHeight) + topPadding + bottomPadding
+
+ contentItem: Flickable {
+ anchors.fill: parent
+
+ contentHeight: legend.implicitHeight
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+
+ implicitHeight: legend.implicitHeight
+ implicitWidth: legend.implicitWidth
+
+ // Limit maximum flick velocity to ensure we can scroll one line per
+ // mouse wheel "tick" when the legend's height is very constrained.
+ maximumFlickVelocity: Theme.gridUnit * 50
+ LegendLayout {
+ id: legend
+
+ width: parent.width
+
+ Repeater {
+ id: legendRepeater
+
+ model: LegendModel { chart: control.chart }
+
+ delegate: LegendDelegate {
+ property var itemData: typeof modelData !== "undefined" ? modelData : model
+
+ name: itemData[control.nameRole] ?? ""
+ shortName: itemData[control.shortNameRole] ?? ""
+ color: itemData[control.colorRole] ?? "white"
+ value: control.formatValue(itemData[control.valueRole] ?? "", index)
+
+ highlighted: control.highlightEnabled && hovered
+
+ maximumValueWidth: {
+ var result = control.maximumValueWidth(model.value, index)
+ if (result > 0) {
+ return result
+ }
+
+ return -1
+ }
+
+ LegendLayout.minimumWidth: minimumWidth
+ LegendLayout.preferredWidth: preferredWidth
+ LegendLayout.maximumWidth: Math.max(control.maximumDelegateWidth, preferredWidth)
+
+ onHoveredChanged: {
+ if (control.highlightEnabled) {
+ if (hovered) {
+ control.highlightedIndex = index
+ } else if (control.highlightedIndex == index) {
+ control.highlightedIndex = -1
+ }
+ }
+ }
+ }
+ }
+
+ horizontalSpacing: Theme.smallSpacing
+ verticalSpacing: Theme.smallSpacing
+ }
+
+ children: [
+ Item {
+ width: parent.width;
+ height: 1;
+ visible: parent.contentY > 0
+
+ ToolButton {
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ top: parent.top
+ }
+
+ width: Theme.smallIconSize
+ height: Theme.smallIconSize
+
+ icon.name: "arrow-up-symbolic"
+ icon.width: Theme.smallIconSize
+ icon.height: Theme.smallIconSize
+ enabled: false
+ }
+ },
+ Item {
+ y: parent.height - height
+ width: parent.width;
+ height: 1;
+ visible: parent.contentY + parent.height < legend.height
+
+ ToolButton {
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ bottom: parent.bottom
+ }
+
+ width: Theme.smallIconSize
+ height: Theme.smallIconSize
+
+ icon.name: "arrow-down-symbolic"
+ icon.width: Theme.smallIconSize
+ icon.height: Theme.smallIconSize
+ enabled: false
+ }
+ }
+ ]
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+
+import org.kde.quickcharts as Charts
+import org.kde.quickcharts.controls
+
+/**
+ * A delegate that can be used as part of a Legend.
+ */
+Control {
+ id: control
+
+ property string name
+ property string shortName
+ property color color
+ property string value
+
+ property real maximumValueWidth
+
+ property Component indicator: Rectangle {
+ implicitWidth: Theme.cornerRadius
+ color: control.color
+ }
+
+ property bool highlighted: false
+
+ readonly property real minimumWidth: contentItem.minimumWidth
+ readonly property real preferredWidth: contentItem.preferredWidth
+
+ implicitHeight: Math.max(implicitContentHeight, implicitBackgroundHeight) + topPadding + bottomPadding
+
+ // Note: Do not use implicitContentWidth here as it indirectly depends on the item width and will lead to a
+ // nasty infinite layout loop. Instead use something more stable that doesn't change depending on the item
+ // width.
+ implicitWidth: Math.max(contentItem.preferredWidth, implicitBackgroundWidth) + leftPadding + rightPadding
+
+ hoverEnabled: true
+
+ leftPadding: 0
+ rightPadding: Theme.cornerRadius
+ topPadding: 0
+ bottomPadding: 0
+
+ spacing: Theme.smallSpacing
+
+ contentItem: RowLayout {
+ property real actualValueWidth: control.maximumValueWidth > 0 ? control.maximumValueWidth : value.implicitWidth
+ property real minimumValueWidth: control.width - indicator.width - control.spacing
+
+ property real minimumWidth: indicator.width + actualValueWidth + control.spacing
+ property real preferredWidth: Math.ceil(indicator.width) + Math.ceil(name.implicitWidth) + Math.ceil(actualValueWidth) + Math.ceil(control.spacing * 2) + control.leftPadding + control.rightPadding
+
+ spacing: control.spacing
+
+ Loader {
+ id: indicator
+
+ Layout.preferredWidth: item ? item.implicitWidth : 0
+ Layout.fillHeight: true
+
+ sourceComponent: control.indicator
+ }
+
+ Label {
+ id: name
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+
+ text: control.name + (control.shortName.length > 0 ? "\x9C" + control.shortName : "")
+ elide: Text.ElideRight
+ font: control.font
+ verticalAlignment: Qt.AlignVCenter
+
+ // In some cases we can trigger a behaviour where the full text is
+ // displayed while we have a size of 0. In that case, explicitly
+ // hide it. This uses opacity to ensure the item is still begin laid
+ // out.
+ opacity: width > 0 ? 1 : 0
+ }
+
+ Label {
+ id: value
+
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+
+ Layout.minimumWidth: Math.min(parent.actualValueWidth, parent.minimumValueWidth)
+
+ text: control.value;
+ elide: Text.ElideRight
+ font: name.font
+
+ verticalAlignment: Qt.AlignVCenter
+ horizontalAlignment: Qt.AlignRight
+ }
+ }
+
+ background: Rectangle {
+ color: Qt.alpha(control.color, 0.25)
+ border.color: control.color
+ border.width: 1
+ radius: Theme.cornerRadius
+ visible: control.highlighted
+ }
+
+ ToolTip.visible: control.hovered && (name.truncated || value.truncated)
+ ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval
+ ToolTip.text: "%1: %2".arg(control.name).arg(control.value)
+}
+
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "LegendLayout.h"
+
+#include <cmath>
+
+#include "Chart.h"
+#include "ItemBuilder.h"
+#include "datasource/ChartDataSource.h"
+
+qreal sizeWithSpacing(int count, qreal size, qreal spacing)
+{
+ return size * count + spacing * (count - 1);
+}
+
+LegendLayoutAttached::LegendLayoutAttached(QObject *parent)
+ : QObject(parent)
+{
+}
+
+qreal LegendLayoutAttached::minimumWidth() const
+{
+ return m_minimumWidth.value_or(0.0);
+}
+
+void LegendLayoutAttached::setMinimumWidth(qreal newMinimumWidth)
+{
+ if (newMinimumWidth == m_minimumWidth) {
+ return;
+ }
+
+ m_minimumWidth = newMinimumWidth;
+ Q_EMIT minimumWidthChanged();
+}
+
+bool LegendLayoutAttached::isMinimumWidthValid() const
+{
+ return m_minimumWidth.has_value();
+}
+
+qreal LegendLayoutAttached::preferredWidth() const
+{
+ return m_preferredWidth.value_or(0.0);
+}
+
+void LegendLayoutAttached::setPreferredWidth(qreal newPreferredWidth)
+{
+ if (newPreferredWidth == m_preferredWidth) {
+ return;
+ }
+
+ m_preferredWidth = newPreferredWidth;
+ Q_EMIT preferredWidthChanged();
+}
+
+bool LegendLayoutAttached::isPreferredWidthValid() const
+{
+ return m_preferredWidth.has_value();
+}
+
+qreal LegendLayoutAttached::maximumWidth() const
+{
+ return m_maximumWidth.value_or(0.0);
+}
+
+void LegendLayoutAttached::setMaximumWidth(qreal newMaximumWidth)
+{
+ if (newMaximumWidth == m_maximumWidth) {
+ return;
+ }
+
+ m_maximumWidth = newMaximumWidth;
+ Q_EMIT maximumWidthChanged();
+}
+
+bool LegendLayoutAttached::isMaximumWidthValid() const
+{
+ return m_maximumWidth.has_value();
+}
+
+LegendLayout::LegendLayout(QQuickItem *parent)
+ : QQuickItem(parent)
+{
+}
+
+qreal LegendLayout::horizontalSpacing() const
+{
+ return m_horizontalSpacing;
+}
+
+void LegendLayout::setHorizontalSpacing(qreal newHorizontalSpacing)
+{
+ if (newHorizontalSpacing == m_horizontalSpacing) {
+ return;
+ }
+
+ m_horizontalSpacing = newHorizontalSpacing;
+ polish();
+ Q_EMIT horizontalSpacingChanged();
+}
+
+qreal LegendLayout::verticalSpacing() const
+{
+ return m_verticalSpacing;
+}
+
+void LegendLayout::setVerticalSpacing(qreal newVerticalSpacing)
+{
+ if (newVerticalSpacing == m_verticalSpacing) {
+ return;
+ }
+
+ m_verticalSpacing = newVerticalSpacing;
+ polish();
+ Q_EMIT verticalSpacingChanged();
+}
+
+qreal LegendLayout::preferredWidth() const
+{
+ return m_preferredWidth;
+}
+
+void LegendLayout::componentComplete()
+{
+ QQuickItem::componentComplete();
+
+ m_completed = true;
+ polish();
+}
+
+void LegendLayout::updatePolish()
+{
+ if (!m_completed) {
+ return;
+ }
+
+ int columns = 0;
+ int rows = 0;
+ qreal itemWidth = 0.0;
+ qreal itemHeight = 0.0;
+
+ qreal layoutWidth = width();
+
+ std::tie(columns, rows, itemWidth, itemHeight) = determineColumns();
+
+ auto column = 0;
+ auto row = 0;
+
+ const auto items = childItems();
+ for (auto item : items) {
+ if (!item->isVisible() || item->implicitWidth() <= 0 || item->implicitHeight() <= 0) {
+ continue;
+ }
+
+ auto attached = static_cast<LegendLayoutAttached *>(qmlAttachedPropertiesObject<LegendLayout>(item, true));
+
+ auto x = (itemWidth + m_horizontalSpacing) * column;
+ auto y = (itemHeight + m_verticalSpacing) * row;
+
+ item->setPosition(QPointF{x, y});
+ item->setWidth(std::clamp(itemWidth, attached->minimumWidth(), attached->maximumWidth()));
+
+ // If we are in single column mode, we are most likely width constrained.
+ // In that case, we should make sure items do not exceed our own width,
+ // so we can trigger things like text eliding.
+ if (layoutWidth > 0 && item->width() > layoutWidth && columns == 1) {
+ item->setWidth(layoutWidth);
+ }
+
+ column++;
+ if (column >= columns) {
+ row++;
+ column = 0;
+ }
+ }
+
+ setImplicitSize(sizeWithSpacing(columns, itemWidth, m_horizontalSpacing), sizeWithSpacing(rows, itemHeight, m_verticalSpacing));
+}
+
+void LegendLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
+{
+ if (newGeometry != oldGeometry) {
+ polish();
+ }
+ QQuickItem::geometryChange(newGeometry, oldGeometry);
+}
+
+void LegendLayout::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
+{
+ if (change == QQuickItem::ItemVisibleHasChanged || change == QQuickItem::ItemSceneChange) {
+ polish();
+ }
+
+ if (change == QQuickItem::ItemChildAddedChange) {
+ auto item = data.item;
+
+ connect(item, &QQuickItem::implicitWidthChanged, this, &LegendLayout::polish);
+ connect(item, &QQuickItem::implicitHeightChanged, this, &LegendLayout::polish);
+ connect(item, &QQuickItem::visibleChanged, this, &LegendLayout::polish);
+
+ auto attached = static_cast<LegendLayoutAttached *>(qmlAttachedPropertiesObject<LegendLayout>(item, true));
+ connect(attached, &LegendLayoutAttached::minimumWidthChanged, this, &LegendLayout::polish);
+ connect(attached, &LegendLayoutAttached::preferredWidthChanged, this, &LegendLayout::polish);
+ connect(attached, &LegendLayoutAttached::maximumWidthChanged, this, &LegendLayout::polish);
+
+ polish();
+ }
+
+ if (change == QQuickItem::ItemChildRemovedChange) {
+ auto item = data.item;
+
+ item->disconnect(this);
+ auto attached = static_cast<LegendLayoutAttached *>(qmlAttachedPropertiesObject<LegendLayout>(item, false));
+ if (attached) {
+ attached->disconnect(this);
+ }
+
+ polish();
+ }
+
+ QQuickItem::itemChange(change, data);
+}
+
+// Determine how many columns and rows should be used for placing items and how
+// large each item should be.
+std::tuple<int, int, qreal, qreal> LegendLayout::determineColumns()
+{
+ auto minWidth = -std::numeric_limits<qreal>::max();
+ auto preferredWidth = -std::numeric_limits<qreal>::max();
+ auto maxWidth = std::numeric_limits<qreal>::max();
+ auto maxHeight = -std::numeric_limits<qreal>::max();
+
+ const auto items = childItems();
+
+ // Keep track of actual visual and visible items, since childItems() also
+ // includes stuff like repeaters.
+ auto itemCount = 0;
+
+ // First, we determine the minimum, preferred and maximum width of all
+ // items. These are determined from the attached object, or implicitWidth
+ // for minimum size if minimumWidth has not been set.
+ //
+ // We also determine the maximum height of items so we do not need to do
+ // that later.
+ for (auto item : items) {
+ if (!item->isVisible() || item->implicitWidth() <= 0 || item->implicitHeight() <= 0) {
+ continue;
+ }
+
+ auto attached = static_cast<LegendLayoutAttached *>(qmlAttachedPropertiesObject<LegendLayout>(item, true));
+
+ if (attached->isMinimumWidthValid()) {
+ minWidth = std::max(minWidth, attached->minimumWidth());
+ } else {
+ minWidth = std::max(minWidth, item->implicitWidth());
+ }
+
+ if (attached->isPreferredWidthValid()) {
+ preferredWidth = std::max(preferredWidth, attached->preferredWidth());
+ }
+
+ if (attached->isMaximumWidthValid()) {
+ maxWidth = std::min(maxWidth, attached->maximumWidth());
+ }
+
+ maxHeight = std::max(maxHeight, item->implicitHeight());
+
+ itemCount++;
+ }
+
+ if (itemCount == 0) {
+ return std::make_tuple(0, 0, 0, 0);
+ }
+
+ auto availableWidth = width();
+ // Check if we have a valid width. If we cannot even fit a horizontalSpacing
+ // we cannot do anything with the width and most likely did not get a width
+ // assigned, so come up with some reasonable default width.
+ //
+ // For the default, layout everything in a full row, using either maxWidth
+ // for each item if we have it or minWidth if we do not.
+ if (availableWidth <= m_horizontalSpacing) {
+ if (maxWidth <= 0.0) {
+ availableWidth = sizeWithSpacing(itemCount, minWidth, m_horizontalSpacing);
+ } else {
+ availableWidth = sizeWithSpacing(itemCount, maxWidth, m_horizontalSpacing);
+ }
+ }
+
+ // If none of the items have a maximum width set, default to filling all
+ // available space.
+ if (maxWidth <= 0.0 || maxWidth >= std::numeric_limits<qreal>::max()) {
+ maxWidth = availableWidth;
+ }
+
+ // Ensure we don't try to size things below their minimum size.
+ if (maxWidth < minWidth) {
+ maxWidth = minWidth;
+ }
+
+ if (preferredWidth != m_preferredWidth) {
+ m_preferredWidth = preferredWidth;
+ Q_EMIT preferredWidthChanged();
+ }
+
+ auto columns = 1;
+ auto rows = itemCount;
+ bool fit = true;
+
+ // Calculate the actual number of rows and columns by trying to fit items
+ // until we find the right number.
+ while (true) {
+ auto minTotalWidth = sizeWithSpacing(columns, minWidth, m_horizontalSpacing);
+ auto maxTotalWidth = sizeWithSpacing(columns, maxWidth, m_horizontalSpacing);
+
+ // If the minimum width is less than our width, but the maximum is
+ // larger, we found a correct solution since we can resize the items to
+ // fit within the provided bounds.
+ if (minTotalWidth <= availableWidth && maxTotalWidth >= availableWidth) {
+ break;
+ }
+
+ // As long as we have more space available than the items' max size,
+ // decrease the number of rows and that way increase the number of
+ // columns we use to place items - unless that results in no rows, as
+ // that means we've reached a state where we simply have more space than
+ // needed.
+ if (maxTotalWidth < availableWidth) {
+ rows--;
+ if (rows >= 1) {
+ columns = std::ceil(itemCount / float(rows));
+ } else {
+ fit = false;
+ break;
+ }
+ }
+
+ // In certain cases, we hit a corner case where decreasing the number of
+ // rows leads to things ending up outside of the item's bounds. If that
+ // happens, increase the number of rows by one and exit the loop.
+ if (minTotalWidth > availableWidth) {
+ rows += 1;
+ columns = std::ceil(itemCount / float(rows));
+ break;
+ }
+ }
+
+ // Calculate item width based on the calculated number of columns.
+ // If it turns out we have more space than needed, use maxWidth
+ // instead to avoid awkward gaps.
+ auto itemWidth = fit ? (availableWidth - m_horizontalSpacing * (columns - 1)) / columns : maxWidth;
+
+ // Recalculate the number of rows, otherwise we may end up with "ghost" rows
+ // since the items wrapped into a new column, but no all of them.
+ rows = std::ceil(itemCount / float(columns));
+
+ return std::make_tuple(columns, rows, itemWidth, maxHeight);
+}
+
+#include "moc_LegendLayout.cpp"
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef LEGENDLAYOUT_H
+#define LEGENDLAYOUT_H
+
+#include <QQuickItem>
+#include <qqmlregistration.h>
+
+class LegendLayoutAttached : public QObject
+{
+ Q_OBJECT
+ QML_ANONYMOUS
+
+public:
+ explicit LegendLayoutAttached(QObject *parent = nullptr);
+
+ Q_PROPERTY(qreal minimumWidth READ minimumWidth WRITE setMinimumWidth NOTIFY minimumWidthChanged)
+ qreal minimumWidth() const;
+ void setMinimumWidth(qreal newMinimumWidth);
+ Q_SIGNAL void minimumWidthChanged();
+ bool isMinimumWidthValid() const;
+
+ Q_PROPERTY(qreal preferredWidth READ preferredWidth WRITE setPreferredWidth NOTIFY preferredWidthChanged)
+ qreal preferredWidth() const;
+ void setPreferredWidth(qreal newPreferredWidth);
+ Q_SIGNAL void preferredWidthChanged();
+ bool isPreferredWidthValid() const;
+
+ Q_PROPERTY(qreal maximumWidth READ maximumWidth WRITE setMaximumWidth NOTIFY maximumWidthChanged)
+ qreal maximumWidth() const;
+ void setMaximumWidth(qreal newMaximumWidth);
+ Q_SIGNAL void maximumWidthChanged();
+ bool isMaximumWidthValid() const;
+
+private:
+ std::optional<qreal> m_minimumWidth;
+ std::optional<qreal> m_preferredWidth;
+ std::optional<qreal> m_maximumWidth;
+};
+
+class LegendLayout : public QQuickItem
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_ATTACHED(LegendLayoutAttached)
+
+public:
+ explicit LegendLayout(QQuickItem *parent = nullptr);
+
+ Q_PROPERTY(qreal horizontalSpacing READ horizontalSpacing WRITE setHorizontalSpacing NOTIFY horizontalSpacingChanged)
+ qreal horizontalSpacing() const;
+ void setHorizontalSpacing(qreal newHorizontalSpacing);
+ Q_SIGNAL void horizontalSpacingChanged();
+
+ Q_PROPERTY(qreal verticalSpacing READ verticalSpacing WRITE setVerticalSpacing NOTIFY verticalSpacingChanged)
+ qreal verticalSpacing() const;
+ void setVerticalSpacing(qreal newVerticalSpacing);
+ Q_SIGNAL void verticalSpacingChanged();
+
+ Q_PROPERTY(qreal preferredWidth READ preferredWidth NOTIFY preferredWidthChanged)
+ qreal preferredWidth() const;
+ Q_SIGNAL void preferredWidthChanged();
+
+ static LegendLayoutAttached *qmlAttachedProperties(QObject *object)
+ {
+ return new LegendLayoutAttached(object);
+ }
+
+protected:
+ void componentComplete() override;
+ void updatePolish() override;
+ void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
+ void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) override;
+
+private:
+ std::tuple<int, int, qreal, qreal> determineColumns();
+
+ qreal m_horizontalSpacing = 0.0;
+ qreal m_verticalSpacing = 0.0;
+ qreal m_preferredWidth = 0.0;
+
+ bool m_completed = false;
+};
+
+#endif // LEGENDLAYOUT_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "LegendModel.h"
+
+#include "Chart.h"
+#include "datasource/ChartDataSource.h"
+
+LegendModel::LegendModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+}
+
+QHash<int, QByteArray> LegendModel::roleNames() const
+{
+ static QHash<int, QByteArray> names = {
+ {NameRole, "name"},
+ {ShortNameRole, "shortName"},
+ {ColorRole, "color"},
+ {ValueRole, "value"},
+ };
+
+ return names;
+}
+
+int LegendModel::rowCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+
+ return m_items.size();
+}
+
+QVariant LegendModel::data(const QModelIndex &index, int role) const
+{
+ if (!checkIndex(index, CheckIndexOption::ParentIsInvalid | CheckIndexOption::IndexIsValid)) {
+ return {};
+ }
+
+ switch (role) {
+ case NameRole:
+ return m_items.at(index.row()).name;
+ case ShortNameRole:
+ return m_items.at(index.row()).shortName;
+ case ColorRole:
+ return m_items.at(index.row()).color;
+ case ValueRole:
+ return m_items.at(index.row()).value;
+ }
+
+ return QVariant{};
+}
+
+Chart *LegendModel::chart() const
+{
+ return m_chart;
+}
+
+void LegendModel::setChart(Chart *newChart)
+{
+ if (newChart == m_chart) {
+ return;
+ }
+
+ if (m_chart) {
+ for (const auto &connection : std::as_const(m_connections)) {
+ disconnect(connection);
+ }
+ m_connections.clear();
+ }
+
+ m_chart = newChart;
+ queueUpdate();
+ Q_EMIT chartChanged();
+}
+
+int LegendModel::sourceIndex() const
+{
+ return m_sourceIndex;
+}
+
+void LegendModel::setSourceIndex(int index)
+{
+ if (index == m_sourceIndex) {
+ return;
+ }
+
+ m_sourceIndex = index;
+ queueUpdate();
+ Q_EMIT sourceIndexChanged();
+}
+
+void LegendModel::queueUpdate()
+{
+ if (!m_updateQueued) {
+ m_updateQueued = true;
+ QMetaObject::invokeMethod(this, &LegendModel::update, Qt::QueuedConnection);
+ }
+}
+
+void LegendModel::queueDataChange()
+{
+ if (!m_dataChangeQueued) {
+ m_dataChangeQueued = true;
+ QMetaObject::invokeMethod(this, &LegendModel::updateData, Qt::QueuedConnection);
+ }
+}
+
+void LegendModel::update()
+{
+ m_updateQueued = false;
+
+ if (!m_chart) {
+ return;
+ }
+
+ beginResetModel();
+ m_items.clear();
+
+ ChartDataSource *colorSource = m_chart->colorSource();
+ ChartDataSource *nameSource = m_chart->nameSource();
+ ChartDataSource *shortNameSource = m_chart->shortNameSource();
+
+ m_connections.push_back(connect(m_chart, &Chart::colorSourceChanged, this, &LegendModel::queueUpdate, Qt::UniqueConnection));
+ m_connections.push_back(connect(m_chart, &Chart::nameSourceChanged, this, &LegendModel::queueUpdate, Qt::UniqueConnection));
+
+ auto sources = m_chart->valueSources();
+ int itemCount = countItems();
+
+ std::transform(sources.cbegin(), sources.cend(), std::back_inserter(m_connections), [this](ChartDataSource *source) {
+ return connect(source, &ChartDataSource::dataChanged, this, &LegendModel::queueDataChange, Qt::UniqueConnection);
+ });
+
+ m_connections.push_back(connect(m_chart, &Chart::valueSourcesChanged, this, &LegendModel::queueUpdate, Qt::UniqueConnection));
+
+ if ((!colorSource && !(nameSource || shortNameSource)) || itemCount <= 0) {
+ endResetModel();
+ return;
+ }
+
+ if (colorSource) {
+ m_connections.push_back(connect(colorSource, &ChartDataSource::dataChanged, this, &LegendModel::queueDataChange, Qt::UniqueConnection));
+ }
+
+ if (nameSource) {
+ m_connections.push_back(connect(nameSource, &ChartDataSource::dataChanged, this, &LegendModel::queueDataChange, Qt::UniqueConnection));
+ }
+
+ if (shortNameSource) {
+ m_connections.push_back(connect(shortNameSource, &ChartDataSource::dataChanged, this, &LegendModel::queueDataChange, Qt::UniqueConnection));
+ }
+
+ for (int i = 0; i < itemCount; ++i) {
+ LegendItem item;
+ item.name = nameSource ? nameSource->item(i).toString() : QString();
+ item.shortName = shortNameSource ? shortNameSource->item(i).toString() : QString();
+ item.color = colorSource ? colorSource->item(i).value<QColor>() : QColor();
+ item.value = getValueForItem(i);
+ m_items.push_back(item);
+ }
+
+ endResetModel();
+}
+
+void LegendModel::updateData()
+{
+ ChartDataSource *colorSource = m_chart->colorSource();
+ ChartDataSource *nameSource = m_chart->nameSource();
+ ChartDataSource *shortNameSource = m_chart->shortNameSource();
+
+ auto itemCount = countItems();
+
+ m_dataChangeQueued = false;
+
+ if (itemCount != int(m_items.size())) {
+ // Number of items changed, so trigger a full update
+ queueUpdate();
+ return;
+ }
+
+ QList<QList<int>> changedRows(itemCount);
+
+ std::for_each(m_items.begin(), m_items.end(), [&, i = 0](LegendItem &item) mutable {
+ auto name = nameSource ? nameSource->item(i).toString() : QString{};
+ if (item.name != name) {
+ item.name = name;
+ changedRows[i] << NameRole;
+ }
+
+ auto shortName = shortNameSource ? shortNameSource->item(i).toString() : QString{};
+ if (item.shortName != shortName) {
+ item.shortName = shortName;
+ changedRows[i] << ShortNameRole;
+ }
+
+ auto color = colorSource ? colorSource->item(i).toString() : QColor{};
+ if (item.color != color) {
+ item.color = color;
+ changedRows[i] << ColorRole;
+ }
+
+ auto value = getValueForItem(i);
+ if (item.value != value) {
+ item.value = value;
+ changedRows[i] << ValueRole;
+ }
+
+ i++;
+ });
+
+ for (auto i = 0; i < changedRows.size(); ++i) {
+ auto changedRoles = changedRows.at(i);
+ if (!changedRoles.isEmpty()) {
+ Q_EMIT dataChanged(index(i, 0), index(i, 0), changedRoles);
+ }
+ }
+}
+
+int LegendModel::countItems()
+{
+ auto sources = m_chart->valueSources();
+ int itemCount = 0;
+
+ switch (m_chart->indexingMode()) {
+ case Chart::IndexSourceValues:
+ if (sources.count() > 0) {
+ itemCount = sources.at(0)->itemCount();
+ }
+ break;
+ case Chart::IndexEachSource:
+ itemCount = sources.count();
+ break;
+ case Chart::IndexAllValues:
+ itemCount = std::accumulate(sources.cbegin(), sources.cend(), 0, [](int current, ChartDataSource *source) -> int {
+ return current + source->itemCount();
+ });
+ break;
+ }
+
+ return itemCount;
+}
+
+QVariant LegendModel::getValueForItem(int item)
+{
+ const auto sources = m_chart->valueSources();
+ auto value = QVariant{};
+
+ switch (m_chart->indexingMode()) {
+ case Chart::IndexSourceValues:
+ value = sources.at(0)->item(item);
+ break;
+ case Chart::IndexEachSource:
+ value = sources.at(item)->first();
+ break;
+ case Chart::IndexAllValues:
+ for (auto source : sources) {
+ if (item >= source->itemCount()) {
+ item -= source->itemCount();
+ } else {
+ value = source->item(item);
+ break;
+ }
+ }
+ break;
+ }
+
+ return value;
+}
+
+#include "moc_LegendModel.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef LEGENDMODEL_H
+#define LEGENDMODEL_H
+
+#include <vector>
+
+#include <QAbstractListModel>
+#include <QColor>
+#include <qqmlregistration.h>
+
+class Chart;
+class ChartDataSource;
+
+struct LegendItem {
+ QString name;
+ QString shortName;
+ QColor color;
+ QVariant value;
+};
+
+/**
+ * A model that extracts information from a chart that can be displayed as a legend.
+ */
+class LegendModel : public QAbstractListModel
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ enum Roles { NameRole = Qt::UserRole, ShortNameRole, ColorRole, ValueRole };
+
+ enum SourceIndex { UseSourceCount = -2 };
+ Q_ENUM(SourceIndex)
+
+ explicit LegendModel(QObject *parent = nullptr);
+
+ Q_PROPERTY(Chart *chart READ chart WRITE setChart NOTIFY chartChanged)
+ Chart *chart() const;
+ void setChart(Chart *newChart);
+ Q_SIGNAL void chartChanged();
+
+ Q_PROPERTY(int sourceIndex READ sourceIndex WRITE setSourceIndex NOTIFY sourceIndexChanged)
+ int sourceIndex() const;
+ void setSourceIndex(int index);
+ Q_SIGNAL void sourceIndexChanged();
+
+ QHash<int, QByteArray> roleNames() const override;
+ int rowCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+
+private:
+ void queueUpdate();
+ void queueDataChange();
+ void update();
+ void updateData();
+ int countItems();
+ QVariant getValueForItem(int item);
+
+ Chart *m_chart = nullptr;
+ int m_sourceIndex = UseSourceCount;
+ bool m_updateQueued = false;
+ bool m_dataChangeQueued = false;
+ std::vector<QMetaObject::Connection> m_connections;
+ std::vector<LegendItem> m_items;
+};
+
+#endif // LEGENDMODEL_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+
+import org.kde.quickcharts as Charts
+import org.kde.quickcharts.controls
+
+/**
+ * A line chart with legend, grid and axis labels.
+ */
+Control {
+
+ property alias valueSources: lineChart.valueSources
+ property alias names: nameSource.array
+ property alias color: colorSource.baseColor
+
+ property alias lineWidth: lineChart.lineWidth
+ property alias fillOpacity: lineChart.fillOpacity
+ property alias stacked: lineChart.stacked
+
+ property alias chart: lineChart
+ property alias legend: legend
+ property alias xLabels: xAxisLabels
+ property alias yLabels: yAxisLabels
+
+ property alias verticalLinesVisible: verticalLines.visible
+ property alias horizontalLinesVisible: horizontalLines.visible
+
+ property alias xRange: lineChart.xRange
+ property alias yRange: lineChart.yRange
+
+ property alias xAxisSource: xAxisLabels.source
+ property alias yAxisSource: yAxisLabels.source
+
+ property alias pointDelegate: lineChart.pointDelegate
+
+ property alias highlightEnabled: legend.highlightEnabled
+
+ background: Rectangle { color: Theme.backgroundColor }
+
+ contentItem: Item {
+ anchors.fill: parent;
+
+ GridLines {
+ id: horizontalLines
+
+ anchors.fill: lineChart
+
+ chart: lineChart
+
+ major.frequency: 2
+ major.lineWidth: 2
+ major.color: Qt.rgba(0.8, 0.8, 0.8, 1.0)
+
+ minor.frequency: 1
+ minor.lineWidth: 1
+ minor.color: Qt.rgba(0.8, 0.8, 0.8, 1.0)
+ }
+
+ GridLines {
+ id: verticalLines
+
+ anchors.fill: lineChart
+
+ chart: lineChart
+
+ direction: GridLines.Vertical;
+
+ major.count: 1
+ major.lineWidth: 2
+ major.color: Qt.rgba(0.8, 0.8, 0.8, 1.0)
+
+ minor.count: 3
+ minor.lineWidth: 1
+ minor.color: Qt.rgba(0.8, 0.8, 0.8, 1.0)
+ }
+
+ AxisLabels {
+ id: yAxisLabels
+
+ anchors {
+ left: parent.left
+ top: parent.top
+ bottom: xAxisLabels.top
+ }
+
+ direction: AxisLabels.VerticalBottomTop
+ delegate: Label { text: AxisLabels.label }
+ source: Charts.ChartAxisSource { chart: lineChart; axis: Charts.ChartAxisSource.YAxis; itemCount: 5 }
+ }
+
+ AxisLabels {
+ id: xAxisLabels
+
+ anchors {
+ left: yAxisLabels.visible ? yAxisLabels.right : parent.left
+ right: parent.right
+ bottom: legend.top
+ }
+
+ delegate: Label { text: AxisLabels.label }
+ source: Charts.ChartAxisSource { chart: lineChart; axis: Charts.ChartAxisSource.XAxis; itemCount: 5 }
+ }
+
+ Legend {
+ id: legend
+
+ anchors {
+ left: yAxisLabels.visible ? yAxisLabels.right : parent.left
+ right: parent.right
+ bottom: parent.bottom
+ bottomMargin: Theme.smallSpacing
+ }
+
+ chart: lineChart
+ }
+
+ Charts.LineChart {
+ id: lineChart
+ anchors {
+ top: parent.top
+ left: yAxisLabels.visible ? yAxisLabels.right : parent.left
+ right: parent.right
+ bottom: xAxisLabels.visible ? xAxisLabels.top : legend.top
+ }
+
+ xRange.automatic: true
+ yRange.automatic: true
+
+ colorSource: Charts.ColorGradientSource { id: colorSource; baseColor: Theme.highlightColor; itemCount: lineChart.valueSources.length }
+ nameSource: Charts.ArraySource { id: nameSource; array: ["1", "2", "3", "4", "5"] }
+
+ highlight: legend.highlightedIndex
+ }
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "LineGridNode.h"
+
+#include <cmath>
+
+#include <QSGFlatColorMaterial>
+
+LineGridNode::LineGridNode()
+{
+ m_geometry = new QSGGeometry{QSGGeometry::defaultAttributes_Point2D(), 0};
+ m_geometry->setDrawingMode(QSGGeometry::DrawLines);
+ m_geometry->setLineWidth(m_lineWidth);
+ setGeometry(m_geometry);
+
+ m_material = new QSGFlatColorMaterial{};
+ m_material->setColor(QColor(255, 0, 0, 255));
+ setMaterial(m_material);
+
+ setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial);
+}
+
+LineGridNode::~LineGridNode()
+{
+}
+
+void LineGridNode::setVisible(bool visible)
+{
+ if (visible == m_visible) {
+ return;
+ }
+
+ m_visible = visible;
+ markDirty(QSGNode::DirtySubtreeBlocked);
+}
+
+void LineGridNode::setVertical(bool vertical)
+{
+ if (vertical == m_vertical) {
+ return;
+ }
+
+ m_vertical = vertical;
+}
+
+void LineGridNode::setRect(const QRectF &rect)
+{
+ if (rect == m_rect) {
+ return;
+ }
+
+ m_rect = rect;
+}
+
+void LineGridNode::setColor(const QColor &color)
+{
+ if (color == m_material->color()) {
+ return;
+ }
+
+ m_material->setColor(color);
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void LineGridNode::setSpacing(float spacing)
+{
+ if (qFuzzyCompare(spacing, m_spacing)) {
+ return;
+ }
+
+ m_spacing = spacing;
+}
+
+void LineGridNode::setLineWidth(float lineWidth)
+{
+ if (qFuzzyCompare(lineWidth, m_lineWidth)) {
+ return;
+ }
+
+ m_lineWidth = lineWidth;
+ m_geometry->setLineWidth(lineWidth);
+ markDirty(QSGNode::DirtyGeometry);
+}
+
+bool LineGridNode::isSubtreeBlocked() const
+{
+ return !m_visible;
+}
+
+void LineGridNode::update()
+{
+ if (!m_rect.isValid()) {
+ return;
+ }
+
+ int totalVertices = 0;
+ if (!m_vertical) {
+ totalVertices = std::floor(m_rect.width() / std::ceil(m_spacing)) * 2 + 4;
+ } else {
+ totalVertices = std::floor(m_rect.height() / std::ceil(m_spacing)) * 2 + 4;
+ }
+
+ if (totalVertices < 4) {
+ return;
+ }
+
+ if (totalVertices != m_geometry->vertexCount()) {
+ m_geometry->allocate(totalVertices, totalVertices);
+ }
+
+ auto vertices = m_geometry->vertexDataAsPoint2D();
+ auto indices = m_geometry->indexDataAsUShort();
+
+ if (!vertices || !indices) {
+ return;
+ }
+
+ int index = 0;
+ if (m_vertical) {
+ line(vertices, indices, index, m_rect.left(), m_rect.top(), m_rect.right(), m_rect.top());
+
+ auto y = m_spacing;
+ for (auto i = 0; i < (totalVertices - 4) / 2; ++i) {
+ line(vertices, indices, index, m_rect.left(), y, m_rect.right(), y);
+ y += m_spacing;
+ }
+
+ line(vertices, indices, index, m_rect.left(), m_rect.bottom(), m_rect.right(), m_rect.bottom());
+ } else {
+ line(vertices, indices, index, m_rect.left(), m_rect.top(), m_rect.left(), m_rect.bottom());
+
+ auto x = m_spacing;
+ for (auto i = 0; i < (totalVertices - 4) / 2; ++i) {
+ line(vertices, indices, index, x, m_rect.top(), x, m_rect.bottom());
+ x += m_spacing;
+ }
+
+ line(vertices, indices, index, m_rect.right(), m_rect.top(), m_rect.right(), m_rect.bottom());
+ }
+
+ m_geometry->markVertexDataDirty();
+ m_geometry->markIndexDataDirty();
+ markDirty(QSGNode::DirtyGeometry);
+}
+
+void LineGridNode::line(QSGGeometry::Point2D *vertices, quint16 *indices, int &index, qreal fromX, qreal fromY, qreal toX, qreal toY)
+{
+ indices[index] = index;
+ vertices[index++].set(fromX, fromY);
+ indices[index] = index;
+ vertices[index++].set(toX, toY);
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef LINEGRIDNODE_H
+#define LINEGRIDNODE_H
+
+#include <QColor>
+#include <QSGGeometryNode>
+
+class QSGFlatColorMaterial;
+
+/**
+ * @todo write docs
+ */
+class LineGridNode : public QSGGeometryNode
+{
+public:
+ LineGridNode();
+ ~LineGridNode();
+
+ void setVisible(bool visible);
+ void setVertical(bool vertical);
+ void setRect(const QRectF &rect);
+ void setColor(const QColor &color);
+ void setSpacing(float spacing);
+ void setLineWidth(float lineWidth);
+
+ bool isSubtreeBlocked() const override;
+
+ void update();
+
+private:
+ void line(QSGGeometry::Point2D *vertices, quint16 *indices, int &index, qreal fromX, qreal fromY, qreal toX, qreal toY);
+
+ QSGGeometry *m_geometry = nullptr;
+ QSGFlatColorMaterial *m_material = nullptr;
+
+ bool m_visible = true;
+ bool m_vertical = false;
+ QRectF m_rect;
+ float m_spacing = 1.0;
+ float m_lineWidth = 1.0;
+};
+
+#endif // LINEGRIDNODE_H
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick 2.12
+
+pragma Singleton;
+
+QtObject {
+ id: log
+
+ property LoggingCategory generalCategory: LoggingCategory {
+ name: "kf.quickcharts.general"
+ defaultLogLevel: LoggingCategory.Warning
+ }
+
+ property LoggingCategory deprecatedCategory: LoggingCategory {
+ name: "kf.quickcharts.deprecated"
+ defaultLogLevel: LoggingCategory.Warning
+ }
+
+ function deprecated(item, entry, since, message) {
+ console.warn(log.deprecatedCategory, "%1::%2 is deprecated (since %3): %4".arg(item).arg(entry).arg(since).arg(message))
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+
+import org.kde.quickcharts as Charts
+import org.kde.quickcharts.controls
+
+/**
+ * A pie chart with text in the middle.
+ */
+Control {
+ property alias valueSources: pie.valueSources
+ property alias names: nameSource.array
+ property alias color: colorSource.baseColor
+ property alias range: pie.range
+ property alias chart: pie
+ property alias highlight: pie.highlight
+
+ property alias text: centerText.text
+
+ implicitWidth: Theme.gridUnit * 5
+ implicitHeight: Theme.gridUnit * 5
+
+ contentItem: Item {
+ Charts.PieChart {
+ id: pie
+
+ anchors.fill: parent
+
+ nameSource: Charts.ArraySource { id: nameSource; array: [ ] }
+ colorSource: Charts.ColorGradientSource { id: colorSource; baseColor: Theme.highlightColor; itemCount: pie.valueSources.length }
+ }
+
+ Label {
+ id: centerText
+
+ anchors.centerIn: parent;
+
+ horizontalAlignment: Qt.AlignHCenter
+ verticalAlignment: Qt.AlignVCenter
+ }
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+
+pragma Singleton;
+
+QtObject {
+ property Loader kirigamiThemeLoader: Loader {
+ source: "KirigamiTheme.qml"
+ }
+
+ property real gridUnit: kirigamiThemeLoader.item?.gridUnit ?? 20
+ property real smallSpacing: kirigamiThemeLoader.item?.smallSpacing ?? 5
+ property real largeSpacing: kirigamiThemeLoader.item?.largeSpacing ?? 10
+ property real cornerRadius: kirigamiThemeLoader.item?.cornerRadius ?? 5
+
+ property real smallIconSize: kirigamiThemeLoader.item?.smallIconSize ?? 16
+
+ property color highlightColor: kirigamiThemeLoader.item?.highlightColor ?? "blue"
+ property color backgroundColor: kirigamiThemeLoader.item?.backgroundColor ?? "white"
+}
--- /dev/null
+EXCLUDE_PATTERNS = */scenegraph/*
--- /dev/null
+add_subdirectory(charts)
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.kquickcontrols
+
+import org.kde.quickcharts as Charts
+import org.kde.quickcharts.controls as ChartsControls
+
+ChartPage {
+ title: "Bar Chart"
+
+ chart: Kirigami.AbstractCard {
+ anchors.centerIn: parent
+ height: 400
+ width: parent.width
+
+ ChartsControls.GridLines {
+ anchors.fill: barChart
+
+ chart: barChart
+
+ major.visible: false
+
+ minor.count: 4
+ minor.lineWidth: 1
+ minor.color: Qt.rgba(0.8, 0.8, 0.8, 1.0)
+ }
+
+ ChartsControls.GridLines {
+ anchors.fill: barChart
+
+ chart: barChart
+
+ direction: ChartsControls.GridLines.Vertical;
+
+ major.count: 1
+ major.lineWidth: 2
+ major.color: Qt.rgba(0.8, 0.8, 0.8, 1.0)
+
+ minor.count: 3
+ minor.lineWidth: 1
+ minor.color: Qt.rgba(0.8, 0.8, 0.8, 1.0)
+ }
+
+ ChartsControls.AxisLabels {
+ id: yAxisLabels
+
+ anchors {
+ left: parent.left
+ top: parent.top
+ bottom: xAxisLabels.top
+ }
+
+ direction: ChartsControls.AxisLabels.VerticalBottomTop
+ delegate: Label { text: ChartsControls.AxisLabels.label }
+ source: Charts.ChartAxisSource { chart: barChart; axis: Charts.ChartAxisSource.YAxis; itemCount: 5 }
+ }
+
+ ChartsControls.AxisLabels {
+ id: xAxisLabels
+
+ anchors {
+ left: yAxisLabels.right
+ right: parent.right
+ bottom: legend.top
+ }
+
+ delegate: Label { text: ChartsControls.AxisLabels.label }
+ source: Charts.ModelSource { model: barModel; roleName: "label" }
+ }
+
+ ChartsControls.Legend {
+ id: legend
+
+ anchors {
+ left: yAxisLabels.right
+ right: parent.right
+ bottom: parent.bottom
+ bottomMargin: Kirigami.Units.smallSpacing
+ }
+
+ chart: barChart
+ highlightEnabled: true
+ }
+
+ Charts.BarChart {
+ id: barChart
+ anchors {
+ top: parent.top
+ left: yAxisLabels.right
+ right: parent.right
+ bottom: xAxisLabels.top
+ }
+
+ xRange {
+ from: 0
+ to: 10
+ automatic: true
+ }
+
+ yRange {
+ from: 0
+ to: 10
+ automatic: true
+ }
+
+ valueSources: [
+ Charts.ModelSource { roleName: "value1"; model: barModel },
+ Charts.ModelSource { roleName: "value2"; model: barModel },
+ Charts.ModelSource { roleName: "value3"; model: barModel }
+ ]
+
+ barWidth: 10
+ spacing: 2
+
+ colorSource: Charts.ColorGradientSource {
+ baseColor: Kirigami.Theme.highlightColor
+ itemCount: 3
+ }
+ nameSource: Charts.ArraySource { array: ["Example 1", "Example 2", "Example 3"] }
+
+ backgroundColor: Qt.rgba(0.0, 0.0, 0.0, 0.1)
+
+ highlight: legend.highlightedIndex
+ }
+ }
+
+ controls: [
+ RangeEditor {
+ label: "X Axis";
+ range: barChart.xRange
+ },
+
+ RangeEditor {
+ label: "Y Axis";
+ range: barChart.yRange
+ },
+
+ RowLayout {
+ Label { text: "Bar Width" }
+ SpinBox { from: -1; to: 1000; value: barChart.barWidth; onValueModified: barChart.barWidth = value; }
+ Label { text: "Bar Spacing" }
+ SpinBox { from: 0; to: 100; value: barChart.spacing; onValueModified: barChart.spacing = value; }
+ CheckBox { text: "Stacked"; checked: barChart.stacked; onToggled: barChart.stacked = checked }
+ Label { text: "Radius" }
+ SpinBox { from: 0; to: 1000; value: barChart.radius; onValueModified: barChart.radius = value; }
+
+ ComboBox {
+ model: [
+ { text: "Vertical", value: Charts.BarChart.VerticalOrientation },
+ { text: "Horizontal", value: Charts.BarChart.HorizontalOrientation }
+ ]
+
+ textRole: "text"
+ valueRole: "value"
+
+ onActivated: barChart.orientation = currentValue
+ }
+ }
+ ]
+
+ itemModel: ListModel {
+ id: barModel;
+ dynamicRoles: true;
+
+ Component.onCompleted: {
+ append({label: "Item 1", value1: 0, value2: 15, value3: 20})
+ append({label: "Item 2", value1: 10, value2: 25, value3: 25})
+ append({label: "Item 3", value1: 15, value2: 20, value3: 30})
+ append({label: "Item 4", value1: 10, value2: 10, value3: 35})
+ append({label: "Item 5", value1: 20, value2: 5, value3: 40})
+ }
+ }
+
+ itemDelegate: ItemDelegate {
+ width: ListView.view.width
+ contentItem: RowLayout {
+ Label { text: "Label" }
+ TextField {
+ Layout.preferredWidth: 75
+ text: model.label;
+ onEditingFinished: barModel.setProperty(index, "label", text)
+ }
+ Label { text: "Value 1" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value1;
+ onValueModified: barModel.setProperty(index, "value1", value)
+ }
+ Label { text: "Value 2" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value2;
+ onValueModified: barModel.setProperty(index, "value2", value)
+ }
+ Label { text: "Value 3" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value3;
+ onValueModified: barModel.setProperty(index, "value3", value)
+ }
+ }
+ }
+
+ itemEditorActions: [
+ Kirigami.Action {
+ text: "Add Item"
+ onTriggered: barModel.append({label: "Item " + (barModel.count + 1), value1: 10, value2: 10, value3: 10})
+ },
+
+ Kirigami.Action {
+ text: "Remove Last"
+ onTriggered: barModel.remove(barModel.count - 1)
+ }
+ ]
+}
+
--- /dev/null
+
+find_package(Qt6Widgets REQUIRED)
+set_package_properties(Qt6Widgets PROPERTIES TYPE REQUIRED PURPOSE "The charts example needs Qt Widgets so the Plasma desktop style works properly.")
+find_package(KF6Kirigami2 REQUIRED)
+set_package_properties(KF6Kirigami2 PROPERTIES TYPE REQUIRED PURPOSE "The charts example uses Kirigami for its UI.")
+find_package(KF6Declarative REQUIRED)
+set_package_properties(KF6Declarative PROPERTIES TYPE REQUIRED PURPOSE "The charts example uses items from KF6Declarative for its UI.")
+
+add_executable(kquickcharts_example)
+ecm_add_qml_module(kquickcharts_example URI "org.kde.quickcharts.example" GENERATE_PLUGIN_SOURCE)
+
+target_sources(kquickcharts_example PRIVATE main.cpp)
+
+ecm_target_qml_sources(kquickcharts_example SOURCES
+ BarChart.qml
+ ChartPage.qml
+ HistoryProxySource.qml
+ Legend.qml
+ LineChart.qml
+ Main.qml
+ PieChart.qml
+ RangeEditor.qml
+)
+
+target_link_libraries(kquickcharts_example PRIVATE Qt6::Quick Qt6::Widgets)
+
+install(TARGETS kquickcharts_example DESTINATION ${KDE_INSTALL_BINDIR})
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2024 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+
+import org.kde.quickcharts as Charts
+import org.kde.quickcharts.controls as ChartsControls
+
+Kirigami.Page {
+ id: page
+
+ leftPadding: 0
+ rightPadding: 0
+ topPadding: 0
+ bottomPadding: 0
+
+ property alias chart: chartArea.data
+ property alias controls: controlsArea.data
+
+ property QtObject itemModel
+ property alias itemDelegate: itemView.delegate
+ property bool itemEditorHovered: false
+ property list<Action> itemEditorActions
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+
+ Item {
+ id: chartArea
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: 400
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ Layout.leftMargin: Kirigami.Units.gridUnit
+ Layout.rightMargin: Kirigami.Units.gridUnit
+ Layout.topMargin: Kirigami.Units.gridUnit
+ }
+
+ ColumnLayout {
+ id: controlsArea
+
+ Layout.fillHeight: false
+ Layout.leftMargin: Kirigami.Units.gridUnit
+ Layout.rightMargin: Kirigami.Units.gridUnit
+ Layout.topMargin: Kirigami.Units.largeSpacing
+ Layout.bottomMargin: Kirigami.Units.largeSpacing
+ }
+
+ Kirigami.Separator { Layout.fillWidth: true; visible: itemEditor.visible }
+
+ Kirigami.InlineViewHeader {
+ Layout.fillWidth: true
+ visible: itemEditor.visible
+ text: "Items"
+ actions: page.itemEditorActions
+ }
+
+ ScrollView {
+ id: itemEditor
+
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ visible: page.itemModel != null
+
+ ListView {
+ id: itemView
+
+ model: page.itemModel
+
+ clip: true
+ }
+
+ background: Rectangle {
+ color: Kirigami.Theme.backgroundColor
+ }
+
+ HoverHandler {
+ onHoveredChanged: page.itemEditorHovered = hovered
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.kquickcontrols
+
+import org.kde.quickcharts as Charts
+
+ChartPage {
+ title: "History Proxy Source"
+
+ ListModel {
+ id: lineModel;
+ dynamicRoles: true;
+ }
+
+ Timer {
+ id: updateTimer
+ running: true
+ repeat: true
+ interval: 16
+
+ property real value
+
+ onTriggered: {
+ value = Math.max(0.0, Math.min(1.0, value + (-0.05 + Math.random() / 10)));
+ }
+ }
+
+ chart: Kirigami.AbstractCard {
+ anchors.centerIn: parent
+ width: parent.width
+ height: 400
+
+ Charts.LineChart {
+ id: chart
+ anchors.fill: parent
+
+ yRange {
+ from: 0
+ to: 1
+ automatic: false
+ }
+
+ valueSources: Charts.HistoryProxySource {
+ id: historySource
+ source: Charts.SingleValueSource { value: updateTimer.value }
+ maximumHistory: 100
+ }
+
+ colorSource: Charts.SingleValueSource { value: "darkRed" }
+
+ lineWidth: 2
+ fillOpacity: 0.2
+ }
+ }
+
+ controls: [
+ RangeEditor { label: "X Axis"; range: chart.xRange },
+ RangeEditor { label: "Y Axis"; range: chart.yRange },
+ RowLayout {
+ Button { icon.name: "media-playback-start"; enabled: !updateTimer.running; onClicked: updateTimer.start() }
+ Button { icon.name: "media-playback-stop"; enabled: updateTimer.running; onClicked: updateTimer.stop() }
+ Label { text: "History Amount" }
+ SpinBox {
+ from: 0
+ to: 99999
+ stepSize: 1
+ value: historySource.maximumHistory
+ onValueModified: historySource.maximumHistory = value
+ }
+ CheckBox {
+ text: "Interpolate"
+ checked: chart.interpolate
+ onToggled: chart.interpolate = checked
+ }
+ Label { text : "Interval" }
+ SpinBox {
+ from: 10
+ to: 99999
+ stepSize: 1
+ value: updateTimer.interval
+ onValueModified: updateTimer.interval = value
+ }
+ },
+ RowLayout {
+ Label { text: "Direction" }
+ ComboBox {
+ model: [
+ { value: Charts.XYChart.ZeroAtStart, text: "Zero at Start" },
+ { value: Charts.XYChart.ZeroAtEnd, text: "Zero at End" }
+ ]
+
+ textRole: "text"
+ valueRole: "value"
+
+ onActivated: chart.direction = currentValue
+ }
+ Label { text: "Fill Mode" }
+ ComboBox {
+ model: [
+ { value: Charts.HistoryProxySource.DoNotFill, text: "Do Not Fill" },
+ { value: Charts.HistoryProxySource.FillFromStart, text: "Fill from Start" },
+ { value: Charts.HistoryProxySource.FillFromEnd, text: "Fill from End" }
+ ]
+
+ textRole: "text"
+ valueRole: "value"
+
+ onActivated: historySource.fillMode = currentValue
+ }
+ }
+ ]
+}
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.kquickcontrols
+
+import org.kde.quickcharts as Charts
+import org.kde.quickcharts.controls as ChartsControls
+
+ChartPage {
+ title: "Legend"
+
+ chart: Kirigami.AbstractCard {
+ anchors.centerIn: parent
+ width: parent.width
+ height: 400
+
+ contentItem: Item {
+ Charts.PieChart {
+ id: chart
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ bottom: legend.top
+ bottomMargin: Kirigami.Units.largeSpacing
+ }
+
+ valueSources: [
+ Charts.ModelSource { roleName: "value1"; model: itemModel },
+ Charts.ModelSource { roleName: "value2"; model: itemModel },
+ Charts.ModelSource { roleName: "value3"; model: itemModel }
+ ]
+
+ colorSource: Charts.ColorGradientSource {
+ baseColor: Kirigami.Theme.highlightColor
+ itemCount: itemModel.count * 3
+ }
+ nameSource: Charts.ArraySource {
+ array: {
+ let result = Array()
+ for (let i = 0; i < itemModel.count * 3; ++i) {
+ if (i < itemModel.count) {
+ result.push(itemModel.get(i).label + " Value 1")
+ } else if (i < itemModel.count * 2) {
+ result.push(itemModel.get(i % itemModel.count).label + " Value 2")
+ } else {
+ result.push(itemModel.get(i % itemModel.count).label + " Value 3")
+ }
+ }
+ return result
+ }
+ }
+
+ indexingMode: Charts.Chart.IndexAllValues
+
+ highlight: legend.highlightedIndex
+ }
+
+ ChartsControls.Legend {
+ id: legend
+
+ anchors {
+ left: parent.left
+ right: parent.right
+ bottom: parent.bottom
+ }
+ height: Math.min(implicitHeight, parent.height * 0.3);
+
+ chart: chart
+ highlightEnabled: true
+ }
+ }
+ }
+
+ itemModel: ListModel {
+ id: itemModel
+ dynamicRoles: true
+
+ Component.onCompleted: {
+ append({label: "Item 1", value1: 10, value2: 9, value3: 3})
+ append({label: "Item 2", value1: 10, value2: 5, value3: 17})
+ append({label: "Item 3", value1: 10, value2: 16, value3: 8})
+ append({label: "Item 4", value1: 10, value2: 12, value3: 11})
+ append({label: "Item 5", value1: 10, value2: 8, value3: 11})
+ }
+ }
+
+ itemDelegate: ItemDelegate {
+ width: ListView.view.width
+ contentItem: RowLayout {
+ Label { text: "Label" }
+ TextField {
+ Layout.preferredWidth: 75
+ text: model.label;
+ onEditingFinished: itemModel.setProperty(index, "label", text)
+ }
+ Label { text: "Value 1" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value1;
+ onValueModified: itemModel.setProperty(index, "value1", value)
+ }
+ Label { text: "Value 2" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value2;
+ onValueModified: itemModel.setProperty(index, "value2", value)
+ }
+ Label { text: "Value 3" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value3;
+ onValueModified: itemModel.setProperty(index, "value3", value)
+ }
+ }
+ }
+
+ itemEditorActions: [
+ Kirigami.Action {
+ text: "Add Item"
+ onTriggered: itemModel.append({label: "Item " + (itemModel.count + 1), value1: 10, value2: 10, value3: 10})
+ },
+
+ Kirigami.Action {
+ text: "Remove Last"
+ onTriggered: itemModel.remove(itemModel.count - 1)
+ }
+ ]
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+import org.kde.kquickcontrols
+
+import org.kde.quickcharts as Charts
+import org.kde.quickcharts.controls as ChartsControls
+
+ChartPage {
+ title: "Line Chart"
+
+ chart: Kirigami.AbstractCard {
+ anchors.centerIn: parent
+ width: parent.width
+ height: 400
+
+ ChartsControls.LineChartControl {
+ id: lineChart
+
+ anchors.fill: parent
+
+ valueSources: [
+ Charts.ModelSource { roleName: "value1"; model: lineModel },
+ Charts.ModelSource { roleName: "value2"; model: lineModel },
+ Charts.ModelSource { roleName: "value3"; model: lineModel }
+ ]
+
+ names: ["Example 1", "Example 2", "Example 3"]
+
+ highlightEnabled: true
+
+ pointDelegate: Item {
+ Rectangle {
+ anchors.centerIn: parent
+ width: lineChart.lineWidth + Kirigami.Units.smallSpacing;
+ height: width
+ radius: width / 2;
+ color: parent.Charts.LineChart.color
+
+ MouseArea {
+ id: mouse
+ anchors.fill: parent
+ hoverEnabled: true
+ }
+
+ ToolTip.visible: mouse.containsMouse
+ ToolTip.text: "%1: %2".arg(parent.Charts.LineChart.name).arg(parent.Charts.LineChart.value)
+ }
+ }
+ }
+ }
+
+ controls: [
+ RangeEditor { label: "X Axis"; range: lineChart.xRange },
+ RangeEditor { label: "Y Axis"; range: lineChart.yRange },
+
+ RowLayout {
+ Label { text: "Line Width" }
+ SpinBox { from: 0; to: 1000; value: lineChart.lineWidth; onValueModified: lineChart.lineWidth = value; }
+ Label { text: "Fill Opacity" }
+ SpinBox { from: 0; to: 100; value: lineChart.fillOpacity * 100; onValueModified: lineChart.fillOpacity = value / 100; }
+ CheckBox { text: "Stacked"; checked: lineChart.stacked; onToggled: lineChart.stacked = checked }
+ CheckBox { text: "Interpolate"; checked: lineChart.chart.interpolate; onToggled: lineChart.chart.interpolate = checked }
+ }
+ ]
+
+ itemModel: ListModel {
+ id: lineModel;
+ dynamicRoles: true;
+
+ Component.onCompleted: {
+ append({label: "Item 1", value1: 10, value2: 15, value3: 20})
+ append({label: "Item 2", value1: 15, value2: 25, value3: 25})
+ append({label: "Item 3", value1: 15, value2: 20, value3: 30})
+ append({label: "Item 4", value1: 10, value2: 10, value3: 35})
+ append({label: "Item 5", value1: 20, value2: 5, value3: 40})
+ }
+ }
+
+ itemDelegate: ItemDelegate {
+ width: ListView.view.width
+ contentItem: RowLayout {
+ Label { text: "Label" }
+ TextField {
+ Layout.preferredWidth: 75
+ text: model.label;
+ onEditingFinished: lineModel.setProperty(index, "label", text)
+ }
+ Label { text: "Value 1" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value1;
+ onValueModified: lineModel.setProperty(index, "value1", value)
+ }
+ Label { text: "Value 2" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value2;
+ onValueModified: lineModel.setProperty(index, "value2", value)
+ }
+ Label { text: "Value 3" }
+ SpinBox {
+ Layout.preferredWidth: 75
+ from: -10000; to: 10000;
+ stepSize: 1;
+ value: model.value3;
+ onValueModified: lineModel.setProperty(index, "value3", value)
+ }
+ }
+ }
+
+ itemEditorActions: [
+ Kirigami.Action {
+ text: "Add Item";
+ onTriggered: lineModel.append({label: "New", value1: 0, value2: 0, value3: 0})
+ },
+ Kirigami.Action {
+ text: "Remove Last";
+ onTriggered: lineModel.remove(lineModel.count - 1)
+ }
+ ]
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+
+import org.kde.kirigami as Kirigami
+
+Kirigami.ApplicationWindow {
+ id: window
+ title: "Charts Example"
+
+ width: Kirigami.Units.gridUnit * 60
+ height: Kirigami.Units.gridUnit * 40
+
+ pageStack.initialPage: Kirigami.ScrollablePage {
+ title: "Charts"
+
+ ListView {
+
+ model: ListModel {
+ id: pagesModel
+ ListElement { label: "Pie Chart"; file: "PieChart"; identifier: "pie" }
+ ListElement { label: "Line Chart"; file: "LineChart"; identifier: "line" }
+ ListElement { label: "Bar Chart"; file: "BarChart"; identifier: "bar" }
+ ListElement { label: "History Proxy Source"; file: "HistoryProxySource"; identifier: "history" }
+ ListElement { label: "Legend"; file: "Legend"; identifier: "legend" }
+ }
+
+ delegate: ItemDelegate {
+ width: ListView.view.width
+
+ text: model.label
+ onClicked: {
+ let component = Qt.createComponent("org.kde.quickcharts.example", model.file)
+ applicationWindow().pageStack.push(component);
+ }
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ if (__commandLinePage !== null) {
+ for (var i = 0; i < pagesModel.count; ++i) {
+ var item = pagesModel.get(i);
+ if (item.identifier == __commandLinePage || item.label == __commandLinePage) {
+ window.pageStack.push(item.file);
+ return;
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+
+import org.kde.quickcharts as Charts
+
+ChartPage {
+ title: "Pie Chart"
+
+ chart: Kirigami.AbstractCard {
+ width: 600
+ height: 400
+ anchors.centerIn: parent
+
+ Charts.PieChart {
+ id: chart
+ anchors.fill: parent
+ anchors.margins: Kirigami.Units.smallSpacing;
+
+ range.to: 150
+
+ valueSources: Charts.ModelSource { roleName: "data"; model: pieModel }
+
+ colorSource: Charts.ColorGradientSource {
+ baseColor: Kirigami.Theme.highlightColor
+ itemCount: pieModel.count
+ }
+
+ thickness: 20;
+ filled: false;
+ backgroundColor: "gray"
+ }
+ }
+
+ controls: [
+ RangeEditor { label: "Range"; range: chart.range },
+
+ RowLayout {
+ Label { text: "Thickness" }
+ SpinBox { from: 0; to: chart.width / 2; value: chart.thickness; onValueModified: chart.thickness = value; }
+ CheckBox { text: "Filled"; checked: chart.filled; onCheckedChanged: chart.filled = checked }
+ CheckBox { text: "Smooth Ends"; checked: chart.smoothEnds; onCheckedChanged: chart.smoothEnds = checked }
+ Label { text: "From Angle" }
+ SpinBox { from: -360; to: 360; value: chart.fromAngle; onValueModified: chart.fromAngle = value; }
+ Label { text: "To Angle" }
+ SpinBox { from: -360; to: 360; value: chart.toAngle; onValueModified: chart.toAngle = value; }
+ }
+ ]
+
+ itemModel: ListModel {
+ id: pieModel;
+ dynamicRoles: true;
+
+ Component.onCompleted: {
+ append({label: "Item 1", data: 50})
+ append({label: "Item 2", data: 50})
+ append({label: "Item 3", data: 50})
+ }
+ }
+
+ itemDelegate: ItemDelegate {
+ width: ListView.view.width
+ onHoveredChanged: {
+ if (hovered) {
+ chart.highlight = index
+ }
+ }
+ contentItem: RowLayout {
+ Label {
+ text: "Label"
+ }
+ TextField {
+ Layout.preferredWidth: 200
+ text: model.label;
+ onEditingFinished: pieModel.setProperty(index, "label", text)
+ }
+ Label {
+ text: "Value"
+ }
+ SpinBox {
+ Layout.preferredWidth: 100
+ from: 0; to: 10000;
+ stepSize: 1;
+ value: model.data;
+ onValueModified: pieModel.setProperty(index, "data", value)
+ }
+ }
+ }
+
+ onItemEditorHoveredChanged: {
+ if (!itemEditorHovered) {
+ chart.highlight = undefined
+ }
+ }
+
+ itemEditorActions: [
+ Kirigami.Action {
+ text: "Add Item"
+ onTriggered: pieModel.append({label: "Item " + (pieModel.count + 1), data: 50})
+ },
+
+ Kirigami.Action {
+ text: "Remove Last"
+ onTriggered: pieModel.remove(pieModel.count - 1)
+ }
+ ]
+}
--- /dev/null
+Charts Gallery Example
+======================
+
+This example both demonstrates the different charts and their usage and can be
+used as a development tool to quick check if certain things still work.
+
+Command Line Arguments
+----------------------
+
+These are mainly intended as development helpers.
+
+* --page <page>
+ Open the application at the specified page. <page> can be either a page title
+ or a short identifier. Currently these are `pie`, `line` and `continuous`.
+* --api <api>
+ Run with the specified "API". This will make sure we use the specified
+ version/profile/api combination and is intended to test how things work when
+ running on a device that would use that combination by default. The possible
+ options for <api> are:
+ * core45
+ Run with OpenGL 4.5, using the Core profile. This will use different shaders
+ from the default, suffixed with _core.
+ * compat45
+ Run with OpenGL 4.5, using the Compatibility profile. Apparently Qt still
+ wants to use the _core suffixed shaders for this though.
+ * compat21
+ Run with OpenGL 2.1, no profile. This is also the default version used,
+ since we require GLSL 1.20 at minimum.
+ * es
+ Run with OpenGL ES 2.0. This does not use different shaders, but does
+ require all stored variables to have a precision qualifier specified, among
+ other differences.
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.kde.kirigami as Kirigami
+
+import org.kde.quickcharts as Charts
+
+RowLayout {
+ property alias label: labelItem.text;
+ property Charts.Range range;
+
+ spacing: Kirigami.Units.smallSpacing
+
+ Label {
+ id: labelItem;
+ }
+ CheckBox {
+ id: automaticCheckbox;
+ checked: range.automatic;
+ text: "Automatic";
+ onToggled: range.automatic = checked
+ }
+ Label {
+ text: "From"
+ }
+ SpinBox {
+ from: -10000;
+ to: 10000;
+ value: range.from;
+ editable: true;
+ enabled: !automaticCheckbox.checked;
+ onValueModified: range.from = value;
+ }
+ Label {
+ text: "To"
+ }
+ SpinBox {
+ from: -10000;
+ to: 10000;
+ value: range.to;
+ editable: true;
+ enabled: !automaticCheckbox.checked;
+ onValueModified: range.to = value;
+ }
+ Label {
+ text: "Minimum"
+ }
+ SpinBox {
+ from: 0
+ to: 10000
+ value: range.minimum
+ editable: true
+ onValueModified: range.minimum = value
+ }
+ Label {
+ text: "Increment"
+ }
+ SpinBox {
+ from: 0
+ to: 10000
+ value: range.increment
+ editable: true
+ onValueModified: range.increment = value
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include <QApplication>
+#include <QCommandLineParser>
+#include <QDebug>
+#include <QQmlApplicationEngine>
+#include <QQmlContext>
+#include <QSurfaceFormat>
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+
+ QCommandLineParser parser;
+ parser.addOption({QStringLiteral("page"), QStringLiteral("The page to show."), QStringLiteral("page")});
+ parser.addHelpOption();
+ parser.process(app);
+
+ QQmlApplicationEngine engine;
+
+ if (parser.isSet(QStringLiteral("page"))) {
+ engine.rootContext()->setContextProperty(QStringLiteral("__commandLinePage"), parser.value(QStringLiteral("page")));
+ } else {
+ engine.rootContext()->setContextProperty(QStringLiteral("__commandLinePage"), nullptr);
+ }
+
+ engine.loadFromModule("org.kde.quickcharts.example", "Main");
+
+ return app.exec();
+}
--- /dev/null
+The examples in this directory are all small and do not include other
+dependencies than Qt. They should all be runnable with qmlscene.
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+//! [example]
+import QtQuick
+import QtQuick.Controls
+
+import org.kde.quickcharts as Charts
+
+Charts.BarChart {
+ id: barChart
+
+ width: 400
+ height: 300
+
+ valueSources: [
+ Charts.ModelSource { roleName: "value1"; model: listModel },
+ Charts.ModelSource { roleName: "value2"; model: listModel },
+ Charts.ModelSource { roleName: "value3"; model: listModel }
+ ]
+
+ colorSource: Charts.ArraySource { array: ["red", "green", "blue"] }
+ nameSource: Charts.ArraySource { array: ["Example 1", "Example 2", "Example 3"] }
+
+ barWidth: 20
+
+ ListModel {
+ id: listModel
+
+ ListElement { value1: 19; value2: 2; value3: 6 }
+ ListElement { value1: 14; value2: 20; value3: 17 }
+ ListElement { value1: 4; value2: 10; value3: 11 }
+ ListElement { value1: 5; value2: 11; value3: 9 }
+ ListElement { value1: 20; value2: 7; value3: 13 }
+ }
+}
+//! [example]
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+//! [example]
+import QtQuick
+import QtQuick.Controls
+
+import org.kde.quickcharts as Charts
+
+Charts.LineChart {
+ width: 400
+ height: 300
+
+ valueSources: [
+ Charts.ModelSource { roleName: "value1"; model: listModel },
+ Charts.ModelSource { roleName: "value2"; model: listModel },
+ Charts.ModelSource { roleName: "value3"; model: listModel }
+ ]
+ colorSource: Charts.ArraySource { array: ["red", "green", "blue"] }
+ nameSource: Charts.ArraySource { array: ["Example 1", "Example 2", "Example 3"]}
+
+ ListModel {
+ id: listModel
+
+ ListElement { value1: 19; value2: 2; value3: 6 }
+ ListElement { value1: 14; value2: 20; value3: 17 }
+ ListElement { value1: 4; value2: 10; value3: 11 }
+ ListElement { value1: 5; value2: 11; value3: 9 }
+ ListElement { value1: 20; value2: 7; value3: 13 }
+ }
+}
+//! [example]
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+//! [example]
+import QtQuick
+import QtQuick.Controls
+
+import org.kde.quickcharts as Charts
+
+ApplicationWindow {
+ width: 500
+ height: 500
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 300
+ height: 200
+ border.width: 2
+
+ Charts.LineChart {
+ anchors.fill: parent
+
+ colorSource: Charts.ArraySource { array: ["red", "green", "blue"] }
+ nameSource: Charts.ArraySource { array: ["First", "Second", "Third"] }
+
+ valueSources: [
+ Charts.ArraySource { array: [1, 2, 2, 1] },
+ Charts.ArraySource { array: [2, 5, 2, 5] },
+ Charts.ArraySource { array: [5, 4, 3, 4] }
+ ]
+ }
+ }
+}
+//! [example]
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+//! [example]
+import QtQuick
+import QtQuick.Controls
+
+import org.kde.quickcharts as Charts
+
+Charts.PieChart {
+ width: 400
+ height: 300
+
+ valueSources: Charts.ModelSource {
+ roleName: "data";
+ model: ListModel {
+ id: listModel
+ ListElement { data: 12 }
+ ListElement { data: 19 }
+ ListElement { data: 10 }
+ ListElement { data: 13 }
+ ListElement { data: 13 }
+ }
+ }
+ colorSource: Charts.ArraySource { array: ["red", "green", "blue", "yellow", "cyan"] }
+ nameSource: Charts.ArraySource { array: ["Red", "Green", "Blue", "Yellow", "Cyan"] }
+
+ thickness: 50
+}
+//! [example]
--- /dev/null
+description: A QtQuick module providing high-performance charts.
+fancyname: KQuickCharts
+group: frameworks
+subgroup: tier 1
+release: true
+type: functional
+platforms:
+ - name: Linux
+ - name: FreeBSD
+ - name: Android
+ - name: Windows
+ - name: macOS
+public_lib: true
+public_source_dirs:
+ - controls
+ - src
+libraries:
+ - cmake: KF6::QuickCharts
+cmakename: KF6QuickCharts
+irc: kde-devel
--- /dev/null
+#!/bin/bash
+
+git clone https://invent.kde.org/frameworks/extra-cmake-modules
+cd extra-cmake-modules
+cmake . -DCMAKE_INSTALL_PREFIX=../ecm -DBUILD_TESTING=OFF -DBUILD_HTML_DOCS=OFF -DBUILD_MAN_DOCS=OFF
+make install
+cd ..
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "BarChart.h"
+
+#include <QDebug>
+#include <QSGNode>
+
+#include "RangeGroup.h"
+#include "datasource/ChartDataSource.h"
+#include "scenegraph/BarChartNode.h"
+
+BarChart::BarChart(QQuickItem *parent)
+ : XYChart(parent)
+{
+}
+
+qreal BarChart::spacing() const
+{
+ return m_spacing;
+}
+
+void BarChart::setSpacing(qreal newSpacing)
+{
+ if (newSpacing == m_spacing) {
+ return;
+ }
+
+ m_spacing = newSpacing;
+ update();
+ Q_EMIT spacingChanged();
+}
+
+qreal BarChart::barWidth() const
+{
+ return m_barWidth;
+}
+
+void BarChart::setBarWidth(qreal newBarWidth)
+{
+ if (newBarWidth == m_barWidth) {
+ return;
+ }
+
+ m_barWidth = newBarWidth;
+ update();
+ Q_EMIT barWidthChanged();
+}
+
+qreal BarChart::radius() const
+{
+ return m_radius;
+}
+
+void BarChart::setRadius(qreal newRadius)
+{
+ if (newRadius == m_radius) {
+ return;
+ }
+
+ m_radius = newRadius;
+ update();
+ Q_EMIT radiusChanged();
+}
+
+BarChart::Orientation BarChart::orientation() const
+{
+ return m_orientation;
+}
+
+void BarChart::setOrientation(BarChart::Orientation newOrientation)
+{
+ if (newOrientation == m_orientation) {
+ return;
+ }
+
+ m_orientation = newOrientation;
+ m_orientationChanged = true;
+ update();
+ Q_EMIT orientationChanged();
+}
+
+QColor BarChart::backgroundColor() const
+{
+ return m_backgroundColor;
+}
+
+void BarChart::setBackgroundColor(const QColor &newBackgroundColor)
+{
+ if (newBackgroundColor == m_backgroundColor) {
+ return;
+ }
+
+ m_backgroundColor = newBackgroundColor;
+ update();
+ Q_EMIT backgroundColorChanged();
+}
+
+QSGNode *BarChart::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *)
+{
+ BarChartNode *barNode = nullptr;
+
+ if (m_orientationChanged) {
+ delete node;
+ node = nullptr;
+ m_orientationChanged = false;
+ }
+
+ if (!node) {
+ barNode = new BarChartNode{};
+ if (m_orientation == VerticalOrientation) {
+ node = barNode;
+ } else {
+ auto transformNode = new QSGTransformNode{};
+ transformNode->appendChildNode(barNode);
+ QMatrix4x4 matrix;
+ matrix.translate(width(), 0.0);
+ matrix.rotate(90.0, 0.0, 0.0, 1.0);
+ transformNode->setMatrix(matrix);
+ node = transformNode;
+ }
+ } else {
+ if (m_orientation == VerticalOrientation) {
+ barNode = static_cast<BarChartNode *>(node);
+ } else {
+ barNode = static_cast<BarChartNode *>(node->childAtIndex(0));
+ }
+ }
+
+ if (m_orientation == VerticalOrientation) {
+ barNode->setRect(boundingRect());
+ } else {
+ QMatrix4x4 matrix;
+ matrix.translate(width(), 0.0);
+ matrix.rotate(90.0, 0.0, 0.0, 1.0);
+ static_cast<QSGTransformNode *>(node)->setMatrix(matrix);
+ barNode->setRect(QRectF{boundingRect().topLeft(), QSizeF{height(), width()}});
+ }
+ barNode->setBars(calculateBars());
+ barNode->setRadius(m_radius);
+ barNode->setBackgroundColor(m_backgroundColor);
+
+ barNode->update();
+
+ return node;
+}
+
+void BarChart::onDataChanged()
+{
+ if (valueSources().size() == 0 || !colorSource()) {
+ return;
+ }
+
+ m_barDataItems.clear();
+
+ updateComputedRange();
+
+ const auto range = computedRange();
+ const auto sources = valueSources();
+ auto colors = colorSource();
+ auto indexMode = indexingMode();
+ auto colorIndex = 0;
+
+ m_barDataItems.fill(QList<BarData>{}, range.distanceX);
+
+ const auto highlightIndex = highlight();
+
+ auto generator = [&, this, i = range.startX]() mutable -> QList<BarData> {
+ QList<BarData> colorInfos;
+
+ for (int j = 0; j < sources.count(); ++j) {
+ auto value = (sources.at(j)->item(i).toReal() - range.startY) / range.distanceY;
+ auto color = colors->item(colorIndex).value<QColor>();
+
+ if (highlightIndex >= 0 && highlightIndex != colorIndex) {
+ color = desaturate(color);
+ }
+
+ colorInfos << BarData{value, color};
+
+ if (indexMode != Chart::IndexSourceValues) {
+ colorIndex++;
+ }
+ }
+
+ if (stacked()) {
+ auto previous = 0.0;
+ for (auto &[colorVal, _] : colorInfos) {
+ colorVal += previous;
+ previous = colorVal;
+ }
+ }
+
+ if (indexMode == Chart::IndexSourceValues) {
+ colorIndex++;
+ } else if (indexMode == Chart::IndexEachSource) {
+ colorIndex = 0;
+ }
+
+ i++;
+ return colorInfos;
+ };
+
+ if (direction() == Direction::ZeroAtStart) {
+ std::generate_n(m_barDataItems.begin(), range.distanceX, generator);
+ } else {
+ std::generate_n(m_barDataItems.rbegin(), range.distanceX, generator);
+ }
+
+ update();
+}
+
+QList<Bar> BarChart::calculateBars()
+{
+ QList<Bar> result;
+
+ // TODO: Find some way to clean this up and simplify it, since this is pretty ugly.
+
+ auto targetWidth = m_orientation == VerticalOrientation ? width() : height();
+
+ float w = m_barWidth;
+ if (w < 0.0) {
+ const auto totalItemCount = stacked() ? m_barDataItems.size() : m_barDataItems.size() * valueSources().count();
+
+ w = targetWidth / totalItemCount - m_spacing;
+
+ auto x = float(m_spacing / 2);
+ const auto itemSpacing = w + m_spacing;
+
+ for (const auto &items : std::as_const(m_barDataItems)) {
+ result.reserve(result.size() + items.size());
+ if (stacked()) {
+ std::transform(items.crbegin(), items.crend(), std::back_inserter(result), [x, w](const BarData &entry) {
+ return Bar{x, w, float(entry.value), entry.color};
+ });
+ x += itemSpacing;
+ } else {
+ std::transform(items.cbegin(), items.cend(), std::back_inserter(result), [&x, itemSpacing, w](const BarData &entry) {
+ Bar bar{x, w, float(entry.value), entry.color};
+ x += itemSpacing;
+ return bar;
+ });
+ }
+ }
+ } else {
+ const auto itemSpacing = targetWidth / m_barDataItems.size();
+ if (stacked()) {
+ auto x = float(itemSpacing / 2 - m_barWidth / 2);
+
+ for (const auto &items : std::as_const(m_barDataItems)) {
+ result.reserve(result.size() + items.size());
+ std::transform(items.crbegin(), items.crend(), std::back_inserter(result), [x, w](const BarData &entry) {
+ return Bar{x, w, float(entry.value), entry.color};
+ });
+
+ x += itemSpacing;
+ }
+ } else {
+ const auto totalWidth = m_barWidth * valueSources().count() + m_spacing * (valueSources().count() - 1);
+
+ auto x = float(itemSpacing / 2 - totalWidth / 2);
+
+ for (const auto &items : std::as_const(m_barDataItems)) {
+ result.reserve(result.size() + items.size());
+ for (int i = 0; i < items.count(); ++i) {
+ auto entry = items.at(i);
+ result << Bar{float(x + i * (m_barWidth + m_spacing)), w, float(entry.value), entry.color};
+ }
+ x += itemSpacing;
+ }
+ }
+ }
+
+ return result;
+}
+
+#include "moc_BarChart.cpp"
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef BARCHART_H
+#define BARCHART_H
+
+#include <qqmlregistration.h>
+
+#include "XYChart.h"
+
+struct Bar;
+
+/**
+ * An item to render a bar chart.
+ *
+ * This chart renders
+ *
+ * ## Usage example
+ *
+ * \snippet snippets/barchart.qml example
+ *
+ * \image html barchart.png "The resulting bar chart."
+ */
+class QUICKCHARTS_EXPORT BarChart : public XYChart
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ /**
+ * Helper enum to provide an easy way to set barWidth to auto.
+ */
+ enum WidthMode { AutoWidth = -2 };
+ Q_ENUM(WidthMode)
+
+ enum Orientation {
+ HorizontalOrientation = Qt::Horizontal, ///< Bars are oriented horizontally, with low values left and high values right.
+ VerticalOrientation = Qt::Vertical ///< Bars are oriented vertically, with low values at the bottom and high values at the top.
+ };
+ Q_ENUM(Orientation)
+
+ explicit BarChart(QQuickItem *parent = nullptr);
+
+ /**
+ * The spacing between bars for each value source.
+ *
+ * Note that spacing between each X axis value is determined automatically
+ * based on barWidth, spacing and total chart width. The default is 0.
+ */
+ Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
+ qreal spacing() const;
+ void setSpacing(qreal newSpacing);
+ Q_SIGNAL void spacingChanged();
+
+ /**
+ * The width of individual bars in the chart.
+ *
+ * If set to WidthMode::AutoWidth (also the default), the width will be
+ * calculated automatically based on total chart width and item count.
+ */
+ Q_PROPERTY(qreal barWidth READ barWidth WRITE setBarWidth NOTIFY barWidthChanged)
+ qreal barWidth() const;
+ void setBarWidth(qreal newBarWidth);
+ Q_SIGNAL void barWidthChanged();
+
+ /**
+ * The radius of the ends of bars in the chart in pixels.
+ *
+ * By default this is 0, which means no rounding will be done.
+ */
+ Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged)
+ qreal radius() const;
+ void setRadius(qreal newRadius);
+ Q_SIGNAL void radiusChanged();
+
+ /**
+ * The orientation of bars in the chart.
+ *
+ * By default this is Vertical.
+ */
+ Q_PROPERTY(Orientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged)
+ Orientation orientation() const;
+ void setOrientation(Orientation newOrientation);
+ Q_SIGNAL void orientationChanged();
+
+ /**
+ * The background color of bars in the chart.
+ *
+ * By default this is Qt::transparent. If set to something non-transparent,
+ * the chart will render backgrounds for the bars. These backgrounds will
+ * have the same width as the bars but stretch the full height.
+ */
+ Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged)
+ QColor backgroundColor() const;
+ void setBackgroundColor(const QColor &newBackgroundColor);
+ Q_SIGNAL void backgroundColorChanged();
+
+protected:
+ /**
+ * Reimplemented from QQuickItem.
+ */
+ QSGNode *updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *) override;
+ /**
+ * Reimplemented from Chart.
+ */
+ void onDataChanged() override;
+
+private:
+ QList<Bar> calculateBars();
+
+ qreal m_spacing = 0.0;
+ qreal m_barWidth = AutoWidth;
+ qreal m_radius = 0.0;
+ Orientation m_orientation = VerticalOrientation;
+ bool m_orientationChanged = false;
+ struct BarData {
+ qreal value = 0;
+ QColor color;
+ };
+ QList<QList<BarData>> m_barDataItems;
+ QColor m_backgroundColor = Qt::transparent;
+};
+
+#endif // BARCHART_H
--- /dev/null
+
+add_library(QuickCharts)
+set_target_properties(QuickCharts PROPERTIES
+ SOVERSION ${KF6QuickCharts_SOVERSION}
+ VERSION ${KF6QuickCharts_VERSION}
+)
+
+ecm_add_qml_module(QuickCharts
+ URI "org.kde.quickcharts"
+ VERSION 1.0
+ DEPENDENCIES QtQuick
+ GENERATE_PLUGIN_SOURCE
+ INSTALLED_PLUGIN_TARGET KF6::QuickChartsplugin
+)
+
+target_sources(QuickCharts PRIVATE
+ BarChart.cpp
+ BarChart.h
+ Chart.cpp
+ Chart.h
+ ItemBuilder.cpp
+ ItemBuilder.h
+ LineChart.cpp
+ LineChart.h
+ PieChart.cpp
+ PieChart.h
+ RangeGroup.cpp
+ RangeGroup.h
+ XYChart.cpp
+ XYChart.h
+ datasource/ArraySource.cpp
+ datasource/ArraySource.h
+ datasource/ChartAxisSource.cpp
+ datasource/ChartAxisSource.h
+ datasource/ChartDataSource.cpp
+ datasource/ChartDataSource.h
+ datasource/ColorGradientSource.cpp
+ datasource/ColorGradientSource.h
+ datasource/HistoryProxySource.cpp
+ datasource/HistoryProxySource.h
+ datasource/MapProxySource.cpp
+ datasource/MapProxySource.h
+ datasource/ModelSource.cpp
+ datasource/ModelSource.h
+ datasource/SingleValueSource.cpp
+ datasource/SingleValueSource.h
+ scenegraph/BarChartMaterial.cpp
+ scenegraph/BarChartMaterial.h
+ scenegraph/BarChartNode.cpp
+ scenegraph/BarChartNode.h
+ scenegraph/LineChartMaterial.cpp
+ scenegraph/LineChartMaterial.h
+ scenegraph/LineChartNode.cpp
+ scenegraph/LineChartNode.h
+ scenegraph/LineSegmentNode.cpp
+ scenegraph/LineSegmentNode.h
+ scenegraph/PieChartMaterial.cpp
+ scenegraph/PieChartMaterial.h
+ scenegraph/PieChartNode.cpp
+ scenegraph/PieChartNode.h
+ scenegraph/SDFShader.cpp
+ scenegraph/SDFShader.h
+)
+
+ecm_qt_declare_logging_category(QuickCharts
+ HEADER charts_general_logging.h
+ IDENTIFIER GENERAL
+ CATEGORY_NAME kf.quickcharts.general
+ DEFAULT_SEVERITY Warning
+ DESCRIPTION "KQuickCharts Library - General"
+ EXPORT KQuickCharts
+)
+
+ecm_qt_declare_logging_category(QuickCharts
+ HEADER charts_datasource_logging.h
+ IDENTIFIER DATASOURCE
+ CATEGORY_NAME kf.quickcharts.datasource
+ DEFAULT_SEVERITY Warning
+ DESCRIPTION "KQuickCharts Library - Data Sources"
+ EXPORT KQuickCharts
+)
+
+ecm_qt_declare_logging_category(QuickCharts
+ HEADER charts_deprecated_logging.h
+ IDENTIFIER DEPRECATED
+ CATEGORY_NAME kf.quickcharts.deprecated
+ DEFAULT_SEVERITY Warning
+ DESCRIPTION "KQuickCharts Library - Deprecated API"
+ EXPORT KQuickCharts
+)
+
+target_link_libraries(QuickCharts PUBLIC
+ Qt6::Core
+ Qt6::Qml
+ Qt6::Quick
+)
+
+target_include_directories(QuickCharts PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/datasource
+)
+
+if (CMAKE_BUILD_TYPE STREQUAL "Debug")
+ set(_extra_args DEBUGINFO)
+else()
+ set(_extra_args OPTIMIZED)
+endif()
+
+qt6_add_shaders(QuickCharts "shaders"
+ BATCHABLE
+ ZORDER_LOC 6
+ PRECOMPILE
+ PREFIX "/qt/qml/org/kde/quickcharts/shaders/"
+ FILES
+ shaders/barchart.vert
+ shaders/barchart.frag
+ shaders/linechart.vert
+ shaders/linechart.frag
+ shaders/piechart.vert
+ shaders/piechart.frag
+ OUTPUTS
+ barchart.vert.qsb
+ barchart.frag.qsb
+ linechart.vert.qsb
+ linechart.frag.qsb
+ piechart.vert.qsb
+ piechart.frag.qsb
+ ${_extra_args}
+ OUTPUT_TARGETS _out_targets
+)
+
+ecm_generate_export_header(QuickCharts
+ BASE_NAME QuickCharts
+ GROUP_BASE_NAME KF
+ VERSION ${KF_VERSION}
+ DEPRECATED_BASE_VERSION 0
+ DEPRECATION_VERSIONS
+ EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
+)
+
+ecm_finalize_qml_module(QuickCharts DESTINATION ${KDE_INSTALL_QMLDIR} EXPORT KF6QuickChartsTargets)
+
+ecm_qt_install_logging_categories(EXPORT KQuickCharts DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR})
+
+install(TARGETS QuickCharts ${_out_targets} EXPORT KF6QuickChartsTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "Chart.h"
+#include "datasource/ChartDataSource.h"
+
+Chart::Chart(QQuickItem *parent)
+ : QQuickItem(parent)
+{
+ setFlag(ItemHasContents, true);
+ connect(this, &Chart::dataChanged, this, &Chart::onDataChanged);
+}
+
+ChartDataSource *Chart::nameSource() const
+{
+ return m_nameSource;
+}
+
+void Chart::setNameSource(ChartDataSource *nameSource)
+{
+ if (m_nameSource == nameSource) {
+ return;
+ }
+
+ m_nameSource = nameSource;
+ Q_EMIT dataChanged();
+ Q_EMIT nameSourceChanged();
+}
+
+ChartDataSource *Chart::shortNameSource() const
+{
+ return m_shortNameSource;
+}
+
+void Chart::setShortNameSource(ChartDataSource *shortNameSource)
+{
+ if (m_shortNameSource == shortNameSource) {
+ return;
+ }
+
+ m_shortNameSource = shortNameSource;
+ Q_EMIT dataChanged();
+ Q_EMIT shortNameSourceChanged();
+}
+
+ChartDataSource *Chart::colorSource() const
+{
+ return m_colorSource;
+}
+
+void Chart::setColorSource(ChartDataSource *colorSource)
+{
+ if (m_colorSource == colorSource) {
+ return;
+ }
+
+ if (m_colorSource) {
+ disconnect(m_colorSource, &ChartDataSource::dataChanged, this, &Chart::dataChanged);
+ }
+
+ m_colorSource = colorSource;
+
+ if (m_colorSource) {
+ connect(m_colorSource, &ChartDataSource::dataChanged, this, &Chart::dataChanged);
+ }
+
+ Q_EMIT dataChanged();
+ Q_EMIT colorSourceChanged();
+}
+
+Chart::DataSourcesProperty Chart::valueSourcesProperty()
+{
+ return DataSourcesProperty{
+ this,
+ this,
+ &Chart::appendSource,
+ &Chart::sourceCount,
+ &Chart::source,
+ &Chart::clearSources,
+ &Chart::replaceSource,
+ &Chart::removeLastSource,
+ };
+}
+
+QList<ChartDataSource *> Chart::valueSources() const
+{
+ return m_valueSources;
+}
+
+void Chart::insertValueSource(int index, ChartDataSource *source)
+{
+ if (index < 0) {
+ return;
+ }
+
+ m_valueSources.insert(index, source);
+ connect(source, &QObject::destroyed, this, qOverload<QObject *>(&Chart::removeValueSource));
+ connect(source, &ChartDataSource::dataChanged, this, &Chart::dataChanged);
+
+ Q_EMIT dataChanged();
+ Q_EMIT valueSourcesChanged();
+}
+
+void Chart::removeValueSource(int index)
+{
+ if (index < 0 || index >= m_valueSources.count()) {
+ return;
+ }
+
+ m_valueSources.at(index)->disconnect(this);
+ m_valueSources.remove(index);
+
+ Q_EMIT dataChanged();
+ Q_EMIT valueSourcesChanged();
+}
+
+void Chart::removeValueSource(QObject *source)
+{
+ auto itr = std::find_if(m_valueSources.begin(), m_valueSources.end(), [source](QObject *dataSource) {
+ return dataSource == source;
+ });
+
+ if (itr != m_valueSources.end()) {
+ (*itr)->disconnect(this);
+ m_valueSources.erase(itr);
+ }
+
+ Q_EMIT dataChanged();
+ Q_EMIT valueSourcesChanged();
+}
+
+Chart::IndexingMode Chart::indexingMode() const
+{
+ return m_indexingMode;
+}
+
+void Chart::setIndexingMode(IndexingMode newIndexingMode)
+{
+ if (newIndexingMode == m_indexingMode) {
+ return;
+ }
+
+ m_indexingMode = newIndexingMode;
+ Q_EMIT dataChanged();
+ Q_EMIT indexingModeChanged();
+}
+
+int Chart::highlight() const
+{
+ return m_highlight;
+}
+
+void Chart::setHighlight(int newHighlight)
+{
+ if (newHighlight == m_highlight) {
+ return;
+ }
+
+ m_highlight = newHighlight;
+ Q_EMIT dataChanged();
+ Q_EMIT highlightChanged();
+}
+
+void Chart::resetHighlight()
+{
+ setHighlight(-1);
+}
+
+void Chart::componentComplete()
+{
+ QQuickItem::componentComplete();
+ Q_EMIT dataChanged();
+}
+
+QColor Chart::desaturate(const QColor &input)
+{
+ auto color = input.convertTo(QColor::Hsl);
+ color.setHslF(color.hueF(), color.saturationF() * 0.5, color.lightnessF() * 0.5, color.alphaF() * 0.5);
+ return color.convertTo(QColor::Rgb);
+}
+
+void Chart::appendSource(Chart::DataSourcesProperty *list, ChartDataSource *source)
+{
+ auto chart = reinterpret_cast<Chart *>(list->data);
+ chart->insertValueSource(chart->valueSources().size(), source);
+}
+qsizetype Chart::sourceCount(Chart::DataSourcesProperty *list)
+{
+ return reinterpret_cast<Chart *>(list->data)->m_valueSources.count();
+}
+
+ChartDataSource *Chart::source(Chart::DataSourcesProperty *list, qsizetype index)
+{
+ return reinterpret_cast<Chart *>(list->data)->m_valueSources.at(index);
+}
+
+void Chart::clearSources(Chart::DataSourcesProperty *list)
+{
+ auto chart = reinterpret_cast<Chart *>(list->data);
+ std::for_each(chart->m_valueSources.cbegin(), chart->m_valueSources.cend(), [chart](ChartDataSource *source) {
+ source->disconnect(chart);
+ });
+ chart->m_valueSources.clear();
+ Q_EMIT chart->dataChanged();
+}
+
+void Chart::replaceSource(DataSourcesProperty *list, qsizetype index, ChartDataSource *source)
+{
+ auto chart = reinterpret_cast<Chart *>(list->data);
+ Q_ASSERT(index > 0 && index < chart->m_valueSources.size());
+ chart->m_valueSources.at(index)->disconnect(chart);
+ chart->m_valueSources.replace(index, source);
+ connect(source, &QObject::destroyed, chart, qOverload<QObject *>(&Chart::removeValueSource));
+ connect(source, &ChartDataSource::dataChanged, chart, &Chart::dataChanged);
+ Q_EMIT chart->dataChanged();
+}
+
+void Chart::removeLastSource(DataSourcesProperty *list)
+{
+ auto chart = reinterpret_cast<Chart *>(list->data);
+ chart->removeValueSource(chart->m_valueSources.size() - 1);
+}
+
+#include "moc_Chart.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef CHART_H
+#define CHART_H
+
+#include <QQuickItem>
+#include <qqmlregistration.h>
+
+#include "datasource/ChartDataSource.h"
+
+#include "quickcharts_export.h"
+
+/**
+ * Abstract base class for all charts.
+ */
+class QUICKCHARTS_EXPORT Chart : public QQuickItem
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("Base class")
+
+public:
+ using DataSourcesProperty = QQmlListProperty<ChartDataSource>;
+
+ /**
+ * How to index color and name sources relative to the different value sources.
+ */
+ enum IndexingMode {
+ IndexSourceValues = 1, ///< Index each value, restart indexing for each value source.
+ IndexEachSource, ///< Index each value source, never index individual values.
+ IndexAllValues ///< Index each value, continuing with the index for each value source.
+ };
+ Q_ENUM(IndexingMode)
+
+ explicit Chart(QQuickItem *parent = nullptr);
+ ~Chart() override = default;
+
+ /**
+ * The data source to use for names of chart items.
+ */
+ Q_PROPERTY(ChartDataSource *nameSource READ nameSource WRITE setNameSource NOTIFY nameSourceChanged)
+ ChartDataSource *nameSource() const;
+ void setNameSource(ChartDataSource *nameSource);
+ Q_SIGNAL void nameSourceChanged();
+
+ /**
+ * The data source to use for short names of chart items.
+ */
+ Q_PROPERTY(ChartDataSource *shortNameSource READ shortNameSource WRITE setShortNameSource NOTIFY shortNameSourceChanged)
+ ChartDataSource *shortNameSource() const;
+ void setShortNameSource(ChartDataSource *shortNameSource);
+ Q_SIGNAL void shortNameSourceChanged();
+
+ /**
+ * The data source to use for colors of chart items.
+ */
+ Q_PROPERTY(ChartDataSource *colorSource READ colorSource WRITE setColorSource NOTIFY colorSourceChanged)
+ ChartDataSource *colorSource() const;
+ void setColorSource(ChartDataSource *colorSource);
+ Q_SIGNAL void colorSourceChanged();
+
+ /**
+ * The data sources providing the data this chart needs to render.
+ */
+ Q_PROPERTY(QQmlListProperty<ChartDataSource> valueSources READ valueSourcesProperty NOTIFY valueSourcesChanged)
+ DataSourcesProperty valueSourcesProperty();
+ QList<ChartDataSource *> valueSources() const;
+ Q_SIGNAL void valueSourcesChanged();
+ Q_INVOKABLE void insertValueSource(int index, ChartDataSource *source);
+ Q_INVOKABLE void removeValueSource(int index);
+ Q_INVOKABLE void removeValueSource(QObject *source);
+
+ /**
+ * The indexing mode used for indexing colors and names.
+ */
+ Q_PROPERTY(IndexingMode indexingMode READ indexingMode WRITE setIndexingMode NOTIFY indexingModeChanged)
+ IndexingMode indexingMode() const;
+ void setIndexingMode(IndexingMode newIndexingMode);
+ Q_SIGNAL void indexingModeChanged();
+
+ /**
+ * The index of a value source to highlight.
+ *
+ * Highlighting is dependant on Chart type, but will generally mean that
+ * other value sources are rendered with lower opacity.
+ *
+ * By default, this is -1 which means nothing is highlighted.
+ */
+ Q_PROPERTY(int highlight READ highlight WRITE setHighlight NOTIFY highlightChanged RESET resetHighlight)
+ int highlight() const;
+ void setHighlight(int highlight);
+ void resetHighlight();
+ Q_SIGNAL void highlightChanged();
+
+ Q_SIGNAL void dataChanged();
+
+protected:
+ /**
+ * Called when the data of a value source changes.
+ *
+ * This method should be reimplemented by subclasses. It is called whenever
+ * the data of one of the value sources changes. Subclasses should use this
+ * to make sure that they update whatever internal state they use for
+ * rendering, then call update() to schedule rendering the item.
+ */
+ virtual void onDataChanged() = 0;
+ void componentComplete() override;
+
+ /**
+ * Desaturate and de-emphasise a color.
+ *
+ * Mainly intended as a standard for ensuring everything but the highlighted
+ * item is desaturated.
+ */
+ QColor desaturate(const QColor &input);
+
+private:
+ static void appendSource(DataSourcesProperty *list, ChartDataSource *source);
+ static qsizetype sourceCount(DataSourcesProperty *list);
+ static ChartDataSource *source(DataSourcesProperty *list, qsizetype index);
+ static void clearSources(DataSourcesProperty *list);
+ static void replaceSource(DataSourcesProperty *list, qsizetype index, ChartDataSource *source);
+ static void removeLastSource(DataSourcesProperty *list);
+
+ ChartDataSource *m_nameSource = nullptr;
+ ChartDataSource *m_shortNameSource = nullptr;
+ ChartDataSource *m_colorSource = nullptr;
+ QList<ChartDataSource *> m_valueSources;
+ IndexingMode m_indexingMode = IndexEachSource;
+ int m_highlight = -1;
+};
+
+#endif // CHART_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "ChartsPlugin.h"
+
+#include "BarChart.h"
+#include "Chart.h"
+#include "LineChart.h"
+#include "PieChart.h"
+#include "RangeGroup.h"
+#include "XYChart.h"
+
+#include "decorations/AxisLabels.h"
+#include "decorations/GridLines.h"
+#include "decorations/LegendModel.h"
+#include "decorations/LegendLayout.h"
+
+#include "datasource/ArraySource.h"
+#include "datasource/ChartAxisSource.h"
+#include "datasource/ColorGradientSource.h"
+#include "datasource/HistoryProxySource.h"
+#include "datasource/MapProxySource.h"
+#include "datasource/ModelSource.h"
+#include "datasource/SingleValueSource.h"
+
+#include "quickcharts_export.h"
+
+QuickChartsPlugin::QuickChartsPlugin(QObject *parent)
+ : QQmlExtensionPlugin(parent)
+{
+}
+
+void QuickChartsPlugin::registerTypes(const char *uri)
+{
+ Q_ASSERT(QString::fromLatin1(uri) == QLatin1String("org.kde.quickcharts"));
+
+ qmlRegisterAnonymousType<QAbstractItemModel>(uri, 1);
+
+ qmlRegisterType<PieChart>(uri, 1, 0, "PieChart");
+ qmlRegisterType<LineChart>(uri, 1, 0, "LineChart");
+ qmlRegisterType<BarChart>(uri, 1, 0, "BarChart");
+ qmlRegisterUncreatableType<XYChart>(uri, 1, 0, "XYChart", QStringLiteral("Just a base class"));
+ qmlRegisterUncreatableType<Chart>(uri, 1, 0, "Chart", QStringLiteral("Just a base class"));
+
+ qmlRegisterUncreatableType<ChartDataSource>(uri, 1, 0, "ChartDataSource", QStringLiteral("Just a base class"));
+ qmlRegisterType<ModelSource>(uri, 1, 0, "ModelSource");
+ qmlRegisterType<SingleValueSource>(uri, 1, 0, "SingleValueSource");
+ qmlRegisterType<ArraySource>(uri, 1, 0, "ArraySource");
+ qmlRegisterType<ChartAxisSource>(uri, 1, 0, "ChartAxisSource");
+ qmlRegisterType<ColorGradientSource>(uri, 1, 0, "ColorGradientSource");
+ qmlRegisterType<MapProxySource>(uri, 1, 0, "MapProxySource");
+ qmlRegisterType<HistoryProxySource>(uri, 1, 0, "HistoryProxySource");
+
+ qmlRegisterUncreatableType<RangeGroup>(uri, 1, 0, "Range", QStringLiteral("Used as a grouped property"));
+
+ qmlRegisterType<GridLines>(uri, 1, 0, "GridLines");
+ qmlRegisterUncreatableType<LinePropertiesGroup>(uri, 1, 0, "LinePropertiesGroup", QStringLiteral("Used as a grouped property"));
+ qmlRegisterType<AxisLabels>(uri, 1, 0, "AxisLabels");
+ qmlRegisterUncreatableType<AxisLabelsAttached>(uri, 1, 0, "AxisLabelsAttached", QStringLiteral("Attached property"));
+ qmlRegisterType<LegendModel>(uri, 1, 0, "LegendModel");
+ qmlRegisterType<LegendLayout>(uri, 1, 0, "LegendLayout");
+}
+
+#include "moc_ChartsPlugin.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef CHARTSPLUGIN_H
+#define CHARTSPLUGIN_H
+
+#include <QQmlExtensionPlugin>
+
+class QuickChartsPlugin : public QQmlExtensionPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
+
+public:
+ explicit QuickChartsPlugin(QObject *parent = nullptr);
+ void registerTypes(const char *uri) override;
+};
+
+#endif // CHARTSPLUGIN_H
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "ItemBuilder.h"
+
+class ItemIncubator : public QQmlIncubator
+{
+public:
+ ItemIncubator(QQmlComponent *component, QQmlContext *context)
+ {
+ m_component = component;
+ m_context = context;
+ }
+
+ void setStateCallback(std::function<void(QQuickItem*)> callback)
+ {
+ m_stateCallback = callback;
+ }
+
+ void setCompletedCallback(std::function<void(ItemIncubator*)> callback)
+ {
+ m_completedCallback = callback;
+ }
+
+ void create()
+ {
+ m_component->create(*this, m_context);
+ }
+
+ bool isFinished()
+ {
+ return m_finished;
+ }
+
+private:
+ void setInitialState(QObject *object) override
+ {
+ auto item = qobject_cast<QQuickItem*>(object);
+ if (item) {
+ m_stateCallback(item);
+ }
+ }
+
+ void statusChanged(QQmlIncubator::Status status) override
+ {
+ if (status == QQmlIncubator::Error) {
+ qWarning() << "Could not create delegate in ItemBuilder";
+ const auto e = errors();
+ for (const auto &error : e) {
+ qWarning() << error;
+ }
+ m_finished = true;
+ }
+
+ if (status == QQmlIncubator::Ready) {
+ m_completedCallback(this);
+ m_finished = true;
+ }
+ }
+
+ QQmlComponent *m_component;
+ QQmlContext *m_context;
+ std::function<void(QQuickItem*)> m_stateCallback;
+ std::function<void(ItemIncubator*)> m_completedCallback;
+ bool m_finished = false;
+};
+
+ItemBuilder::ItemBuilder(QObject *parent)
+ : QObject(parent)
+{
+}
+
+ItemBuilder::~ItemBuilder()
+{
+ clear();
+}
+
+QQmlComponent *ItemBuilder::component() const
+{
+ return m_component;
+}
+
+void ItemBuilder::setComponent(QQmlComponent *newComponent)
+{
+ if (newComponent == m_component) {
+ return;
+ }
+
+ m_component = newComponent;
+ clear();
+}
+
+QQmlContext *ItemBuilder::context() const
+{
+ return m_context;
+}
+
+void ItemBuilder::setContext(QQmlContext *newContext)
+{
+ if (newContext == m_context) {
+ return;
+ }
+
+ m_context = newContext;
+ clear();
+}
+
+
+int ItemBuilder::count() const
+{
+ return m_count;
+}
+
+void ItemBuilder::setCount(int newCount)
+{
+ if (newCount == m_count) {
+ return;
+ }
+
+ m_count = newCount;
+ clear();
+}
+
+QQmlIncubator::IncubationMode ItemBuilder::incubationMode() const
+{
+ return m_incubationMode;
+}
+
+void ItemBuilder::setIncubationMode(QQmlIncubator::IncubationMode newIncubationMode)
+{
+ if (newIncubationMode == m_incubationMode) {
+ return;
+ }
+
+ m_incubationMode = newIncubationMode;
+}
+
+QVariantMap ItemBuilder::initialProperties() const
+{
+ return m_initialProperties;
+}
+
+void ItemBuilder::setInitialProperties(const QVariantMap & newInitialProperties)
+{
+ if (newInitialProperties == m_initialProperties) {
+ return;
+ }
+
+ m_initialProperties = newInitialProperties;
+}
+
+void ItemBuilder::build(QQuickItem *parent)
+{
+ if ((int(m_items.size()) == m_count && m_incubators.empty()) || !m_incubators.empty() || !m_component) {
+ return;
+ }
+
+ m_incubators.reserve(m_count);
+ std::fill_n(std::back_inserter(m_items), m_count, std::shared_ptr<QQuickItem>());
+
+ for (int i = 0; i < m_count; ++i) {
+ auto context = m_context ? m_context : qmlContext(m_component);
+ auto incubator = std::make_unique<ItemIncubator>(m_component, context);
+
+ incubator->setStateCallback([this, parent, i](QQuickItem *item) {
+ item->setParentItem(parent);
+
+ for (auto itr = m_initialProperties.keyValueBegin(); itr != m_initialProperties.keyValueEnd(); ++itr) {
+ item->setProperty((*itr).first.toUtf8().data(), (*itr).second);
+ }
+
+ Q_EMIT beginCreate(i, item);
+ });
+
+ incubator->setCompletedCallback([this, i](ItemIncubator *incubator) {
+ auto item = std::shared_ptr<QQuickItem>(qobject_cast<QQuickItem*>(incubator->object()));
+ m_items[i] = item;
+
+ Q_EMIT endCreate(i, item.get());
+
+ m_completed++;
+ if (m_completed == m_count) {
+ QMetaObject::invokeMethod(this, [this]() {
+ m_incubators.clear();
+ }, Qt::QueuedConnection);
+ Q_EMIT finished();
+ }
+ });
+
+ incubator->create();
+
+ m_incubators.push_back(std::move(incubator));
+ }
+}
+
+bool ItemBuilder::isFinished() const
+{
+ return m_completed == m_count;
+}
+
+std::vector<std::shared_ptr<QQuickItem>> ItemBuilder::items() const
+{
+ return m_items;
+}
+
+void ItemBuilder::clear()
+{
+ m_items.clear();
+
+ if (m_incubators.size() > 0) {
+ for (auto &incubator : m_incubators) {
+ incubator->clear();
+ }
+ }
+ m_incubators.clear();
+
+ m_completed = 0;
+}
+
+#include "moc_ItemBuilder.cpp"
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef ITEMBUILDER_H
+#define ITEMBUILDER_H
+
+#include <vector>
+#include <memory>
+
+#include <QObject>
+#include <QQmlComponent>
+#include <QQmlIncubator>
+#include <QQuickItem>
+
+#include "quickcharts_export.h"
+
+class ItemIncubator;
+
+/**
+ * A helper class that instantiates QML items from QML components.
+ *
+ * Effectively this is a C++ version of Repeater.
+ */
+class QUICKCHARTS_EXPORT ItemBuilder : public QObject
+{
+ Q_OBJECT
+
+public:
+ ItemBuilder(QObject *parent = nullptr);
+ ~ItemBuilder() override;
+
+ QQmlComponent *component() const;
+ void setComponent(QQmlComponent *newComponent);
+
+ QQmlContext *context() const;
+ void setContext(QQmlContext *newContext);
+
+ int count() const;
+ void setCount(int newCount);
+
+ QQmlIncubator::IncubationMode incubationMode() const;
+ void setIncubationMode(QQmlIncubator::IncubationMode newIncubationMode);
+
+ QVariantMap initialProperties() const;
+ void setInitialProperties(const QVariantMap &newInitialProperties);
+
+ void build(QQuickItem *parent);
+
+ Q_SIGNAL void beginCreate(int index, QQuickItem *item);
+ Q_SIGNAL void endCreate(int index, QQuickItem *item);
+
+ bool isFinished() const;
+ Q_SIGNAL void finished();
+
+ std::vector<std::shared_ptr<QQuickItem>> items() const;
+
+ void clear();
+
+private:
+ QQmlComponent *m_component = nullptr;
+ QQmlContext *m_context = nullptr;
+ int m_count = 0;
+ int m_completed = 0;
+ QQmlIncubator::IncubationMode m_incubationMode = QQmlIncubator::IncubationMode::AsynchronousIfNested;
+ QVariantMap m_initialProperties;
+
+ std::vector<std::unique_ptr<ItemIncubator>> m_incubators;
+ std::vector<std::shared_ptr<QQuickItem>> m_items;
+};
+
+#endif // ITEMBUILDER_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "LineChart.h"
+
+#include <cmath>
+
+#include <QPainter>
+#include <QPainterPath>
+#include <QQuickWindow>
+
+#include "RangeGroup.h"
+#include "datasource/ChartDataSource.h"
+#include "scenegraph/LineChartNode.h"
+
+static const float PixelsPerStep = 2.0;
+
+
+QList<QVector2D> interpolatePoints(const QList<QVector2D> &points, float height);
+QList<float> calculateTangents(const QList<QVector2D> &points, float height);
+QVector2D cubicHermite(const QVector2D &first, const QVector2D &second, float step, float mFirst, float mSecond);
+
+QColor colorWithAlpha(const QColor &color, qreal opacity)
+{
+ auto result = color;
+ result.setRedF(result.redF() * opacity);
+ result.setGreenF(result.greenF() * opacity);
+ result.setBlueF(result.blueF() * opacity);
+ result.setAlphaF(opacity);
+ return result;
+}
+
+LineChartAttached::LineChartAttached(QObject *parent)
+ : QObject(parent)
+{
+}
+
+QVariant LineChartAttached::value() const
+{
+ return m_value;
+}
+
+void LineChartAttached::setValue(const QVariant &value)
+{
+ if (value == m_value) {
+ return;
+ }
+
+ m_value = value;
+ Q_EMIT valueChanged();
+}
+
+QColor LineChartAttached::color() const
+{
+ return m_color;
+}
+
+void LineChartAttached::setColor(const QColor &color)
+{
+ if (color == m_color) {
+ return;
+ }
+
+ m_color = color;
+ Q_EMIT colorChanged();
+}
+
+QString LineChartAttached::name() const
+{
+ return m_name;
+}
+
+void LineChartAttached::setName(const QString &newName)
+{
+ if (newName == m_name) {
+ return;
+ }
+
+ m_name = newName;
+ Q_EMIT nameChanged();
+}
+
+QString LineChartAttached::shortName() const
+{
+ if (m_shortName.isEmpty()) {
+ return m_name;
+ } else {
+ return m_shortName;
+ }
+}
+
+void LineChartAttached::setShortName(const QString &newShortName)
+{
+ if (newShortName == m_shortName) {
+ return;
+ }
+
+ m_shortName = newShortName;
+ Q_EMIT shortNameChanged();
+}
+
+LineChart::LineChart(QQuickItem *parent)
+ : XYChart(parent)
+{
+}
+
+bool LineChart::interpolate() const
+{
+ return m_interpolate;
+}
+
+qreal LineChart::lineWidth() const
+{
+ return m_lineWidth;
+}
+
+qreal LineChart::fillOpacity() const
+{
+ return m_fillOpacity;
+}
+
+void LineChart::setInterpolate(bool newInterpolate)
+{
+ if (newInterpolate == m_interpolate) {
+ return;
+ }
+
+ m_interpolate = newInterpolate;
+ polish();
+ Q_EMIT interpolateChanged();
+}
+
+void LineChart::setLineWidth(qreal width)
+{
+ if (qFuzzyCompare(m_lineWidth, width)) {
+ return;
+ }
+
+ m_lineWidth = width;
+ update();
+ Q_EMIT lineWidthChanged();
+}
+
+void LineChart::setFillOpacity(qreal opacity)
+{
+ if (qFuzzyCompare(m_fillOpacity, opacity)) {
+ return;
+ }
+
+ m_fillOpacity = opacity;
+ update();
+ Q_EMIT fillOpacityChanged();
+}
+
+ChartDataSource *LineChart::fillColorSource() const
+{
+ return m_fillColorSource;
+}
+
+void LineChart::setFillColorSource(ChartDataSource *newFillColorSource)
+{
+ if (newFillColorSource == m_fillColorSource) {
+ return;
+ }
+
+ m_fillColorSource = newFillColorSource;
+ update();
+ Q_EMIT fillColorSourceChanged();
+}
+
+QQmlComponent *LineChart::pointDelegate() const
+{
+ return m_pointDelegate;
+}
+
+void LineChart::setPointDelegate(QQmlComponent *newPointDelegate)
+{
+ if (newPointDelegate == m_pointDelegate) {
+ return;
+ }
+
+ m_pointDelegate = newPointDelegate;
+ for (auto entry : std::as_const(m_pointDelegates)) {
+ qDeleteAll(entry);
+ }
+ m_pointDelegates.clear();
+ polish();
+ Q_EMIT pointDelegateChanged();
+}
+
+void LineChart::updatePolish()
+{
+ if (m_rangeInvalid) {
+ updateComputedRange();
+ m_rangeInvalid = false;
+ }
+
+ QList<QVector2D> previousValues;
+
+ const auto range = computedRange();
+ const auto sources = valueSources();
+ for (int i = 0; i < sources.size(); ++i) {
+ auto valueSource = sources.at(i);
+
+ float stepSize = width() / (range.distanceX - 1);
+ QList<QVector2D> values(range.distanceX);
+ auto generator = [&, i = range.startX]() mutable -> QVector2D {
+ float value = 0;
+ if (range.distanceY != 0) {
+ value = (valueSource->item(i).toFloat() - range.startY) / range.distanceY;
+ }
+
+ auto result = QVector2D{direction() == Direction::ZeroAtStart ? i * stepSize : float(boundingRect().right()) - i * stepSize, value};
+ i++;
+ return result;
+ };
+
+ if (direction() == Direction::ZeroAtStart) {
+ std::generate_n(values.begin(), range.distanceX, generator);
+ } else {
+ std::generate_n(values.rbegin(), range.distanceX, generator);
+ }
+
+ if (stacked() && !previousValues.isEmpty()) {
+ if (values.size() != previousValues.size()) {
+ qWarning() << "Value source" << valueSource->objectName()
+ << "has a different number of elements from the previous source. Ignoring stacking for this source.";
+ } else {
+ std::for_each(values.begin(), values.end(), [previousValues, i = 0](QVector2D &point) mutable {
+ point.setY(point.y() + previousValues.at(i++).y());
+ });
+ }
+ }
+ previousValues = values;
+
+ if (m_pointDelegate) {
+ auto &delegates = m_pointDelegates[valueSource];
+ if (delegates.size() != values.size()) {
+ qDeleteAll(delegates);
+ createPointDelegates(values, i);
+ } else {
+ for (int item = 0; item < values.size(); ++item) {
+ auto delegate = delegates.at(item);
+ updatePointDelegate(delegate, values.at(item), valueSource->item(item), i);
+ }
+ }
+ }
+
+ if (m_interpolate) {
+ m_values[valueSource] = interpolatePoints(values, height());
+ } else {
+ m_values[valueSource] = values;
+ }
+ }
+
+ const auto pointKeys = m_pointDelegates.keys();
+ for (auto key : pointKeys) {
+ if (!sources.contains(key)) {
+ qDeleteAll(m_pointDelegates[key]);
+ m_pointDelegates.remove(key);
+ }
+ }
+
+ update();
+}
+
+QSGNode *LineChart::updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data)
+{
+ Q_UNUSED(data);
+
+ if (!node) {
+ node = new QSGNode();
+ }
+
+ const auto highlightIndex = highlight();
+ const auto sources = valueSources();
+ for (int i = 0; i < sources.size(); ++i) {
+ int childIndex = sources.size() - 1 - i;
+ while (childIndex >= node->childCount()) {
+ node->appendChildNode(new LineChartNode{});
+ }
+ auto lineNode = static_cast<LineChartNode *>(node->childAtIndex(childIndex));
+ auto color = colorSource() ? colorSource()->item(i).value<QColor>() : Qt::black;
+ auto fillColor = m_fillColorSource ? m_fillColorSource->item(i).value<QColor>() : colorWithAlpha(color, m_fillOpacity);
+ auto lineWidth = i == highlightIndex ? std::max(m_lineWidth, 3.0) : m_lineWidth;
+
+ if (highlightIndex >= 0 && i != highlightIndex) {
+ color = desaturate(color);
+ fillColor = desaturate(fillColor);
+ }
+
+ updateLineNode(lineNode, sources.at(i), color, fillColor, lineWidth);
+ }
+
+ while (node->childCount() > sources.size()) {
+ // removeChildNode unfortunately does not take care of deletion so we
+ // need to handle this manually.
+ auto lastNode = node->childAtIndex(node->childCount() - 1);
+ node->removeChildNode(lastNode);
+ delete lastNode;
+ }
+
+ if (highlightIndex >= 0) {
+ // Move highlighted node to the end to ensure we always show the
+ // highlighted chart on top. This is done after the above removal to
+ // ensure we don't suddenly remove the highlighted node.
+ auto highlightNode = node->childAtIndex(node->childCount() - 1 - highlightIndex);
+ node->removeChildNode(highlightNode);
+ node->appendChildNode(highlightNode);
+ }
+
+ return node;
+}
+
+void LineChart::onDataChanged()
+{
+ m_rangeInvalid = true;
+ polish();
+}
+
+void LineChart::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
+{
+ XYChart::geometryChange(newGeometry, oldGeometry);
+ if (newGeometry != oldGeometry) {
+ polish();
+ }
+}
+
+void LineChart::updateLineNode(LineChartNode *node, ChartDataSource *valueSource, const QColor &lineColor, const QColor &fillColor, qreal lineWidth)
+{
+ if (window()) {
+ node->setRect(boundingRect(), window()->devicePixelRatio());
+ } else {
+ node->setRect(boundingRect(), 1.0);
+ }
+ node->setLineColor(lineColor);
+ node->setFillColor(fillColor);
+ node->setLineWidth(lineWidth);
+
+ auto values = m_values.value(valueSource);
+ node->setValues(values);
+
+ node->updatePoints();
+}
+
+void LineChart::createPointDelegates(const QList<QVector2D> &values, int sourceIndex)
+{
+ auto valueSource = valueSources().at(sourceIndex);
+
+ QList<QQuickItem *> delegates;
+ for (int i = 0; i < values.size(); ++i) {
+ auto delegate = qobject_cast<QQuickItem *>(m_pointDelegate->beginCreate(qmlContext(m_pointDelegate)));
+ if (!delegate) {
+ qWarning() << "Delegate creation for point" << i << "of value source" << valueSource->objectName()
+ << "failed, make sure pointDelegate is a QQuickItem";
+ delegate = new QQuickItem(this);
+ }
+
+ delegate->setParent(this);
+ delegate->setParentItem(this);
+ updatePointDelegate(delegate, values.at(i), valueSource->item(i), sourceIndex);
+
+ m_pointDelegate->completeCreate();
+
+ delegates.append(delegate);
+ }
+
+ m_pointDelegates.insert(valueSource, delegates);
+}
+
+void LineChart::updatePointDelegate(QQuickItem *delegate, const QVector2D &position, const QVariant &value, int sourceIndex)
+{
+ auto pos = QPointF{position.x() - delegate->width() / 2, (1.0 - position.y()) * height() - delegate->height() / 2};
+ delegate->setPosition(pos);
+
+ auto color = colorSource() ? colorSource()->item(sourceIndex).value<QColor>() : QColor();
+ auto highlightIndex = highlight();
+ if (highlightIndex >= 0) {
+ if (sourceIndex == highlightIndex) {
+ delegate->setZ(1.0);
+ } else {
+ color = desaturate(color);
+ }
+ } else {
+ delegate->setZ(0.0);
+ }
+
+ auto attached = static_cast<LineChartAttached *>(qmlAttachedPropertiesObject<LineChart>(delegate, true));
+ attached->setValue(value);
+ attached->setColor(color);
+ attached->setName(nameSource() ? nameSource()->item(sourceIndex).toString() : QString{});
+ attached->setShortName(shortNameSource() ? shortNameSource()->item(sourceIndex).toString() : QString{});
+}
+
+// Smoothly interpolate between points, using monotonic cubic interpolation.
+QList<QVector2D> interpolatePoints(const QList<QVector2D> &points, float height)
+{
+ if (points.size() < 2) {
+ return points;
+ }
+
+ auto tangents = calculateTangents(points, height);
+
+ QList<QVector2D> result;
+
+ auto current = QVector2D{0.0, points.first().y() * height};
+ result.append(QVector2D{0.0, points.first().y()});
+
+ for (int i = 0; i < points.size() - 1; ++i) {
+ auto next = QVector2D{points.at(i + 1).x(), points.at(i + 1).y() * height};
+
+ auto currentTangent = tangents.at(i);
+ auto nextTangent = tangents.at(i + 1);
+
+ auto stepCount = int(std::max(1.0f, (next.x() - current.x()) / PixelsPerStep));
+ auto stepSize = (next.x() - current.x()) / stepCount;
+
+ if (stepCount == 1 || qFuzzyIsNull(next.y() - current.y())) {
+ result.append(QVector2D{next.x(), next.y() / height});
+ current = next;
+ continue;
+ }
+
+ for (auto delta = current.x(); delta < next.x(); delta += stepSize) {
+ auto interpolated = cubicHermite(current, next, delta, currentTangent, nextTangent);
+ interpolated.setY(interpolated.y() / height);
+ result.append(interpolated);
+ }
+
+ current = next;
+ }
+
+ current.setY(current.y() / height);
+ result.append(current);
+
+ return result;
+}
+
+// This calculates the tangents for monotonic cubic spline interpolation.
+// See https://en.wikipedia.org/wiki/Monotone_cubic_interpolation for details.
+QList<float> calculateTangents(const QList<QVector2D> &points, float height)
+{
+ QList<float> secantSlopes;
+ secantSlopes.reserve(points.size());
+
+ QList<float> tangents;
+ tangents.reserve(points.size());
+
+ float previousSlope = 0.0;
+ float slope = 0.0;
+
+ for (int i = 0; i < points.size() - 1; ++i) {
+ auto current = points.at(i);
+ auto next = points.at(i + 1);
+
+ previousSlope = slope;
+ slope = (next.y() * height - current.y() * height) / (next.x() - current.x());
+
+ secantSlopes.append(slope);
+
+ if (i == 0) {
+ tangents.append(slope);
+ } else if (previousSlope * slope < 0.0) {
+ tangents.append(0.0);
+ } else {
+ tangents.append((previousSlope + slope) / 2.0);
+ }
+ }
+ tangents.append(secantSlopes.last());
+
+ for (int i = 0; i < points.size() - 1; ++i) {
+ auto slope = secantSlopes.at(i);
+
+ if (qFuzzyIsNull(slope)) {
+ tangents[i] = 0.0;
+ tangents[i + 1] = 0.0;
+ continue;
+ }
+
+ auto alpha = tangents.at(i) / slope;
+ auto beta = tangents.at(i + 1) / slope;
+
+ if (alpha < 0.0) {
+ tangents[i] = 0.0;
+ }
+
+ if (beta < 0.0) {
+ tangents[i + 1] = 0.0;
+ }
+
+ auto length = alpha * alpha + beta * beta;
+ if (length > 9) {
+ auto tau = 3.0 / sqrt(length);
+ tangents[i] = tau * alpha * slope;
+ tangents[i + 1] = tau * beta * slope;
+ }
+ }
+
+ return tangents;
+}
+
+// Cubic Hermite Interpolation between two points
+// Given two points, an X value between those two points and two tangents, this
+// will perform cubic hermite interpolation between the two points.
+// See https://en.wikipedia.org/wiki/Cubic_Hermite_spline for details as well as
+// the above mentioned article on monotonic interpolation.
+QVector2D cubicHermite(const QVector2D &first, const QVector2D &second, float step, float mFirst, float mSecond)
+{
+ const auto delta = second.x() - first.x();
+ const auto t = (step - first.x()) / delta;
+
+ // Hermite basis values
+ // h₀₀(t) = 2t³ - 3t² + 1
+ const auto h00 = 2.0f * std::pow(t, 3.0f) - 3.0f * std::pow(t, 2.0f) + 1.0f;
+ // h₁₀(t) = t³ - 2t² + t
+ const auto h10 = std::pow(t, 3.0f) - 2.0f * std::pow(t, 2.0f) + t;
+ // h₀₁(t) = -2t³ + 3t²
+ const auto h01 = -2.0f * std::pow(t, 3.0f) + 3.0f * std::pow(t, 2.0f);
+ // h₁₁(t) = t³ - t²
+ const auto h11 = std::pow(t, 3.0f) - std::pow(t, 2.0f);
+
+ auto result = QVector2D{step, first.y() * h00 + delta * mFirst * h10 + second.y() * h01 + delta * mSecond * h11};
+ return result;
+}
+
+#include "moc_LineChart.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef LINECHART_H
+#define LINECHART_H
+
+#include <memory>
+
+#include <qqmlregistration.h>
+
+#include "XYChart.h"
+
+class LineChartNode;
+
+/**
+ * An attached property that is exposed to point delegates created in line charts.
+ *
+ * \sa LineChart::pointDelegate
+ */
+class LineChartAttached : public QObject
+{
+ Q_OBJECT
+ QML_ANONYMOUS
+
+public:
+ LineChartAttached(QObject *parent = nullptr);
+
+ /**
+ * The value at the current point.
+ */
+ Q_PROPERTY(QVariant value READ value NOTIFY valueChanged)
+ QVariant value() const;
+ void setValue(const QVariant &value);
+ Q_SIGNAL void valueChanged();
+
+ /**
+ * The color at the current point.
+ */
+ Q_PROPERTY(QColor color READ color NOTIFY colorChanged)
+ QColor color() const;
+ void setColor(const QColor &color);
+ Q_SIGNAL void colorChanged();
+
+ /**
+ * The name at the current point.
+ */
+ Q_PROPERTY(QString name READ name NOTIFY nameChanged)
+ QString name() const;
+ void setName(const QString &newName);
+ Q_SIGNAL void nameChanged();
+
+ /**
+ * The short name at the current point.
+ */
+ Q_PROPERTY(QString shortName READ shortName NOTIFY shortNameChanged)
+ QString shortName() const;
+ void setShortName(const QString &newShortName);
+ Q_SIGNAL void shortNameChanged();
+
+private:
+ QVariant m_value;
+ QColor m_color;
+ QString m_name;
+ QString m_shortName;
+};
+
+/**
+ * A line chart.
+ *
+ * ## Usage example
+ *
+ * \snippet snippets/linechart.qml example
+ *
+ * \image html linechart.png "The resulting line chart."
+ */
+class QUICKCHARTS_EXPORT LineChart : public XYChart
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_ATTACHED(LineChartAttached)
+
+public:
+ explicit LineChart(QQuickItem *parent = nullptr);
+
+ /**
+ * Interpolate the values in the chart so that the lines become smoothed.
+ */
+ Q_PROPERTY(bool interpolate READ interpolate WRITE setInterpolate NOTIFY interpolateChanged)
+ bool interpolate() const;
+ void setInterpolate(bool newInterpolate);
+ Q_SIGNAL void interpolateChanged();
+ /**
+ * The width of a line in the chart.
+ */
+ Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged)
+ qreal lineWidth() const;
+ void setLineWidth(qreal width);
+ Q_SIGNAL void lineWidthChanged();
+ /**
+ * The opacity of the area below a line.
+ *
+ * The default is 0.0. Note that if fillColorSource is set, this value is
+ * ignored.
+ */
+ Q_PROPERTY(qreal fillOpacity READ fillOpacity WRITE setFillOpacity NOTIFY fillOpacityChanged)
+ qreal fillOpacity() const;
+ void setFillOpacity(qreal opacity);
+ Q_SIGNAL void fillOpacityChanged();
+ /**
+ * A data source that supplies color values for the line charts' fill area.
+ *
+ * If this is not set (the default), the normal color source will be used,
+ * with the fillOpacity used as its opacity.
+ */
+ Q_PROPERTY(ChartDataSource *fillColorSource READ fillColorSource WRITE setFillColorSource NOTIFY fillColorSourceChanged)
+ ChartDataSource *fillColorSource() const;
+ void setFillColorSource(ChartDataSource *newFillColorSource);
+ Q_SIGNAL void fillColorSourceChanged();
+ /**
+ * A delegate that will be placed at each line chart point.
+ *
+ * When this is not null, the specified component will be used to
+ * instantiate an object for each point in the chart. These objects will
+ * then be placed centered at positions corresponding to the points on the
+ * chart. Each instance will have access to the attached properties of
+ * LineChartAttached through LineChart attached object.
+ *
+ * \note The component assigned to this property is expected to create a
+ * QQuickItem, since the created object needs to be positioned.
+ */
+ Q_PROPERTY(QQmlComponent *pointDelegate READ pointDelegate WRITE setPointDelegate NOTIFY pointDelegateChanged)
+ QQmlComponent *pointDelegate() const;
+ void setPointDelegate(QQmlComponent *newPointDelegate);
+ Q_SIGNAL void pointDelegateChanged();
+
+ static LineChartAttached *qmlAttachedProperties(QObject *object)
+ {
+ return new LineChartAttached(object);
+ }
+
+protected:
+ void updatePolish() override;
+ QSGNode *updatePaintNode(QSGNode *node, QQuickItem::UpdatePaintNodeData *data) override;
+ void onDataChanged() override;
+ void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
+
+private:
+ void updateLineNode(LineChartNode *node, ChartDataSource *valueSource, const QColor &lineColor, const QColor &fillColor, qreal lineWidth);
+ void createPointDelegates(const QList<QVector2D> &values, int sourceIndex);
+ void updatePointDelegate(QQuickItem *delegate, const QVector2D &position, const QVariant &value, int sourceIndex);
+
+ bool m_interpolate = false;
+ qreal m_lineWidth = 1.0;
+ qreal m_fillOpacity = 0.0;
+ bool m_rangeInvalid = true;
+ ChartDataSource *m_fillColorSource = nullptr;
+ QHash<ChartDataSource *, QList<QVector2D>> m_values;
+ QQmlComponent *m_pointDelegate = nullptr;
+ QHash<ChartDataSource *, QList<QQuickItem *>> m_pointDelegates;
+};
+
+#endif // LINECHART_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "PieChart.h"
+
+#include <QAbstractItemModel>
+#include <QDebug>
+
+#include "RangeGroup.h"
+#include "datasource/ChartDataSource.h"
+#include "scenegraph/PieChartNode.h"
+
+PieChart::PieChart(QQuickItem *parent)
+ : Chart(parent)
+{
+ setIndexingMode(Chart::IndexSourceValues);
+ m_range = std::make_unique<RangeGroup>();
+ connect(m_range.get(), &RangeGroup::rangeChanged, this, &PieChart::onDataChanged);
+}
+
+RangeGroup *PieChart::range() const
+{
+ return m_range.get();
+}
+
+bool PieChart::filled() const
+{
+ return m_filled;
+}
+
+void PieChart::setFilled(bool newFilled)
+{
+ if (newFilled == m_filled) {
+ return;
+ }
+
+ m_filled = newFilled;
+ update();
+ Q_EMIT filledChanged();
+}
+
+qreal PieChart::thickness() const
+{
+ return m_thickness;
+}
+
+void PieChart::setThickness(qreal newThickness)
+{
+ if (newThickness == m_thickness) {
+ return;
+ }
+
+ m_thickness = newThickness;
+ update();
+ Q_EMIT thicknessChanged();
+}
+
+qreal PieChart::spacing() const
+{
+ return m_spacing;
+}
+
+void PieChart::setSpacing(qreal newSpacing)
+{
+ if (newSpacing == m_spacing) {
+ return;
+ }
+
+ m_spacing = newSpacing;
+ update();
+ Q_EMIT spacingChanged();
+}
+
+QColor PieChart::backgroundColor() const
+{
+ return m_backgroundColor;
+}
+
+void PieChart::setBackgroundColor(const QColor &color)
+{
+ if (color == m_backgroundColor) {
+ return;
+ }
+ m_backgroundColor = color;
+ update();
+ Q_EMIT backgroundColorChanged();
+}
+
+qreal PieChart::fromAngle() const
+{
+ return m_fromAngle;
+}
+
+void PieChart::setFromAngle(qreal newFromAngle)
+{
+ if (qFuzzyCompare(newFromAngle, m_fromAngle)) {
+ return;
+ }
+
+ m_fromAngle = newFromAngle;
+ update();
+ Q_EMIT fromAngleChanged();
+}
+
+qreal PieChart::toAngle() const
+{
+ return m_toAngle;
+}
+
+void PieChart::setToAngle(qreal newToAngle)
+{
+ if (qFuzzyCompare(newToAngle, m_toAngle)) {
+ return;
+ }
+
+ m_toAngle = newToAngle;
+ update();
+ Q_EMIT toAngleChanged();
+}
+
+bool PieChart::smoothEnds() const
+{
+ return m_smoothEnds;
+}
+
+void PieChart::setSmoothEnds(bool newSmoothEnds)
+{
+ if (newSmoothEnds == m_smoothEnds) {
+ return;
+ }
+
+ m_smoothEnds = newSmoothEnds;
+ update();
+ Q_EMIT smoothEndsChanged();
+}
+
+QSGNode *PieChart::updatePaintNode(QSGNode *node, UpdatePaintNodeData *data)
+{
+ Q_UNUSED(data);
+ if (!node) {
+ node = new QSGNode{};
+ }
+
+ auto sourceCount = valueSources().size();
+
+ if (m_sections.count() < sourceCount) {
+ return node;
+ }
+
+ auto minDimension = std::min(width(), height());
+
+ float outerRadius = minDimension;
+ for (int i = 0; i < sourceCount; ++i) {
+ float innerRadius = i == sourceCount - 1 && m_filled ? 0.0 : outerRadius - m_thickness * 2.0;
+
+ if (node->childCount() <= i) {
+ node->appendChildNode(new PieChartNode{});
+ }
+
+ auto pieNode = static_cast<PieChartNode *>(node->childAtIndex(i));
+ pieNode->setRect(boundingRect());
+ pieNode->setInnerRadius(innerRadius);
+ pieNode->setOuterRadius(outerRadius);
+ pieNode->setSections(m_sections.at(i));
+ pieNode->setBackgroundColor(m_backgroundColor);
+ pieNode->setColors(m_colors.at(i));
+ pieNode->setFromAngle(m_fromAngle);
+ pieNode->setToAngle(m_toAngle);
+ pieNode->setSmoothEnds(m_smoothEnds);
+
+ outerRadius = innerRadius - m_spacing * 2.0;
+ }
+
+ while (node->childCount() > sourceCount) {
+ auto lastNode = node->childAtIndex(node->childCount() - 1);
+ node->removeChildNode(lastNode);
+ delete lastNode;
+ }
+
+ return node;
+}
+
+void PieChart::onDataChanged()
+{
+ m_sections.clear();
+ m_colors.clear();
+
+ const auto sources = valueSources();
+ const auto colors = colorSource();
+
+ if (!colors || sources.isEmpty() || !m_range->isValid()) {
+ return;
+ }
+
+ auto maximum = [](ChartDataSource *source) {
+ qreal result = 0.0;
+ for (int i = 0; i < source->itemCount(); ++i) {
+ result += source->item(i).toDouble();
+ }
+ return std::max(result, source->maximum().toDouble());
+ };
+
+ auto indexMode = indexingMode();
+ auto colorIndex = 0;
+ const auto highlightIndex = highlight();
+ auto calculateZeroRange = [](ChartDataSource *) {
+ return 0.0;
+ };
+ auto range = m_range->calculateRange(valueSources(), calculateZeroRange, maximum);
+
+ for (auto source : sources) {
+ qreal threshold = range.start;
+ qreal total = 0.0;
+
+ QList<qreal> sections;
+ QList<QColor> sectionColors;
+
+ for (int i = 0; i < source->itemCount(); ++i) {
+ auto value = source->item(i).toReal();
+ auto limited = value - threshold;
+ if (limited > 0.0) {
+ if (total + limited >= range.end) {
+ limited = range.end - total;
+ }
+
+ sections << limited;
+ total += limited;
+
+ auto color = colors->item(colorIndex).value<QColor>();
+
+ if (highlightIndex >= 0 && highlightIndex != colorIndex) {
+ color = desaturate(color);
+ }
+
+ sectionColors << color;
+ }
+ threshold = std::max(0.0, threshold - value);
+
+ if (indexMode != IndexEachSource) {
+ colorIndex++;
+ }
+ }
+
+ if (qFuzzyCompare(total, 0.0)) {
+ m_sections << QList<qreal>{0.0};
+ m_colors << QList<QColor>{colors->item(colorIndex).value<QColor>()};
+ }
+
+ for (auto &value : sections) {
+ value = value / range.distance;
+ }
+
+ m_sections << sections;
+ m_colors << sectionColors;
+
+ if (indexMode == IndexEachSource) {
+ colorIndex++;
+ } else if (indexMode == IndexSourceValues) {
+ colorIndex = 0;
+ }
+ }
+
+ update();
+}
+
+#include "moc_PieChart.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef PIECHART_H
+#define PIECHART_H
+
+#include <memory>
+
+#include "Chart.h"
+#include "RangeGroup.h"
+
+/**
+ * An item to render a pie chart.
+ *
+ * This item will render a Pie chart based on the [valueSources] supplied to it.
+ * By default it will set [indexingMode] to [IndexSourceValues], meaning that it
+ * will use the individual values of the valueSources to render sections of the
+ * chart.
+ *
+ * [valueSources]: \ref Chart::valueSources
+ * [indexingMode]: \ref Chart::indexingMode
+ * [IndexSourceValues]: \ref Chart::IndexingMode
+ *
+ * ### Usage example
+ *
+ * \snippet snippets/piechart.qml example
+ *
+ * \image html piechart.png "The resulting pie chart."
+ *
+ * ### Multiple Value Sources
+ *
+ * Multiple valueSources are rendered as consecutive pie charts in the same
+ * item. The first valueSource will become the outermost circle and consecutive
+ * valueSources will be rendered as continuously smaller circles. When rendering
+ * multiple value sources, [filled] will only affect the last value source. All
+ * other value sources will be rendered according to [thickness], with [spacing]
+ * amount of space between them.
+ *
+ * [filled]: \ref PieChart::filled
+ * [thickness]: \ref PieChart::thickness
+ * [spacing]: \ref PieChart::spacing
+ *
+ */
+class QUICKCHARTS_EXPORT PieChart : public Chart
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ explicit PieChart(QQuickItem *parent = nullptr);
+
+ /**
+ * The range of values to display in this PieChart.
+ *
+ * When set to "automatic", the values will be divided across the entire
+ * chart.
+ *
+ * \sa RangeGroup
+ */
+ Q_PROPERTY(RangeGroup *range READ range CONSTANT)
+ RangeGroup *range() const;
+ /**
+ * Whether to use a filled pie or not.
+ *
+ * If true, the last pie rendered will be rendered as a filled circle.
+ * The default is false.
+ */
+ Q_PROPERTY(bool filled READ filled WRITE setFilled NOTIFY filledChanged)
+ bool filled() const;
+ void setFilled(bool newFilled);
+ Q_SIGNAL void filledChanged();
+ /**
+ * The thickness of an individual pie, in pixels.
+ *
+ * If filled is set, this is ignored for the last pie. The default is 10px.
+ */
+ Q_PROPERTY(qreal thickness READ thickness WRITE setThickness NOTIFY thicknessChanged)
+ qreal thickness() const;
+ void setThickness(qreal newThickness);
+ Q_SIGNAL void thicknessChanged();
+ /**
+ * The amount of spacing between pies when rendering multiple value sources.
+ *
+ * The default is 0.
+ */
+ Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
+ qreal spacing() const;
+ void setSpacing(qreal newSpacing);
+ Q_SIGNAL void spacingChanged();
+ /**
+ * Sets a colour to use to fill remaining space on the pie.
+ *
+ * The default is transparent.
+ */
+ Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged)
+ QColor backgroundColor() const;
+ void setBackgroundColor(const QColor &color);
+ Q_SIGNAL void backgroundColorChanged();
+ /**
+ * The starting angle of the arc used for the entire pie.
+ *
+ * When set, instead of rendering a full circle, the pie will be rendered as
+ * an arc. The default is 0.
+ */
+ Q_PROPERTY(qreal fromAngle READ fromAngle WRITE setFromAngle NOTIFY fromAngleChanged)
+ qreal fromAngle() const;
+ void setFromAngle(qreal newFromAngle);
+ Q_SIGNAL void fromAngleChanged();
+ /**
+ * The end angle of the arc used for the entire pie. When set, instead of
+ * rendering a full circle, the pie will be rendered as an arc. The default
+ * is 360.
+ */
+ Q_PROPERTY(qreal toAngle READ toAngle WRITE setToAngle NOTIFY toAngleChanged)
+ qreal toAngle() const;
+ void setToAngle(qreal newToAngle);
+ Q_SIGNAL void toAngleChanged();
+ /**
+ * Smooth the ends of pie sections.
+ *
+ * When true, this will try to smooth the ends of sections. This works best
+ * when [filled] is false. The default is false.
+ *
+ * [filled]: \ref PieChart::filled
+ */
+ Q_PROPERTY(bool smoothEnds READ smoothEnds WRITE setSmoothEnds NOTIFY smoothEndsChanged)
+ bool smoothEnds() const;
+ void setSmoothEnds(bool newSmoothEnds);
+ Q_SIGNAL void smoothEndsChanged();
+
+protected:
+ /**
+ * Reimplemented from QQuickItem.
+ */
+ QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *data) override;
+ /**
+ * Reimplemented from Chart.
+ */
+ void onDataChanged() override;
+
+private:
+ std::unique_ptr<RangeGroup> m_range;
+ bool m_filled = false;
+ qreal m_thickness = 10.0;
+ qreal m_spacing = 0.0;
+ QColor m_backgroundColor = Qt::transparent;
+ qreal m_fromAngle = 0.0;
+ qreal m_toAngle = 360.0;
+ bool m_smoothEnds = false;
+
+ QList<QList<qreal>> m_sections;
+ QList<QList<QColor>> m_colors;
+};
+
+#endif // PIECHART_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "RangeGroup.h"
+
+#include <cmath>
+
+#include <QList>
+
+RangeGroup::RangeGroup(QObject *parent)
+ : QObject(parent)
+{
+ connect(this, &RangeGroup::fromChanged, this, &RangeGroup::rangeChanged);
+ connect(this, &RangeGroup::toChanged, this, &RangeGroup::rangeChanged);
+ connect(this, &RangeGroup::automaticChanged, this, &RangeGroup::rangeChanged);
+ connect(this, &RangeGroup::minimumChanged, this, &RangeGroup::rangeChanged);
+ connect(this, &RangeGroup::incrementChanged, this, &RangeGroup::rangeChanged);
+}
+
+qreal RangeGroup::from() const
+{
+ return m_from;
+}
+
+void RangeGroup::setFrom(qreal from)
+{
+ if (qFuzzyCompare(m_from, from)) {
+ return;
+ }
+
+ m_from = from;
+ Q_EMIT fromChanged();
+}
+
+qreal RangeGroup::to() const
+{
+ return m_to;
+}
+
+void RangeGroup::setTo(qreal to)
+{
+ if (qFuzzyCompare(m_to, to)) {
+ return;
+ }
+
+ m_to = to;
+ Q_EMIT toChanged();
+}
+
+bool RangeGroup::automatic() const
+{
+ return m_automatic;
+}
+
+void RangeGroup::setAutomatic(bool automatic)
+{
+ if (m_automatic == automatic) {
+ return;
+ }
+
+ m_automatic = automatic;
+ Q_EMIT automaticChanged();
+}
+
+qreal RangeGroup::distance() const
+{
+ return m_to - m_from;
+}
+
+qreal RangeGroup::minimum() const
+{
+ return m_minimum;
+}
+
+void RangeGroup::setMinimum(qreal newMinimum)
+{
+ if (newMinimum == m_minimum) {
+ return;
+ }
+
+ m_minimum = newMinimum;
+ Q_EMIT minimumChanged();
+}
+
+qreal RangeGroup::increment() const
+{
+ return m_increment;
+}
+
+void RangeGroup::setIncrement(qreal newIncrement)
+{
+ if (newIncrement == m_increment) {
+ return;
+ }
+
+ m_increment = newIncrement;
+ Q_EMIT incrementChanged();
+}
+
+bool RangeGroup::isValid() const
+{
+ return m_automatic || (m_to > m_from);
+}
+
+RangeGroup::RangeResult RangeGroup::calculateRange(const QList<ChartDataSource *> &sources,
+ std::function<qreal(ChartDataSource *)> minimumCallback,
+ std::function<qreal(ChartDataSource *)> maximumCallback)
+{
+ RangeResult result;
+
+ auto min = std::numeric_limits<qreal>::max();
+ auto max = std::numeric_limits<qreal>::min();
+
+ if (!m_automatic) {
+ min = m_from;
+ max = m_to;
+ } else {
+ std::for_each(sources.begin(), sources.end(), [&](ChartDataSource *source) {
+ min = std::min(min, minimumCallback(source));
+ max = std::max(max, maximumCallback(source));
+ });
+ }
+
+ max = std::max(max, m_minimum);
+ if (m_increment > 0.0) {
+ max = m_increment * std::ceil(max / m_increment);
+ }
+
+ result.start = min;
+ result.end = max;
+ result.distance = max - min;
+
+ return result;
+}
+
+#include "moc_RangeGroup.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef RANGEGROUP_H
+#define RANGEGROUP_H
+
+#include <functional>
+
+#include <QObject>
+#include <qqmlregistration.h>
+
+#include "quickcharts_export.h"
+
+class ChartDataSource;
+
+/**
+ * An object that can be used as a grouped property to provide a value range for charts.
+ *
+ */
+class QUICKCHARTS_EXPORT RangeGroup : public QObject
+{
+ Q_OBJECT
+ QML_NAMED_ELEMENT(Range)
+ QML_UNCREATABLE("Grouped Property")
+
+public:
+ struct RangeResult {
+ qreal start = 0.0;
+ qreal end = 0.0;
+ qreal distance = 0.0;
+ };
+
+ explicit RangeGroup(QObject *parent = nullptr);
+
+ /**
+ * The start of this range.
+ *
+ * The default is 0.
+ */
+ Q_PROPERTY(qreal from READ from WRITE setFrom NOTIFY fromChanged)
+ qreal from() const;
+ void setFrom(qreal from);
+ Q_SIGNAL void fromChanged();
+ /**
+ * The end of this range.
+ *
+ * The default is 100.
+ */
+ Q_PROPERTY(qreal to READ to WRITE setTo NOTIFY toChanged)
+ qreal to() const;
+ void setTo(qreal to);
+ Q_SIGNAL void toChanged();
+ /**
+ * Whether to determine the range based on values of a chart.
+ *
+ * If true (the default), `from` and `to` are ignored and instead calculated
+ * from the minimum and maximum values of a chart's valueSources.
+ */
+ Q_PROPERTY(bool automatic READ automatic WRITE setAutomatic NOTIFY automaticChanged)
+ bool automatic() const;
+ void setAutomatic(bool newAutomatic);
+ Q_SIGNAL void automaticChanged();
+ /**
+ * The distance between from and to.
+ */
+ Q_PROPERTY(qreal distance READ distance NOTIFY rangeChanged)
+ qreal distance() const;
+ /**
+ * The minimum size of the range.
+ *
+ * This is mostly relevant when automatic is true. Setting this value will
+ * ensure that the range will never be smaller than this value. The default
+ * is `std::numeric_limits<qreal>::min`, which means minimum is disabled.
+ */
+ Q_PROPERTY(qreal minimum READ minimum WRITE setMinimum NOTIFY minimumChanged)
+ qreal minimum() const;
+ void setMinimum(qreal newMinimum);
+ Q_SIGNAL void minimumChanged();
+ /**
+ * The amount with which the range increases.
+ *
+ * The total range will be limited to a multiple of this value. This is
+ * mostly useful when automatic is true. The default is 0.0, which means do
+ * not limit the range increment.
+ */
+ Q_PROPERTY(qreal increment READ increment WRITE setIncrement NOTIFY incrementChanged)
+ qreal increment() const;
+ void setIncrement(qreal newIncrement);
+ Q_SIGNAL void incrementChanged();
+
+ bool isValid() const;
+
+ Q_SIGNAL void rangeChanged();
+
+ RangeResult calculateRange(const QList<ChartDataSource *> &sources,
+ std::function<qreal(ChartDataSource *)> minimumCallback,
+ std::function<qreal(ChartDataSource *)> maximumCallback);
+
+private:
+ qreal m_from = 0.0;
+ qreal m_to = 100.0;
+ bool m_automatic = true;
+ qreal m_minimum = std::numeric_limits<qreal>::min();
+ qreal m_increment = 0.0;
+};
+
+#endif // RANGEGROUP_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "XYChart.h"
+
+#include "RangeGroup.h"
+#include "datasource/ChartDataSource.h"
+
+bool operator==(const ComputedRange &first, const ComputedRange &second)
+{
+ return first.startX == second.startX && first.endX == second.endX && qFuzzyCompare(first.startY, second.startY) && qFuzzyCompare(first.endY, second.endY);
+}
+
+XYChart::XYChart(QQuickItem *parent)
+ : Chart(parent)
+{
+ m_xRange = new RangeGroup{this};
+ connect(m_xRange, &RangeGroup::rangeChanged, this, &XYChart::updateComputedRange);
+ m_yRange = new RangeGroup{this};
+ connect(m_yRange, &RangeGroup::rangeChanged, this, &XYChart::updateComputedRange);
+}
+
+RangeGroup *XYChart::xRange() const
+{
+ return m_xRange;
+}
+
+RangeGroup *XYChart::yRange() const
+{
+ return m_yRange;
+}
+
+XYChart::Direction XYChart::direction() const
+{
+ return m_direction;
+}
+
+void XYChart::setDirection(XYChart::Direction newDirection)
+{
+ if (newDirection == m_direction) {
+ return;
+ }
+
+ m_direction = newDirection;
+ onDataChanged();
+ Q_EMIT directionChanged();
+}
+
+bool XYChart::stacked() const
+{
+ return m_stacked;
+}
+
+void XYChart::setStacked(bool newStacked)
+{
+ if (newStacked == m_stacked) {
+ return;
+ }
+
+ m_stacked = newStacked;
+ onDataChanged();
+ Q_EMIT stackedChanged();
+}
+
+ComputedRange XYChart::computedRange() const
+{
+ return m_computedRange;
+}
+
+void XYChart::updateComputedRange()
+{
+ if (valueSources().count() == 0) {
+ return;
+ }
+
+ ComputedRange result;
+
+ auto xRange = m_xRange->calculateRange(
+ valueSources(),
+ [](ChartDataSource *) {
+ return 0;
+ },
+ [](ChartDataSource *source) {
+ return source->itemCount();
+ });
+ result.startX = xRange.start;
+ result.endX = xRange.end;
+ result.distanceX = xRange.distance;
+
+ auto maximumY = [this, xRange](ChartDataSource *source) {
+ if (!m_stacked) {
+ return source->maximum().toDouble();
+ } else {
+ qreal max = std::numeric_limits<qreal>::min();
+ for (int i = xRange.start; i < xRange.end; ++i) {
+ qreal yDistance = 0.0;
+ for (auto source : valueSources()) {
+ yDistance += source->item(i).toDouble();
+ }
+ max = std::max(max, yDistance);
+ }
+ return max;
+ }
+ };
+
+ auto yRange = m_yRange->calculateRange(
+ valueSources(),
+ [](ChartDataSource *source) {
+ return std::min(0.0, source->minimum().toDouble());
+ },
+ maximumY);
+ result.startY = yRange.start;
+ result.endY = yRange.end;
+ result.distanceY = yRange.distance;
+
+ setComputedRange(result);
+}
+
+void XYChart::setComputedRange(ComputedRange range)
+{
+ if (range == m_computedRange) {
+ return;
+ }
+
+ m_computedRange = range;
+ Q_EMIT computedRangeChanged();
+}
+
+QDebug operator<<(QDebug debug, const ComputedRange &range)
+{
+ debug << "Range: startX" << range.startX << "endX" << range.endX << "distance" << range.distanceX << "startY" << range.startY << "endY" << range.endY
+ << "distance" << range.distanceY;
+ return debug;
+}
+
+#include "moc_XYChart.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef XYCHART_H
+#define XYCHART_H
+
+#include "Chart.h"
+
+class RangeGroup;
+
+/**
+ * A helper containing the calculated X and Y ranges of a chart.
+ */
+struct ComputedRange {
+ int startX = 0;
+ int endX = 0;
+ int distanceX = 0;
+ float startY = 0.0;
+ float endY = 0.0;
+ float distanceY = 0.0;
+};
+
+bool operator==(const ComputedRange &first, const ComputedRange &second);
+
+/**
+ * A base class for Charts that are based on an X/Y grid.
+ */
+class QUICKCHARTS_EXPORT XYChart : public Chart
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("Base Class")
+
+public:
+ /**
+ * The direction of values on the X axis.
+ *
+ * "Start" is defined as the starting direction of the chart, when using a
+ * left-to-right language it will be the left side, with a right-to-left
+ * language it will be right.
+ */
+ enum class Direction {
+ ZeroAtStart, ///< Zero is at the beginning of the chart, values run from begin to end.
+ ZeroAtEnd ///< Zero is at the end of the chart, values run from end to begin.
+ };
+ Q_ENUM(Direction)
+
+ /**
+ * Constructor
+ *
+ * @param parent The QObject parent.
+ */
+ explicit XYChart(QQuickItem *parent = nullptr);
+
+ /**
+ * Destructor
+ */
+ ~XYChart() override = default;
+
+ /**
+ * The range of values on the X axis.
+ */
+ Q_PROPERTY(RangeGroup *xRange READ xRange CONSTANT)
+ virtual RangeGroup *xRange() const;
+ /**
+ * The range of values on the Y axis.
+ */
+ Q_PROPERTY(RangeGroup *yRange READ yRange CONSTANT)
+ virtual RangeGroup *yRange() const;
+ /**
+ * Which direction this chart's X axis runs.
+ */
+ Q_PROPERTY(Direction direction READ direction WRITE setDirection NOTIFY directionChanged)
+ virtual XYChart::Direction direction() const;
+ virtual void setDirection(XYChart::Direction newDirection);
+ Q_SIGNAL void directionChanged();
+ /**
+ * Whether the values of each value source should be stacked.
+ *
+ * When true, Y values will be added on top of each other. The precise
+ * meaning of this property depends on the specific chart. The default is
+ * false.
+ */
+ Q_PROPERTY(bool stacked READ stacked WRITE setStacked NOTIFY stackedChanged)
+ bool stacked() const;
+ void setStacked(bool newStacked);
+ Q_SIGNAL void stackedChanged();
+
+ /**
+ * Get the complete, calculated range for this chart.
+ */
+ ComputedRange computedRange() const;
+ /**
+ * Emitted whenever the complete range is recalculated.
+ */
+ Q_SIGNAL void computedRangeChanged();
+
+protected:
+ /**
+ * Re-calculate the chart's range.
+ *
+ * By default, this will make use of the xRange/yRange properties and
+ * calculate a proper range. This method can be overridden by subclasses if
+ * some special calculation is needed.
+ */
+ virtual void updateComputedRange();
+
+ /**
+ * Set the computed range.
+ *
+ * \param range The new range.
+ */
+ void setComputedRange(ComputedRange range);
+
+private:
+ RangeGroup *m_xRange = nullptr;
+ RangeGroup *m_yRange = nullptr;
+ Direction m_direction = Direction::ZeroAtStart;
+ bool m_stacked = false;
+ ComputedRange m_computedRange;
+};
+
+QDebug operator<<(QDebug debug, const ComputedRange &range);
+
+#endif // XYCHART_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "ArraySource.h"
+
+ArraySource::ArraySource(QObject *parent)
+ : ChartDataSource(parent)
+{
+}
+
+int ArraySource::itemCount() const
+{
+ return m_array.count();
+}
+
+QVariant ArraySource::item(int index) const
+{
+ if (m_array.isEmpty()) {
+ return {};
+ }
+
+ if (!m_wrap && (index < 0 || index > m_array.count() - 1)) {
+ return {};
+ }
+
+ return m_array.at(index % m_array.count());
+}
+
+QVariant ArraySource::minimum() const
+{
+ auto itr = std::min_element(m_array.cbegin(), m_array.cend(), variantCompare);
+ if (itr != m_array.cend()) {
+ return *itr;
+ }
+ return QVariant{};
+}
+
+QVariant ArraySource::maximum() const
+{
+ auto itr = std::max_element(m_array.cbegin(), m_array.cend(), variantCompare);
+ if (itr != m_array.cend()) {
+ return *itr;
+ }
+ return QVariant{};
+}
+
+QVariantList ArraySource::array() const
+{
+ return m_array;
+}
+
+bool ArraySource::wrap() const
+{
+ return m_wrap;
+}
+
+void ArraySource::setArray(const QVariantList &array)
+{
+ if (m_array == array) {
+ return;
+ }
+
+ m_array = array;
+ Q_EMIT dataChanged();
+}
+
+void ArraySource::setWrap(bool wrap)
+{
+ if (m_wrap == wrap) {
+ return;
+ }
+
+ m_wrap = wrap;
+ Q_EMIT dataChanged();
+}
+
+#include "moc_ArraySource.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef ARRAYSOURCE_H
+#define ARRAYSOURCE_H
+
+#include <QVariantList>
+
+#include "ChartDataSource.h"
+
+/**
+ * A data source that provides entries of an array as data.
+ */
+class QUICKCHARTS_EXPORT ArraySource : public ChartDataSource
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ /**
+ * Constructor
+ *
+ * @param parent TODO
+ */
+ explicit ArraySource(QObject *parent = nullptr);
+
+ int itemCount() const override;
+ QVariant item(int index) const override;
+ QVariant minimum() const override;
+ QVariant maximum() const override;
+
+ Q_PROPERTY(QVariantList array READ array WRITE setArray NOTIFY dataChanged)
+ QVariantList array() const;
+ void setArray(const QVariantList &array);
+
+ Q_PROPERTY(bool wrap READ wrap WRITE setWrap NOTIFY dataChanged)
+ bool wrap() const;
+ void setWrap(bool wrap);
+
+private:
+ QVariantList m_array;
+ bool m_wrap = false;
+};
+
+#endif // ARRAYSOURCE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "ChartAxisSource.h"
+
+#include <QDebug>
+#include <QVariant>
+
+#include "XYChart.h"
+
+ChartAxisSource::ChartAxisSource(QObject *parent)
+ : ChartDataSource(parent)
+{
+ connect(this, &ChartAxisSource::itemCountChanged, this, &ChartAxisSource::dataChanged);
+ connect(this, &ChartAxisSource::chartChanged, this, &ChartAxisSource::dataChanged);
+ connect(this, &ChartAxisSource::axisChanged, this, &ChartAxisSource::dataChanged);
+}
+
+QVariant ChartAxisSource::item(int index) const
+{
+ if (!m_chart || index < 0 || index > m_itemCount) {
+ return {};
+ }
+
+ auto range = m_chart->computedRange();
+ if (m_axis == Axis::XAxis) {
+ return range.startX + (range.distanceX / (m_itemCount - 1)) * index;
+ } else {
+ return range.startY + (range.distanceY / (m_itemCount - 1)) * index;
+ }
+}
+
+QVariant ChartAxisSource::minimum() const
+{
+ if (!m_chart) {
+ return {};
+ }
+
+ if (m_axis == Axis::XAxis) {
+ return m_chart->computedRange().startX;
+ } else {
+ return m_chart->computedRange().startY;
+ }
+}
+
+QVariant ChartAxisSource::maximum() const
+{
+ if (!m_chart) {
+ return {};
+ }
+
+ if (m_axis == Axis::XAxis) {
+ return m_chart->computedRange().endX;
+ } else {
+ return m_chart->computedRange().endY;
+ }
+}
+
+XYChart *ChartAxisSource::chart() const
+{
+ return m_chart;
+}
+
+void ChartAxisSource::setChart(XYChart *newChart)
+{
+ if (newChart == m_chart) {
+ return;
+ }
+
+ if (m_chart) {
+ disconnect(m_chart, &XYChart::computedRangeChanged, this, &ChartAxisSource::dataChanged);
+ }
+
+ m_chart = newChart;
+ if (m_chart) {
+ connect(m_chart, &XYChart::computedRangeChanged, this, &ChartAxisSource::dataChanged);
+ }
+ Q_EMIT chartChanged();
+}
+
+ChartAxisSource::Axis ChartAxisSource::axis() const
+{
+ return m_axis;
+}
+
+void ChartAxisSource::setAxis(ChartAxisSource::Axis newAxis)
+{
+ if (newAxis == m_axis) {
+ return;
+ }
+
+ m_axis = newAxis;
+ Q_EMIT axisChanged();
+}
+
+int ChartAxisSource::itemCount() const
+{
+ return m_itemCount;
+}
+
+void ChartAxisSource::setItemCount(int newItemCount)
+{
+ if (newItemCount == m_itemCount) {
+ return;
+ }
+
+ m_itemCount = newItemCount;
+ Q_EMIT itemCountChanged();
+}
+
+#include "moc_ChartAxisSource.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef CHARTAXISSOURCE_H
+#define CHARTAXISSOURCE_H
+
+#include "ChartDataSource.h"
+
+class XYChart;
+
+/**
+ * A data source that provides values from a chart's axis as data.
+ */
+class QUICKCHARTS_EXPORT ChartAxisSource : public ChartDataSource
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ enum class Axis { XAxis, YAxis };
+ Q_ENUM(Axis)
+
+ /**
+ * Constructor
+ *
+ * @param parent TODO
+ */
+ ChartAxisSource(QObject *parent = nullptr);
+
+ Q_PROPERTY(XYChart *chart READ chart WRITE setChart NOTIFY chartChanged)
+ XYChart *chart() const;
+ Q_SLOT void setChart(XYChart *newChart);
+ Q_SIGNAL void chartChanged();
+
+ Q_PROPERTY(ChartAxisSource::Axis axis READ axis WRITE setAxis NOTIFY axisChanged)
+ ChartAxisSource::Axis axis() const;
+ Q_SLOT void setAxis(ChartAxisSource::Axis newAxis);
+ Q_SIGNAL void axisChanged();
+
+ Q_PROPERTY(int itemCount READ itemCount WRITE setItemCount NOTIFY itemCountChanged)
+ int itemCount() const override;
+ Q_SLOT void setItemCount(int newItemCount);
+ Q_SIGNAL void itemCountChanged();
+
+ QVariant item(int index) const override;
+ QVariant minimum() const override;
+ QVariant maximum() const override;
+
+private:
+ XYChart *m_chart = nullptr;
+ Axis m_axis = Axis::XAxis;
+ int m_itemCount = 2;
+};
+
+#endif // CHARTAXISSOURCE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "ChartDataSource.h"
+
+#include <QColor>
+#include <QVariant>
+
+ChartDataSource::ChartDataSource(QObject *parent)
+ : QObject(parent)
+{
+}
+
+QVariant ChartDataSource::first() const
+{
+ return item(0);
+}
+
+bool ChartDataSource::variantCompare(const QVariant &lhs, const QVariant &rhs)
+{
+ return QVariant::compare(lhs, rhs) == QPartialOrdering::Less;
+}
+
+#include "moc_ChartDataSource.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef DATASOURCE_H
+#define DATASOURCE_H
+
+#include <QObject>
+#include <qqmlregistration.h>
+
+#include "quickcharts_export.h"
+
+/**
+ * Abstract base class for data sources.
+ */
+class QUICKCHARTS_EXPORT ChartDataSource : public QObject
+{
+ Q_OBJECT
+ QML_NAMED_ELEMENT(DataSource)
+ QML_UNCREATABLE("Abstract Base Class")
+
+public:
+ explicit ChartDataSource(QObject *parent = nullptr);
+ virtual ~ChartDataSource() = default;
+
+ virtual int itemCount() const = 0;
+ virtual QVariant item(int index) const = 0;
+ virtual QVariant minimum() const = 0;
+ virtual QVariant maximum() const = 0;
+
+ virtual QVariant first() const;
+
+ Q_SIGNAL void dataChanged();
+
+protected:
+ static bool variantCompare(const QVariant &lhs, const QVariant &rhs);
+};
+
+#endif // DATASOURCE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
+ * SPDX-FileCopyrightText: 2019 David Edmundson <davidedmundson@kde.org>
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "ColorGradientSource.h"
+
+#include <QVariant>
+
+#include <QDebug>
+
+ColorGradientSource::ColorGradientSource(QObject *parent)
+ : ChartDataSource(parent)
+{
+}
+
+int ColorGradientSource::itemCount() const
+{
+ return m_itemCount;
+}
+
+QVariant ColorGradientSource::item(int index) const
+{
+ if (index < 0 || index >= m_colors.size()) {
+ return QVariant{};
+ }
+
+ return m_colors.at(index);
+}
+
+QVariant ColorGradientSource::minimum() const
+{
+ return QVariant{};
+}
+
+QVariant ColorGradientSource::maximum() const
+{
+ return QVariant{};
+}
+
+QColor ColorGradientSource::baseColor() const
+{
+ return m_baseColor;
+}
+
+void ColorGradientSource::setBaseColor(const QColor &newBaseColor)
+{
+ if (newBaseColor == m_baseColor) {
+ return;
+ }
+
+ m_baseColor = newBaseColor;
+ regenerateColors();
+ Q_EMIT baseColorChanged();
+}
+
+void ColorGradientSource::setItemCount(int newItemCount)
+{
+ if (newItemCount == m_itemCount) {
+ return;
+ }
+
+ m_itemCount = newItemCount;
+ regenerateColors();
+ Q_EMIT itemCountChanged();
+}
+
+QVariantList ColorGradientSource::colors() const
+{
+ QVariantList colorsVariant;
+ colorsVariant.reserve(m_colors.count());
+ for (const QColor &color : std::as_const(m_colors)) {
+ colorsVariant.append(color);
+ }
+ return colorsVariant;
+}
+
+void ColorGradientSource::regenerateColors()
+{
+ if (!m_baseColor.isValid() || m_itemCount <= 0) {
+ return;
+ }
+
+ m_colors.clear();
+
+ for (int i = 0; i < m_itemCount; ++i) {
+ auto newHue = m_baseColor.hsvHueF() + i * (1.0 / m_itemCount);
+ newHue = newHue - int(newHue);
+ m_colors.append(QColor::fromHsvF(newHue, m_baseColor.saturationF(), m_baseColor.valueF(), m_baseColor.alphaF()));
+ }
+
+ Q_EMIT dataChanged();
+}
+
+#include "moc_ColorGradientSource.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
+ * SPDX-FileCopyrightText: 2019 David Edmundson <davidedmundson@kde.org>
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef COLORGRADIENTSOURCE_H
+#define COLORGRADIENTSOURCE_H
+
+#include <QColor>
+#include <QList>
+
+#include "ChartDataSource.h"
+
+/**
+ * A data source that provides a hue-shifted color as data.
+ */
+class QUICKCHARTS_EXPORT ColorGradientSource : public ChartDataSource
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ explicit ColorGradientSource(QObject *parent = nullptr);
+
+ Q_PROPERTY(QColor baseColor READ baseColor WRITE setBaseColor NOTIFY baseColorChanged)
+ QColor baseColor() const;
+ void setBaseColor(const QColor &newBaseColor);
+ Q_SIGNAL void baseColorChanged();
+
+ Q_PROPERTY(int itemCount READ itemCount WRITE setItemCount NOTIFY itemCountChanged)
+ void setItemCount(int newItemCount);
+ Q_SIGNAL void itemCountChanged();
+
+ Q_PROPERTY(QVariantList colors READ colors NOTIFY dataChanged)
+ QVariantList colors() const;
+
+ int itemCount() const override;
+ QVariant item(int index) const override;
+ QVariant minimum() const override;
+ QVariant maximum() const override;
+
+private:
+ void regenerateColors();
+
+ QColor m_baseColor = Qt::blue;
+ int m_itemCount = 0;
+ QList<QColor> m_colors;
+};
+
+#endif // COLORGRADIENTSOURCE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "HistoryProxySource.h"
+
+#include <QDebug>
+
+HistoryProxySource::HistoryProxySource(QObject *parent)
+ : ChartDataSource(parent)
+{
+}
+
+int HistoryProxySource::itemCount() const
+{
+ if (m_fillMode == DoNotFill) {
+ return m_history.size();
+ } else {
+ return m_maximumHistory;
+ }
+}
+
+QVariant HistoryProxySource::item(int index) const
+{
+ if (index < 0 || !m_dataSource || m_dataSource->itemCount() == 0) {
+ return QVariant{};
+ }
+
+ if (m_fillMode == DoNotFill && index >= m_history.count()) {
+ return QVariant{};
+ }
+
+ if (m_fillMode == FillFromStart && index >= m_history.count()) {
+ return QVariant{QMetaType(m_dataSource->item(0).userType())};
+ }
+
+ if (m_fillMode == FillFromEnd && m_history.count() != m_maximumHistory) {
+ auto actualIndex = index - (m_maximumHistory - m_history.count());
+ if (actualIndex < 0 || actualIndex >= m_history.size()) {
+ return QVariant{QMetaType(m_dataSource->item(0).userType())};
+ } else {
+ return m_history.at(actualIndex);
+ }
+ }
+
+ if (index < m_history.count()) {
+ return m_history.at(index);
+ } else {
+ return QVariant{};
+ }
+}
+
+QVariant HistoryProxySource::minimum() const
+{
+ if (m_history.isEmpty() || !m_dataSource) {
+ return QVariant{};
+ }
+
+ // TODO: Find a nicer solution for data sources to indicate
+ // "I provide a min/max value not derived from my items"
+ auto model = m_dataSource->property("model").value<QObject *>();
+ if (model) {
+ auto minProperty = model->property("minimum");
+ auto maxProperty = model->property("maximum");
+ if (minProperty.isValid() && minProperty != maxProperty) {
+ return minProperty;
+ }
+ }
+
+ return *std::min_element(m_history.begin(), m_history.end(), variantCompare);
+}
+
+QVariant HistoryProxySource::maximum() const
+{
+ if (m_history.isEmpty() || !m_dataSource) {
+ return QVariant{};
+ }
+
+ auto model = m_dataSource->property("model").value<QObject *>();
+ if (model) {
+ auto minProperty = model->property("minimum");
+ auto maxProperty = model->property("maximum");
+ if (maxProperty.isValid() && maxProperty != minProperty) {
+ return maxProperty;
+ }
+ }
+
+ return *std::max_element(m_history.begin(), m_history.end(), variantCompare);
+}
+
+QVariant HistoryProxySource::first() const
+{
+ if (!m_history.isEmpty()) {
+ return m_history.first();
+ }
+ return QVariant{};
+}
+
+ChartDataSource *HistoryProxySource::source() const
+{
+ return m_dataSource;
+}
+
+void HistoryProxySource::setSource(ChartDataSource *newSource)
+{
+ if (newSource == m_dataSource) {
+ return;
+ }
+
+ if (m_dataSource) {
+ m_dataSource->disconnect(this);
+ }
+
+ m_dataSource = newSource;
+ clear();
+ if (m_dataSource) {
+ connect(m_dataSource, &ChartDataSource::dataChanged, this, [this]() {
+ if (!m_updateTimer) {
+ update();
+ }
+ });
+ }
+ Q_EMIT sourceChanged();
+}
+
+int HistoryProxySource::item() const
+{
+ return m_item;
+}
+
+void HistoryProxySource::setItem(int newItem)
+{
+ if (newItem == m_item) {
+ return;
+ }
+
+ m_item = newItem;
+ clear();
+ Q_EMIT itemChanged();
+}
+
+int HistoryProxySource::maximumHistory() const
+{
+ return m_maximumHistory;
+}
+
+void HistoryProxySource::setMaximumHistory(int newMaximumHistory)
+{
+ if (newMaximumHistory == m_maximumHistory) {
+ return;
+ }
+
+ m_maximumHistory = newMaximumHistory;
+ while (m_history.size() > 0 && m_history.size() > m_maximumHistory) {
+ m_history.removeLast();
+ }
+
+ Q_EMIT maximumHistoryChanged();
+}
+
+int HistoryProxySource::interval() const
+{
+ return m_updateTimer ? m_updateTimer->interval() : -1;
+}
+
+void HistoryProxySource::setInterval(int newInterval)
+{
+ if (m_updateTimer && newInterval == m_updateTimer->interval()) {
+ return;
+ }
+
+ if (newInterval > 0) {
+ if (!m_updateTimer) {
+ m_updateTimer = std::make_unique<QTimer>();
+ // We need precise timers to avoid missing updates when dealing with semi-constantly
+ // updating source. That is, if the source updates at 500ms and we also update at that
+ // rate, a drift of 2ms can cause us to miss updates.
+ m_updateTimer->setTimerType(Qt::PreciseTimer);
+ connect(m_updateTimer.get(), &QTimer::timeout, this, &HistoryProxySource::update);
+ }
+ m_updateTimer->setInterval(newInterval);
+ m_updateTimer->start();
+ } else {
+ m_updateTimer.reset();
+ }
+
+ Q_EMIT intervalChanged();
+}
+
+HistoryProxySource::FillMode HistoryProxySource::fillMode() const
+{
+ return m_fillMode;
+}
+
+void HistoryProxySource::setFillMode(FillMode newFillMode)
+{
+ if (newFillMode == m_fillMode) {
+ return;
+ }
+
+ m_fillMode = newFillMode;
+ clear();
+ Q_EMIT fillModeChanged();
+}
+
+void HistoryProxySource::clear()
+{
+ m_history.clear();
+ Q_EMIT dataChanged();
+}
+
+void HistoryProxySource::update()
+{
+ if (!m_dataSource) {
+ return;
+ }
+
+ m_history.prepend(m_dataSource->item(m_item));
+ while (m_history.size() > 0 && m_history.size() > m_maximumHistory) {
+ m_history.removeLast();
+ }
+
+ Q_EMIT dataChanged();
+}
+
+#include "moc_HistoryProxySource.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef HISTORYPROXYSOURCE_H
+#define HISTORYPROXYSOURCE_H
+
+#include <QList>
+#include <QTimer>
+#include <QVariant>
+#include <memory>
+
+#include "ChartDataSource.h"
+
+/**
+ * A data source that provides a history of a single item of a different data source.
+ *
+ * This data source will monitor a single item of another data source for changes
+ * and record them, exposing historical values as
+ */
+class QUICKCHARTS_EXPORT HistoryProxySource : public ChartDataSource
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ /**
+ * The different fill modes.
+ *
+ * These determine the value returned by \ref itemCount and what happens
+ * when requesting an item that does not have a value in the history.
+ */
+ enum FillMode {
+ /**
+ * Do not fill with any items. The source's \ref itemCount will be equal
+ * to the number of items in the history.
+ */
+ DoNotFill,
+ /**
+ * Fill with empty values, starting at 0. The source's \ref itemCount
+ * will be equal to \ref maximumHistory. Items that do not have a value
+ * in the history will return a default-constructed value based on the
+ * first item of the source.
+ */
+ FillFromStart,
+ /**
+ * Fill with empty values, placing partial history at the end. This
+ * means that the first recorded history item will be shown at position
+ * `maximumHistory - 1`, the second at `maximumHistory - 2` and so on,
+ * until \ref maximumHistory is reached, after which items will be
+ * discarded normally.
+ */
+ FillFromEnd
+ };
+ Q_ENUM(FillMode)
+
+ explicit HistoryProxySource(QObject *parent = nullptr);
+
+ /**
+ * The data source to read data from.
+ *
+ * This will use the item at \ref item from the provided source and use that
+ * to fill the history of this source.
+ *
+ * \note Changing this property will clear the existing history.
+ */
+ Q_PROPERTY(ChartDataSource *source READ source WRITE setSource NOTIFY sourceChanged)
+ ChartDataSource *source() const;
+ void setSource(ChartDataSource *newSource);
+ Q_SIGNAL void sourceChanged();
+ /**
+ * The item of the data source to read data from.
+ *
+ * This item will be read either when the source's dataChanged has been
+ * emitted or on an interval if \ref interval has been set.
+ *
+ * The default is 0.
+ *
+ * \note Changing this property will clear the existing history.
+ */
+ Q_PROPERTY(int item READ item WRITE setItem NOTIFY itemChanged)
+ int item() const;
+ void setItem(int newItem);
+ Q_SIGNAL void itemChanged();
+ /**
+ * The maximum amount of history to keep.
+ *
+ * The default is 10.
+ */
+ Q_PROPERTY(int maximumHistory READ maximumHistory WRITE setMaximumHistory NOTIFY maximumHistoryChanged)
+ int maximumHistory() const;
+ void setMaximumHistory(int maximumHistory);
+ Q_SIGNAL void maximumHistoryChanged();
+ /**
+ * The interval, in milliseconds, with which to query the data source.
+ *
+ * If set to a value <= 0, a new item will be added whenever \ref source
+ * changes. Otherwise, source will be sampled every interval milliseconds
+ * and a new item will be added with whatever value it has at that point,
+ * even if it did not change.
+ *
+ * The default is 0.
+ */
+ Q_PROPERTY(int interval READ interval WRITE setInterval NOTIFY intervalChanged)
+ int interval() const;
+ void setInterval(int newInterval);
+ Q_SIGNAL void intervalChanged();
+ /**
+ * The fill mode.
+ *
+ * This determines what happens when there is not enough history yet. See
+ * \ref FillMode for which modes are available.
+ *
+ * The default is DoNotFill.
+ *
+ * \note Changing this property will clear the existing history.
+ */
+ Q_PROPERTY(FillMode fillMode READ fillMode WRITE setFillMode NOTIFY fillModeChanged)
+ FillMode fillMode() const;
+ void setFillMode(FillMode newFillMode);
+ Q_SIGNAL void fillModeChanged();
+
+ /**
+ * Clear the entire history of this source.
+ */
+ Q_INVOKABLE void clear();
+
+ int itemCount() const override;
+ QVariant item(int index) const override;
+ QVariant minimum() const override;
+ QVariant maximum() const override;
+ QVariant first() const override;
+
+private:
+ void update();
+
+ ChartDataSource *m_dataSource = nullptr;
+ int m_item = 0;
+ int m_maximumHistory = 10;
+ FillMode m_fillMode = DoNotFill;
+ std::unique_ptr<QTimer> m_updateTimer;
+ QList<QVariant> m_history;
+};
+
+#endif // HISTORYPROXYSOURCE_H
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "MapProxySource.h"
+
+MapProxySource::MapProxySource(QObject *parent)
+ : ChartDataSource(parent)
+{
+ connect(this, &MapProxySource::sourceChanged, this, &ChartDataSource::dataChanged);
+ connect(this, &MapProxySource::mapChanged, this, &ChartDataSource::dataChanged);
+}
+
+int MapProxySource::itemCount() const
+{
+ if (m_source) {
+ return m_source->itemCount();
+ }
+
+ return 0;
+}
+
+QVariant MapProxySource::minimum() const
+{
+ auto itr = std::min_element(m_map.cbegin(), m_map.cend(), variantCompare);
+ if (itr != m_map.cend()) {
+ return *itr;
+ }
+ return QVariant{};
+}
+
+QVariant MapProxySource::maximum() const
+{
+ auto itr = std::max_element(m_map.cbegin(), m_map.cend(), variantCompare);
+ if (itr != m_map.cend()) {
+ return *itr;
+ }
+ return QVariant{};
+}
+
+QVariant MapProxySource::item(int index) const
+{
+ if (!m_source) {
+ return QVariant{};
+ }
+
+ auto mapIndex = m_source->item(index).toString();
+ if (mapIndex.isEmpty()) {
+ return QVariant{};
+ }
+
+ return m_map.value(mapIndex);
+}
+
+ChartDataSource *MapProxySource::source() const
+{
+ return m_source;
+}
+
+void MapProxySource::setSource(ChartDataSource *newSource)
+{
+ if (newSource == m_source) {
+ return;
+ }
+
+ if (m_source) {
+ m_source->disconnect(this);
+ }
+
+ m_source = newSource;
+ if (m_source) {
+ connect(m_source, &ChartDataSource::dataChanged, this, &ChartDataSource::dataChanged);
+ }
+ Q_EMIT sourceChanged();
+}
+
+QVariantMap MapProxySource::map() const
+{
+ return m_map;
+}
+
+void MapProxySource::setMap(const QVariantMap &newMap)
+{
+ if (newMap == m_map) {
+ return;
+ }
+
+ m_map = newMap;
+
+ Q_EMIT mapChanged();
+}
+
+#include "moc_MapProxySource.cpp"
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#pragma once
+
+#include "ChartDataSource.h"
+
+#include <QVariant>
+
+/**
+ * A source that uses the values of another source to produce values from a map.
+ *
+ * This source reads values from another source, then uses those as an index to
+ * a map of different values and returns the appropriate value from that map.
+ * This source's itemCount matches that of the other source.
+ *
+ * @since 5.71
+ */
+class QUICKCHARTS_EXPORT MapProxySource : public ChartDataSource
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ MapProxySource(QObject *parent = nullptr);
+
+ /**
+ * A ChartDataSource that is used as map indexes.
+ */
+ Q_PROPERTY(ChartDataSource *source READ source WRITE setSource NOTIFY sourceChanged)
+ ChartDataSource *source() const;
+ void setSource(ChartDataSource *newSource);
+ Q_SIGNAL void sourceChanged();
+
+ /**
+ * The map to index for values.
+ */
+ Q_PROPERTY(QVariantMap map READ map WRITE setMap NOTIFY mapChanged)
+ QVariantMap map() const;
+ void setMap(const QVariantMap &newMap);
+ Q_SIGNAL void mapChanged();
+
+ int itemCount() const override;
+ QVariant item(int index) const override;
+ QVariant minimum() const override;
+ QVariant maximum() const override;
+
+private:
+ ChartDataSource *m_source = nullptr;
+ QVariantMap m_map;
+};
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "ModelSource.h"
+
+#include <QMetaProperty>
+
+#include "charts_datasource_logging.h"
+
+ModelSource::ModelSource(QObject *parent)
+ : ChartDataSource(parent)
+{
+ connect(this, &ModelSource::modelChanged, this, &ModelSource::dataChanged);
+ connect(this, &ModelSource::columnChanged, this, &ModelSource::dataChanged);
+ connect(this, &ModelSource::roleChanged, this, &ModelSource::dataChanged);
+ connect(this, &ModelSource::indexColumnsChanged, this, &ModelSource::dataChanged);
+}
+
+int ModelSource::role() const
+{
+ if (!m_model) {
+ return -1;
+ }
+
+ if (m_role < 0 && !m_roleName.isEmpty()) {
+ m_role = m_model->roleNames().key(m_roleName.toLatin1(), -1);
+ }
+
+ return m_role;
+}
+
+QString ModelSource::roleName() const
+{
+ return m_roleName;
+}
+
+int ModelSource::column() const
+{
+ return m_column;
+}
+
+QAbstractItemModel *ModelSource::model() const
+{
+ return m_model;
+}
+
+bool ModelSource::indexColumns() const
+{
+ return m_indexColumns;
+}
+
+int ModelSource::itemCount() const
+{
+ if (!m_model) {
+ return 0;
+ }
+
+ return m_indexColumns ? m_model->columnCount() : m_model->rowCount();
+}
+
+QVariant ModelSource::item(int index) const
+{
+ if (!m_model) {
+ return {};
+ }
+
+ // For certain model (QML ListModel for example), the roleNames() are more
+ // dynamic and may only be valid when this method gets called. So try and
+ // lookup the role first before anything else.
+ if (m_role < 0) {
+ if (m_roleName.isEmpty()) {
+ return QVariant{};
+ }
+
+ m_role = m_model->roleNames().key(m_roleName.toLatin1(), -1);
+ if (m_role < 0) {
+ qCWarning(DATASOURCE) << "ModelSource: Invalid role " << m_role << m_roleName;
+ return QVariant{};
+ }
+ }
+
+ if (!m_indexColumns && (m_column < 0 || m_column > m_model->columnCount())) {
+ qCDebug(DATASOURCE) << "ModelSource: Invalid column" << m_column;
+ return QVariant{};
+ }
+
+ auto modelIndex = m_indexColumns ? m_model->index(0, index) : m_model->index(index, m_column);
+ if (modelIndex.isValid()) {
+ return m_model->data(modelIndex, m_role);
+ }
+
+ return QVariant{};
+}
+
+QVariant ModelSource::minimum() const
+{
+ if (!m_model || itemCount() <= 0) {
+ return {};
+ }
+
+ if (m_minimum.isValid()) {
+ return m_minimum;
+ }
+
+ auto minProperty = m_model->property("minimum");
+ auto maxProperty = m_model->property("maximum");
+ if (minProperty.isValid() && minProperty != maxProperty) {
+ return minProperty;
+ }
+
+ QVariant result = std::numeric_limits<float>::max();
+ for (int i = 0; i < itemCount(); ++i) {
+ result = std::min(result, item(i), variantCompare);
+ }
+ return result;
+}
+
+QVariant ModelSource::maximum() const
+{
+ if (!m_model || itemCount() <= 0) {
+ return {};
+ }
+
+ if (m_maximum.isValid()) {
+ return m_maximum;
+ }
+
+ auto minProperty = m_model->property("minimum");
+ auto maxProperty = m_model->property("maximum");
+ if (maxProperty.isValid() && maxProperty != minProperty) {
+ return maxProperty;
+ }
+
+ QVariant result = std::numeric_limits<float>::min();
+ for (int i = 0; i < itemCount(); ++i) {
+ result = std::max(result, item(i), variantCompare);
+ }
+ return result;
+}
+
+void ModelSource::setRole(int role)
+{
+ if (role == m_role) {
+ return;
+ }
+
+ m_role = role;
+ if (m_model) {
+ m_roleName = QString::fromLatin1(m_model->roleNames().value(role));
+ Q_EMIT roleNameChanged();
+ }
+ Q_EMIT roleChanged();
+}
+
+void ModelSource::setRoleName(const QString &name)
+{
+ if (name == m_roleName) {
+ return;
+ }
+
+ m_roleName = name;
+ if (m_model) {
+ m_role = m_model->roleNames().key(m_roleName.toLatin1(), -1);
+ Q_EMIT roleChanged();
+ }
+ Q_EMIT roleNameChanged();
+}
+
+void ModelSource::setColumn(int column)
+{
+ if (column == m_column) {
+ return;
+ }
+
+ m_column = column;
+ Q_EMIT columnChanged();
+}
+
+void ModelSource::setIndexColumns(bool index)
+{
+ if (index == m_indexColumns) {
+ return;
+ }
+
+ m_indexColumns = index;
+ Q_EMIT indexColumnsChanged();
+}
+
+void ModelSource::setModel(QAbstractItemModel *model)
+{
+ if (m_model == model) {
+ return;
+ }
+
+ if (m_model) {
+ m_model->disconnect(this);
+ m_minimum = QVariant{};
+ m_maximum = QVariant{};
+ }
+
+ m_model = model;
+ if (m_model) {
+ connect(m_model, &QAbstractItemModel::rowsInserted, this, &ModelSource::dataChanged);
+ connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ModelSource::dataChanged);
+ connect(m_model, &QAbstractItemModel::rowsMoved, this, &ModelSource::dataChanged);
+ connect(m_model, &QAbstractItemModel::modelReset, this, &ModelSource::dataChanged);
+ connect(m_model, &QAbstractItemModel::dataChanged, this, &ModelSource::dataChanged);
+ connect(m_model, &QAbstractItemModel::layoutChanged, this, &ModelSource::dataChanged);
+
+ connect(m_model, &QAbstractItemModel::destroyed, this, [this]() {
+ m_minimum = QVariant{};
+ m_maximum = QVariant{};
+ m_model = nullptr;
+ });
+
+ auto minimumIndex = m_model->metaObject()->indexOfProperty("minimum");
+ if (minimumIndex != -1) {
+ auto minimum = m_model->metaObject()->property(minimumIndex);
+ if (minimum.hasNotifySignal()) {
+ auto slot = metaObject()->method(metaObject()->indexOfSlot("onMinimumChanged()"));
+ connect(m_model, minimum.notifySignal(), this, slot);
+ m_minimum = minimum.read(m_model);
+ }
+ }
+
+ auto maximumIndex = m_model->metaObject()->indexOfProperty("maximum");
+ if (maximumIndex != -1) {
+ auto maximum = m_model->metaObject()->property(maximumIndex);
+ if (maximum.hasNotifySignal()) {
+ auto slot = metaObject()->method(metaObject()->indexOfSlot("onMaximumChanged()"));
+ connect(m_model, maximum.notifySignal(), this, slot);
+ m_maximum = maximum.read(m_model);
+ }
+ }
+ }
+
+ Q_EMIT modelChanged();
+}
+
+void ModelSource::onMinimumChanged()
+{
+ auto newMinimum = m_model->property("minimum");
+ if (newMinimum.isValid() && newMinimum != m_minimum) {
+ m_minimum = newMinimum;
+ }
+}
+
+void ModelSource::onMaximumChanged()
+{
+ auto newMaximum = m_model->property("maximum");
+ if (newMaximum.isValid() && newMaximum != m_maximum) {
+ m_maximum = newMaximum;
+ }
+}
+
+#include "moc_ModelSource.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef MODELSOURCE_H
+#define MODELSOURCE_H
+
+#include <QAbstractItemModel>
+#include <QPointer>
+
+#include "ChartDataSource.h"
+
+/**
+ * A data source that reads data from a QAbstractItemModel.
+ *
+ *
+ */
+class QUICKCHARTS_EXPORT ModelSource : public ChartDataSource
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ explicit ModelSource(QObject *parent = nullptr);
+
+ Q_PROPERTY(int role READ role WRITE setRole NOTIFY roleChanged)
+ int role() const;
+ void setRole(int role);
+ Q_SIGNAL void roleChanged();
+
+ Q_PROPERTY(QString roleName READ roleName WRITE setRoleName NOTIFY roleNameChanged)
+ QString roleName() const;
+ void setRoleName(const QString &name);
+ Q_SIGNAL void roleNameChanged();
+
+ Q_PROPERTY(int column READ column WRITE setColumn NOTIFY columnChanged)
+ int column() const;
+ void setColumn(int column);
+ Q_SIGNAL void columnChanged();
+
+ Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged)
+ QAbstractItemModel *model() const;
+ void setModel(QAbstractItemModel *model);
+ Q_SIGNAL void modelChanged();
+
+ Q_PROPERTY(bool indexColumns READ indexColumns WRITE setIndexColumns NOTIFY indexColumnsChanged)
+ bool indexColumns() const;
+ void setIndexColumns(bool index);
+ Q_SIGNAL void indexColumnsChanged();
+
+ int itemCount() const override;
+ QVariant item(int index) const override;
+ QVariant minimum() const override;
+ QVariant maximum() const override;
+
+private:
+ Q_SLOT void onMinimumChanged();
+ Q_SLOT void onMaximumChanged();
+
+ mutable int m_role = -1;
+ QString m_roleName;
+ int m_column = 0;
+ bool m_indexColumns = false;
+ QAbstractItemModel *m_model = nullptr;
+
+ QVariant m_minimum;
+ QVariant m_maximum;
+};
+
+#endif // MODELSOURCE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "SingleValueSource.h"
+
+SingleValueSource::SingleValueSource(QObject *parent)
+ : ChartDataSource(parent)
+{
+}
+
+QVariant SingleValueSource::item(int index) const
+{
+ Q_UNUSED(index);
+ return m_value;
+}
+
+int SingleValueSource::itemCount() const
+{
+ return 1;
+}
+
+QVariant SingleValueSource::minimum() const
+{
+ return m_value;
+}
+
+QVariant SingleValueSource::maximum() const
+{
+ return m_value;
+}
+
+QVariant SingleValueSource::value() const
+{
+ return m_value;
+}
+
+void SingleValueSource::setValue(const QVariant &value)
+{
+ if (m_value == value) {
+ return;
+ }
+
+ m_value = value;
+ Q_EMIT dataChanged();
+}
+
+#include "moc_SingleValueSource.cpp"
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef SINGLEVALUESOURCE_H
+#define SINGLEVALUESOURCE_H
+
+#include <QVariant>
+
+#include "ChartDataSource.h"
+
+/**
+ * A data source that provides a single value as data.
+ */
+class QUICKCHARTS_EXPORT SingleValueSource : public ChartDataSource
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ explicit SingleValueSource(QObject *parent = nullptr);
+
+ int itemCount() const override;
+ QVariant item(int index) const override;
+ QVariant minimum() const override;
+ QVariant maximum() const override;
+
+ Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY dataChanged)
+ QVariant value() const;
+ void setValue(const QVariant &value);
+
+private:
+ QVariant m_value;
+};
+
+#endif // SINGLEVALUESOURCE_H
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "BarChartMaterial.h"
+
+BarChartMaterial::BarChartMaterial()
+{
+ setFlag(QSGMaterial::Blending);
+}
+
+BarChartMaterial::~BarChartMaterial()
+{
+}
+
+QSGMaterialType *BarChartMaterial::type() const
+{
+ static QSGMaterialType type;
+ return &type;
+}
+
+QSGMaterialShader *BarChartMaterial::createShader(QSGRendererInterface::RenderMode) const
+{
+ return new BarChartShader();
+}
+
+int BarChartMaterial::compare(const QSGMaterial *other) const
+{
+ auto material = static_cast<const BarChartMaterial *>(other);
+
+ /* clang-format off */
+ if (material->aspect == aspect
+ && qFuzzyCompare(material->radius, radius)
+ && material->backgroundColor == backgroundColor) { /* clang-format on */
+ return 0;
+ }
+
+ return QSGMaterial::compare(other);
+}
+
+BarChartShader::BarChartShader()
+{
+ setShaders(QStringLiteral("barchart.vert"), QStringLiteral("barchart.frag"));
+}
+
+BarChartShader::~BarChartShader()
+{
+}
+
+bool BarChartShader::updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
+{
+ bool changed = false;
+
+ UniformDataStream uniformData(state);
+
+ if (state.isMatrixDirty()) {
+ uniformData << state.combinedMatrix();
+ changed = true;
+ } else {
+ uniformData.skip<QMatrix4x4>();
+ }
+
+ if (state.isOpacityDirty()) {
+ uniformData << state.opacity();
+ changed = true;
+ } else {
+ uniformData.skip<float>();
+ }
+
+ if (!oldMaterial || newMaterial->compare(oldMaterial) != 0) {
+ const auto material = static_cast<BarChartMaterial *>(newMaterial);
+ uniformData << material->aspect;
+ uniformData << material->radius;
+ uniformData << material->backgroundColor;
+ changed = true;
+ }
+
+ return changed;
+}
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef PIECHARTMATERIAL_H
+#define PIECHARTMATERIAL_H
+
+#include <QColor>
+#include <QSGMaterial>
+#include <QSGMaterialShader>
+
+#include "SDFShader.h"
+
+class BarChartMaterial : public QSGMaterial
+{
+public:
+ BarChartMaterial();
+ ~BarChartMaterial();
+
+ QSGMaterialType *type() const override;
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override;
+ int compare(const QSGMaterial *other) const override;
+
+ QVector2D aspect = QVector2D{1.0, 1.0};
+ float radius = 0.0;
+ QColor backgroundColor = Qt::transparent;
+};
+
+class BarChartShader : public SDFShader
+{
+public:
+ BarChartShader();
+ ~BarChartShader();
+
+ bool updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
+};
+
+#endif // PIECHARTMATERIAL_H
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "BarChartNode.h"
+
+#include <QColor>
+#include <QDebug>
+
+#include "BarChartMaterial.h"
+
+struct BarVertex {
+ float x;
+ float y;
+
+ float u;
+ float v;
+
+ float r;
+ float g;
+ float b;
+ float a;
+
+ float value;
+
+ void set(const QPointF &position, const QVector2D &uv, const QColor &color, float newValue)
+ {
+ x = position.x();
+ y = position.y();
+ u = uv.x();
+ v = uv.y();
+ r = color.redF();
+ g = color.greenF();
+ b = color.blueF();
+ a = color.alphaF();
+ value = newValue;
+ }
+};
+
+/* clang-format off */
+QSGGeometry::Attribute BarAttributes[] = {
+ QSGGeometry::Attribute::create(0, 2, QSGGeometry::FloatType, true),
+ QSGGeometry::Attribute::create(1, 2, QSGGeometry::FloatType, false),
+ QSGGeometry::Attribute::create(2, 4, QSGGeometry::FloatType, false),
+ QSGGeometry::Attribute::create(3, 1, QSGGeometry::FloatType, false)
+};
+/* clang-format on */
+
+QSGGeometry::AttributeSet BarAttributeSet = {4, sizeof(BarVertex), BarAttributes};
+
+void updateBarGeometry(QSGGeometry *geometry, const QRectF &rect, const QColor &color, float value)
+{
+ auto vertices = static_cast<BarVertex *>(geometry->vertexData());
+ vertices[0].set(rect.topLeft(), {0.0, 0.0}, color, value);
+ vertices[1].set(rect.bottomLeft(), {0.0, 1.0}, color, value);
+ vertices[2].set(rect.topRight(), {1.0, 0.0}, color, value);
+ vertices[3].set(rect.bottomRight(), {1.0, 1.0}, color, value);
+ geometry->markVertexDataDirty();
+}
+
+class BarNode : public QSGGeometryNode
+{
+public:
+ BarNode(const QRectF &r)
+ {
+ geometry = new QSGGeometry(BarAttributeSet, 4);
+ geometry->setVertexDataPattern(QSGGeometry::DynamicPattern);
+ updateBarGeometry(geometry, r, Qt::transparent, 0.0);
+ setGeometry(geometry);
+
+ rect = r;
+
+ material = new BarChartMaterial{};
+ setMaterial(material);
+
+ setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial);
+ }
+
+ void update()
+ {
+ auto minSize = std::min(rect.width(), rect.height());
+ auto aspect = rect.height() / minSize;
+ updateBarGeometry(geometry, rect, color, value * aspect);
+
+ markDirty(QSGNode::DirtyGeometry);
+ }
+
+ QSGGeometry *geometry;
+ BarChartMaterial *material;
+ QRectF rect;
+ QColor color;
+ float value;
+};
+
+BarChartNode::BarChartNode()
+{
+}
+
+void BarChartNode::setRect(const QRectF &rect)
+{
+ m_rect = rect;
+}
+
+void BarChartNode::setBars(const QList<Bar> &bars)
+{
+ m_bars = bars;
+}
+
+void BarChartNode::setRadius(qreal radius)
+{
+ m_radius = radius;
+}
+
+void BarChartNode::setBackgroundColor(const QColor &color)
+{
+ m_backgroundColor = color;
+}
+
+void BarChartNode::update()
+{
+ if (!m_rect.isValid() || m_bars.isEmpty()) {
+ return;
+ }
+
+ for (auto index = 0; index < m_bars.count(); ++index) {
+ auto entry = m_bars.at(index);
+
+ auto rect = QRectF{QPointF{entry.x, m_rect.top()}, QSizeF{entry.width, m_rect.height()}};
+
+ if (childCount() <= index) {
+ appendChildNode(new BarNode{rect});
+ }
+
+ auto child = static_cast<BarNode *>(childAtIndex(index));
+
+ auto minSize = std::min(rect.width(), rect.height());
+ auto aspect = QVector2D{float(rect.width() / minSize), float(rect.height() / minSize)};
+
+ if (aspect != child->material->aspect) {
+ child->material->aspect = aspect;
+ child->markDirty(QSGNode::DirtyMaterial);
+ }
+
+ float correctedRadius = (std::min(m_radius, entry.width / 2.0) / minSize) * 2.0;
+ if (!qFuzzyCompare(correctedRadius, child->material->radius)) {
+ child->material->radius = correctedRadius;
+ child->markDirty(QSGNode::DirtyMaterial);
+ }
+
+ if (m_backgroundColor != child->material->backgroundColor) {
+ child->material->backgroundColor = m_backgroundColor;
+ child->markDirty(QSGNode::DirtyMaterial);
+ }
+
+ if (child->rect != rect || !qFuzzyCompare(child->value, entry.value) || child->color != entry.color) {
+ child->rect = rect;
+ child->value = entry.value;
+ child->color = entry.color;
+ child->update();
+ }
+ }
+
+ while (childCount() > m_bars.count()) {
+ auto child = childAtIndex(childCount() - 1);
+ removeChildNode(child);
+ delete child;
+ }
+}
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef BARCHARTNODE_H
+#define BARCHARTNODE_H
+
+#include <QColor>
+#include <QSGGeometryNode>
+
+struct Bar {
+ float x;
+ float width;
+ float value;
+ QColor color;
+};
+
+class BarChartNode : public QSGNode
+{
+public:
+ BarChartNode();
+
+ void setRect(const QRectF &rect);
+ void setBars(const QList<Bar> &bars);
+ void setRadius(qreal radius);
+ void setBackgroundColor(const QColor &color);
+ void update();
+
+private:
+ QRectF m_rect;
+ QList<Bar> m_bars;
+ qreal m_radius = 0.0;
+ QColor m_backgroundColor = Qt::transparent;
+};
+
+#endif // BARCHARTNODE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "LineChartMaterial.h"
+
+LineChartMaterial::LineChartMaterial()
+{
+ setFlag(QSGMaterial::Blending);
+}
+
+LineChartMaterial::~LineChartMaterial()
+{
+}
+
+QSGMaterialType *LineChartMaterial::type() const
+{
+ static QSGMaterialType type;
+ return &type;
+}
+
+QSGMaterialShader *LineChartMaterial::createShader(QSGRendererInterface::RenderMode) const
+{
+ return new LineChartShader();
+}
+
+int LineChartMaterial::compare(const QSGMaterial *other) const
+{
+ auto material = static_cast<const LineChartMaterial *>(other);
+
+ /* clang-format off */
+ if (qFuzzyCompare(material->aspect, aspect)
+ && qFuzzyCompare(material->lineWidth, lineWidth)
+ && qFuzzyCompare(material->smoothing, smoothing)) { /* clang-format on */
+ return 0;
+ }
+
+ return QSGMaterial::compare(other);
+}
+
+LineChartShader::LineChartShader()
+{
+ setShaders(QStringLiteral("linechart.vert"), QStringLiteral("linechart.frag"));
+}
+
+LineChartShader::~LineChartShader()
+{
+}
+
+bool LineChartShader::updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
+{
+ bool changed = false;
+ UniformDataStream uniformData(state);
+
+ if (state.isMatrixDirty()) {
+ uniformData << state.combinedMatrix();
+ changed = true;
+ } else {
+ uniformData.skip<QMatrix4x4>();
+ }
+
+ if (state.isOpacityDirty()) {
+ uniformData << state.opacity();
+ changed = true;
+ } else {
+ uniformData.skip<float>();
+ }
+
+ if (!oldMaterial || newMaterial->compare(oldMaterial) != 0) {
+ const auto material = static_cast<LineChartMaterial *>(newMaterial);
+ uniformData << material->lineWidth;
+ uniformData << material->aspect;
+ uniformData << material->smoothing;
+ changed = true;
+ }
+
+ return changed;
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef LINECHARTMATERIAL_H
+#define LINECHARTMATERIAL_H
+
+#include <QColor>
+#include <QSGMaterial>
+#include <QSGMaterialShader>
+
+#include "SDFShader.h"
+
+class LineChartMaterial : public QSGMaterial
+{
+public:
+ LineChartMaterial();
+ ~LineChartMaterial();
+
+ QSGMaterialType *type() const override;
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override;
+ int compare(const QSGMaterial *other) const override;
+
+ float aspect = 1.0;
+ float lineWidth = 0.0;
+ float smoothing = 0.1;
+};
+
+class LineChartShader : public SDFShader
+{
+public:
+ LineChartShader();
+ ~LineChartShader();
+
+ bool updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
+};
+
+#endif // LINECHARTMATERIAL_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "LineChartNode.h"
+
+#include <QtMath>
+
+#include "LineChartMaterial.h"
+#include "LineSegmentNode.h"
+
+static const int MaxPointsInSegment = 6;
+
+qreal calculateNormalizedLineWidth(qreal pixelWidth, const QRectF &rect)
+{
+ if (qFuzzyIsNull(pixelWidth)) {
+ return 0.0;
+ }
+
+ // This needs to account for both height and vertical aspect, resulting in
+ // a formula similar to `height / (height / width)` which funnily enough
+ // simplifies to just width.
+ return pixelWidth * 0.5 / rect.width();
+}
+
+LineChartNode::LineChartNode()
+{
+}
+
+LineChartNode::~LineChartNode()
+{
+}
+
+void LineChartNode::setRect(const QRectF &rect, qreal devicePixelRatio)
+{
+ if (rect == m_rect) {
+ return;
+ }
+
+ m_rect = rect;
+ m_aspect = m_rect.height() / m_rect.width();
+
+ auto nativeSize = QSizeF(m_rect.width() * devicePixelRatio, m_rect.height() * devicePixelRatio);
+ auto diagonal = std::sqrt(nativeSize.width() * nativeSize.width() + nativeSize.height() * nativeSize.height());
+ m_smoothing = 1.0 / diagonal;
+}
+
+void LineChartNode::setLineWidth(float width)
+{
+ if (qFuzzyCompare(width, m_lineWidth)) {
+ return;
+ }
+
+ m_lineWidth = width;
+}
+
+void LineChartNode::setLineColor(const QColor &color)
+{
+ if (m_lineColor == color) {
+ return;
+ }
+
+ m_lineColor = color;
+}
+
+void LineChartNode::setFillColor(const QColor &color)
+{
+ if (m_fillColor == color) {
+ return;
+ }
+
+ m_fillColor = color;
+}
+
+void LineChartNode::setValues(const QList<QVector2D> &values)
+{
+ m_values = values;
+}
+
+void LineChartNode::updatePoints()
+{
+ if (m_values.isEmpty()) {
+ return;
+ }
+
+ auto segmentCount = qCeil(qreal(m_values.count()) / MaxPointsInSegment);
+
+ auto currentX = m_rect.left();
+ auto pointStart = 0;
+ auto pointsPerSegment = MaxPointsInSegment;
+
+ for (int i = 0; i < segmentCount; ++i) {
+ if (i >= childCount()) {
+ appendChildNode(new LineSegmentNode{});
+ }
+
+ auto segment = static_cast<LineSegmentNode *>(childAtIndex(i));
+
+ auto segmentPoints = m_values.mid(pointStart, pointsPerSegment);
+ pointStart += pointsPerSegment;
+
+ auto segmentWidth = segmentPoints.last().x() - currentX;
+ auto rect = QRectF(currentX, m_rect.top(), segmentWidth, m_rect.height());
+
+ segment->setRect(rect);
+ segment->setAspect(segmentWidth / m_rect.width(), m_aspect);
+ segment->setSmoothing(m_smoothing);
+ segment->setLineWidth(calculateNormalizedLineWidth(m_lineWidth, m_rect));
+ segment->setLineColor(m_lineColor);
+ segment->setFillColor(m_fillColor);
+ segment->setValues(segmentPoints);
+ segment->setFarLeft(m_values.at(std::max(0, pointStart - pointsPerSegment - 1)));
+ segment->setFarRight(m_values.at(std::min<int>(m_values.count() - 1, pointStart + 1)));
+ segment->update();
+
+ currentX += segmentWidth;
+ }
+
+ while (childCount() > segmentCount) {
+ auto child = childAtIndex(childCount() - 1);
+ removeChildNode(child);
+ delete child;
+ }
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef LINECHARTNODE_H
+#define LINECHARTNODE_H
+
+#include <QColor>
+#include <QSGNode>
+
+class QRectF;
+class LineChartMaterial;
+class LineSegmentNode;
+
+/**
+ * @todo write docs
+ */
+class LineChartNode : public QSGNode
+{
+public:
+ LineChartNode();
+
+ /**
+ * Destructor
+ */
+ ~LineChartNode();
+
+ void setRect(const QRectF &rect, qreal devicePixelRatio);
+ void setLineWidth(float width);
+ void setLineColor(const QColor &color);
+ void setFillColor(const QColor &color);
+ void setValues(const QList<QVector2D> &values);
+ void updatePoints();
+
+private:
+ QRectF m_rect;
+ float m_lineWidth = 0.0;
+ float m_aspect = 1.0;
+ float m_smoothing = 0.1;
+ QColor m_lineColor;
+ QColor m_fillColor;
+ QList<QVector2D> m_values;
+};
+
+#endif // LINECHARTNODE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "LineSegmentNode.h"
+
+#include <QSGGeometry>
+
+#include "LineChartMaterial.h"
+
+constexpr int MaxPointsSize = (6 + 8) * 2;
+
+struct LineVertex {
+ float position[2];
+
+ float uv[2];
+
+ float lineColor[4];
+ float fillColor[4];
+
+ float bounds[2];
+
+ float pointCount;
+ float points[MaxPointsSize];
+
+ void set(const QPointF &newPosition,
+ const QPointF &newUv,
+ const QList<QVector2D> &newPoints,
+ const QColor &newLineColor,
+ const QColor &newFillColor,
+ const QVector2D &newBounds)
+ {
+ position[0] = newPosition.x();
+ position[1] = newPosition.y();
+
+ uv[0] = newUv.x();
+ uv[1] = newUv.y();
+
+ lineColor[0] = newLineColor.redF();
+ lineColor[1] = newLineColor.greenF();
+ lineColor[2] = newLineColor.blueF();
+ lineColor[3] = newLineColor.alphaF();
+
+ fillColor[0] = newFillColor.redF();
+ fillColor[1] = newFillColor.greenF();
+ fillColor[2] = newFillColor.blueF();
+ fillColor[3] = newFillColor.alphaF();
+
+ bounds[0] = newBounds.x();
+ bounds[1] = newBounds.y();
+
+ setPoints(newPoints);
+ }
+
+ void setPoints(const QList<QVector2D> &newPoints)
+ {
+ memset(points, 0, MaxPointsSize * sizeof(float));
+
+ Q_ASSERT_X(newPoints.size() <= (MaxPointsSize / 2),
+ "LineVertex::setPoints",
+ qPrintable(QStringLiteral("Too many points in new points array: %1").arg(newPoints.size())));
+
+ for (int i = 0; i < newPoints.size(); ++i) {
+ points[i * 2 + 0] = newPoints[i].x();
+ points[i * 2 + 1] = newPoints[i].y();
+ }
+
+ pointCount = newPoints.size();
+ }
+};
+
+/* clang-format off */
+QSGGeometry::Attribute LineAttributes[] = {
+ QSGGeometry::Attribute::createWithAttributeType(0, 2, QSGGeometry::FloatType, QSGGeometry::PositionAttribute), // in_position
+ QSGGeometry::Attribute::createWithAttributeType(1, 2, QSGGeometry::FloatType, QSGGeometry::TexCoordAttribute), // in_uv
+
+ QSGGeometry::Attribute::createWithAttributeType(2, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_lineColor
+ QSGGeometry::Attribute::createWithAttributeType(3, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_fillColor
+
+ QSGGeometry::Attribute::createWithAttributeType(4, 2, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_bounds
+
+ QSGGeometry::Attribute::createWithAttributeType(5, 1, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_count
+
+ QSGGeometry::Attribute::createWithAttributeType(8, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_points_0
+ QSGGeometry::Attribute::createWithAttributeType(9, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_points_1
+ QSGGeometry::Attribute::createWithAttributeType(10, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_points_2
+ QSGGeometry::Attribute::createWithAttributeType(11, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_points_3
+ QSGGeometry::Attribute::createWithAttributeType(12, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_points_4
+ QSGGeometry::Attribute::createWithAttributeType(13, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_points_5
+ QSGGeometry::Attribute::createWithAttributeType(14, 4, QSGGeometry::FloatType, QSGGeometry::UnknownAttribute), // in_points_6
+};
+/* clang-format on */
+
+QSGGeometry::AttributeSet LineAttributeSet = {13, sizeof(LineVertex), LineAttributes};
+
+void updateLineGeometry(QSGGeometry *geometry,
+ const QRectF &rect,
+ const QRectF &uvRect,
+ const QList<QVector2D> &points,
+ const QColor &lineColor,
+ const QColor &fillColor,
+ const QVector2D &bounds)
+{
+ auto vertices = static_cast<LineVertex *>(geometry->vertexData());
+ vertices[0].set(rect.topLeft(), uvRect.topLeft(), points, lineColor, fillColor, bounds);
+ vertices[1].set(rect.bottomLeft(), uvRect.bottomLeft(), points, lineColor, fillColor, bounds);
+ vertices[2].set(rect.topRight(), uvRect.topRight(), points, lineColor, fillColor, bounds);
+ vertices[3].set(rect.bottomRight(), uvRect.bottomRight(), points, lineColor, fillColor, bounds);
+ geometry->markVertexDataDirty();
+}
+
+LineSegmentNode::LineSegmentNode()
+ : LineSegmentNode(QRectF{})
+{
+}
+
+LineSegmentNode::LineSegmentNode(const QRectF &rect)
+{
+ m_geometry = new QSGGeometry{LineAttributeSet, 4};
+ m_geometry->setVertexDataPattern(QSGGeometry::DynamicPattern);
+
+ setGeometry(m_geometry);
+
+ m_rect = rect;
+
+ m_material = new LineChartMaterial{};
+ setMaterial(m_material);
+
+ setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial);
+}
+
+LineSegmentNode::~LineSegmentNode()
+{
+}
+
+void LineSegmentNode::setRect(const QRectF &rect)
+{
+ m_rect = rect;
+}
+
+void LineSegmentNode::setAspect(float xAspect, float yAspect)
+{
+ if (qFuzzyCompare(xAspect, m_xAspect) && qFuzzyCompare(yAspect, m_yAspect)) {
+ return;
+ }
+
+ m_yAspect = yAspect;
+ m_material->aspect = m_yAspect;
+ markDirty(QSGNode::DirtyMaterial);
+
+ m_xAspect = xAspect;
+}
+
+void LineSegmentNode::setSmoothing(float smoothing)
+{
+ if (qFuzzyCompare(smoothing, m_smoothing)) {
+ return;
+ }
+
+ m_smoothing = smoothing;
+ m_material->smoothing = m_smoothing;
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void LineSegmentNode::setLineWidth(float width)
+{
+ if (qFuzzyCompare(width, m_lineWidth)) {
+ return;
+ }
+
+ m_lineWidth = width;
+ m_material->lineWidth = m_lineWidth;
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void LineSegmentNode::setLineColor(const QColor &color)
+{
+ m_lineColor = color;
+}
+
+void LineSegmentNode::setFillColor(const QColor &color)
+{
+ m_fillColor = color;
+}
+
+void LineSegmentNode::setValues(const QList<QVector2D> &values)
+{
+ m_values = values;
+}
+
+void LineSegmentNode::setFarLeft(const QVector2D &value)
+{
+ m_farLeft = value;
+}
+
+void LineSegmentNode::setFarRight(const QVector2D &value)
+{
+ m_farRight = value;
+}
+
+void LineSegmentNode::update()
+{
+ if (m_values.isEmpty() || !m_rect.isValid()) {
+ updateLineGeometry(m_geometry, QRectF{}, QRectF{}, QList<QVector2D>{}, m_lineColor, m_fillColor, QVector2D{});
+ markDirty(QSGNode::DirtyGeometry);
+ return;
+ }
+
+ QList<QVector2D> points;
+ points.reserve(m_values.size() + 8);
+
+ points << QVector2D{0.0, -0.5};
+ points << QVector2D{-0.5, -0.5};
+
+ auto min = std::numeric_limits<float>::max();
+ auto max = std::numeric_limits<float>::min();
+
+ if (!m_farLeft.isNull()) {
+ points << QVector2D(-0.5, m_farLeft.y() * m_yAspect);
+ points << QVector2D(((m_farLeft.x() - m_rect.left()) / m_rect.width()) * m_xAspect, m_farLeft.y() * m_yAspect);
+ min = std::min(m_farLeft.y() * m_yAspect, min);
+ max = std::max(m_farLeft.y() * m_yAspect, max);
+ } else {
+ points << QVector2D(-0.5, m_values[0].y() * m_yAspect);
+ }
+
+ for (auto value : std::as_const(m_values)) {
+ auto x = ((value.x() - m_rect.left()) / m_rect.width()) * m_xAspect;
+ points << QVector2D(x, value.y() * m_yAspect);
+ min = std::min(value.y() * m_yAspect, min);
+ max = std::max(value.y() * m_yAspect, max);
+ }
+
+ if (!m_farRight.isNull()) {
+ points << QVector2D(((m_farRight.x() - m_rect.left()) / m_rect.width()) * m_xAspect, m_farRight.y() * m_yAspect);
+ points << QVector2D(1.5, m_farRight.y() * m_yAspect);
+ min = std::min(m_farRight.y() * m_yAspect, min);
+ max = std::max(m_farRight.y() * m_yAspect, max);
+ } else {
+ points << QVector2D(1.5, points.last().y());
+ }
+
+ points << QVector2D{1.5, -0.5};
+ points << QVector2D{0.0, -0.5};
+
+ updateLineGeometry(m_geometry, m_rect, {0.0, 0.0, m_xAspect, 1.0}, points, m_lineColor, m_fillColor, QVector2D{min, max});
+ markDirty(QSGNode::DirtyGeometry);
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef LINESEGMENTNODE_H
+#define LINESEGMENTNODE_H
+
+#include <QColor>
+#include <QSGGeometryNode>
+#include <QVector2D>
+
+class QRectF;
+class LineChartMaterial;
+
+/**
+ * @todo write docs
+ */
+class LineSegmentNode : public QSGGeometryNode
+{
+public:
+ LineSegmentNode();
+
+ /**
+ * Default constructor
+ */
+ explicit LineSegmentNode(const QRectF &rect);
+
+ /**
+ * Destructor
+ */
+ ~LineSegmentNode();
+
+ void setRect(const QRectF &rect);
+ void setAspect(float xAspect, float yAspect);
+ void setSmoothing(float smoothing);
+ void setLineWidth(float width);
+ void setLineColor(const QColor &color);
+ void setFillColor(const QColor &color);
+ void setValues(const QList<QVector2D> &values);
+ void setFarLeft(const QVector2D &value);
+ void setFarRight(const QVector2D &value);
+
+ void update();
+
+private:
+ QRectF m_rect;
+ float m_lineWidth = 0.0;
+ float m_xAspect = 1.0;
+ float m_yAspect = 1.0;
+ float m_smoothing = 0.1;
+ QVector2D m_farLeft;
+ QVector2D m_farRight;
+ QList<QVector2D> m_values;
+ QSGGeometry *m_geometry = nullptr;
+ LineChartMaterial *m_material = nullptr;
+ QColor m_lineColor;
+ QColor m_fillColor;
+};
+
+#endif // LINESEGMENTNODE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "PieChartMaterial.h"
+
+constexpr int MaximumSegmentCount = 100;
+
+PieChartMaterial::PieChartMaterial()
+{
+ setFlag(QSGMaterial::Blending);
+}
+
+PieChartMaterial::~PieChartMaterial()
+{
+}
+
+QSGMaterialType *PieChartMaterial::type() const
+{
+ static QSGMaterialType type;
+ return &type;
+}
+
+QSGMaterialShader *PieChartMaterial::createShader(QSGRendererInterface::RenderMode) const
+{
+ return new PieChartShader();
+}
+
+QVector2D PieChartMaterial::aspectRatio() const
+{
+ return m_aspectRatio;
+}
+
+float PieChartMaterial::innerRadius() const
+{
+ return m_innerRadius;
+}
+
+float PieChartMaterial::outerRadius() const
+{
+ return m_outerRadius;
+}
+
+QColor PieChartMaterial::backgroundColor() const
+{
+ return m_backgroundColor;
+}
+
+QList<QVector2D> PieChartMaterial::segments() const
+{
+ return m_segments;
+}
+
+QList<QVector4D> PieChartMaterial::colors() const
+{
+ return m_colors;
+}
+
+bool PieChartMaterial::smoothEnds() const
+{
+ return m_smoothEnds;
+}
+
+float PieChartMaterial::fromAngle() const
+{
+ return m_fromAngle;
+}
+
+float PieChartMaterial::toAngle() const
+{
+ return m_toAngle;
+}
+
+void PieChartMaterial::setAspectRatio(const QVector2D &aspect)
+{
+ m_aspectRatio = aspect;
+}
+
+void PieChartMaterial::setInnerRadius(float radius)
+{
+ m_innerRadius = radius;
+}
+
+void PieChartMaterial::setOuterRadius(float radius)
+{
+ m_outerRadius = radius;
+}
+
+void PieChartMaterial::setBackgroundColor(const QColor &color)
+{
+ m_backgroundColor = color;
+}
+
+void PieChartMaterial::setSegments(const QList<QVector2D> &segments)
+{
+ m_segments = segments;
+}
+
+void PieChartMaterial::setColors(const QList<QVector4D> &colors)
+{
+ m_colors = colors;
+}
+
+void PieChartMaterial::setSmoothEnds(bool smooth)
+{
+ m_smoothEnds = smooth;
+}
+
+void PieChartMaterial::setFromAngle(float angle)
+{
+ m_fromAngle = angle;
+}
+
+void PieChartMaterial::setToAngle(float angle)
+{
+ m_toAngle = angle;
+}
+
+PieChartShader::PieChartShader()
+{
+ setShaders(QStringLiteral("piechart.vert"), QStringLiteral("piechart.frag"));
+}
+
+PieChartShader::~PieChartShader()
+{
+}
+
+bool PieChartShader::updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
+{
+ bool changed = false;
+
+ UniformDataStream uniformData(state);
+
+ if (state.isMatrixDirty()) {
+ uniformData << state.combinedMatrix();
+ changed = true;
+ } else {
+ uniformData.skip<QMatrix4x4>();
+ }
+
+ if (state.isOpacityDirty()) {
+ uniformData << state.opacity();
+ changed = true;
+ } else {
+ uniformData.skip<float>();
+ }
+
+ if (!oldMaterial || newMaterial->compare(oldMaterial) != 0) {
+ const auto material = static_cast<PieChartMaterial *>(newMaterial);
+
+ uniformData << material->aspectRatio() << material->innerRadius() << material->outerRadius() << material->backgroundColor() //
+ << int(material->smoothEnds()) << material->fromAngle() << material->toAngle();
+
+ const auto segmentCount = uint(material->segments().size());
+ uniformData << segmentCount;
+
+ uniformData << material->segments();
+ uniformData.skipComponents((MaximumSegmentCount - segmentCount) * 4);
+
+ uniformData << material->colors();
+ uniformData.skipComponents((MaximumSegmentCount - segmentCount) * 4);
+
+ changed = true;
+ }
+
+ return changed;
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef PIECHARTMATERIAL_H
+#define PIECHARTMATERIAL_H
+
+#include <QColor>
+#include <QSGMaterial>
+#include <QSGMaterialShader>
+
+#include "SDFShader.h"
+
+class PieChartMaterial : public QSGMaterial
+{
+public:
+ PieChartMaterial();
+ ~PieChartMaterial();
+
+ QSGMaterialType *type() const override;
+ QSGMaterialShader *createShader(QSGRendererInterface::RenderMode) const override;
+
+ QVector2D aspectRatio() const;
+ float innerRadius() const;
+ float outerRadius() const;
+ QColor backgroundColor() const;
+ bool smoothEnds() const;
+ float fromAngle() const;
+ float toAngle() const;
+
+ QList<QVector2D> segments() const;
+ QList<QVector4D> colors() const;
+
+ void setAspectRatio(const QVector2D &aspect);
+ void setInnerRadius(float radius);
+ void setOuterRadius(float radius);
+ void setBackgroundColor(const QColor &color);
+ void setSmoothEnds(bool smooth);
+ void setFromAngle(float angle);
+ void setToAngle(float angle);
+
+ void setSegments(const QList<QVector2D> &triangles);
+ void setColors(const QList<QVector4D> &colors);
+
+private:
+ QVector2D m_aspectRatio;
+ float m_innerRadius = 0.0f;
+ float m_outerRadius = 0.0f;
+ QColor m_backgroundColor;
+ bool m_smoothEnds = false;
+ float m_fromAngle = 0.0;
+ float m_toAngle = 6.28318; // 2 * pi
+
+ QList<QVector2D> m_segments;
+ QList<QVector4D> m_colors;
+};
+
+class PieChartShader : public SDFShader
+{
+public:
+ PieChartShader();
+ ~PieChartShader();
+
+ bool updateUniformData(QSGMaterialShader::RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
+};
+
+#endif // PIECHARTMATERIAL_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "PieChartNode.h"
+
+#include <algorithm>
+
+#include <QColor>
+#include <QSGGeometry>
+#include <cmath>
+
+#include "PieChartMaterial.h"
+
+static const qreal pi = std::acos(-1.0);
+
+inline QVector4D colorToVec4(const QColor &color)
+{
+ return QVector4D{float(color.redF()), float(color.greenF()), float(color.blueF()), float(color.alphaF())};
+}
+
+inline qreal degToRad(qreal deg)
+{
+ return (deg / 180.0) * pi;
+}
+
+PieChartNode::PieChartNode()
+ : PieChartNode(QRectF{})
+{
+}
+
+PieChartNode::PieChartNode(const QRectF &rect)
+{
+ m_geometry = new QSGGeometry{QSGGeometry::defaultAttributes_TexturedPoint2D(), 4};
+ QSGGeometry::updateTexturedRectGeometry(m_geometry, rect, QRectF{0, 0, 1, 1});
+ setGeometry(m_geometry);
+
+ m_material = new PieChartMaterial{};
+ setMaterial(m_material);
+
+ setFlags(QSGNode::OwnsGeometry | QSGNode::OwnsMaterial);
+}
+
+PieChartNode::~PieChartNode()
+{
+}
+
+void PieChartNode::setRect(const QRectF &rect)
+{
+ if (rect == m_rect) {
+ return;
+ }
+
+ m_rect = rect;
+ QSGGeometry::updateTexturedRectGeometry(m_geometry, m_rect, QRectF{0, 0, 1, 1});
+ markDirty(QSGNode::DirtyGeometry);
+
+ auto minDimension = qMin(m_rect.width(), m_rect.height());
+
+ QVector2D aspect{1.0, 1.0};
+ aspect.setX(rect.width() / minDimension);
+ aspect.setY(rect.height() / minDimension);
+ m_material->setAspectRatio(aspect);
+
+ m_material->setInnerRadius(m_innerRadius / minDimension);
+ m_material->setOuterRadius(m_outerRadius / minDimension);
+
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void PieChartNode::setInnerRadius(qreal radius)
+{
+ if (qFuzzyCompare(radius, m_innerRadius)) {
+ return;
+ }
+
+ m_innerRadius = radius;
+
+ auto minDimension = qMin(m_rect.width(), m_rect.height());
+ m_material->setInnerRadius(m_innerRadius / minDimension);
+
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void PieChartNode::setOuterRadius(qreal radius)
+{
+ if (qFuzzyCompare(radius, m_outerRadius)) {
+ return;
+ }
+
+ m_outerRadius = radius;
+
+ auto minDimension = qMin(m_rect.width(), m_rect.height());
+ m_material->setOuterRadius(m_outerRadius / minDimension);
+
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void PieChartNode::setColors(const QList<QColor> &colors)
+{
+ m_colors = colors;
+ updateSegments();
+}
+
+void PieChartNode::setSections(const QList<qreal> §ions)
+{
+ m_sections = sections;
+ updateSegments();
+}
+
+void PieChartNode::setBackgroundColor(const QColor &color)
+{
+ if (color == m_backgroundColor) {
+ return;
+ }
+
+ m_backgroundColor = color;
+ m_material->setBackgroundColor(color);
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void PieChartNode::setFromAngle(qreal angle)
+{
+ if (qFuzzyCompare(angle, m_fromAngle)) {
+ return;
+ }
+
+ m_fromAngle = angle;
+ m_material->setFromAngle(degToRad(angle));
+ updateSegments();
+}
+
+void PieChartNode::setToAngle(qreal angle)
+{
+ if (qFuzzyCompare(angle, m_fromAngle)) {
+ return;
+ }
+
+ m_toAngle = angle;
+ m_material->setToAngle(degToRad(angle));
+ updateSegments();
+}
+
+void PieChartNode::setSmoothEnds(bool smooth)
+{
+ if (smooth == m_smoothEnds) {
+ return;
+ }
+
+ m_smoothEnds = smooth;
+ m_material->setSmoothEnds(smooth);
+ markDirty(QSGNode::DirtyMaterial);
+}
+
+void PieChartNode::updateSegments()
+{
+ if (m_sections.isEmpty() || m_sections.size() != m_colors.size()) {
+ return;
+ }
+
+ qreal startAngle = degToRad(m_fromAngle);
+ qreal totalAngle = degToRad(m_toAngle - m_fromAngle);
+
+ QList<QVector2D> segments;
+ QList<QVector4D> colors;
+
+ for (int i = 0; i < m_sections.size(); ++i) {
+ QVector2D segment{float(startAngle), float(startAngle + m_sections.at(i) * totalAngle)};
+ segments << segment;
+ startAngle = segment.y();
+ colors << colorToVec4(m_colors.at(i));
+ }
+
+ if (m_sections.size() == 1 && qFuzzyCompare(m_sections.at(0), 0.0)) {
+ segments.clear();
+ }
+
+ m_material->setSegments(segments);
+ m_material->setColors(colors);
+
+ markDirty(QSGNode::DirtyMaterial);
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef PIECHARTNODE_H
+#define PIECHARTNODE_H
+
+#include <QColor>
+#include <QSGGeometryNode>
+
+class QRectF;
+class PieChartMaterial;
+
+/**
+ * @todo write docs
+ */
+class PieChartNode : public QSGGeometryNode
+{
+public:
+ PieChartNode();
+
+ /**
+ * Default constructor
+ */
+ explicit PieChartNode(const QRectF &rect);
+
+ /**
+ * Destructor
+ */
+ ~PieChartNode();
+
+ void setRect(const QRectF &rect);
+ void setInnerRadius(qreal radius);
+ void setOuterRadius(qreal radius);
+ void setSections(const QList<qreal> §ions);
+ void setColors(const QList<QColor> &colors);
+ void setBackgroundColor(const QColor &color);
+ void setFromAngle(qreal angle);
+ void setToAngle(qreal angle);
+ void setSmoothEnds(bool smooth);
+
+private:
+ void updateSegments();
+
+ QRectF m_rect;
+ qreal m_innerRadius = 0.0;
+ qreal m_outerRadius = 0.0;
+ QColor m_backgroundColor;
+ qreal m_fromAngle = 0.0;
+ qreal m_toAngle = 360.0;
+ bool m_smoothEnds = false;
+
+ QList<qreal> m_sections;
+ QList<QColor> m_colors;
+
+ QSGGeometry *m_geometry = nullptr;
+ PieChartMaterial *m_material = nullptr;
+};
+
+#endif // PIECHARTNODE_H
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#include "SDFShader.h"
+
+#include <QOpenGLContext>
+#include <QSurfaceFormat>
+
+static const char shaderRoot[] = ":/qt/qml/org/kde/quickcharts/shaders/";
+
+SDFShader::SDFShader()
+{
+}
+
+SDFShader::~SDFShader()
+{
+}
+
+void SDFShader::setShaders(const QString &vertex, const QString &fragment)
+{
+ setShaderFileName(QSGMaterialShader::VertexStage, QLatin1String(shaderRoot) + vertex + QLatin1String(".qsb"));
+ setShaderFileName(QSGMaterialShader::FragmentStage, QLatin1String(shaderRoot) + fragment + QLatin1String(".qsb"));
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#ifndef SDF_SHADER_H
+#define SDF_SHADER_H
+
+#include <QSGMaterialShader>
+
+struct UniformDataStream {
+ inline UniformDataStream(QSGMaterialShader::RenderState &state) noexcept
+ : bytes(state.uniformData()->data())
+ {
+ }
+
+ ~UniformDataStream()
+ {
+ }
+
+ template<typename Data>
+ friend inline UniformDataStream &operator<<(UniformDataStream &stream, const Data &data)
+ {
+ constexpr uint dataSize = sizeof(Data);
+ stream.align(dataSize);
+ memcpy(stream.bytes, &data, dataSize);
+ stream.bytes += dataSize;
+ stream.offset += dataSize;
+ return stream;
+ }
+
+ template<typename Data>
+ inline void skip(const Data &data = {})
+ {
+ constexpr uint dataSize = sizeof(Data);
+
+ align(dataSize);
+ Q_UNUSED(data);
+ bytes += dataSize;
+ offset += dataSize;
+ }
+
+ inline void skipComponents(uint count)
+ {
+ const uint skipCount = count * 4;
+ align(4);
+ bytes += skipCount;
+ offset += skipCount;
+ }
+
+ friend inline UniformDataStream &operator<<(UniformDataStream &stream, const QMatrix4x4 &m)
+ {
+ constexpr uint Matrix4x4Size = 4 * 4 * 4;
+
+ stream.align(Matrix4x4Size);
+ memcpy(stream.bytes, m.constData(), Matrix4x4Size);
+ stream.bytes += Matrix4x4Size;
+ stream.offset += Matrix4x4Size;
+ return stream;
+ }
+
+ friend inline UniformDataStream &operator<<(UniformDataStream &stream, const QColor &color)
+ {
+ constexpr uint ColorSize = 4 * 4;
+
+ stream.align(ColorSize);
+ std::array<float, 4> colorArray;
+ color.getRgbF(&colorArray[0], &colorArray[1], &colorArray[2], &colorArray[3]);
+ memcpy(stream.bytes, colorArray.data(), ColorSize);
+ stream.bytes += ColorSize;
+ stream.offset += ColorSize;
+ return stream;
+ }
+
+ template<typename T>
+ friend inline UniformDataStream &operator<<(UniformDataStream &stream, const QList<T> &v)
+ {
+ for (const auto &item : v) {
+ stream << item;
+ // Using std140, array elements are padded to a size of 16 bytes per element.
+ stream.align(16);
+ }
+ return stream;
+ }
+
+ char *bytes;
+ size_t padding = 16;
+ size_t offset = 0;
+
+private:
+ // Encode alignment rules for std140.
+ // Minimum alignment is 4 bytes.
+ // Vec2 alignment is 8 bytes.
+ // Vec3 and Vec4 alignment is 16 bytes.
+ inline void align(uint size)
+ {
+ if (size <= 4) {
+ const auto padding = offset % 4 > 0 ? 4 - offset % 4 : 0;
+ offset += padding;
+ bytes += padding;
+ } else if (size <= 8) {
+ auto padding = offset % 8 > 0 ? 8 - offset % 8 : 0;
+ offset += padding;
+ bytes += padding;
+ } else {
+ auto padding = offset % 16 > 0 ? 16 - offset % 16 : 0;
+ offset += padding;
+ bytes += padding;
+ }
+ }
+};
+
+class SDFShader : public QSGMaterialShader
+{
+public:
+ SDFShader();
+ virtual ~SDFShader();
+
+ void setShaders(const QString &vertex, const QString &fragment);
+};
+
+#endif // SDF_SHADER_H
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf.glsl"
+
+layout(std140, binding = 0) uniform buf {
+ highp mat4 matrix;
+ lowp float opacity;
+ lowp vec2 aspect;
+ lowp float radius;
+ lowp vec4 backgroundColor;
+} ubuf;
+
+layout (location = 0) in lowp vec2 uv;
+layout (location = 1) in mediump vec4 foregroundColor;
+layout (location = 2) in mediump float value;
+
+layout (location = 0) out lowp vec4 out_color;
+
+void main()
+{
+ lowp vec4 color = vec4(0.0);
+
+ lowp float background = sdf_round(sdf_rectangle(uv, vec2(1.0, ubuf.aspect.y) - ubuf.radius), ubuf.radius);
+
+ color = sdf_render(background, color, ubuf.backgroundColor);
+
+ lowp float foreground = sdf_round(sdf_rectangle(vec2(uv.x, -ubuf.aspect.y + uv.y + value), vec2(1.0, value) - ubuf.radius), ubuf.radius);
+
+ color = sdf_render(foreground, color, foregroundColor);
+
+ out_color = color * ubuf.opacity;
+}
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2021 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#version 440
+
+layout(std140, binding = 0) uniform buf {
+ highp mat4 matrix;
+ lowp float opacity;
+ lowp vec2 aspect;
+ lowp float radius;
+ lowp vec4 backgroundColor;
+} ubuf;
+
+layout (location = 0) in highp vec4 in_vertex;
+layout (location = 1) in mediump vec2 in_uv;
+layout (location = 2) in mediump vec4 in_color;
+layout (location = 3) in mediump float in_value;
+
+layout (location = 0) out mediump vec2 uv;
+layout (location = 1) out mediump vec4 foregroundColor;
+layout (location = 2) out mediump float value;
+
+void main() {
+ uv = (-1.0 + 2.0 * in_uv) * ubuf.aspect;
+ value = in_value;
+ foregroundColor = in_color;
+ gl_Position = ubuf.matrix * in_vertex;
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ * SPDX-FileCopyrightText: 2022 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf.glsl"
+#line 13
+
+layout(std140, binding = 0) uniform buf {
+ highp mat4 matrix;
+ lowp float opacity; // inherited opacity of this item - offset 64
+ lowp float lineWidth; // offset 68
+ lowp float aspect; // offset 72
+ lowp float smoothing; // offset 76
+} ubuf; // size 80
+
+#define MAXIMUM_POINT_COUNT 14
+
+layout (location = 0) in mediump vec2 uv;
+layout (location = 1) in mediump vec4 pointTuples[MAXIMUM_POINT_COUNT / 2];
+layout (location = 19) in highp float pointCount;
+layout (location = 20) in mediump vec2 bounds;
+layout (location = 21) in mediump vec4 lineColor;
+layout (location = 22) in mediump vec4 fillColor;
+layout (location = 0) out lowp vec4 out_color;
+
+// ES2 does not support array function arguments. So instead we need to
+// reference the uniform array directly. So this copies the implementation of
+// sdf_polygon from sdf.glsl, changing it to refer to the points array directly.
+// For simplicity, we use the same function also for other APIs.
+lowp float sdf_polygon(in lowp vec2 point, in int count)
+{
+ mediump vec2 points[MAXIMUM_POINT_COUNT];
+ points[0] = pointTuples[0].xy;
+ points[1] = pointTuples[0].zw;
+ points[2] = pointTuples[1].xy;
+ points[3] = pointTuples[1].zw;
+ points[4] = pointTuples[2].xy;
+ points[5] = pointTuples[2].zw;
+ points[6] = pointTuples[3].xy;
+ points[7] = pointTuples[3].zw;
+ points[8] = pointTuples[4].xy;
+ points[9] = pointTuples[4].zw;
+ points[10] = pointTuples[5].xy;
+ points[11] = pointTuples[5].zw;
+ points[12] = pointTuples[6].xy;
+ points[13] = pointTuples[6].zw;
+
+ lowp float d = dot(point - points[0], point - points[0]);
+ lowp float s = 1.0;
+ for (int i = 0, j = count - 1; i < count && i < MAXIMUM_POINT_COUNT; j = i, i++)
+ {
+ lowp vec2 e = points[j] - points[i];
+ lowp vec2 w = point - points[i];
+ lowp float h = clamp( dot(w, e) / dot(e, e), 0.0, 1.0 );
+ lowp vec2 b = w - e * h;
+ d = min(d, dot(b, b));
+
+ bvec3 c = bvec3(point.y >= points[i].y, point.y < points[j].y, e.x * w.y > e.y * w.x);
+ if(all(c) || all(not(c))) s *= -1.0;
+ }
+ return s * sqrt(d);
+}
+
+void main()
+{
+ lowp vec2 point = uv;
+ lowp float fwidthPoint = fwidth(point.x + point.y);
+
+ lowp vec4 color = vec4(0.0, 0.0, 0.0, 0.0);
+
+ lowp float bounds_range = max(0.01, ubuf.lineWidth);
+
+ // bounds.y contains the line segment's maximum value. If we are a bit above
+ // that, we will never render anything, so just discard the pixel.
+ if (point.y > bounds.y + bounds_range) {
+ discard;
+ }
+
+ // bounds.x contains the line segment's minimum value. If we are a bit below
+ // that, we know we will always be inside the polygon described by points.
+ // So just return a pixel with fillColor.
+ if (point.y < bounds.x - bounds_range) {
+ out_color = fillColor * ubuf.opacity;
+ return;
+ }
+
+ lowp float polygon = sdf_polygon(point, int(pointCount));
+
+ color = sdf_render(polygon, fwidthPoint, color, fillColor, 1.0, ubuf.smoothing);
+
+ if (ubuf.lineWidth > 0.0) {
+ color = mix(color, lineColor, 1.0 - smoothstep(-ubuf.smoothing, ubuf.smoothing, sdf_annular(polygon, ubuf.lineWidth)));
+ }
+
+ out_color = color * ubuf.opacity;
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ * SPDX-FileCopyrightText: 2022 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+#version 440
+#line 8
+
+layout(std140, binding = 0) uniform buf {
+ highp mat4 matrix;
+ lowp float opacity;
+ lowp float lineWidth;
+ lowp float aspect;
+ lowp float smoothing;
+} ubuf;
+
+#define MAXIMUM_POINT_COUNT 14
+
+layout (location = 0) in highp vec4 in_vertex;
+layout (location = 1) in mediump vec2 in_uv;
+
+layout (location = 2) in mediump vec4 in_lineColor;
+layout (location = 3) in mediump vec4 in_fillColor;
+layout (location = 4) in mediump vec2 in_bounds;
+
+layout (location = 5) in highp float in_count;
+
+// Input points. Since OpenGL 2.1/GLSL 1.10 does not support array vertex
+// attributes, we have to manually declare a number of attributes. We use
+// array of vec4 point tuples instead of vec2 to not cross the OpenGL limits
+// like e.g. GL_MAX_VERTEX_ATTRIBS for some drivers.
+layout (location = 8) in mediump vec4 in_points_0;
+layout (location = 9) in mediump vec4 in_points_1;
+layout (location = 10) in mediump vec4 in_points_2;
+layout (location = 11) in mediump vec4 in_points_3;
+layout (location = 12) in mediump vec4 in_points_4;
+layout (location = 13) in mediump vec4 in_points_5;
+layout (location = 14) in mediump vec4 in_points_6;
+
+layout (location = 0) out mediump vec2 uv;
+layout (location = 1) out mediump vec4 pointTuples[MAXIMUM_POINT_COUNT / 2];
+layout (location = 19) out highp float pointCount;
+layout (location = 20) out mediump vec2 bounds;
+layout (location = 21) out mediump vec4 lineColor;
+layout (location = 22) out mediump vec4 fillColor;
+
+void main() {
+ uv = in_uv;
+ uv.y = (1.0 + -1.0 * uv.y) * ubuf.aspect;
+
+ pointTuples[0] = in_points_0;
+ pointTuples[1] = in_points_1;
+ pointTuples[2] = in_points_2;
+ pointTuples[3] = in_points_3;
+ pointTuples[4] = in_points_4;
+ pointTuples[5] = in_points_5;
+ pointTuples[6] = in_points_6;
+
+ pointCount = in_count;
+ bounds = in_bounds;
+
+ lineColor = in_lineColor;
+ fillColor = in_fillColor;
+
+ gl_Position = ubuf.matrix * in_vertex;
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#version 440
+
+#extension GL_GOOGLE_include_directive: enable
+#include "sdf.glsl"
+
+// The maximum number of segments we can support for a single pie.
+// This is based on OpenGL's MAX_FRAGMENT_UNIFORM_COMPONENTS.
+// MAX_FRAGMENT_UNIFORM_COMPONENTS is required to be at least 1024.
+// Assuming a segment of size 1, each segment needs
+// 2 (size of a vec2) * 2 (number of points) + 4 (size of vec4) + 1 (segment size)
+// components. We also need to leave some room for the other uniforms.
+#define MAX_SEGMENTS 100
+
+layout(std140, binding = 0) uniform buf {
+ highp mat4 matrix;
+ lowp float opacity;
+ lowp vec2 aspect;
+ lowp float innerRadius;
+ lowp float outerRadius;
+ lowp vec4 backgroundColor;
+ int smoothEnds;
+ lowp float fromAngle;
+ lowp float toAngle;
+
+ int segmentCount;
+ lowp vec2 segments[MAX_SEGMENTS];
+ lowp vec4 colors[MAX_SEGMENTS];
+} ubuf;
+
+layout (location = 0) in lowp vec2 uv;
+layout (location = 0) out lowp vec4 out_color;
+
+const lowp vec2 origin = vec2(0.0, 0.0);
+const lowp float lineSmooth = 0.001;
+
+lowp float rounded_segment(lowp float from, lowp float to, lowp float inner, lowp float outer, lowp float rounding)
+{
+ return sdf_torus_segment(uv, from + rounding, to - rounding, inner + rounding, outer - rounding) - rounding;
+}
+
+void main()
+{
+ lowp vec4 color = vec4(0.0);
+
+ lowp float thickness = (ubuf.outerRadius - ubuf.innerRadius) / 2.0;
+ lowp float rounding = ubuf.smoothEnds > 0 ? thickness : 0.0;
+
+ // Background first, slightly smaller than the actual pie to avoid antialiasing artifacts.
+ lowp float background_rounding = (ubuf.toAngle - ubuf.fromAngle) >= 2.0 * pi ? 0.001 : rounding + 0.001;
+ lowp float background = rounded_segment(ubuf.fromAngle, ubuf.toAngle, ubuf.innerRadius, ubuf.outerRadius, background_rounding);
+ color = sdf_render(background, color, ubuf.backgroundColor);
+
+ for (int i = 0; i < ubuf.segmentCount && i < MAX_SEGMENTS; ++i) {
+ lowp vec2 segment = ubuf.segments[i];
+
+ lowp float segment_sdf = rounded_segment(segment.x, segment.y, ubuf.innerRadius, ubuf.outerRadius, rounding);
+ color = sdf_render(segment_sdf, color, ubuf.colors[i]);
+ }
+
+ out_color = color * ubuf.opacity;
+}
--- /dev/null
+/*
+ * This file is part of KQuickCharts
+ * SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+ */
+
+#version 440
+
+#define MAX_SEGMENTS 100
+layout(std140, binding = 0) uniform buf {
+ highp mat4 matrix;
+ lowp float opacity;
+ lowp vec2 aspect;
+ lowp float innerRadius;
+ lowp float outerRadius;
+ lowp vec4 backgroundColor;
+ int smoothEnds;
+ lowp float fromAngle;
+ lowp float toAngle;
+
+ int segmentCount;
+ lowp vec2 segments[MAX_SEGMENTS];
+ lowp vec4 colors[MAX_SEGMENTS];
+} ubuf;
+
+layout (location = 0) in highp vec4 in_vertex;
+layout (location = 1) in mediump vec2 in_uv;
+layout (location = 0) out mediump vec2 uv;
+
+void main() {
+ uv = (-1.0 + 2.0 * in_uv) * ubuf.aspect;
+ uv.y *= -1.0;
+ gl_Position = ubuf.matrix * in_vertex;
+}
--- /dev/null
+// SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
+// SPDX-FileCopyrightText: 2017 Inigo Quilez
+//
+// SPDX-License-Identifier: MIT
+//
+// This file is based on
+// https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
+
+// A maximum point count to be used for sdf_polygon input arrays.
+// Unfortunately even function inputs require a fixed size at declaration time
+// for arrays, unless we were to use OpenGL 4.5.
+// Since the polygon is most likely to be defined in a uniform, this should be
+// at least less than MAX_FRAGMENT_UNIFORM_COMPONENTS / 2 (since we need vec2).
+#define SDF_POLYGON_MAX_POINT_COUNT 400
+
+// A constant for pi
+const lowp float pi = 3.1415926535897932384626433832795;
+
+/*********************************
+ Shapes
+*********************************/
+
+// Distance field for a circle.
+//
+// \param point A point on the distance field.
+// \param radius The radius of the circle.
+//
+// \return The signed distance from point to the circle. If negative, point is
+// inside the circle.
+lowp float sdf_circle(in lowp vec2 point, in lowp float radius)
+{
+ return length(point) - radius;
+}
+
+// Distance field for a triangle.
+//
+// \param point A point on the distance field.
+// \param p0 The first vertex of the triangle.
+// \param p0 The second vertex of the triangle.
+// \param p0 The third vertex of the triangle.
+//
+// \note The ordering of the three vertices does not matter.
+//
+// \return The signed distance from point to triangle. If negative, point is
+// inside the triangle.
+lowp float sdf_triangle(in lowp vec2 point, in lowp vec2 p0, in lowp vec2 p1, in lowp vec2 p2)
+{
+ lowp vec2 e0 = p1 - p0;
+ lowp vec2 e1 = p2 - p1;
+ lowp vec2 e2 = p0 - p2;
+
+ lowp vec2 v0 = point - p0;
+ lowp vec2 v1 = point - p1;
+ lowp vec2 v2 = point - p2;
+
+ lowp vec2 pq0 = v0 - e0 * clamp( dot(v0, e0) / dot(e0, e0), 0.0, 1.0 );
+ lowp vec2 pq1 = v1 - e1 * clamp( dot(v1, e1) / dot(e1, e1), 0.0, 1.0 );
+ lowp vec2 pq2 = v2 - e2 * clamp( dot(v2, e2) / dot(e2, e2), 0.0, 1.0 );
+
+ lowp float s = sign( e0.x*e2.y - e0.y*e2.x );
+ lowp vec2 d = min(min(vec2(dot(pq0,pq0), s*(v0.x*e0.y-v0.y*e0.x)),
+ vec2(dot(pq1,pq1), s*(v1.x*e1.y-v1.y*e1.x))),
+ vec2(dot(pq2,pq2), s*(v2.x*e2.y-v2.y*e2.x)));
+
+ return -sqrt(d.x)*sign(d.y);
+}
+
+#ifndef API_ES2
+// Distance field for an arbitrary polygon.
+//
+// \param point A point on the distance field.
+// \param vertices An array of points that make up the polygon.
+// \param count The amount of points to use for the polygon.
+//
+// \note points should be an array of vec2 of size SDF_POLYGON_MAX_POINT_COUNT.
+// Use count to indicate how many items of that array should be used.
+//
+// \return The signed distance from point to triangle. If negative, point is
+// inside the triangle.
+lowp float sdf_polygon(in lowp vec2 point, in lowp vec2[SDF_POLYGON_MAX_POINT_COUNT] vertices, in lowp int count)
+{
+ lowp float d = dot(point - vertices[0], point - vertices[0]);
+ lowp float s = 1.0;
+ for (int i = 0, j = count - 1; i < count && i < SDF_POLYGON_MAX_POINT_COUNT; j = i, i++)
+ {
+ lowp vec2 e = vertices[j] - vertices[i];
+ lowp vec2 w = point - vertices[i];
+ lowp float h = clamp( dot(w, e) / dot(e, e), 0.0, 1.0 );
+ lowp vec2 b = w - e * h;
+ d = min(d, dot(b, b));
+
+ bvec3 c = bvec3(point.y >= vertices[i].y, point.y < vertices[j].y, e.x * w.y > e.y * w.x);
+ if(all(c) || all(not(c))) s *= -1.0;
+ }
+ return s * sqrt(d);
+}
+#endif
+
+// Distance field for a rectangle.
+//
+// \param point A point on the distance field.
+// \param rect A vec2 with the size of the rectangle.
+//
+// \return The signed distance from point to rectangle. If negative, point is
+// inside the rectangle.
+lowp float sdf_rectangle(in lowp vec2 point, in lowp vec2 rect)
+{
+ lowp vec2 d = abs(point) - rect;
+ return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
+}
+
+// Distance field for a torus segment.
+//
+// \param point A point on the distance field.
+// \param start The start angle in radians of the segment.
+// \param end The end angle in radians of the segment.
+// \param inner_radius The inner radius of the torus.
+// \param outer_radius The outer radius of the torus.
+//
+// \return The signed distance from point to the torus segment. If negative,
+// point is inside the segment.
+lowp float sdf_torus_segment(in lowp vec2 point, in lowp float start, in lowp float end, in lowp float inner_radius, in lowp float outer_radius)
+{
+ start = clamp(start, end - 2.0 * pi, end);
+ end = clamp(end, start, start + 2.0 * pi);
+
+ lowp float angle = (end - start) / 2.0;
+ lowp float rotation = (start + end) / 2.0;
+
+ lowp vec2 rotated = point * mat2(cos(rotation), -sin(rotation), sin(rotation), cos(rotation));
+ lowp vec2 c = vec2(sin(angle), cos(angle));
+
+ rotated.x = abs(rotated.x);
+
+ lowp float t = (outer_radius - inner_radius) / 2.0;
+ lowp float l = abs(length(rotated) - (inner_radius + t)) - t;
+
+ lowp float m = length(rotated - c * clamp(dot(rotated, c), inner_radius, outer_radius));
+ return max(l, m * sign(c.y * rotated.x - c.x * rotated.y));
+}
+
+/*********************
+ Operators
+*********************/
+
+// Convert a distance field to an annular (hollow) distance field.
+//
+// \param sdf The result of an sdf shape to convert.
+// \param thickness The thickness of the resulting shape.
+//
+// \return The value of sdf modified to an annular shape.
+lowp float sdf_annular(in lowp float sdf, in lowp float thickness)
+{
+ return abs(sdf) - thickness;
+}
+
+// Union two sdf shapes together.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return The union of sdf1 and sdf2, that is, the distance to both sdf1 and
+// sdf2.
+lowp float sdf_union(in lowp float sdf1, in lowp float sdf2)
+{
+ return min(sdf1, sdf2);
+}
+
+// Subtract two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return sdf1 with sdf2 subtracted from it.
+lowp float sdf_subtract(in lowp float sdf1, in lowp float sdf2)
+{
+ return max(sdf1, -sdf2);
+}
+
+// Intersect two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+//
+// \return The intersection between sdf1 and sdf2, that is, the area where both
+// sdf1 and sdf2 provide the same distance value.
+lowp float sdf_intersect(in lowp float sdf1, in lowp float sdf2)
+{
+ return max(sdf1, sdf2);
+}
+
+// Smoothly intersect two sdf shapes.
+//
+// \param sdf1 The first sdf shape.
+// \param sdf2 The second sdf shape.
+// \param smoothing The amount of smoothing to apply.
+//
+// \return A smoothed version of the intersect operation.
+lowp float sdf_intersect_smooth(in lowp float sdf1, in lowp float sdf2, in lowp float smoothing)
+{
+ lowp float h = clamp(0.5 - 0.5 * (sdf1 - sdf2) / smoothing, 0.0, 1.0);
+ return mix(sdf1, sdf2, h) + smoothing * h * (1.0 - h);
+}
+
+// Round an sdf shape.
+//
+// \param sdf The sdf shape to round.
+// \param amount The amount of rounding to apply.
+//
+// \return The rounded shape of sdf.
+// Note that rounding happens by basically selecting an isoline of sdf,
+// therefore, the resulting shape may be larger than the input shape.
+lowp float sdf_round(in lowp float sdf, in lowp float amount)
+{
+ return sdf - amount;
+}
+
+// Convert an sdf shape to an outline of its shape.
+//
+// \param sdf The sdf shape to turn into an outline.
+//
+// \return The outline of sdf.
+lowp float sdf_outline(in lowp float sdf)
+{
+ return abs(sdf);
+}
+
+/********************
+ Convenience
+********************/
+
+// A constant to represent a "null" value of an sdf.
+// Since 0 is a point exactly on the outline of an sdf shape, and negative
+// values are inside the shape, this uses a very large positive constant to
+// indicate a value that is really far away from the actual sdf shape.
+const lowp float sdf_null = 99999.0;
+
+// A constant for a default level of smoothing when rendering an sdf.
+const lowp float sdf_default_smoothing = 0.625;
+
+/**
+ * Render an sdf shape alpha-blended onto an existing color.
+ *
+ * \param sdf The sdf shape to render.
+ * \param fwidthSdf fwidth() of the sdf. This is a separte argument because
+ * fwidth() cannot be used in conditional contexts.
+ * \param sourceColor The source color to render on top of.
+ * \param sdfColor The color to use for rendering the sdf shape.
+ * \param alpha The alpha to use for blending.
+ * \param smoothing The amount of smoothing to apply to the sdf.
+ *
+ * \return sourceColor with the sdf shape rendered on top.
+ */
+lowp vec4 sdf_render(in lowp float sdf, in lowp float fwidthSdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float alpha, in lowp float smoothing)
+{
+ lowp float g = smoothing * fwidthSdf;
+ return mix(sourceColor, sdfColor, alpha * (1.0 - smoothstep(-g, g, sdf)));
+}
+
+/**
+ * Render an sdf shape.
+ *
+ * This is an overload of sdf_render(float, vec4, vec4, float, float) that uses
+ * 1.0 for blending value and sdf_default_smoothing for smoothing.
+ */
+lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor)
+{
+ return sdf_render(sdf, fwidth(sdf), sourceColor, sdfColor, 1.0, sdf_default_smoothing);
+}
+
+/**
+ * Render an sdf shape.
+ *
+ * This is an overload of sdf_render(float, vec4, vec4, float, float) that uses
+ * 1.0 for blending value but allows specifying a smoothing amount.
+ */
+lowp vec4 sdf_render(in lowp float sdf, in lowp vec4 sourceColor, in lowp vec4 sdfColor, in lowp float smoothing)
+{
+ return sdf_render(sdf, fwidth(sdf), sourceColor, sdfColor, 1.0, smoothing);
+}