Adding data to line chart (chart.js with Vue.js) results in 'too much recursion' error - vue.js

I'm using chart.js with vue.js. I have a line-chart and I want to add data (later automatically by SSE). I modified another sample, but the error remains the same. It 'crashes' in the call to this.moonData.push (or this.testData.datasets[0].data.push). It must have to do with the ref() of moonData. When I use just the non-ref version, the push succeeds, but the chart isn't updated. BTW, pushing labels succeeds
I'm using chart.js#3.7.0, vue#3.2.29
In Firefox:
Uncaught InternalError: too much recursion
get reactivity.esm-bundler.js:406
toRaw reactivity.esm-bundler.js:927
key reactivity.esm-bundler.js:398
value helpers.segment.js:1554
key reactivity.esm-bundler.js:398
value helpers.segment.js:1554
key reactivity.esm-bundler.js:398
value helpers.segment.js:1554
in chrome:
runtime-core.esm-bundler.js?5c40:218 Uncaught RangeError: Maximum call stack size exceeded
at Object.get (reactivity.esm-bundler.js?a1e9:406:1)
at toRaw (reactivity.esm-bundler.js?a1e9:927:1)
at Proxy.instrumentations.<computed> (reactivity.esm-bundler.js?a1e9:398:1)
at Proxy.value (helpers.segment.js?dd3d:1554:1)
at Proxy.instrumentations.<computed> (reactivity.esm-bundler.js?a1e9:398:1)
at Proxy.value (helpers.segment.js?dd3d:1554:1)
at Proxy.instrumentations.<computed> (reactivity.esm-bundler.js?a1e9:398:1)
at Proxy.value (helpers.segment.js?dd3d:1554:1)
at Proxy.instrumentations.<computed> (reactivity.esm-bundler.js?a1e9:398:1)
at Proxy.value (helpers.segment.js?dd3d:1554:1)
export default defineComponent({
// name: "PlanetChart",
setup() {
let moonData = ref<number[]>([]);
const testData = computed<ChartData<"line">>(() => ({
labels: ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"],
datasets: [
{
label: "Number of Moons",
data: moonData.value,
backgroundColor: "rgba(54,73,93,.5)",
borderColor: "#36495d",
borderWidth: 3,
},
{
label: "Planetary Mass (relative to the Sun x 10^-6)",
data: [0.166, 2.081, 3.003, 0.323, 954.792, 285.886, 43.662, 51.514],
backgroundColor: "rgba(71, 183,132,.5)",
borderColor: "#47b784",
borderWidth: 3,
},
],
}));
const options = ref<ChartOptions<"line">>({
elements: {
line: {
tension: 0,
fill: false,
},
},
scales: {
yAxes: {
ticks: {
padding: 25,
stepSize: 50,
},
},
},
});
return {
testData,
options,
moonData,
};
},
mounted() {
const ctx = document.getElementById("my-planet-chart") as HTMLCanvasElement;
console.log("Found context: ", ctx);
let c = new Chart(ctx, {
type: "line",
data: this.testData,
options: this.options,
});
console.log("Created chart: ", c);
},
methods: {
AddData() {
console.log("Appending data...");
this.moonData.push(Math.round(Math.random() * 1000));
console.log("moonData: ", this.moonData.length);
// this.testData.datasets[0].data.push(Math.round(Math.random() * 1000));
console.log("moonData: ", this.testData.datasets[0].data);
},
},
});
Any ideas?

I had exactly the same problem yesterday. Got it working using a shallowRef instead of a ref.

Related

jsPlumb + Panzoom infinite droppable canvas

