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,35 +74,12 @@ 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: {
readonly property var lineConfig: {
return {
type: 'line',
labels: d.timeRange[graphDetail.timeRangeTabBarIndex][graphDetail.selectedTimeRange],
datasets: [{
label: 'Price',
@ -118,26 +89,14 @@ SplitView {
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 {
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,
@ -197,6 +157,273 @@ SplitView {
}
}
}
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: [{
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,
//data: d.generateData()
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

@ -1,32 +1,14 @@
/*!
* Chart.js v2.9.3
* Chart.js v2.9.4
* https://www.chartjs.org
* (c) 2019 Chart.js Contributors
* (c) 2020 Chart.js Contributors
* Released under the MIT License
*/
/*!
* adaptions by Elypson (Michael A. Voelkel) to get it working for QML:
* 1) changed overall library structure with UMD and global function build
* 2) animations are triggered via QML animator
* 3) tooltips do not use DOM events but QML events that are injected via bindEvents now
* 4) many smaller modifications because Chart.js relied on and used the DOM that is not available in QML in the same way
* 5) some fixes that occured in QML like setting dashed line to solid
* 6) personal customization, where we assumed that it leads to better looks
*
* also note that modifications are inspired by Shuirna (github.com/shuirna) and their changes were inspired by Julien Wintz
*
* (c) 2020 ChartJs2QML contributors (starting with Elypson, Michael A. Voelkel, https://github.com/Elypson)
* those customizations are also licensed under MIT for all matters and purposes
*/
function UMD(global, factory) {
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Chart = factory());
};
function Chart (item, config) { 'use strict';
}(this, (function () { 'use strict';
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@ -2125,6 +2107,10 @@ if (typeof window !== 'undefined') {
var chartjsColor = Color;
function isValidKey(key) {
return ['__proto__', 'prototype', 'constructor'].indexOf(key) === -1;
}
/**
* @namespace Chart.helpers
*/
@ -2300,7 +2286,7 @@ var helpers = {
}
if (helpers.isObject(source)) {
var target = {};
var target = Object.create(source);
var keys = Object.keys(source);
var klen = keys.length;
var k = 0;
@ -2321,6 +2307,12 @@ var helpers = {
* @private
*/
_merger: function(key, target, source, options) {
if (!isValidKey(key)) {
// We want to ensure we do not copy prototypes over
// as this can pollute global namespaces
return;
}
var tval = target[key];
var sval = source[key];
@ -2336,6 +2328,12 @@ var helpers = {
* @private
*/
_mergerIf: function(key, target, source) {
if (!isValidKey(key)) {
// We want to ensure we do not copy prototypes over
// as this can pollute global namespaces
return;
}
var tval = target[key];
var sval = source[key];
@ -3504,13 +3502,10 @@ var core_animations = {
requestAnimationFrame: function() {
var me = this;
if (me.request === null) {
return;
// TBD: animation should work somehow; what is startDigest?
// Skip animation frame requests until the active one is executed.
// This can happen when processing mouse events, e.g. 'mousemove'
// and 'mouseout' events will trigger multiple renders.
me.request = helpers$1.requestAnimFrame.call(undefined, function() {
me.request = helpers$1.requestAnimFrame.call(window, function() {
me.request = null;
me.startDigest();
});
@ -3837,7 +3832,7 @@ helpers$1.extend(DatasetController.prototype, {
*/
_configure: function() {
var me = this;
me._config = helpers$1.merge({}, [
me._config = helpers$1.merge(Object.create(null), [
me.chart.options.datasets[me._type],
me.getDataset(),
], {
@ -4405,11 +4400,6 @@ var element_line = core_element.extend({
ctx.setLineDash(vm.borderDash || globalOptionLineElements.borderDash);
}
// line dash fix for QML
if(ctx.getLineDash && ctx.getLineDash().length === 0) {
ctx.setLineDash([99999]);
}
ctx.lineDashOffset = valueOrDefault$1(vm.borderDashOffset, globalOptionLineElements.borderDashOffset);
ctx.lineJoin = vm.borderJoinStyle || globalOptionLineElements.borderJoinStyle;
ctx.lineWidth = valueOrDefault$1(vm.borderWidth, globalOptionLineElements.borderWidth);
@ -4679,6 +4669,7 @@ var element_rectangle = core_element.extend({
if (outer.w !== inner.w || outer.h !== inner.h) {
ctx.beginPath();
ctx.strokeStyle = vm.borderColor;
ctx.lineWidth = vm.borderWidth;
ctx.strokeRect(inner.x, inner.y, inner.w, inner.h);
ctx.fill('evenodd');
}
@ -6147,9 +6138,9 @@ var controller_line = core_datasetController.extend({
};
model.backgroundColor = valueOrDefault$6(options.hoverBackgroundColor, getHoverColor(options.backgroundColor));
model.borderColor = "rgba(255,0,0,1.)";//valueOrDefault$6(options.hoverBorderColor, getHoverColor(options.borderColor));
model.borderWidth = 1;//valueOrDefault$6(options.hoverBorderWidth, options.borderWidth);
model.radius = 2;//valueOrDefault$6(options.hoverRadius, options.radius);
model.borderColor = valueOrDefault$6(options.hoverBorderColor, getHoverColor(options.borderColor));
model.borderWidth = valueOrDefault$6(options.hoverBorderWidth, options.borderWidth);
model.radius = valueOrDefault$6(options.hoverRadius, options.radius);
},
});
@ -7110,7 +7101,8 @@ function updateDims(chartArea, params, layout) {
chartArea.h = newHeight;
// return true if chart area changed in layout's direction
return layout.horizontal ? newWidth !== chartArea.w : newHeight !== chartArea.h;
var sizes = layout.horizontal ? [newWidth, chartArea.w] : [newHeight, chartArea.h];
return sizes[0] !== sizes[1] && (!isNaN(sizes[0]) || !isNaN(sizes[1]));
}
}
@ -7414,7 +7406,7 @@ var platform_basic = {
}
};
var platform_dom = "/*\n * DOM element rendering detection\n * https://davidwalsh.name/detect-node-insertion\n */\n@keyframes chartjs-render-animation {\n\tfrom { opacity: 0.99; }\n\tto { opacity: 1; }\n}\n\n.chartjs-render-monitor {\n\tanimation: chartjs-render-animation 0.001s;\n}\n\n/*\n * DOM element resizing detection\n * https://github.com/marcj/css-element-queries\n */\n.chartjs-size-monitor,\n.chartjs-size-monitor-expand,\n.chartjs-size-monitor-shrink {\n\tposition: absolute;\n\tdirection: ltr;\n\tleft: 0;\n\ttop: 0;\n\tright: 0;\n\tbottom: 0;\n\toverflow: hidden;\n\tpointer-events: none;\n\tvisibility: hidden;\n\tz-index: -1;\n}\n\n.chartjs-size-monitor-expand > div {\n\tposition: absolute;\n\twidth: 1000000px;\n\theight: 1000000px;\n\tleft: 0;\n\ttop: 0;\n}\n\n.chartjs-size-monitor-shrink > div {\n\tposition: absolute;\n\twidth: 200%;\n\theight: 200%;\n\tleft: 0;\n\ttop: 0;\n}\n";
var platform_dom = "/*\r\n * DOM element rendering detection\r\n * https://davidwalsh.name/detect-node-insertion\r\n */\r\n@keyframes chartjs-render-animation {\r\n\tfrom { opacity: 0.99; }\r\n\tto { opacity: 1; }\r\n}\r\n\r\n.chartjs-render-monitor {\r\n\tanimation: chartjs-render-animation 0.001s;\r\n}\r\n\r\n/*\r\n * DOM element resizing detection\r\n * https://github.com/marcj/css-element-queries\r\n */\r\n.chartjs-size-monitor,\r\n.chartjs-size-monitor-expand,\r\n.chartjs-size-monitor-shrink {\r\n\tposition: absolute;\r\n\tdirection: ltr;\r\n\tleft: 0;\r\n\ttop: 0;\r\n\tright: 0;\r\n\tbottom: 0;\r\n\toverflow: hidden;\r\n\tpointer-events: none;\r\n\tvisibility: hidden;\r\n\tz-index: -1;\r\n}\r\n\r\n.chartjs-size-monitor-expand > div {\r\n\tposition: absolute;\r\n\twidth: 1000000px;\r\n\theight: 1000000px;\r\n\tleft: 0;\r\n\ttop: 0;\r\n}\r\n\r\n.chartjs-size-monitor-shrink > div {\r\n\tposition: absolute;\r\n\twidth: 200%;\r\n\theight: 200%;\r\n\tleft: 0;\r\n\ttop: 0;\r\n}\r\n";
var platform_dom$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
@ -8122,7 +8114,7 @@ var core_scaleService = {
},
getScaleDefaults: function(type) {
// Return the scale defaults merged with the global settings so that we always use the latest ones
return this.defaults.hasOwnProperty(type) ? helpers$1.merge({}, [core_defaults.scale, this.defaults[type]]) : {};
return this.defaults.hasOwnProperty(type) ? helpers$1.merge(Object.create(null), [core_defaults.scale, this.defaults[type]]) : {};
},
updateScaleDefaults: function(type, additions) {
var me = this;
@ -9197,7 +9189,7 @@ core_defaults._set('global', {
* returns a deep copy of the result, thus doesn't alter inputs.
*/
function mergeScaleConfig(/* config objects ... */) {
return helpers$1.merge({}, [].slice.call(arguments), {
return helpers$1.merge(Object.create(null), [].slice.call(arguments), {
merger: function(key, target, source, options) {
if (key === 'xAxes' || key === 'yAxes') {
var slen = source[key].length;
@ -9237,9 +9229,9 @@ function mergeScaleConfig(/* config objects ... */) {
* a deep copy of the result, thus doesn't alter inputs.
*/
function mergeConfig(/* config objects ... */) {
return helpers$1.merge({}, [].slice.call(arguments), {
return helpers$1.merge(Object.create(null), [].slice.call(arguments), {
merger: function(key, target, source, options) {
var tval = target[key] || {};
var tval = target[key] || Object.create(null);
var sval = source[key];
if (key === 'scales') {
@ -9256,7 +9248,7 @@ function mergeConfig(/* config objects ... */) {
}
function initConfig(config) {
config = config || {};
config = config || Object.create(null);
// Do NOT use mergeConfig for the data object because this method merges arrays
// and so would change references to labels and datasets, preventing data updates.
@ -9441,6 +9433,8 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ {
canvas.width = me.width = newWidth;
canvas.height = me.height = newHeight;
canvas.style.width = newWidth + 'px';
canvas.style.height = newHeight + 'px';
helpers$1.retinaScale(me, options.devicePixelRatio);
@ -9783,7 +9777,6 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ {
}
var animationOptions = me.options.animation;
var duration = valueOrDefault$9(config.duration, animationOptions && animationOptions.duration);
var lazy = config.lazy;
@ -9920,7 +9913,6 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ {
}
metasets = me._getSortedVisibleDatasetMetas();
for (i = metasets.length - 1; i >= 0; --i) {
me.drawDataset(metasets[i], easingValue);
}
@ -9991,7 +9983,6 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ {
getElementsAtEventForMode: function(e, mode, options) {
var method = core_interaction.modes[mode];
if (typeof method === 'function') {
// TBD: method might be nearest here
return method(this, e, options);
}
@ -10105,7 +10096,7 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ {
/**
* @private
*/
bindEvents: function(setHandler) {
bindEvents: function() {
var me = this;
var listeners = me._listeners = {};
var listener = function() {
@ -10113,12 +10104,7 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ {
};
helpers$1.each(me.options.events, function(type) {
if(setHandler) {
setHandler(listener);
}
else
platform.addEventListener(me, type, listener);
listeners[type] = listener;
});
@ -10239,7 +10225,7 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ {
// Invoke onHover hook
// Need to call with native event here to not break backwards compatibility
helpers$1.callback(options.onHover || options.hover.onHover, [e.native, me.active, e], me);
helpers$1.callback(options.onHover || options.hover.onHover, [e.native, me.active], me);
if (e.type === 'mouseup' || e.type === 'click') {
if (options.onClick) {
@ -10665,9 +10651,9 @@ var core_helpers = function() {
// -- DOM methods
helpers$1.getRelativePosition = function(evt, chart) {
var mouseX, mouseY;
var e = evt;//evt.originalEvent || evt;
var e = evt.originalEvent || evt;
var canvas = evt.target || evt.srcElement;
var boundingRect = {left: 0, top: 0, right: canvas.width, bottom: canvas.height};//canvas.getBoundingClientRect();
var boundingRect = canvas.getBoundingClientRect();
var touches = e.touches;
if (touches && touches.length > 0) {
@ -10682,12 +10668,10 @@ var core_helpers = function() {
// Scale mouse coordinates into canvas coordinates
// by following the pattern laid out by 'jerryj' in the comments of
// https://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/
// Elypson: remove paddings left/right
var paddingLeft = parseFloat(0);//helpers$1.getStyle(canvas, 'padding-left'));
var paddingTop = parseFloat(0);//helpers$1.getStyle(canvas, 'padding-top'));
var paddingRight = parseFloat(0);//helpers$1.getStyle(canvas, 'padding-right'));
var paddingBottom = parseFloat(0);//helpers$1.getStyle(canvas, 'padding-bottom'));
var paddingLeft = parseFloat(helpers$1.getStyle(canvas, 'padding-left'));
var paddingTop = parseFloat(helpers$1.getStyle(canvas, 'padding-top'));
var paddingRight = parseFloat(helpers$1.getStyle(canvas, 'padding-right'));
var paddingBottom = parseFloat(helpers$1.getStyle(canvas, 'padding-bottom'));
var width = boundingRect.right - boundingRect.left - paddingLeft - paddingRight;
var height = boundingRect.bottom - boundingRect.top - paddingTop - paddingBottom;
@ -10779,10 +10763,32 @@ var core_helpers = function() {
return parent;
};
helpers$1.getMaximumWidth = function(domNode) {
return domNode.width;
var container = helpers$1._getParentNode(domNode);
if (!container) {
return domNode.clientWidth;
}
var clientWidth = container.clientWidth;
var paddingLeft = helpers$1._calculatePadding(container, 'padding-left', clientWidth);
var paddingRight = helpers$1._calculatePadding(container, 'padding-right', clientWidth);
var w = clientWidth - paddingLeft - paddingRight;
var cw = helpers$1.getConstraintWidth(domNode);
return isNaN(cw) ? w : Math.min(w, cw);
};
helpers$1.getMaximumHeight = function(domNode) {
return domNode.height;
var container = helpers$1._getParentNode(domNode);
if (!container) {
return domNode.clientHeight;
}
var clientHeight = container.clientHeight;
var paddingTop = helpers$1._calculatePadding(container, 'padding-top', clientHeight);
var paddingBottom = helpers$1._calculatePadding(container, 'padding-bottom', clientHeight);
var h = clientHeight - paddingTop - paddingBottom;
var ch = helpers$1.getConstraintHeight(domNode);
return isNaN(ch) ? h : Math.min(h, ch);
};
helpers$1.getStyle = function(el, property) {
return el.currentStyle ?
@ -10900,7 +10906,10 @@ var core_helpers = function() {
};
helpers$1.getHoverColor = function(colorValue) {
return Color(colorValue).saturate(0.5).darken(0.1).rgbString();
/* global CanvasPattern */
return (colorValue instanceof CanvasPattern || colorValue instanceof CanvasGradient) ?
colorValue :
helpers$1.color(colorValue).saturate(0.5).darken(0.1).rgbString();
};
};
@ -11220,6 +11229,8 @@ function computeLabelSizes(ctx, tickFonts, ticks, caches) {
var widths = [];
var heights = [];
var offsets = [];
var widestLabelSize = 0;
var highestLabelSize = 0;
var i, j, jlen, label, tickFont, fontString, cache, lineHeight, width, height, nestedLabel, widest, highest;
for (i = 0; i < length; ++i) {
@ -11247,11 +11258,13 @@ function computeLabelSizes(ctx, tickFonts, ticks, caches) {
widths.push(width);
heights.push(height);
offsets.push(lineHeight / 2);
widestLabelSize = Math.max(width, widestLabelSize);
highestLabelSize = Math.max(height, highestLabelSize);
}
garbageCollect(caches, length);
widest = widths.indexOf(Math.max.apply(null, widths));
highest = heights.indexOf(Math.max.apply(null, heights));
widest = widths.indexOf(widestLabelSize);
highest = heights.indexOf(highestLabelSize);
function valueAt(idx) {
return {
@ -11569,6 +11582,7 @@ var Scale = core_element.extend({
// Generate labels using all non-skipped ticks
labels = me._convertTicksToLabels(me._ticksToDraw);
}
me.ticks = labels; // BACKWARD COMPATIBILITY
// IMPORTANT: after this point, we consider that `this.ticks` will NEVER change!
@ -12336,11 +12350,6 @@ var Scale = core_element.extend({
ctx.lineDashOffset = item.borderDashOffset;
}
// MV workaround, as it seems that emptying line dash does not work
if(ctx.getLineDash && ctx.getLineDash().length === 0) {
ctx.setLineDash([9999]);
}
ctx.beginPath();
if (gridLines.drawTicks) {
@ -13051,12 +13060,7 @@ var scale_linear = scale_linearbase.extend({
},
getLabelForIndex: function(index, datasetIndex) {
var scaleLabel = this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]);
var optionsTooltips = this.chart.options.tooltips;
if (optionsTooltips && optionsTooltips.format && optionsTooltips.format.enabled && optionsTooltips.format.valueCallback) {
return optionsTooltips.format.valueCallback(scaleLabel);
}
return scaleLabel;
return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]);
},
// Utils
@ -13647,11 +13651,6 @@ function drawRadiusLine(scale, gridLineOpts, radius, index) {
ctx.lineDashOffset = gridLineOpts.borderDashOffset || 0.0;
}
// line dash fix for QML
if(ctx.getLineDash && ctx.getLineDash().length === 0) {
ctx.setLineDash([99999]);
}
ctx.beginPath();
if (circular) {
// Draw circular arcs between the points
@ -13869,11 +13868,6 @@ var scale_radialLinear = scale_linearbase.extend({
ctx.lineDashOffset = resolve$4([angleLineOpts.borderDashOffset, gridLineOpts.borderDashOffset, 0.0]);
}
// line dash fix for QML
if(ctx.getLineDash && ctx.getLineDash().length === 0) {
ctx.setLineDash([99999]);
}
for (i = me.chart.data.labels.length - 1; i >= 0; i--) {
offset = me.getDistanceFromCenterForValue(opts.ticks.reverse ? me.min : me.max);
position = me.getPointPosition(i, offset);
@ -14570,11 +14564,6 @@ var scale_time = core_scale.extend({
if (typeof label === 'string') {
return label;
}
var tooltipsFormat = me.chart.options.tooltips.format;
if (tooltipsFormat && tooltipsFormat.enabled && tooltipsFormat.callback) {
return tooltipsFormat.callback(label)
}
return adapter.format(toTimestamp(me, label), timeOpts.displayFormats.datetime);
},
@ -14593,13 +14582,7 @@ var scale_time = core_scale.extend({
var tick = ticks[index];
var tickOpts = options.ticks;
var major = majorUnit && majorFormat && tick && tick.major;
var labelFormat = me.chart.options.scales.labelFormat;
var label;
if (labelFormat && labelFormat.enabled && labelFormat.callback)
label = labelFormat.callback(time);
else
label = adapter.format(time, format ? format : major ? majorFormat : minorFormat);
var label = adapter.format(time, format ? format : major ? majorFormat : minorFormat);
var nestedTickOpts = major ? tickOpts.major : tickOpts.minor;
var formatter = resolve$5([
nestedTickOpts.callback,
@ -20152,10 +20135,6 @@ var Legend = core_element.extend({
ctx.setLineDash(valueOrDefault$e(legendItem.lineDash, lineDefault.borderDash));
}
if(ctx.getLineDash && ctx.getLineDash().length === 0) {
ctx.setLineDash([99999]); // fix for QML because it does not make lines solid once being unsolid once
}
if (labelOpts && labelOpts.usePointStyle) {
// Recalculate x and y for drawPoint() because its expecting
// x and y to be center of figure (instead of top left)
@ -20687,7 +20666,6 @@ for (var k in plugins) {
core_controller.platform.initialize();
var src = core_controller;
if (typeof window !== 'undefined') {
window.Chart = core_controller;
}
@ -20792,11 +20770,6 @@ core_controller.helpers.each(
}
);
return new src(item, config);
}
return src;
function build(item, config) {
//UMD(me, meChart);
//var y = new test();
return new Chart(item, config);
}
})));

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,11 +183,10 @@ Item {
LocaleUtils.getDayMonth(value) :
LocaleUtils.getMonthYear(value)
}
chart.chartType: 'line'
chart.chartData: {
return {
labels: RootStore.marketHistoryIsLoading ? [] : graphDetail.labelsData,
datasets: [{
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)',
@ -198,9 +197,8 @@ Item {
parsing: false,
}]
}
}
chart.chartOptions: {
chart.options: {
return {
maintainAspectRatio: false,
responsive: true,