feat: Add initial support for ChartJs plugins (#14433)

+ adding plugin for crosshair and zoom
+ adding plugin for data labels
This commit is contained in:
Alex Jbanca 2024-06-04 13:08:16 +03:00 committed by GitHub
parent aa03edf17c
commit f1308f3b28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 23640 additions and 21111 deletions

View File

@ -6,9 +6,12 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import Storybook 1.0
import utils 1.0
SplitView {
id: root
@ -27,15 +30,6 @@ SplitView {
{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") {
@ -80,64 +74,29 @@ SplitView {
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();
return result;
}
}
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
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 {
readonly property var lineConfig: {
return {
type: 'line',
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()
}],
options: {
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,
@ -157,6 +116,7 @@ SplitView {
xAxes: [{
id: 'x-axis-1',
position: 'bottom',
//type: 'linear',
gridLines: {
drawOnChartArea: false,
drawBorder: false,
@ -169,34 +129,301 @@ SplitView {
}
}],
yAxes: [{
position: 'left',
id: 'y-axis-1',
gridLines: {
borderDash: [8, 4],
drawBorder: false,
drawTicks: false,
color: (Theme.palette.name === "dark") ? '#909090' : '#939BA1'
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: 8,
min: d.minStep,
max: d.maxStep,
stepSize: d.stepSize,
callback: function(value, index, ticks) {
return '$' + value;
},
beforeDataLimits: (axis) => {
axis.paddingTop = 25;
axis.paddingBottom = 0;
},
ticks: {
fontSize: 10,
fontColor: (Theme.palette.name === "dark") ? '#909090' : '#939BA1',
padding: 8,
min: d.minStep,
max: d.maxStep,
stepSize: d.stepSize,
callback: function(value, index, ticks) {
return '$' + value;
},
}
}]
}
}]
}
}
}
}
readonly property var barConfig: {
return {
type:"bar",
options: {
onHover: function(event, activeElements) {
if (activeElements.length === 0) {
toolTip.close()
return
}
toolTip.text = "StatusMenu triggered by " + activeElements[0]._model.label
toolTip.popup()
toolTip.x += 10
toolTip.y -= toolTip.height + 10
},
tooltips: {
enabled:false
},
scales:{
xAxes:[{
id: "x-axis-1",
position: "bottom",
stacked: false,
gridLines: {
drawOnChartArea: false,
drawBorder: false,
drawTicks: false
},
ticks: {
fontSize:10,
fontColor: "#939ba1",
padding:16
}
}],
yAxes: [{
position: "left",
id: "y-axis-1",
stacked: false,
gridLines: {
borderDash: [5,3],
lineWidth: 1,
drawBorder: false,
drawTicks: false,
color: "#33939ba1"
},
ticks: {
fontSize: 10,
fontColor: "#939ba1",
padding: 8,
maxTicksLimit: 10,
beginAtZero: true,
stepSize: 1
}
}]
}
},
labels:["16:40","16:50","17:00","17:10","17:20","17:30"],
datasets: [{
xAxisId: "x-axis-1",
yAxisId: "y-axis-1",
backgroundColor: "#334360df",
pointRadius: 0,
hoverBackgroundColor: "#334360df",
hoverBorderColor: "#4360df",
hoverBorderWidth: 2,
data: [8,3,5,4,3,10]
}]
}
}
readonly property var crosshairConfig: {
//binding to regenerate data and reset zoom
chartType.currentText
const generateDataset = (shift, label, color) => {
var data = [];
var x = 0;
while (x < 30) {
data.push({ x: x, y: Math.sin(shift + x / 3) });
x += Math.random();
}
var dataset = {
backgroundColor: color,
borderColor: color,
showLine: true,
fill: false,
pointRadius: 2,
label: label,
data: data,
lineTension: 0,
interpolate: true
};
return dataset;
}
return {
type: "scatter",
options: {
plugins: {
crosshair: {
enabled: true,
sync: {
enabled: false
}
}
},
tooltips: {
mode: "interpolate",
intersect: true,
},
scales: {
xAxes: [{
id: 'x-axis-1',
}],
yAxes: [{
position: 'left',
id: 'y-axis-1',
}]
}
},
data: {
datasets: [
generateDataset(0, "A", "red"),
generateDataset(1, "B", "green"),
generateDataset(2, "C", "blue")
]
}
};
}
readonly property var minimisedConfig: {
let config = Object.assign({}, d.lineConfig)
config.datasets = [{
label: 'Price',
xAxisId: 'x-axis-1',
yAxisId: 'y-axis-1',
backgroundColor: "transparent",
borderColor: (Theme.palette.name === "dark") ? 'rgba(136, 176, 255, 1)' : 'rgba(67, 96, 223, 1)',
borderWidth: 3,
pointRadius: 0,
data: d.generateData()
}]
config.options = Object.assign({}, d.lineConfig.options)
config.options.scales = {
xAxes: [{
id: 'x-axis-1',
display: false
}],
yAxes: [{
id: 'y-axis-1',
display: false
}]
}
return config;
}
readonly property var dataLabelsConfig: {
var DATA_COUNT = 8;
var labels = [];
for (var i = 0; i < DATA_COUNT; ++i) {
labels.push('' + i);
}
return {
type: 'line',
labels: labels,
datasets: [{
backgroundColor: "blue",
borderColor: "green",
data: [5, 10, 15, 10, 5, 0, 5, 10],
datalabels: {
align: 'start',
anchor: 'start'
}
}, {
backgroundColor:"red",
borderColor:"orance",
data: [58, 80, 60, 70, 50, 60, 70, 80],
}, {
backgroundColor: "yellow",
borderColor: "green",
data: [30, 40, 30, 40, 30, 40, 30, 40],
datalabels: {
align: 'end',
anchor: 'end'
}
}],
options: {
plugins: {
datalabels: {
backgroundColor: "red",
borderRadius: 4,
color: 'white',
font: {
weight: 'bold',
size: 12
},
formatter: Math.round,
padding: 6
}
},
// Core options
aspectRatio: 5 / 3,
layout: {
padding: {
top: 32,
right: 16,
bottom: 16,
left: 8
}
},
elements: {
line: {
fill: false
}
},
scales: {
yAxes: [{
stacked: true
}]
}
}
}
}
}
StatusMenu {
id: toolTip
width: 243 //By design
topPadding: Style.current.padding
bottomPadding: topPadding
leftPadding: topPadding
rightPadding: topPadding
parent: Overlay.overlay
property alias text: label.text
Label {
id: label
text: "Tooltip"
anchors.centerIn: parent
}
}
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
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
chart.type: d.lineConfig.type
chart.labels: d.lineConfig.labels
chart.datasets: d.lineConfig.datasets
chart.options: d.lineConfig.options
}
}
LogsAndControlsPanel {
@ -204,6 +431,49 @@ SplitView {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 150
ComboBox {
id: chartType
model: d.lineConfig ? ["line", "bar", "with crosshair", "line minimised", "data labels"] : []
currentIndex: 0
onCurrentTextChanged: {
if (chartType.currentText === "line") {
graphDetail.chart.type = d.lineConfig.type;
graphDetail.chart.labels = d.lineConfig.labels;
graphDetail.chart.datasets = d.lineConfig.datasets;
graphDetail.chart.options = d.lineConfig.options;
graphDetail.chart.plugins = []
graphDetail.chart.rebuild();
} else if (chartType.currentText === "bar") {
graphDetail.chart.type = d.barConfig.type;
graphDetail.chart.labels = d.barConfig.labels;
graphDetail.chart.datasets = d.barConfig.datasets;
graphDetail.chart.options = d.barConfig.options;
graphDetail.chart.plugins = []
graphDetail.chart.rebuild();
} else if (chartType.currentText === "with crosshair") {
graphDetail.chart.type = d.crosshairConfig.type;
graphDetail.chart.options = d.crosshairConfig.options;
graphDetail.chart.datasets = d.crosshairConfig.data.datasets;
graphDetail.chart.plugins = []
graphDetail.chart.rebuild();
} else if (chartType.currentText === "line minimised") {
graphDetail.chart.type = d.minimisedConfig.type;
graphDetail.chart.labels = d.minimisedConfig.labels;
graphDetail.chart.datasets = d.minimisedConfig.datasets;
graphDetail.chart.options = d.minimisedConfig.options;
graphDetail.chart.plugins = []
graphDetail.chart.rebuild();
} else if (chartType.currentText === "data labels") {
graphDetail.chart.type = d.dataLabelsConfig.type;
graphDetail.chart.datasets = d.dataLabelsConfig.datasets;
graphDetail.chart.labels = d.dataLabelsConfig.labels;
graphDetail.chart.options = d.dataLabelsConfig.options;
graphDetail.chart.plugins = [graphDetail.chart.availablePlugins.datalabels]
graphDetail.chart.rebuild();
}
}
}
}
}