I have created a codepen that uses jquery ui droppable(for drag/drop), jsPlumb (for flowcharting) and Panzoom (panning and zooming) to create a flowchart builder. You could drag the list items from the draggable container (1st column) to the flowchart (2nd column) and then connect the items using the dots to create a flowchart. The #flowchart is a Panzoom target with both pan and zoom enabled. This all works fine.
However, I would like to have the #flowchart div always span the whole area of the flowchart-wrapper i.e. the #flowchart should be an infinite canvas that supports panning, zooming and is a droppable container.
It should have the same effect as flowchart-builder-demo. The canvas there is infinite where you can drag and drop items (Questions, Actions, Outputs) from the right column.
Any pointers on how to achieve this (like the relevant events or multiple panzoom elements and/or css changes) would be greatly appreciated.
const BG_SRC_TGT = "#2C7BE5";
const HEX_SRC_ENDPOINT = BG_SRC_TGT;
const HEX_TGT_ENDPOINT = BG_SRC_TGT;
const HEX_ENDPOINT_HOVER = "#fd7e14";
const HEX_CONNECTOR = "#39afd1";
const HEX_CONNECTOR_HOVER = "#fd7e14";
const connectorPaintStyle = {
strokeWidth: 2,
stroke: HEX_CONNECTOR,
joinstyle: "round",
outlineStroke: "white",
outlineWidth: 1
},
connectorHoverStyle = {
strokeWidth: 3,
stroke: HEX_CONNECTOR_HOVER,
outlineWidth: 2,
outlineStroke: "white"
},
endpointHoverStyle = {
fill: HEX_ENDPOINT_HOVER,
stroke: HEX_ENDPOINT_HOVER
},
sourceEndpoint = {
endpoint: "Dot",
paintStyle: {
stroke: HEX_SRC_ENDPOINT,
fill: "transparent",
radius: 4,
strokeWidth: 3
},
isSource: true,
connector: ["Flowchart", { stub: [40, 60], gap: 8, cornerRadius: 5, alwaysRespectStubs: true }],
connectorStyle: connectorPaintStyle,
hoverPaintStyle: endpointHoverStyle,
connectorHoverStyle: connectorHoverStyle,
dragOptions: {},
overlays: [
["Label", {
location: [0.5, 1.5],
label: "Drag",
cssClass: "endpointSourceLabel",
visible: false
}]
]
},
targetEndpoint = {
endpoint: "Dot",
paintStyle: {
fill: HEX_TGT_ENDPOINT,
radius: 5
},
hoverPaintStyle: endpointHoverStyle,
maxConnections: -1,
dropOptions: { hoverClass: "hover", activeClass: "active" },
isTarget: true,
overlays: [
["Label", { location: [0.5, -0.5], label: "Drop", cssClass: "endpointTargetLabel", visible: false }]
]
};
const getUniqueId = () => Math.random().toString(36).substring(2, 8);
// Setup jquery ui draggable, droppable
$("li.list-group-item").draggable({
helper: "clone",
zIndex: 100,
scroll: false,
start: function (event, ui) {
var width = event.target.getBoundingClientRect().width;
$(ui.helper).css({
'width': Math.ceil(width)
});
}
});
$('#flowchart').droppable({
hoverClass: "drop-hover",
tolerance: "pointer",
drop: function (event, ui) {
var helper = $(ui.helper);
var fieldId = getUniqueId();
var offset = $(this).offset(),
x = event.pageX - offset.left,
y = event.pageY - offset.top;
helper.find('div.field').clone(false)
.animate({ 'min-height': '40px', width: '180px' })
.css({ position: 'absolute', left: x, top: y })
.attr('id', fieldId)
.appendTo($(this)).fadeIn('fast', function () {
var field = $("#" + fieldId);
jsPlumbInstance.draggable(field, {
containment: "parent",
scroll: true,
grid: [5, 5],
stop: function (event, ui) {
}
});
field.addClass('panzoom-exclude');
var bottomEndpoints = ["BottomCenter"];
var topEndPoints = ["TopCenter"];
addEndpoints(fieldId, bottomEndpoints, topEndPoints);
jsPlumbInstance.revalidate(fieldId);
});
}
});
const addEndpoints = (toId, sourceAnchors, targetAnchors) => {
for (var i = 0; i < sourceAnchors.length; i++) {
var sourceUUID = toId + sourceAnchors[i];
jsPlumbInstance.addEndpoint(toId, sourceEndpoint, { anchor: sourceAnchors[i], uuid: sourceUUID });
}
for (var j = 0; j < targetAnchors.length; j++) {
var targetUUID = toId + targetAnchors[j];
jsPlumbInstance.addEndpoint(toId, targetEndpoint, { anchor: targetAnchors[j], uuid: targetUUID });
}
$('.jtk-endpoint').addClass('panzoom-exclude');
}
// Setup jsPlumbInstance
var jsPlumbInstance = jsPlumb.getInstance({
DragOptions: { cursor: 'pointer', zIndex: 12000 },
ConnectionOverlays: [
["Arrow", { location: 1 }],
["Label", {
location: 0.1,
id: "label",
cssClass: "aLabel"
}]
],
Container: 'flowchart'
});
// Setup Panzoom
const elem = document.getElementById('flowchart');
const panzoom = Panzoom(elem, {
excludeClass: 'panzoom-exclude',
canvas: true
});
const parent = elem.parentElement;
parent.addEventListener('wheel', panzoom.zoomWithWheel);
I've just been working on the exact same issue and came across this as the only answer
Implementing pan and zoom in jsPlumb
The PanZoom used looks to be quite old - but the idea was the same, use the JQuery Draggable plugin for the movable elements, instead of the in-built JsPlumb one. This allows the elements to move out of bounds.
The below draggable function fixed it for me using the PanZoom library.
var that = this;
var currentScale = 1;
var element = $('.element');
element.draggable({
start: function (e) {
//we need current scale factor to adjust coordinates of dragging element
currentScale = that.panzoom.getScale();
$(this).css("cursor", "move");
that.panzoom.setOptions({ disablePan: true });
},
drag: function (e, ui) {
ui.position.left = ui.position.left / currentScale;
ui.position.top = ui.position.top / currentScale;
if ($(this).hasClass("jtk-connected")) {
that.jsPlumbInstance.repaintEverything();
}
},
stop: function (e, ui) {
var nodeId = $(this).attr('id');
that.jsPlumbInstance.repaintEverything();
$(this).css("cursor", "");
that.panzoom.setOptions({ disablePan: false });
}
});
I'm not sure if redrawing everything on drag is that efficient - so maybe just redraw both the connecting elements.

