- Qt 6.7
- Qt Graphs
- Simple Bar Graph
- Qt Graphs | Commercial or GPLv3

# Simple Bar Graph

     // Copyright (C) 2023 The Qt Company Ltd.
     // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

     import QtQuick
     import QtQuick.Controls
     import QtQuick.Layouts
     import QtGraphs
     import Qt.labs.qmlmodels

     pragma ComponentBehavior: Bound

     Item {
         id: mainview
         width: 1280
         height: 1024

         property int buttonLayoutHeight: 180
         property int currentRow
         state: Screen.width < Screen.height ? "portrait" : "landscape"

         Data {
             id: graphData
         }

         Axes {
             id: graphAxes
         }

         property Bar3DSeries selectedSeries
         selectedSeries: barSeries

         function handleSelectionChange(series, position) {
             if (position !== series.invalidSelectionPosition)
                 selectedSeries = series;

             // Set tableView current row to selected bar
             var rowRole = series.dataProxy.rowLabels[position.x];
             var colRole;
             if (barGraph.columnAxis == graphAxes.total)
                 colRole = "01";
             else
                 colRole = series.dataProxy.columnLabels[position.y];
             var checkTimestamp = rowRole + "-" + colRole;

             if (currentRow === -1 || checkTimestamp !== graphData.model.get(currentRow).timestamp) {
                 var totalRows = tableView.rows;
                 for (var i = 0; i < totalRows; i++) {
                     var modelTimestamp = graphData.model.get(i).timestamp;
                     if (modelTimestamp === checkTimestamp) {
                         currentRow = i;
                         break;
                     }
                 }
             }
         }

         ColumnLayout {
             id: tableViewLayout

             anchors.top: parent.top
             anchors.left: parent.left

             HorizontalHeaderView {
                 id: headerView
                 readonly property var columnNames: ["Month", "Expenses", "Income"]

                 syncView: tableView
                 Layout.fillWidth: true
                 delegate: Text {
                     required property int index
                     padding: 3
                     text: headerView.columnNames[index]
                     color: barGraph.theme.labelTextColor
                     horizontalAlignment: Text.AlignHCenter
                     verticalAlignment: Text.AlignVCenter
                     elide: Text.ElideRight
                 }
             }

             TableView {
                 id: tableView
                 Layout.fillWidth: true
                 Layout.fillHeight: true

                 reuseItems: false
                 clip: true

                 model: TableModel {
                     id: tableModel
                     TableModelColumn { display: "timestamp" }
                     TableModelColumn { display: "expenses" }
                     TableModelColumn { display: "income" }

                     rows: graphData.modelAsJsArray
                 }

                 delegate: Rectangle {
                     id: delegateRoot
                     required property int row
                     required property int column
                     required property string display
                     implicitHeight: 30
                     implicitWidth: column === 0 ? tableView.width / 2 : tableView.width / 4
                     color: row === mainview.currentRow ? barGraph.theme.gridLineColor
                                                        : barGraph.theme.windowColor
                     border.color: row === mainview.currentRow ? barGraph.theme.labelTextColor
                                                               : barGraph.theme.gridLineColor
                     border.width: 1
                     MouseArea {
                         anchors.fill: parent
                         onClicked: mainview.currentRow = delegateRoot.row;
                     }

                     Text {
                         id: delegateText
                         anchors.verticalCenter: parent.verticalCenter
                         width: parent.width
                         anchors.leftMargin: 4
                         anchors.left: parent.left
                         anchors.right: parent.right
                         text: formattedText
                         property string formattedText: {
                             if (delegateRoot.column === 0) {
                                 if (delegateRoot.display !== "") {
                                     var pattern = /(\d\d\d\d)-(\d\d)/;
                                     var matches = pattern.exec(delegateRoot.display);
                                     var colIndex = parseInt(matches[2], 10) - 1;
                                     return matches[1] + " - " + graphAxes.column.labels[colIndex];
                                 }
                             } else {
                                 return delegateRoot.display;
                             }
                         }
                         color: barGraph.theme.labelTextColor
                         horizontalAlignment: delegateRoot.column === 0 ? Text.AlignLeft
                                                                        : Text.AlignHCenter
                         elide: Text.ElideRight
                     }
                 }
             }
         }

         onCurrentRowChanged: {
             var timestamp = graphData.model.get(mainview.currentRow).timestamp;
             var pattern = /(\d\d\d\d)-(\d\d)/;
             var matches = pattern.exec(timestamp);
             var rowIndex = modelProxy.rowCategoryIndex(matches[1]);
             var colIndex;
             if (barGraph.columnAxis == graphAxes.total)
                 colIndex = 0 ;// Just one column when showing yearly totals
             else
                 colIndex = modelProxy.columnCategoryIndex(matches[2]);
             if (selectedSeries.visible)
                 mainview.selectedSeries.selectedBar = Qt.point(rowIndex, colIndex);
             else if (barSeries.visible)
                 barSeries.selectedBar = Qt.point(rowIndex, colIndex);
             else
                 secondarySeries.selectedBar = Qt.point(rowIndex, colIndex);
         }

         ColumnLayout {
             id: controlLayout
             spacing: 0

             Button {
                 id: changeDataButton
                 Layout.fillWidth: true
                 Layout.fillHeight: true
                 text: "Show 2020 - 2022"
                 clip: true
                 onClicked: {
                     if (text === "Show yearly totals") {
                         modelProxy.autoRowCategories = true;
                         secondaryProxy.autoRowCategories = true;
                         modelProxy.columnRolePattern = /^.*$/;
                         secondaryProxy.columnRolePattern = /^.*$/;
                         graphAxes.value.autoAdjustRange = true;
                         barGraph.columnAxis = graphAxes.total;
                         text = "Show all years";
                     } else if (text === "Show all years") {
                         modelProxy.autoRowCategories = true;
                         secondaryProxy.autoRowCategories = true;
                         modelProxy.columnRolePattern = /^.*-(\d\d)$/;
                         secondaryProxy.columnRolePattern = /^.*-(\d\d)$/;
                         graphAxes.value.min = 0;
                         graphAxes.value.max = 35;
                         barGraph.columnAxis = graphAxes.column;
                         text = "Show 2020 - 2022";
                     } else { // text === "Show 2020 - 2022"
                         // Explicitly defining row categories, since we do not want to show data for
                         // all years in the model, just for the selected ones.
                         modelProxy.autoRowCategories = false;
                         secondaryProxy.autoRowCategories = false;
                         modelProxy.rowCategories = ["2020", "2021", "2022"];
                         secondaryProxy.rowCategories = ["2020", "2021", "2022"];
                         text = "Show yearly totals";
                     }
                 }

                 contentItem: Text {
                     text: changeDataButton.text
                     opacity: changeDataButton.enabled ? 1.0 : 0.3
                     color: barGraph.theme.labelTextColor
                     horizontalAlignment: Text.AlignHCenter
                     verticalAlignment: Text.AlignVCenter
                     elide: Text.ElideRight
                 }

                 background: Rectangle {
                     opacity: changeDataButton.enabled ? 1 : 0.3
                     color: changeDataButton.down ? barGraph.theme.gridLineColor : barGraph.theme.windowColor
                     border.color: changeDataButton.down ? barGraph.theme.labelTextColor : barGraph.theme.gridLineColor
                     border.width: 1
                     radius: 2
                 }
             }

             Button {
                 id: shadowToggle
                 Layout.fillWidth: true
                 Layout.fillHeight: true
                 text: "Hide Shadows"
                 clip: true
                 onClicked: {
                     if (barGraph.shadowQuality == AbstractGraph3D.ShadowQuality.None) {
                         barGraph.shadowQuality = AbstractGraph3D.ShadowQuality.SoftHigh;
                         text = "Hide Shadows";
                     } else {
                         barGraph.shadowQuality = AbstractGraph3D.ShadowQuality.None;
                         text = "Show Shadows";
                     }
                 }
                 contentItem: Text {
                     text: shadowToggle.text
                     opacity: shadowToggle.enabled ? 1.0 : 0.3
                     color: barGraph.theme.labelTextColor
                     horizontalAlignment: Text.AlignHCenter
                     verticalAlignment: Text.AlignVCenter
                     elide: Text.ElideRight
                 }

                 background: Rectangle {
                     opacity: shadowToggle.enabled ? 1 : 0.3
                     color: shadowToggle.down ? barGraph.theme.gridLineColor : barGraph.theme.windowColor
                     border.color: shadowToggle.down ? barGraph.theme.labelTextColor : barGraph.theme.gridLineColor
                     border.width: 1
                     radius: 2
                 }
             }

             Button {
                 id: seriesToggle
                 Layout.fillWidth: true
                 Layout.fillHeight: true
                 text: "Show Expenses"
                 clip: true
                 onClicked: {
                     if (text === "Show Expenses") {
                         barSeries.visible = false;
                         secondarySeries.visible = true;
                         barGraph.valueAxis.labelFormat = "-%.2f M\u20AC";
                         secondarySeries.itemLabelFormat = "Expenses, @colLabel, @rowLabel: @valueLabel";
                         text = "Show Both";
                     } else if (text === "Show Both") {
                         barSeries.visible = true;
                         barGraph.valueAxis.labelFormat = "%.2f M\u20AC";
                         secondarySeries.itemLabelFormat = "Expenses, @colLabel, @rowLabel: -@valueLabel";
                         text = "Show Income";
                     } else { // text === "Show Income"
                         secondarySeries.visible = false;
                         text = "Show Expenses";
                     }
                 }
                 contentItem: Text {
                     text: seriesToggle.text
                     opacity: seriesToggle.enabled ? 1.0 : 0.3
                     color: barGraph.theme.labelTextColor
                     horizontalAlignment: Text.AlignHCenter
                     verticalAlignment: Text.AlignVCenter
                     elide: Text.ElideRight
                 }

                 background: Rectangle {
                     opacity: seriesToggle.enabled ? 1 : 0.3
                     color: seriesToggle.down ? barGraph.theme.gridLineColor : barGraph.theme.windowColor
                     border.color: seriesToggle.down ? barGraph.theme.labelTextColor : barGraph.theme.gridLineColor
                     border.width: 1
                     radius: 2
                 }
             }

             Button {
                 id: marginToggle
                 Layout.fillWidth: true
                 Layout.fillHeight: true
                 text: "Use Margin"
                 clip: true

                 onClicked: {
                     if (text === "Use Margin") {
                         barGraph.barSeriesMargin = Qt.size(0.2, 0.2);
                         barGraph.barSpacing = Qt.size(0.0, 0.0);
                         text = "Use Spacing"
                     } else if (text === "Use Spacing") {
                         barGraph.barSeriesMargin = Qt.size(0.0, 0.0);
                         barGraph.barSpacing = Qt.size(0.5, 0.5);
                         text = "Use Margin";
                     }
                 }
                 contentItem: Text {
                     text: marginToggle.text
                     opacity: marginToggle.enabled ? 1.0 : 0.3
                     color: barGraph.theme.labelTextColor
                     horizontalAlignment: Text.AlignHCenter
                     verticalAlignment: Text.AlignVCenter
                     elide: Text.ElideRight
                 }

                 background: Rectangle {
                     opacity: marginToggle.enabled ? 1 : 0.3
                     color: marginToggle.down ? barGraph.theme.gridLineColor : barGraph.theme.windowColor
                     border.color: marginToggle.down ? barGraph.theme.labelTextColor : barGraph.theme.gridLineColor
                     border.width: 1
                     radius: 2
                 }
             }
         }

         Item {
             id: dataView
             anchors.right: mainview.right
             anchors.bottom: mainview.bottom

             Bars3D {
                 id: barGraph
                 anchors.fill: parent
                 shadowQuality: AbstractGraph3D.ShadowQuality.SoftHigh
                 selectionMode: AbstractGraph3D.SelectionItem
                 theme: Theme3D {
                     type: Theme3D.Theme.Ebony
                     labelBorderEnabled: true
                     font.pointSize: 35
                     labelBackgroundEnabled: true
                     colorStyle: Theme3D.ColorStyle.RangeGradient
                     singleHighlightGradient: customGradient

                     Gradient {
                         id: customGradient
                         GradientStop { position: 1.0; color: "#FFFF00" }
                         GradientStop { position: 0.0; color: "#808000" }
                     }
                 }
                 barThickness: 0.7
                 barSpacing: Qt.size(0.5, 0.5)
                 barSpacingRelative: false
                 cameraPreset: AbstractGraph3D.CameraPreset.IsometricLeftHigh
                 columnAxis: graphAxes.column
                 rowAxis: graphAxes.row
                 valueAxis: graphAxes.value

                 Bar3DSeries {
                     id: secondarySeries
                     visible: false
                     itemLabelFormat: "Expenses, @colLabel, @rowLabel: -@valueLabel"
                     baseGradient: secondaryGradient

                     ItemModelBarDataProxy {
                         id: secondaryProxy
                         itemModel: graphData.model
                         rowRole: "timestamp"
                         columnRole: "timestamp"
                         valueRole: "expenses"
                         rowRolePattern: /^(\d\d\d\d).*$/
                         columnRolePattern: /^.*-(\d\d)$/
                         valueRolePattern: /-/
                         rowRoleReplace: "\\1"
                         columnRoleReplace: "\\1"
                         multiMatchBehavior: ItemModelBarDataProxy.MultiMatchBehavior.Cumulative
                     }

                     Gradient {
                         id: secondaryGradient
                         GradientStop { position: 1.0; color: "#FF0000" }
                         GradientStop { position: 0.0; color: "#600000" }
                     }

                     onSelectedBarChanged: (position) => mainview.handleSelectionChange(secondarySeries,
                                                                                        position);
                 }

                 Bar3DSeries {
                     id: barSeries
                     itemLabelFormat: "Income, @colLabel, @rowLabel: @valueLabel"
                     baseGradient: barGradient

                     ItemModelBarDataProxy {
                         id: modelProxy
                         itemModel: graphData.model
                         rowRole: "timestamp"
                         columnRole: "timestamp"
                         valueRole: "income"
                         rowRolePattern: /^(\d\d\d\d).*$/
                         columnRolePattern: /^.*-(\d\d)$/
                         rowRoleReplace: "\\1"
                         columnRoleReplace: "\\1"
                         multiMatchBehavior: ItemModelBarDataProxy.MultiMatchBehavior.Cumulative
                     }

                     Gradient {
                         id: barGradient
                         GradientStop { position: 1.0; color: "#00FF00" }
                         GradientStop { position: 0.0; color: "#006000" }
                     }

                     onSelectedBarChanged: (position) => mainview.handleSelectionChange(barSeries,
                                                                                        position);
                 }
             }
         }

         states: [
             State  {
                 name: "landscape"
                 PropertyChanges {
                     target: dataView
                     width: mainview.width / 4 * 3
                     height: mainview.height
                 }
                 PropertyChanges  {
                     target: tableViewLayout
                     height: mainview.height - buttonLayoutHeight
                     anchors.right: dataView.left
                     anchors.left: mainview.left
                     anchors.bottom: undefined
                 }
                 PropertyChanges  {
                     target: controlLayout
                     width: mainview.width / 4
                     height: buttonLayoutHeight
                     anchors.top: tableViewLayout.bottom
                     anchors.bottom: mainview.bottom
                     anchors.left: mainview.left
                     anchors.right: dataView.left
                 }
             },
             State  {
                 name: "portrait"
                 PropertyChanges {
                     target: dataView
                     width: mainview.width
                     height: mainview.width
                 }
                 PropertyChanges  {
                     target: tableViewLayout
                     height: mainview.width
                     anchors.right: controlLayout.left
                     anchors.left: mainview.left
                     anchors.bottom: dataView.top
                 }
                 PropertyChanges  {
                     target: controlLayout
                     width: mainview.height / 4
                     height: mainview.width / 4
                     anchors.top: mainview.top
                     anchors.bottom: dataView.top
                     anchors.left: undefined
                     anchors.right: mainview.right
                 }
             }
         ]
     }