View File

@ -75,8 +75,8 @@ Item {
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();
graphDetail.chart.datasets[0].data = result;
graphDetail.chart.refresh();
}
}
@ -97,24 +97,21 @@ Item {
//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.type: 'line'
chart.labels: d.timeRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange]
chart.datasets: {
return [{
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
}]
}
chart.chartOptions: {
chart.options: {
return {
maintainAspectRatio: false,
responsive: true,

View File

@ -170,7 +170,7 @@ Page {
}
contentItem: Item {
Chart {
ChartCanvas {
id: graphComponent
anchors.fill: parent
}

View File

@ -1,148 +0,0 @@
/*!
* 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 updateToNewData()
{
if(!jsChart) return
jsChart.update('none');
root.requestPaint();
}
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,155 @@
import QtQuick 2.15
import "./Library/Library.js" as Lib
Canvas {
id: canvas
readonly property var availablePlugins: {
return { datalabels: ChartDataLabels }
}
property string type: chartType
property var options: chartOptions
property var plugins: []
property var labels: []
property var datasets: []
signal resized()
function refresh() {
if (d.instance) {
Qt.callLater(d.refresh)
}
}
function rebuild() {
if (available) {
Qt.callLater(d.rebuild)
}
}
// [WORKAROUND] context.lineWidth > 1 makes the scene graph polish step very slow
// in case of "Image" render target, so by default let's draw with OpenGL when
// possible (which seems only possible with "Cooperative" strategy).
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
// https://www.w3.org/TR/2012/WD-html5-author-20120329/the-canvas-element.html#the-canvas-element
implicitHeight: 150
implicitWidth: 300
// [polyfill] Element
readonly property alias clientHeight: canvas.height
readonly property alias clientWidth: canvas.width
// [polyfill] canvas.style
readonly property var style: ({
height: canvas.height,
width: canvas.width
})
// [polyfill] element.getBoundingClientRect
// https://developer.mozilla.org/docs/Web/API/Element/getBoundingClientRect
function getBoundingClientRect() {
return {top: 0, right: canvas.width, bottom: canvas.height, left: 0}
}
/**
\internal object used to forward events to the Chart.js instance
\see Library.js for the list of events
*/
property QtObject _eventSource: QtObject {
signal resized(var event)
signal clicked(var event)
signal positionChanged(var event)
signal entered(var event)
signal exited(var event)
signal pressed(var event)
signal released(var event)
property var connectedHandlers: []
readonly property Connections canvasConn: Connections {
target: canvas
function onResized() {
_eventSource.resized(null)
}
}
readonly property Connections mouseConn: Connections {
target: mouse
function onPositionChanged(event) {
_eventSource.positionChanged(event)
}
function onEntered(event) {
_eventSource.entered(event)
}
function onExited(event) {
_eventSource.exited(event)
}
function onPressed(event) {
_eventSource.pressed(event)
}
function onReleased(event) {
_eventSource.released(event)
}
}
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: enabled
}
onTypeChanged: rebuild()
onOptionsChanged: refresh()
onPluginsChanged: refresh()
onLabelsChanged: refresh()
onDatasetsChanged: rebuild()
onHeightChanged: resized()
onWidthChanged: resized()
onAvailableChanged: {
if (!d.instance) {
rebuild()
}
}
QtObject {
id: d
property var instance: null
function refresh() {
instance.config.options = canvas.options
instance.config.plugins = canvas.plugins
instance.data.labels = canvas.labels
instance.update()
}
function rebuild() {
if (instance) {
instance.destroy()
instance = null
}
var ctx = canvas.getContext('2d');
const config = {
type: canvas.type,
options: canvas.options,
plugins: canvas.plugins,
data: {
labels: canvas.labels,
datasets: canvas.datasets
}
}
instance = new Chart(ctx, config)
}
}
Component.onDestruction: {
if (d.instance) {
d.instance.destroy()
}
}
}

View File

@ -1,21 +0,0 @@
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

@ -0,0 +1,58 @@
import QtQuick 2.15
QtObject {
id: root
property double chartAnimationProgress: 0.1
property int chartAnimationDuration: 500
property var animationEasingType: Easing.InOutExpo
property var _requests: []
signal animationFinished()
readonly property PropertyAnimation animator : PropertyAnimation {
id: chartAnimator
target: root
property: "chartAnimationProgress"
alwaysRunToEnd: true
to: 1
duration: root.chartAnimationDuration
easing.type: root.animationEasingType
onFinished: {
root.chartAnimationProgress = 0.1
root.animationFinished()
}
}
onChartAnimationProgressChanged: {
root.animate();
}
function requestAnimation(callback) {
_requests.push({
callback: callback,
scope: this
})
if (!chartAnimator.running) {
root.chartAnimationProgress = 0.1
chartAnimator.restart();
}
return -1
}
function animate() {
var requests = _requests
var ilen = requests.length
var requestItem = null
var i = 0
_requests = []
for (; i < ilen; ++i) {
requestItem = requests[i]
requestItem.callback.call(requestItem.scope)
}
}
}

View File

@ -0,0 +1,141 @@
.import "Polyfills.js" as Polyfills
.import "Chart.bundle.js" as ChartJs
.import "chartjs-plugin-crosshair.js" as CrosshairLib
.import "chartjs-plugin-datalabels.js" as DataLabelsLib
.import StatusQ.Core.Theme 0.1 as SQTheme
/*!
/This file is used to provide the necessary wrappers for Chart.js to work in QML
/It is loaded after Chart.js and plugins and provides the necessary functions
/NOTE: Some plugins work out of the box, others need to be adapted to work in QML
!*/
(function(global){
Chart.helpers.merge(Chart.defaults.global, {
// Default options
events: [
"mousemove",
"mouseout",
"click"
]
})
// QML rendering plugin
Chart.plugins.register({
afterDraw: function(chart) {
chart.canvas.requestPaint()
}
})
var EVENTS = {
/*chartJS event: QML event*/
click: "clicked",
mousemove: "positionChanged",
mouseenter: "entered",
mouseout: "exited",
mousedown: "pressed",
mouseup: "released",
resize: "resized"
}
function createEvent(type, chart, x, y, native, target) {
return {
type: type,
chart: chart,
native: native || null,
x: x !== undefined ? x : null,
y: y !== undefined ? y : null,
target: target || null,
}
}
// QML platform implementation
Chart.helpers.merge(Chart.platform, {
addEventListener: function(chart, type, listener) {
const mapped = EVENTS[type]
if (!mapped) {
console.warn("Unsupported event:", type)
return
}
const canvas = chart.canvas
const qmlHandler = (event) => {
listener(createEvent(
type,
chart,
event && event.x,
event && event.y,
event,
canvas))
}
canvas._eventSource[mapped].connect(qmlHandler)
canvas._eventSource.connectedHandlers[listener] = qmlHandler
},
removeEventListener: function(chart, type, listener) {
const canvas = chart.canvas
if (!canvas._eventSource.connectedHandlers[listener]) {
return
}
const mapped = EVENTS[type]
const qmlHandler = canvas._eventSource.connectedHandlers[listener]
canvas._eventSource[mapped].disconnect(qmlHandler)
delete canvas._eventSource.connectedHandlers[listener]
}
})
Chart.helpers.merge(Chart.helpers, {
color: function(c) {
return Color(c)
},
getHoverColor: function(c) {
const hoverColor = SQTheme.Theme.palette.hoverColor(c)
if (!hoverColor) {
return Color(c)
}
return hoverColor
}
})
Chart.helpers.merge(Chart.prototype, {
// Resync chart internals with current canvas size, then update
resize: function(silent) {
var me = this
if (!me.canvas) {
return
}
var opts = me.options
var h = Math.max(0, me.canvas.height)
var w = Math.max(0, me.canvas.width)
if (h === me.height && w === me.width) {
return
}
me.height = h
me.width = w
if (silent) {
return
}
// Notify any plugins about the resize
var size = {width: w, height: h}
Chart.plugins.notify(me, "resize", [size])
// Notify of resize
if (opts.onResize) {
opts.onResize(me, newSize)
}
me.stop()
me.update(opts.responsiveAnimationDuration)
}
})
})(this)

View File

@ -0,0 +1,23 @@
.pragma library
/*!
/This file is used to provide the necessary polyfills for Chart.js to work in QML
/It is loaded before Chart.js and fills the global object with the necessary functions
/to make Chart.js work in QML.
/The following polyfills are provided:
/- requestAnimationFrame https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
/- window https://developer.mozilla.org/en-US/docs/Web/API/window
!*/
(function(global){
// ChartJs needs a global object to work. Simulating the window object
global.window = global
var _animator = Qt.createComponent("Animator.qml").createObject()
// TODO: Find a way to use the canvas `requestAnimation`
global.requestAnimationFrame = function(callback) {
return _animator.requestAnimation(callback)
}
})(this)

View File

@ -0,0 +1,702 @@
/*
* @license
* chartjs-plugin-crosshair
* http://abelheinsbroek.nl/
* Version: 1.1.5
*
* Copyright 2024 Abel Heinsbroek
* Released under the MIT license
* https://github.com/abelheinsbroek/chartjs-plugin-crosshair/blob/master/LICENSE.md
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js')) :
typeof define === 'function' && define.amd ? define(['chart.js'], factory) :
(factory(global.Chart));
}(this, (function (Chart) { 'use strict';
Chart = Chart && Chart.hasOwnProperty('default') ? Chart['default'] : Chart;
var Interpolate = function(Chart$$1) {
Chart$$1.Interaction.modes.interpolate = function(chart, e, options) {
var items = [];
for (var datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
// check for interpolate setting
if (!chart.data.datasets[datasetIndex].interpolate) {
continue;
}
var meta = chart.getDatasetMeta(datasetIndex);
// do not interpolate hidden charts
if (meta.hidden) {
continue;
}
var xScale = chart.scales[meta.xAxisID];
var yScale = chart.scales[meta.yAxisID];
if (!xScale || !yScale) {
continue;
}
var xValue = xScale.getValueForPixel(e.x);
var data = chart.data.datasets[datasetIndex].data;
var index = data.findIndex(function(o) {
return o.x >= xValue;
});
if (index === -1) {
continue;
}
// linear interpolate value
var prev = data[index - 1];
var next = data[index];
if (prev && next) {
var slope = (next.y - prev.y) / (next.x - prev.x);
var interpolatedValue = prev.y + (xValue - prev.x) * slope;
}
if (chart.data.datasets[datasetIndex].steppedLine && prev) {
interpolatedValue = prev.y;
}
if (isNaN(interpolatedValue)) {
continue;
}
var yPosition = yScale.getPixelForValue(interpolatedValue);
// do not interpolate values outside of the axis limits
if (isNaN(yPosition)) {
continue;
}
// create a 'fake' event point
var fakePoint = {
value: interpolatedValue,
xValue: xValue,
tooltipPosition: function() {
return this._model;
},
hasValue: function() {
return true;
},
_model: {
x: e.x,
y: yPosition
},
_datasetIndex: datasetIndex,
_index: items.length,
_xScale: {
getLabelForIndex: function(indx) {
return items[indx].xValue;
}
},
_yScale: {
getLabelForIndex: function(indx) {
return items[indx].value;
}
},
_chart: chart
};
items.push(fakePoint);
}
// add other, not interpolated, items
var xItems = Chart$$1.Interaction.modes.x(chart, e, options);
for (index = 0; index < xItems.length; index++) {
var item = xItems[index];
if (!chart.data.datasets[item._datasetIndex].interpolate) {
items.push(item);
}
}
return items;
};
};
var TracePlugin = function(Chart$$1) {
var helpers = Chart$$1.helpers;
var defaultOptions = {
enabled: false,
line: {
color: '#F66',
width: 1,
dashPattern: []
},
sync: {
enabled: false,
group: 1,
suppressTooltips: false
},
zoom: {
enabled: true,
zoomboxBackgroundColor: 'rgba(66,133,244,0.2)',
zoomboxBorderColor: '#48F',
zoomButtonText: 'Reset Zoom',
zoomButtonClass: 'reset-zoom',
},
snap: {
enabled: false,
},
callbacks: {
beforeZoom: function(start, end) {
return true;
},
afterZoom: function(start, end) {
}
}
};
var crosshairPlugin = {
id: 'crosshair',
afterInit: function(chart) {
if (!chart.config.options.scales || chart.config.options.scales.xAxes.length == 0) {
return
}
var xScaleType = chart.config.options.scales.xAxes[0].type;
if (xScaleType !== 'linear' && xScaleType !== 'time' && xScaleType !== 'category' && xscaleType !== 'logarithmic') {
return;
}
if (chart.options.plugins.crosshair === undefined || chart.options.plugins.crosshair.enabled === false) {
return;
}
chart.crosshair = {
enabled: false,
x: null,
originalData: [],
originalXRange: {},
dragStarted: false,
dragStartX: null,
dragEndX: null,
suppressTooltips: false,
reset: function() {
this.resetZoom(chart, false, false);
}.bind(this)
};
var syncEnabled = this.getOption(chart, 'sync', 'enabled');
if (syncEnabled) {
chart.crosshair.syncEventHandler = function(e) {
this.handleSyncEvent(chart, e);
}.bind(this);
chart.crosshair.resetZoomEventHandler = function(e) {
var syncGroup = this.getOption(chart, 'sync', 'group');
if (e.chartId !== chart.id && e.syncGroup === syncGroup) {
this.resetZoom(chart, true);
}
}.bind(this);
window.addEventListener('sync-event', chart.crosshair.syncEventHandler);
window.addEventListener('reset-zoom-event', chart.crosshair.resetZoomEventHandler);
}
chart.panZoom = this.panZoom.bind(this, chart);
},
destroy: function(chart) {
if (!chart.crosshair){
return;
}
var syncEnabled = this.getOption(chart, 'sync', 'enabled');
if (syncEnabled) {
window.removeEventListener('sync-event', chart.crosshair.syncEventHandler);
window.removeEventListener('reset-zoom-event', chart.crosshair.resetZoomEventHandler);
}
},
panZoom: function(chart, increment) {
if (chart.crosshair.originalData.length === 0) {
return;
}
var diff = chart.crosshair.end - chart.crosshair.start;
var min = chart.crosshair.min;
var max = chart.crosshair.max;
if (increment < 0) { // left
chart.crosshair.start = Math.max(chart.crosshair.start + increment, min);
chart.crosshair.end = chart.crosshair.start === min ? min + diff : chart.crosshair.end + increment;
} else { // right
chart.crosshair.end = Math.min(chart.crosshair.end + increment, chart.crosshair.max);
chart.crosshair.start = chart.crosshair.end === max ? max - diff : chart.crosshair.start + increment;
}
this.doZoom(chart, chart.crosshair.start, chart.crosshair.end);
},
getOption: function(chart, category, name) {
if (!chart.crosshair) {
return;
}
return helpers.getValueOrDefault(chart.options.plugins.crosshair[category] ? chart.options.plugins.crosshair[category][name] : undefined, defaultOptions[category][name]);
},
getXScale: function(chart) {
return chart.data.datasets.length ? chart.scales[chart.getDatasetMeta(0).xAxisID] : null;
},
getYScale: function(chart) {
return chart.scales[chart.getDatasetMeta(0).yAxisID];
},
handleSyncEvent: function(chart, e) {
var syncGroup = this.getOption(chart, 'sync', 'group');
// stop if the sync event was fired from this chart
if (e.chartId === chart.id) {
return;
}
// stop if the sync event was fired from a different group
if (e.syncGroup !== syncGroup) {
return;
}
var xScale = this.getXScale(chart);
if (!xScale) {
return;
}
// Safari fix
var buttons = (e.original.native.buttons === undefined ? e.original.native.which : e.original.native.buttons);
if (e.original.type === 'mouseup') {
buttons = 0;
}
var newEvent = {
type: e.original.type,
chart: chart,
x: xScale.getPixelForValue(e.xValue),
y: e.original.y,
native: {
buttons: buttons
},
stop: true
};
chart.controller.eventHandler(newEvent);
},
afterEvent: function(chart, e) {
if (!chart.crosshair) {
return;
}
if (chart.config.options.scales.xAxes.length == 0) {
return
}
var xScaleType = chart.config.options.scales.xAxes[0].type;
if (xScaleType !== 'linear' && xScaleType !== 'time' && xScaleType !== 'category' && xscaleType !== 'logarithmic') {
return;
}
var xScale = this.getXScale(chart);
if (!xScale) {
return;
}
// fix for Safari
var buttons = e.native ? (e.native.buttons === undefined ? e.native.which : e.native.buttons) : 0;
if (e.native && e.native.type === 'mouseup') {
buttons = 0;
}
var syncEnabled = this.getOption(chart, 'sync', 'enabled');
var syncGroup = this.getOption(chart, 'sync', 'group');
// fire event for all other linked charts
if (!e.stop && syncEnabled) {
var event = new CustomEvent('sync-event');
event.chartId = chart.id;
event.syncGroup = syncGroup;
event.original = e;
event.xValue = xScale.getValueForPixel(e.x);
window.dispatchEvent(event);
}
// suppress tooltips for linked charts
var suppressTooltips = this.getOption(chart, 'sync', 'suppressTooltips');
chart.crosshair.suppressTooltips = e.stop && suppressTooltips;
chart.crosshair.enabled = (e.type !== 'mouseout' && (e.x > xScale.getPixelForValue(xScale.min) && e.x < xScale.getPixelForValue(xScale.max)));
if (!chart.crosshair.enabled) {
if (e.x > xScale.getPixelForValue(xScale.max)) {
chart.update();
}
return true;
}
// handle drag to zoom
var zoomEnabled = this.getOption(chart, 'zoom', 'enabled');
if (buttons === 1 && !chart.crosshair.dragStarted && zoomEnabled) {
chart.crosshair.dragStartX = e.x;
chart.crosshair.dragStarted = true;
}
// handle drag to zoom
if (chart.crosshair.dragStarted && buttons === 0) {
chart.crosshair.dragStarted = false;
var start = xScale.getValueForPixel(chart.crosshair.dragStartX);
var end = xScale.getValueForPixel(chart.crosshair.x);
if (Math.abs(chart.crosshair.dragStartX - chart.crosshair.x) > 1) {
this.doZoom(chart, start, end);
}
chart.update();
}
chart.crosshair.x = e.x;
chart.draw();
},
afterDraw: function(chart) {
if (!chart.crosshair) {
return;
}
if (chart.crosshair.dragStarted) {
this.drawZoombox(chart);
} else {
this.drawTraceLine(chart);
this.interpolateValues(chart);
this.drawTracePoints(chart);
}
return true;
},
beforeTooltipDraw: function(chart) {
// suppress tooltips on dragging
if (!chart.crosshair) {
return;
}
return !chart.crosshair.dragStarted && !chart.crosshair.suppressTooltips;
},
resetZoom: function(chart) {
var stop = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var update = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
if (update) {
for (var datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
var dataset = chart.data.datasets[datasetIndex];
dataset.data = chart.crosshair.originalData.shift(0);
}
var range = 'ticks';
if (chart.options.scales.xAxes[0].time) {
range = 'time';
}
// reset original xRange
if (chart.crosshair.originalXRange.min) {
chart.options.scales.xAxes[0][range].min = chart.crosshair.originalXRange.min;
chart.crosshair.originalXRange.min = null;
} else {
delete chart.options.scales.xAxes[0][range].min;
}
if (chart.crosshair.originalXRange.max) {
chart.options.scales.xAxes[0][range].max = chart.crosshair.originalXRange.max;
chart.crosshair.originalXRange.max = null;
} else {
delete chart.options.scales.xAxes[0][range].max;
}
}
if (chart.crosshair.button && chart.crosshair.button.parentNode) {
chart.crosshair.button.parentNode.removeChild(chart.crosshair.button);
chart.crosshair.button = false;
}
var syncEnabled = this.getOption(chart, 'sync', 'enabled');
if (!stop && update && syncEnabled) {
var syncGroup = this.getOption(chart, 'sync', 'group');
var event = new CustomEvent('reset-zoom-event');
event.chartId = chart.id;
event.syncGroup = syncGroup;
window.dispatchEvent(event);
}
if (update) {
var anim = chart.options.animation;
chart.options.animation = false;
chart.update();
chart.options.animation = anim;
}
},
doZoom: function(chart, start, end) {
// swap start/end if user dragged from right to left
if (start > end) {
var tmp = start;
start = end;
end = tmp;
}
// notify delegate
var beforeZoomCallback = helpers.getValueOrDefault(chart.options.plugins.crosshair.callbacks ? chart.options.plugins.crosshair.callbacks.beforeZoom : undefined, defaultOptions.callbacks.beforeZoom);
if (!beforeZoomCallback(start, end)) {
return false;
}
if (chart.options.scales.xAxes[0].type === 'time') {
if (chart.options.scales.xAxes[0].time.min && chart.crosshair.originalData.length === 0) {
chart.crosshair.originalXRange.min = chart.options.scales.xAxes[0].time.min;
}
if (chart.options.scales.xAxes[0].time.max && chart.crosshair.originalData.length === 0) {
chart.crosshair.originalXRange.max = chart.options.scales.xAxes[0].time.max;
}
} else {
if (chart.options.scales.xAxes[0].ticks.min && chart.crosshair.originalData.length === undefined) {
chart.crosshair.originalXRange.min = chart.options.scales.xAxes[0].ticks.min;
}
if (chart.options.scales.xAxes[0].ticks.max && chart.crosshair.originalData.length === undefined) {
chart.crosshair.originalXRange.max = chart.options.scales.xAxes[0].ticks.max;
}
}
// if (!chart.crosshair.button) {
// // add restore zoom button
// var button = document.createElement('button');
// var buttonText = this.getOption(chart, 'zoom', 'zoomButtonText');
// var buttonClass = this.getOption(chart, 'zoom', 'zoomButtonClass');
// var buttonLabel = document.createTextNode(buttonText);
// button.appendChild(buttonLabel);
// button.className = buttonClass;
// button.addEventListener('click', function() {
// this.resetZoom(chart);
// }.bind(this));
// chart.canvas.parentNode.appendChild(button);
// chart.crosshair.button = button;
// }
// set axis scale
if (chart.options.scales.xAxes[0].time) {
chart.options.scales.xAxes[0].time.min = start;
chart.options.scales.xAxes[0].time.max = end;
} else {
chart.options.scales.xAxes[0].ticks.min = start;
chart.options.scales.xAxes[0].ticks.max = end;
}
// make a copy of the original data for later restoration
var storeOriginals = (chart.crosshair.originalData.length === 0) ? true : false;
// filter dataset
for (var datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
var newData = [];
var index = 0;
var started = false;
var stop = false;
if (storeOriginals) {
chart.crosshair.originalData[datasetIndex] = chart.data.datasets[datasetIndex].data;
}
var sourceDataset = chart.crosshair.originalData[datasetIndex];
for (var oldDataIndex = 0; oldDataIndex < sourceDataset.length; oldDataIndex++) {
var oldData = sourceDataset[oldDataIndex];
var oldDataX = this.getXScale(chart).getRightValue(oldData);
// append one value outside of bounds
if (oldDataX >= start && !started && index > 0) {
newData.push(sourceDataset[index - 1]);
started = true;
}
if (oldDataX >= start && oldDataX <= end) {
newData.push(oldData);
}
if (oldDataX > end && !stop && index < sourceDataset.length) {
newData.push(oldData);
stop = true;
}
index += 1;
}
chart.data.datasets[datasetIndex].data = newData;
}
chart.crosshair.start = start;
chart.crosshair.end = end;
if (storeOriginals) {
var xAxes = this.getXScale(chart);
chart.crosshair.min = xAxes.min;
chart.crosshair.max = xAxes.max;
}
chart.update();
var afterZoomCallback = this.getOption(chart, 'callbacks', 'afterZoom');
afterZoomCallback(start, end);
},
drawZoombox: function(chart) {
var yScale = this.getYScale(chart);
var borderColor = this.getOption(chart, 'zoom', 'zoomboxBorderColor');
var fillColor = this.getOption(chart, 'zoom', 'zoomboxBackgroundColor');
chart.ctx.beginPath();
chart.ctx.rect(chart.crosshair.dragStartX, yScale.getPixelForValue(yScale.max), chart.crosshair.x - chart.crosshair.dragStartX, yScale.getPixelForValue(yScale.min) - yScale.getPixelForValue(yScale.max));
chart.ctx.lineWidth = 1;
chart.ctx.strokeStyle = borderColor;
chart.ctx.fillStyle = fillColor;
chart.ctx.fill();
chart.ctx.fillStyle = '';
chart.ctx.stroke();
chart.ctx.closePath();
},
drawTraceLine: function(chart) {
var yScale = this.getYScale(chart);
var lineWidth = this.getOption(chart, 'line', 'width');
var color = this.getOption(chart, 'line', 'color');
var dashPattern = this.getOption(chart, 'line', 'dashPattern');
var snapEnabled = this.getOption(chart, 'snap', 'enabled');
var lineX = chart.crosshair.x;
var isHoverIntersectOff = chart.config.options.hover.intersect === false;
if (snapEnabled && isHoverIntersectOff && chart.active.length) {
lineX = chart.active[0]._view.x;
}
chart.ctx.beginPath();
chart.ctx.setLineDash(dashPattern);
chart.ctx.moveTo(lineX, yScale.getPixelForValue(yScale.max));
chart.ctx.lineWidth = lineWidth;
chart.ctx.strokeStyle = color;
chart.ctx.lineTo(lineX, yScale.getPixelForValue(yScale.min));
chart.ctx.stroke();
chart.ctx.setLineDash([]);
},
drawTracePoints: function(chart) {
for (var chartIndex = 0; chartIndex < chart.data.datasets.length; chartIndex++) {
var dataset = chart.data.datasets[chartIndex];
var meta = chart.getDatasetMeta(chartIndex);
var yScale = chart.scales[meta.yAxisID];
if (meta.hidden || !dataset.interpolate) {
continue;
}
chart.ctx.beginPath();
chart.ctx.arc(chart.crosshair.x, yScale.getPixelForValue(dataset.interpolatedValue), 3, 0, 2 * Math.PI, false);
chart.ctx.fillStyle = 'white';
chart.ctx.lineWidth = 2;
chart.ctx.strokeStyle = dataset.borderColor;
chart.ctx.fill();
chart.ctx.stroke();
}
},
interpolateValues: function(chart) {
for (var chartIndex = 0; chartIndex < chart.data.datasets.length; chartIndex++) {
var dataset = chart.data.datasets[chartIndex];
var meta = chart.getDatasetMeta(chartIndex);
var xScale = chart.scales[meta.xAxisID];
var xValue = xScale.getValueForPixel(chart.crosshair.x);
if (meta.hidden || !dataset.interpolate) {
continue;
}
var data = dataset.data;
var index = data.findIndex(function(o) {
return o.x >= xValue;
});
var prev = data[index - 1];
var next = data[index];
if (chart.data.datasets[chartIndex].steppedLine && prev) {
dataset.interpolatedValue = prev.y;
} else if (prev && next) {
var slope = (next.y - prev.y) / (next.x - prev.x);
dataset.interpolatedValue = prev.y + (xValue - prev.x) * slope;
} else {
dataset.interpolatedValue = NaN;
}
}
}
};
Chart$$1.plugins.register(crosshairPlugin);
};
Interpolate(Chart);
TracePlugin(Chart);
})));

View File

@ -68,9 +68,13 @@
<file>StatusQ/Components/WebEngineLoader.qml</file>
<file>StatusQ/Components/private/StatusComboboxBackground.qml</file>
<file>StatusQ/Components/private/StatusComboboxIndicator.qml</file>
<file>StatusQ/Components/private/chart/Chart.js</file>
<file>StatusQ/Components/private/chart/Chart.qml</file>
<file>StatusQ/Components/private/chart/LICENSE</file>
<file>StatusQ/Components/private/chart/ChartCanvas.qml</file>
<file>StatusQ/Components/private/chart/Library/Animator.qml</file>
<file>StatusQ/Components/private/chart/Library/Chart.bundle.js</file>
<file>StatusQ/Components/private/chart/Library/chartjs-plugin-crosshair.js</file>
<file>StatusQ/Components/private/chart/Library/chartjs-plugin-datalabels.js</file>
<file>StatusQ/Components/private/chart/Library/Library.js</file>
<file>StatusQ/Components/private/chart/Library/Polyfills.js</file>
<file>StatusQ/Components/private/qmldir</file>
<file>StatusQ/Components/private/qwebchannel/helpers.js</file>
<file>StatusQ/Components/private/qwebchannel/qwebchannel.js</file>

View File

@ -30,7 +30,7 @@ StatusChartPanel {
onVisibleChanged: if(visible) d.resetWithSpamProtection()
onTimeRangeTabBarIndexChanged: reset()
onModelChanged: chart.updateToNewData()
onModelChanged: chart.refresh()
onCollectCommunityMetricsMessagesCount: d.lastRequestModelMetadata = d.selectedTabInfo.modelItems
QtObject {
@ -41,6 +41,7 @@ StatusChartPanel {
readonly property string twentyPercentBaseColor1: Theme.palette.alphaColor(baseColor1, 0.2)
readonly property string barColor: Theme.palette.primaryColor2
readonly property string barBorderColor: Theme.palette.primaryColor1
readonly property string messagesLabel: qsTr("Messages")
property int hoveredBarIndex: 0
@ -266,17 +267,15 @@ StatusChartPanel {
graphsModel: d.graphTabsModel
timeRangeModel: d.modelMetadata
onHeaderTabClicked: {
root.chart.animateToNewData();
chart.refresh()
}
/////////////////////////////
// Chartjs configuration //
/////////////////////////////
chart.chartType: 'bar'
chart.chartData: {
return {
labels: d.labels,
datasets: [{
chart.type: 'bar'
chart.labels: d.labels
chart.datasets: [{
xAxisId: 'x-axis-1',
yAxisId: 'y-axis-1',
backgroundColor: d.barColor,
@ -286,31 +285,27 @@ StatusChartPanel {
hoverBorderWidth: 2,
data: d.chartData
}]
}
}
chart.chartOptions: {
chart.options: {
return {
maintainAspectRatio: false,
responsive: true,
legend: {
display: false
},
// Popup follows the cursor
onHover: function(arg1, hoveredItems, event) {
if(!event || hoveredItems.length == 0) {
onHover: function(arg1, hoveredItems) {
if(!arg1 || hoveredItems.length == 0) {
toolTip.close()
return
}
arg1.target = chart
d.hoveredBarIndex = hoveredItems[0]._index
d.hoveredBarValue = hoveredItems[0]._chart.config.data.datasets[0].data[hoveredItems[0]._index]
const position = d.getAdjustedTooltipPosition(event)
const position = d.getAdjustedTooltipPosition(arg1)
toolTip.popup(position.x, position.y)
},
tooltips: {
enabled: false,
},
legend: {
display: false
},
scales: {
xAxes: [{
id: 'x-axis-1',

View File

@ -183,24 +183,22 @@ Item {
LocaleUtils.getDayMonth(value) :
LocaleUtils.getMonthYear(value)
}
chart.chartType: 'line'
chart.chartData: {
return {
labels: RootStore.marketHistoryIsLoading ? [] : graphDetail.labelsData,
datasets: [{
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: graphDetail.selectedGraphType === AssetsDetailView.GraphType.Price ? 3 : 2,
pointRadius: 0,
data: RootStore.marketHistoryIsLoading ? [] : graphDetail.dataRange,
parsing: false,
}]
}
chart.type: 'line'
chart.labels: RootStore.marketHistoryIsLoading ? [] : graphDetail.labelsData
chart.datasets: {
return [{
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: graphDetail.selectedGraphType === AssetsDetailView.GraphType.Price ? 3 : 2,
pointRadius: 0,
data: RootStore.marketHistoryIsLoading ? [] : graphDetail.dataRange,
parsing: false,
}]
}
chart.chartOptions: {
chart.options: {
return {
maintainAspectRatio: false,
responsive: true,