Highcharts-vue - Calling my own tooltip label formatter function

I have been trying to call my own function for formatting the x and y axis values in a tooltip in Highcharts vue.
Consider the following;
data() {
return {
currencySymbol: "$",
};
},
computed: {
chartOptions() {
var symbol = this.currencySymbol;
return {
chart: {
type: "spline"
},
title: {
text: "Sin chart"
},
yAxis: {
gridLineDashStyle: "Dot",
labels: {
style: {
color: "#000"
},
formatter: label => {
return (
symbol + Highcharts.Axis.prototype.defaultLabelFormatter.call(label)
);
}
}
},
tooltip: {
formatter: function () {
return Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '<br/>' +
this.formatNumber(this.y, this.fractionalDigits, this.locale, this.currencySymbol);
}
},
series: [
{
data: [10, 0, 8, 2, 6, 4, 5, 5],
color: "#6fcd98"
}
]
};
}
}
The labels work fine but the tooltip function call will not work. I have tried putting my formatNumber() function in both methods() and outside of any of the Vue hooks. Neither work.
Note that fractionDigits, locale and currencySymbol have been resolved at this point.
Wondering if someone can advise on the correct approach?
Also note that the formatter works when I remove the call to my formatNumber() function. It's lack of scope appears to be where the problem lies.
If I should assume that this.formatNumber, this.fractionDigits, this.locale, and this.currencySymbol would be references to a component's internal data, then the problem would occurs because of this context within tooltip's formatter function, which does not actually indicate on the component, but on the object on which the formatter was called, namely the TooltipFormatterContextObject.
In order to fix it, you can save appropriate context in the beginning of the chartOptions computed property function, and just refer it when calling component functions. Please take a look on the example below, where I've put the 'template' function named like yours, and presented how it could be implemented.
Live example: https://codesandbox.io/s/highcharts-vue-demo-wqwzu
Kind regards!
Not sure if it's the most elegant but I found a solution to this problem for myself.
I created a utility js file called helper.js, added my exported function (I'll need it in other places anyway) and put it in a directory called utils.
The contents are as follows;
export function formatNumber(number, maxFractionDigits, locale, currencySymbol) {
// function logic here
}
Then I imported same into my component and simply called the method as follows;
import {formatNumber} from "../../utils/helper";
export default {
data() {
return {
currencySymbol: "$",
};
},
computed: {
chartOptions() {
var symbol = this.currencySymbol;
return {
chart: {
type: "spline"
},
title: {
text: "Sin chart"
},
...
tooltip: {
formatter: function () {
return Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '<br/>' +
formatNumber(this.y, this.fractionalDigits, this.locale, this.currencySymbol);
}
},
series: [
{
data: [10, 0, 8, 2, 6, 4, 5, 5],
color: "#6fcd98"
}
]
};
}
}
}

FullCalendar 4.0 within a Vue application

I am using fullCalendar v4.0 with no jquery. I have initialized it like this
<div id="calendar"></div>
In data object I have this.
calendar: null,
config: {
plugins: [ interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin, momentPlugin],
axisFormat: 'HH',
defaultView: 'timeGridWeek',
allDaySlot: false,
slotDuration: '00:60:00',
columnFormat: 'dddd',
titleFormat: 'dddd, MMMM D, YYYY',
defaultDate: '1970-01-01',
dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
eventLimit: true,
eventOverlap: false,
eventColor: '#458CC7',
firstDay: 1,
height: 'auto',
selectHelper: true,
selectable: true,
timezone: 'local',
header: {
left: '',
center: '',
right: '',
},
select: (event) => {
this.selectCalendar(event)
},
header: false,
events: null
}
}
while having a calendar in data variable, Now I can render() and destroy() it from anywhere.
But I am having an issue for handling Calendar events:
Such as
select: (event) => {
this.selectCalendar(event)
}
I have defined another method in methods: {} as selectCalendar() to call it in select but I am getting an error as
Uncaught TypeError: Cannot read property 'selectCalendar' of undefined
I want to do few operations on select, eventClick, eventDrop, eventResize, but I am unable to call a method within the config.
Also is there any way possible to define select or any method as
select: this.selectCalendar
So that it will just straight send an event to the defined method?
I have tried vue-fullcalendar but it doesn't work for my cause. Any help will be thankful.
Vue v.2.5.21
I am using vue full calendar, you can handle event of fullcalendar like code below
<full-calendar :event-sources="eventSources" #event-selected="myEventSelected"></full-calendar>
export default{
methods:{
caculateSomething(event){
//do st here
},
myEventSelected(event){
//do st here
this.caculateSomething(event)
console.log(event)
}
}
}
This is how I sorted this out.
in html <div id="calendar"></div>
in your data() => {}
calendar: null,
config: {
plugins: [ interactionPlugin, dayGridPlugin, timeGridPlugin, listPlugin, momentPlugin],
axisFormat: 'HH',
defaultView: 'timeGridWeek',
allDaySlot: false,
slotDuration: '00:60:00',
columnFormat: 'dddd',
columnHeaderFormat: { weekday: 'short' },
defaultDate: '1970-01-01',
dayNamesShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
eventLimit: true,
eventOverlap: false,
eventColor: '#458CC7',
firstDay: 1,
height: 'auto',
selectHelper: true,
selectable: true,
timezone: 'UTC',
header: {
left: '',
center: '',
right: '',
},
header: false,
editable: true,
events: null
}
Don't define any select, resize or dropEvent in config for the first time, but then the part where you are going to render the calendar do something like this
if (this.calendar == null) {
console.log(this.schedule)
let calendarEl = document.getElementById('calendar');
let calendarConfig = this.config
let select = (event) => {
this.selectCalendar(event)
}
let eventClick = (event) => {
this.clickCalendar(event)
}
let eventDrop = (event) => {
this.dropCalendar(event)
}
let eventResize = (event) => {
this.resizeCalendar(event)
}
calendarConfig.select = select;
calendarConfig.eventClick = eventClick;
calendarConfig.eventDrop = eventDrop;
calendarConfig.eventResize = eventResize;
this.calendar = new Calendar(calendarEl, calendarConfig);
this.calendar.render();
this.renderEvents()
}
Now you can handle those events of FullCalendar in your own methods.
Also having the calendar in this.calendar gives you the power to destroy it from anywhere, in the methods: {}
In FullCalendar 4.0 things have been changes but quite simpler.
these are the methods attached to FullCalendar Events
selectCalendar(event) {
this.calendar.addEvent(event)
},
clickCalendar(event) {
if (window.confirm("Are you sure you want to delete this event?")) {
let findingID = null
if (event.event.id === "") {
findingID = event.el
} else {
findingID = event.event.id
}
let removeEvent = this.calendar.getEventById( findingID )
removeEvent.remove()
}
},
dropCalendar(event) {
},
resizeCalendar(event) {
},
destroyCalendar() {
if (this.calendar != null) {
this.calendar.destroy();
this.calendar = null
}
}
when an event is added by you. You can find it through el in an event, but the custom events should have a unique ID. through which you will find and delete it.

