// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtDataVisualization 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; } } } } Item { id: dataView anchors.right: mainview.right anchors.bottom: mainview.bottom Bars3D { id: barGraph anchors.fill: parent shadowQuality: AbstractGraph3D.ShadowQualitySoftHigh selectionMode: AbstractGraph3D.SelectionItem theme: Theme3D { type: Theme3D.ThemeEbony labelBorderEnabled: true font.pointSize: 35 labelBackgroundEnabled: true colorStyle: Theme3D.ColorStyleRangeGradient singleHighlightGradient: customGradient ColorGradient { id: customGradient ColorGradientStop { position: 1.0; color: "#FFFF00" } ColorGradientStop { position: 0.0; color: "#808000" } } } barThickness: 0.7 barSpacing: Qt.size(0.5, 0.5) barSpacingRelative: false scene.activeCamera.cameraPreset: Camera3D.CameraPresetIsometricLeftHigh columnAxis: graphAxes.column rowAxis: graphAxes.row valueAxis: graphAxes.value //! [4] 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.MMBCumulative } //! [4] ColorGradient { id: secondaryGradient ColorGradientStop { position: 1.0; color: "#FF0000" } ColorGradientStop { position: 0.0; color: "#600000" } } onSelectedBarChanged: (position) => mainview.handleSelectionChange(secondarySeries, position); } //! [3] 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.MMBCumulative } //! [3] ColorGradient { id: barGradient ColorGradientStop { position: 1.0; color: "#00FF00" } ColorGradientStop { position: 0.0; color: "#006000" } } onSelectedBarChanged: (position) => mainview.handleSelectionChange(barSeries, position); } } } 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 } } } } //! [2] 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); } //! [2] ColumnLayout { id: controlLayout spacing: 0 Button { id: changeDataButton Layout.fillWidth: true Layout.fillHeight: true text: "Show 2020 - 2022" clip: true //! [1] 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"; } } //! [1] 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: barGraph.shadowsSupported ? "Hide Shadows" : "Shadows not supported" clip: true enabled: barGraph.shadowsSupported onClicked: { if (barGraph.shadowQuality == AbstractGraph3D.ShadowQualityNone) { barGraph.shadowQuality = AbstractGraph3D.ShadowQualitySoftHigh; text = "Hide Shadows"; } else { barGraph.shadowQuality = AbstractGraph3D.ShadowQualityNone; 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 //! [0] 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"; } } //! [0] 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 } } } 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 } } ] }