feat(StatusChart): Adding chart component (#893)

Needed for https://github.com/status-im/status-desktop/issues/6490
This commit is contained in:
Alexandra Betouni 2022-09-14 14:21:59 +03:00 committed by Michał Cieślak
parent 44b6cda99a
commit 8be1af7059
8 changed files with 21291 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -307,6 +307,11 @@ StatusWindow {
selected: viewLoader.source.toString().includes(title)
onClicked: mainPageView.page(title, true);
}
StatusNavigationListItem {
title: "StatusChartPanel"
selected: viewLoader.source.toString().includes(title)
onClicked: mainPageView.page(title, true);
}
StatusListSectionHeadline { text: "StatusQ.Popup" }
StatusNavigationListItem {
title: "StatusPopupMenu"

View File

@ -0,0 +1,193 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import Sandbox 0.1
Item {
QtObject {
id: d
//dummy data
property real stepSize: 1000
property real minStep: 12000
property real maxStep: 22000
property var graphTabsModel: [{text: "Price", enabled: true}, {text: "Balance", enabled: false}]
property var timeRangeTabsModel: [{text: "1H", enabled: true},
{text: "1D", enabled: true},{text: "7D", enabled: true},
{text: "1M", enabled: true}, {text: "6M", enabled: true},
{text: "1Y", enabled: true}, {text: "ALL", enabled: true}]
property var simTimer: Timer {
running: true
interval: 3000
repeat: true
onTriggered: {
d.generateData();
}
}
function minutes(minutes = 0) {
var newMinute = new Date(new Date().getTime() - (minutes * 60 * 1000)).toString();
if (newMinute.slice(10,12) === "00") {
var dateToday = new Date(Date.now()).toString();
return dateToday.slice(4,7) + " " + dateToday.slice(8,10);
}
return newMinute.slice(10,16);
}
function hour(hours = 0) {
var newHour = new Date(new Date().getTime() - (hours * 60 * 60 * 1000)).toString();
if (newHour.slice(10,12) === "00") {
var dateToday = new Date(Date.now()).toString();
return dateToday.slice(4,7) + " " + dateToday.slice(8,10);
}
return newHour.slice(10,16);
}
function day(before = 0) {
var newDay = new Date(Date.now() - before * 24 * 60 * 60 * 1000).toString();
return newDay.slice(4,7) + " " + newDay.slice(8,10);
}
function month(before = 0) {
var newMonth = new Date(Date.now() - before * 24 * 60 * 60 * 1000).toString();
return newMonth.slice(4,7) + " '" + newMonth.slice(newMonth.indexOf("G")-3, newMonth.indexOf("G")-1);
}
property var timeRange: [
{'1H': [minutes(60), minutes(55), minutes(50), minutes(45), minutes(40), minutes(35), minutes(30), minutes(25), minutes(20), minutes(15), minutes(10), minutes(5), minutes()]},
{'1D': [hour(24), hour(23), hour(22), hour(21), hour(20), hour(19), hour(18), hour(17), hour(16), hour(15), hour(14), hour(13),
hour(12), hour(11), hour(10), hour(9), hour(8), hour(7), hour(6), hour(5), hour(4), hour(3), hour(2), hour(1), hour()]},
{'7D': [day(6), day(5), day(4), day(3), day(2), day(1), day()]},
{'1M': [day(30), day(28), day(26), day(24), day(22), day(20), day(18), day(16), day(14), day(12), day(10), day(8), day(6), day(4), day()]},
{'6M': [month(150), month(120), month(90), month(60), month(30), month()]},
{'1Y': [month(330), month(300), month(270), month(240), month(210), month(180), month(150), month(120), month(90), month(60), month(30), month()]},
{'ALL': ['2016', '2017', '2018', '2019', '2020', '2021', '2022']}
]
function generateData() {
var result = [];
for (var i = 0; i < timeRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange].length; ++i) {
result[i] = Math.random() * (maxStep - minStep) + minStep;
}
graphDetail.chart.chartData.datasets[0].data = result;
graphDetail.chart.animateToNewData();
}
}
StatusChartPanel {
id: graphDetail
height: 290
anchors.left: parent.left
anchors.leftMargin: 24
anchors.right: parent.right
anchors.rightMargin: 24
anchors.verticalCenter: parent.verticalCenter
graphsModel: d.graphTabsModel
timeRangeModel: d.timeRangeTabsModel
onHeaderTabClicked: {
//TODO
//if time range tab
d.generateData();
//if graph bar
//switch graph
}
chart.chartType: 'line'
chart.chartData: {
return {
labels: d.timeRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange],
datasets: [{
label: 'Price',
xAxisId: 'x-axis-1',
yAxisId: 'y-axis-1',
backgroundColor: (Theme.palette.name === "dark") ? 'rgba(136, 176, 255, 0.2)' : 'rgba(67, 96, 223, 0.2)',
borderColor: (Theme.palette.name === "dark") ? 'rgba(136, 176, 255, 1)' : 'rgba(67, 96, 223, 1)',
borderWidth: 3,
pointRadius: 0,
//data: d.generateData()
}]
}
}
chart.chartOptions: {
return {
maintainAspectRatio: false,
responsive: true,
legend: {
display: false
},
//TODO enable zoom
// zoom: {
// enabled: true,
// drag: true,
// speed: 0.1,
// threshold: 2
// },
// pan:{enabled:true,mode:'x'},
tooltips: {
intersect: false,
displayColors: false,
callbacks: {
footer: function(tooltipItem, data) { return 'Vol: $43,042,678,876'; },
label: function(tooltipItem, data) {
let label = data.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
label += tooltipItem.yLabel.toFixed(2);
return label.slice(0,label.indexOf(":")+1)+ " $"+label.slice(label.indexOf(":")+2, label.length);
}
}
},
scales: {
xAxes: [{
id: 'x-axis-1',
position: 'bottom',
gridLines: {
drawOnChartArea: false,
drawBorder: false,
drawTicks: false,
},
ticks: {
fontSize: 10,
fontColor: (Theme.palette.name === "dark") ? '#909090' : '#939BA1',
padding: 16,
}
}],
yAxes: [{
position: 'left',
id: 'y-axis-1',
gridLines: {
borderDash: [8, 4],
drawBorder: false,
drawTicks: false,
color: (Theme.palette.name === "dark") ? '#909090' : '#939BA1'
},
beforeDataLimits: (axis) => {
axis.paddingTop = 25;
axis.paddingBottom = 0;
},
ticks: {
fontSize: 10,
fontColor: (Theme.palette.name === "dark") ? '#909090' : '#939BA1',
padding: 24,
min: d.minStep,
max: d.maxStep,
stepSize: d.stepSize,
callback: function(value, index, ticks) {
return '$' + value;
},
}
}]
}
}
}
}
}