I am animate the pie chart using Handler event class of google visualization API

if (element.Category.ChartType == "PieChart") {
console.log(sliceOptions);
var animate = 0.05;
console.log("for your test" + rows[4][1]);
console.log("for your test" + rows[3][1]);
console.log("for your test" + rows[2][1]);
console.log("for your test" + rows[1][1]);
wrapper = new google.visualization.ChartWrapper({
chartType: element.Category.ChartType,
dataTable: rows,
containerId: element.Category.Key,
options: {
is3D:true, height: 270, slices: sliceOptions,
pieStartAngle: 100,
animation: { startup: true, duration: 15000, easing: 'Out' },
backgroundColor: '#F8F8FF',
pieSliceText: 'label',
tooltip: { color: '#FF0000' },
slices: {
1: { offset:animate },
2: { offset: animate },
3: { offset: animate },
4: { offset: animate },
},
}
});
google.visualization.events.addListener(wrapper, 'select', selectHandler);
function selectHandler() {
//alert('A table row was selected');
var data = wrapper.getSelection();
alert(data);
}
}
But it throws the above exception.
Uncaught TypeError: wrapper.getSelection is not a function.
I am animate the pie chart using Handler event class of google visualization API.
please provide me solution.
getSelection() is a Chart method, not a ChartWrapper method
try this instead...
wrapper.getChart().getSelection()

ExtJs3.4.0 to ExtJs4.1.1 upgrade issues

ExtJS4: I am having problems while upgrading my application ExtJs version from 3.4.0 to 4.1.1a.
My 3.4.0 version code:
this.jsonStore = new Ext.data.JsonStore({
proxy : new Ext.data.HttpProxy({
url: 'rs/environments',
disableCaching: true
}),
restful : true,
storeId : 'Environments',
idProperty: 'env',
fields : [
'ConnectionName', 'Type'
]
});
this.colmodel = new Ext.grid.ColumnModel({
defaults: {
align: 'center'
},
columns: [{
header: Accero.Locale.text.adminlogin.connectionsHeading,
width : 140,
dataIndex: 'ConnectionName'
},
{
header: Accero.Locale.text.adminlogin.connectionTypeHeader,
width : 120,
dataIndex: 'Type'
}]
});
config = Ext.apply({
enableHdMenu: false,
border : true,
stripeRows : true,
store : this.jsonStore,
view : new Ext.grid.GridView(),
header : false,
colModel : this.colmodel,
sm : new Ext.grid.RowSelectionModel({singleSelect: true}),
loadMask: {
msg: Accero.Locale.text.adminlogin.loadingmask
}
}, config);
I made below changes to make application work with ExtJs4.1.1:
var sm = new Ext.selection.CheckboxModel( {
listeners:{
selectionchange: function(selectionModel, selected, options){
// Must refresh the view after every selection
myGrid.getView().refresh();
// other code for this listener
}
}
});
var getSelectedSumFn = function(column){
return function(){
var records = myGrid.getSelectionModel().getSelection(),
result = 0;
Ext.each(records, function(record){
result += record.get(column) * 1;
});
return result;
};
}
var config = Ext.create('Ext.grid.Panel', {
autoScroll:true,
features: [{
ftype: 'summary'
}],
store: this.jsonStore,
defaults: { // defaults are applied to items, not the container
sortable:true
},
selModel: sm,
columns: [
{header: Accero.Locale.text.adminlogin.connectionsHeading, width: 140, dataIndex: 'ConnectionName'},
{header: Accero.Locale.text.adminlogin.connectionTypeHeader, width: 120, dataIndex: 'Type'}
],
loadMask: {
msg: Accero.Locale.text.adminlogin.loadingmask
},
viewConfig: {
stripeRows: true
}
}, config);
With these changes, I am getting the error at my local file 'ext-override.js' saying 'this.el is not defined'.
I debug the code and found that, in the current object this, there is no el object.
ext-override.js code:
(function() {
var originalInitValue = Ext.form.TextField.prototype.initValue;
Ext.override(Ext.form.TextField, {
initValue: function() {
originalInitValue.apply( this, arguments );
if (!isNaN(this.maxLength) && (this.maxLength *1) > 0 && (this.maxLength != Number.MAX_VALUE)) {
this.el.dom.maxLength = this.maxLength *1;
}
}
}
);
})();
Kindly suggest where am I going wrong?
Thanks in advance...
Seriously, use more lazy initialization! Your code is a hell of objects, all unstructured.
First of all, you can override and use the overridden method more easily with something like that (since 4.1)
Ext.override('My.Override.for.TextField', {
override : 'Ext.form.TextField',
initValue: function() {
this.callOverridden(arguments);
if (!isNaN(this.maxLength) && (this.maxLength *1) > 0 && (this.maxLength != Number.MAX_VALUE)) {
this.el.dom.maxLength = this.maxLength *1;
}
}
});
But: The method initValue is called in initField (and this in initComponent) so that you cannot have a reference to this.me because the component is actually not (fully) rendered.
So, this should help (not tested):
Ext.override('My.Override.for.TextField', {
override : 'Ext.form.TextField',
afterRender: function() {
this.callOverridden(arguments);
if (!isNaN(this.maxLength) && (this.maxLength *1) > 0 && (this.maxLength != Number.MAX_VALUE)) {
this.el.dom.maxLength = this.maxLength *1;
}
}
});
But I'm strongly recommend not to use such things within overrides. Make dedicated components which will improve code readibility.