View File

@ -0,0 +1,144 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import StatusQ.Controls 0.1
import "private/chart"
/*!
\qmltype StatusChartPanel
\inherits Page
\inqmlmodule StatusQ.Components
\since StatusQ.Components 0.1
\brief Displays a chart component together with an optional header in order to add a list of options.
Inherits \l{https://doc.qt.io/qt-5/qml-qtquick-controls2-page.html}{Page}.
The \c StatusChartPanel displays a customizable chart component. Additionally, options
can be added in the header in both right and left sides given graphTabsModel and timeRangeTabsModel
property are set respectively.
For example:
\qml
StatusChartPanel {
id: graphDetail
width: parent.width
height: 290
anchors.verticalCenter: parent.verticalCenter
graphsModel: d.graphTabsModel
timeRangeModel: d.timeRangeTabsModel
chart.chartType: 'line'
chart.chartData: {
return {
datasets: [{
data: d.data
}],
...
}
}
chart.chartOptions: {
return {
legend: {
display: false
},
...
}
}
}
\endqml
\image status_chart_panel.png
For a list of components available see StatusQ.
*/
Page {
id: root
/*!
\qmlproperty real StatusChartPanel::graphsModel
This property holds the graphs model options to be set on the left side tab bar of the header.
*/
property var graphsModel
/*!
\qmlproperty real StatusChartPanel::timeRangeModel
This property holds the time range options to be set on the right side tab bar of the header.
*/
property var timeRangeModel
/*!
\qmlproperty real StatusChartPanel::graphComponent
This property holds holds a reference to the graph component.
*/
property alias chart: graphComponent
/*!
\qmlproperty real StatusChartPanel::timeRangeTabBarIndex
This property holds holds a reference to the time range tab bar current index.
*/
property alias timeRangeTabBarIndex: timeRangeTabBar.currentIndex
/*!
\qmlproperty real StatusChartPanel::selectedTimeRange
This property holds holds the text of the current time range tab bar selected tab.
*/
property string selectedTimeRange: timeRangeTabBar.currentItem.text
/*!
\qmlsignal
This signal is emitted when a header tab bar is clicked.
*/
signal headerTabClicked(string text)
Component {
id: tabButton
StatusTabButton {
leftPadding: 0
width: implicitWidth
onClicked: {
root.headerTabClicked(text);
}
}
}
Component.onCompleted: {
if (!!timeRangeModel) {
for (var i = 0; i < timeRangeModel.length; i++) {
var timeTab = tabButton.createObject(root, { text: qsTr(timeRangeModel[i].text.toString()),
enabled: timeRangeModel[i].enabled });
timeRangeTabBar.addItem(timeTab);
}
}
if (!!graphsModel) {
for (var j = 0; j < graphsModel.length; j++) {
var graphTab = tabButton.createObject(root, { text: qsTr(graphsModel[j].text.toString()),
enabled: graphsModel[j].enabled });
graphsTabBar.addItem(graphTab);
}
}
}
background: null
header: Item {
height: childrenRect.height
RowLayout {
anchors.left: parent.left
anchors.leftMargin: 40
anchors.right: parent.right
StatusTabBar {
id: graphsTabBar
}
StatusTabBar {
id: timeRangeTabBar
Layout.alignment: Qt.AlignRight
}
}
}
contentItem: Item {
Chart {
id: graphComponent
anchors.fill: parent
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,139 @@
/*!
* Elypson's Chart.qml adaptor to Chart.js
* (c) 2020 ChartJs2QML contributors (starting with Elypson, Michael A. Voelkel, https://github.com/Elypson)
* Released under the MIT License
*/
import QtQuick 2.13
import "Chart.js" as Chart
Canvas {
id: root
property var jsChart: undefined
property string chartType
property var chartData
property var chartOptions
property double chartAnimationProgress: 0.1
property var animationEasingType: Easing.InOutExpo
property double animationDuration: 500
property var memorizedContext
property var memorizedData
property var memorizedOptions
property alias animationRunning: chartAnimator.running
signal animationFinished()
function animateToNewData()
{
chartAnimationProgress = 0.1;
jsChart.update();
chartAnimator.restart();
}
MouseArea {
id: event
anchors.fill: root
hoverEnabled: true
enabled: true
property var handler: undefined
property QtObject mouseEvent: QtObject {
property int left: 0
property int top: 0
property int x: 0
property int y: 0
property int clientX: 0
property int clientY: 0
property string type: ""
property var target
}
function submitEvent(mouse, type) {
mouseEvent.type = type
mouseEvent.clientX = mouse ? mouse.x : 0;
mouseEvent.clientY = mouse ? mouse.y : 0;
mouseEvent.x = mouse ? mouse.x : 0;
mouseEvent.y = mouse ? mouse.y : 0;
mouseEvent.left = 0;
mouseEvent.top = 0;
mouseEvent.target = root;
if(handler) {
handler(mouseEvent);
}
root.requestPaint();
}
onClicked: {
submitEvent(mouse, "click");
}
onPositionChanged: {
submitEvent(mouse, "mousemove");
}
onExited: {
submitEvent(undefined, "mouseout");
}
onEntered: {
submitEvent(undefined, "mouseenter");
}
onPressed: {
submitEvent(mouse, "mousedown");
}
onReleased: {
submitEvent(mouse, "mouseup");
}
}
PropertyAnimation {
id: chartAnimator
target: root
property: "chartAnimationProgress"
alwaysRunToEnd: true
to: 1
duration: root.animationDuration
easing.type: root.animationEasingType
onFinished: {
root.animationFinished();
}
}
onChartAnimationProgressChanged: {
root.requestPaint();
}
onPaint: {
if(root.getContext('2d') != null && memorizedContext != root.getContext('2d') || memorizedData != root.chartData || memorizedOptions != root.chartOptions) {
var ctx = root.getContext('2d');
jsChart = new Chart.build(ctx, {
type: root.chartType,
data: root.chartData,
options: root.chartOptions
});
memorizedData = root.chartData ;
memorizedContext = root.getContext('2d');
memorizedOptions = root.chartOptions;
root.jsChart.bindEvents(function(newHandler) {event.handler = newHandler;});
chartAnimator.start();
}
jsChart.draw(chartAnimationProgress);
}
onWidthChanged: {
if(jsChart) {
jsChart.resize();
}
}
onHeightChanged: {
if(jsChart) {
jsChart.resize();
}
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 ChartJs2QML contributors (starting with Elypson, Michael A. Voelkel, https://github.com/Elypson)
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 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.

View File

@ -43,3 +43,5 @@ StatusCommunityTags 0.1 StatusCommunityTags.qml
StatusItemSelector 0.1 StatusItemSelector.qml
StatusCard 0.1 StatusCard.qml
StatusDatePicker 0.1 StatusDatePicker.qml
StatusChart 0.1 StatusChart.qml
StatusChartPanel 0.1 StatusChartPanel.qml