I'd like to rewrite vizwit using Chart.js, and I'm having a hard time figuring out how to get the date/time chart interaction to work. If you try selecting a date range on this demo, you'll see that it filters the other charts. How do I get Chart.js to let me select a range like that on its time scale chart? It seems like by default it only lets me click on a specific date point.
Thanks for your time.
Building on #jordanwillis's and your answers, you can easily achieve anything you want, by placing another canvas on top on your chart.
Just add pointer-events:none to it's style to make sure it doesn't intefere with the chart's events.
No need to use the annotations plugin.
For example (in this example canvas is the original chart canvas and overlay is your new canvas placed on top):
var options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderWidth: 1
},
{
label: '# of Points',
data: [7, 11, 5, 8, 3, 7],
borderWidth: 1
}
]
},
options: {
scales: {
yAxes: [{
ticks: {
reverse: false
}
}]
}
}
}
var canvas = document.getElementById('chartJSContainer');
var ctx = canvas.getContext('2d');
var chart = new Chart(ctx, options);
var overlay = document.getElementById('overlay');
var startIndex = 0;
overlay.width = canvas.width;
overlay.height = canvas.height;
var selectionContext = overlay.getContext('2d');
var selectionRect = {
w: 0,
startX: 0,
startY: 0
};
var drag = false;
canvas.addEventListener('pointerdown', evt => {
const points = chart.getElementsAtEventForMode(evt, 'index', {
intersect: false
});
startIndex = points[0]._index;
const rect = canvas.getBoundingClientRect();
selectionRect.startX = evt.clientX - rect.left;
selectionRect.startY = chart.chartArea.top;
drag = true;
// save points[0]._index for filtering
});
canvas.addEventListener('pointermove', evt => {
const rect = canvas.getBoundingClientRect();
if (drag) {
const rect = canvas.getBoundingClientRect();
selectionRect.w = (evt.clientX - rect.left) - selectionRect.startX;
selectionContext.globalAlpha = 0.5;
selectionContext.clearRect(0, 0, canvas.width, canvas.height);
selectionContext.fillRect(selectionRect.startX,
selectionRect.startY,
selectionRect.w,
chart.chartArea.bottom - chart.chartArea.top);
} else {
selectionContext.clearRect(0, 0, canvas.width, canvas.height);
var x = evt.clientX - rect.left;
if (x > chart.chartArea.left) {
selectionContext.fillRect(x,
chart.chartArea.top,
1,
chart.chartArea.bottom - chart.chartArea.top);
}
}
});
canvas.addEventListener('pointerup', evt => {
const points = chart.getElementsAtEventForMode(evt, 'index', {
intersect: false
});
drag = false;
console.log('implement filter between ' + options.data.labels[startIndex] + ' and ' + options.data.labels[points[0]._index]);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.0/Chart.js"></script>
<body>
<canvas id="overlay" width="600" height="400" style="position:absolute;pointer-events:none;"></canvas>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
</body>
Notice we're basing our events and coordinates on the original canvas, but we draw on the overlay. This way we don't mess the chart's functionality.
For all of you interested in Jony Adamits solution, I created a ChartJs plugin based on his implementation. Additionaly I fixed some minor issues in regard to resizing the chart and detection of the selected data points.
Feel free to use it or to create a plugin github repo for it.
Installation
import "chart.js";
import {Chart} from 'chart.js';
import {ChartJsPluginRangeSelect} from "./chartjs-plugin-range-select";
Chart.pluginService.register(new ChartJsPluginRangeSelect());
Configuration
let chartOptions = rangeSelect: {
onSelectionChanged: (result: Array<Array<any>>) => {
console.log(result);
}
}
Plugin Code
import {Chart, ChartSize, PluginServiceGlobalRegistration, PluginServiceRegistrationOptions} from "chart.js";
interface ChartJsPluginRangeSelectExtendedOptions {
rangeSelect?: RangeSelectOptions;
}
interface RangeSelectOptions {
onSelectionChanged?: (filteredDataSets: Array<Array<any>>) => void;
fillColor?: string | CanvasGradient | CanvasPattern;
cursorColor?: string | CanvasGradient | CanvasPattern;
cursorWidth?: number;
state?: RangeSelectState;
}
interface RangeSelectState {
canvas: HTMLCanvasElement;
}
interface ActiveSelection {
x: number;
w: number;
}
export class ChartJsPluginRangeSelect implements PluginServiceRegistrationOptions, PluginServiceGlobalRegistration {
public id = 'rangeSelect';
beforeInit(chartInstance: Chart, options?: any) {
const opts = (chartInstance.config.options as ChartJsPluginRangeSelectExtendedOptions);
if (opts.rangeSelect) {
const canvas = this.createOverlayCanvas(chartInstance);
opts.rangeSelect = Object.assign({}, opts.rangeSelect, {state: {canvas: canvas}});
chartInstance.canvas.parentElement.prepend(canvas);
}
}
resize(chartInstance: Chart, newChartSize: ChartSize, options?: any) {
const rangeSelectOptions = (chartInstance.config.options as ChartJsPluginRangeSelectExtendedOptions).rangeSelect;
if (rangeSelectOptions) {
rangeSelectOptions.state.canvas.width = newChartSize.width;
rangeSelectOptions.state.canvas.height = newChartSize.height;
}
}
destroy(chartInstance: Chart) {
const rangeSelectOptions = (chartInstance.config.options as ChartJsPluginRangeSelectExtendedOptions).rangeSelect;
if (rangeSelectOptions) {
rangeSelectOptions.state.canvas.remove();
delete rangeSelectOptions.state;
}
}
private createOverlayCanvas(chart: Chart): HTMLCanvasElement {
const rangeSelectOptions = (chart.config.options as ChartJsPluginRangeSelectExtendedOptions).rangeSelect;
const overlay = this.createOverlayHtmlCanvasElement(chart);
const ctx = overlay.getContext('2d');
let selection: ActiveSelection = {x: 0, w: 0};
let isDragging = false;
chart.canvas.addEventListener('pointerdown', evt => {
const rect = chart.canvas.getBoundingClientRect();
selection.x = this.getXInChartArea(evt.clientX - rect.left, chart);
isDragging = true;
});
chart.canvas.addEventListener('pointerleave', evt => {
if (!isDragging) {
ctx.clearRect(0, 0, overlay.width, overlay.height);
}
});
chart.canvas.addEventListener('pointermove', evt => {
ctx.clearRect(0, 0, chart.canvas.width, chart.canvas.height);
const chartContentRect = chart.canvas.getBoundingClientRect();
const currentX = this.getXInChartArea(evt.clientX - chartContentRect.left, chart);
if (isDragging) {
selection.w = currentX - selection.x;
ctx.fillStyle = rangeSelectOptions.fillColor || '#00000044';
ctx.fillRect(selection.x, chart.chartArea.top, selection.w, chart.chartArea.bottom - chart.chartArea.top);
} else {
const cursorWidth = rangeSelectOptions.cursorWidth || 1;
ctx.fillStyle = rangeSelectOptions.cursorColor || '#00000088';
ctx.fillRect(currentX, chart.chartArea.top, cursorWidth, chart.chartArea.bottom - chart.chartArea.top);
}
});
chart.canvas.addEventListener('pointerup', evt => {
const onSelectionChanged = rangeSelectOptions.onSelectionChanged;
if (onSelectionChanged) {
onSelectionChanged(this.getDataSetDataInSelection(selection, chart));
}
selection = {w: 0, x: 0};
isDragging = false;
ctx.clearRect(0, 0, overlay.width, overlay.height);
});
return overlay;
}
private createOverlayHtmlCanvasElement(chartInstance: Chart): HTMLCanvasElement {
const overlay = document.createElement('canvas');
overlay.style.position = 'absolute';
overlay.style.pointerEvents = 'none';
overlay.width = chartInstance.canvas.width;
overlay.height = chartInstance.canvas.height;
return overlay;
}
private getXInChartArea(val: number, chartInstance: Chart) {
return Math.min(Math.max(val, chartInstance.chartArea.left), chartInstance.chartArea.right);
}
private getDataSetDataInSelection(selection: ActiveSelection, chartInstance: Chart): Array<any> {
const result = [];
const xMin = Math.min(selection.x, selection.x + selection.w);
const xMax = Math.max(selection.x, selection.x + selection.w);
for (let i = 0; i < chartInstance.data.datasets.length; i++) {
result[i] = chartInstance.getDatasetMeta(i)
.data
.filter(data => xMin <= data._model.x && xMax >= data._model.x)
.map(data => chartInstance.data.datasets[i].data[data._index]);
}
return result;
}
}
Unfortunately, nothing like this is built into chart.js. You would have to implement your own event hooks and handlers that would render a highlighted section on a chart and then use the .getElementsAtEvent(e) prototype method to figure out what data has been highlighted. Even these hooks that are built in may not be enough to implement what you are wanting.
Event hook options are:
Add event handlers on the canvas element itself (see example below)
canvas.onclick = function(evt){
var activePoints = myLineChart.getElementsAtEvent(evt);
// => activePoints is an array of points on the canvas that are at the same position as the click event.
};
Add event handler on the chart.js chart object using the onClick config option (explained here).
Extend some of the core charts event hooks and add your own. (see here for some guidance).
Assuming this approach works, then you could then filter your original chart data array accordingly (in the underlying chart.js object) and call the .update() prototype method to paint a new chart.
Update a few months later based on #jordanwillis' answer: I've got the beginnings of range selection.
canvas.onpointerdown = function (evt) {
clearAnnotations()
const points = chart.getElementsAtEventForMode(evt, 'index', { intersect: false })
const label = chart.data.labels[points[0]._index]
addAnnotation(label)
}
canvas.onpointerup = function (evt) {
const points = chart.getElementsAtEventForMode(evt, 'index', { intersect: false })
const label = chart.data.labels[points[0]._index]
addAnnotation(label)
}
function clearAnnotations () {
if (chart.options.annotation) {
chart.options.annotation.annotations = []
}
}
function addAnnotation (label) {
const annotation = {
scaleID: 'x-axis-0',
type: 'line',
mode: 'vertical',
value: label,
borderColor: 'red'
}
chart.options.annotation = chart.options.annotation || {}
chart.options.annotation.annotations = chart.options.annotation.annotations || []
chart.options.annotation.annotations.push(annotation)
chart.update()
}
Still need to figure out how to show a visual hover indicator as in the demo linked in the question, but it's a start.
The people who made ChartJS also made a plugin called chartjs-plugin-zoom. To install the plugin type:
npm install chartjs-plugin-zoom.
Implement:
import { Chart } from 'chart.js';
import zoomPlugin from 'chartjs-plugin-zoom';
Chart.register(zoomPlugin);
To add zooming functionality by dragging, add this to the chart configuration:
options: {
plugins: {
zoom: {
pan: {
enabled: true,
mode: 'x',
modifierKey: 'ctrl',
},
zoom: {
drag: {
enabled: true
},
mode: 'x'
}
}
}
}
A more thorough installation and use tutorial can be found here.
Instructions on how to implement zooming functionality can be found here.
Related
I am trying to make a customize object just like in the tutorial http://fabricjs.com/fabric-intro-part-3 (the LabeledRect). The object needs to be cloned, so I would have to implement the fromObject method.
I am using react with typescript and I am not sure how to implement the fromObject method.
const addLabeledRect = (canv: fabric.Canvas | null) => {
var LabeledRect = fabric.util.createClass(fabric.Rect, {
type: 'labeledRect',
initialize: function (options: { label?: any; }) {
options || (options = {});
this.callSuper('initialize', options);
this.set('label', options.label || '');
},
toObject: function () {
return fabric.util.object.extend(this.callSuper('toObject'), {
label: this.get('label')
});
},
_render: function (ctx: { font: string; fillStyle: string; fillText: (arg0: any, arg1: number, arg2: number) => void; }) {
this.callSuper('_render', ctx);
ctx.font = '20px Helvetica';
ctx.fillStyle = '#333';
ctx.fillText(this.label, -this.width / 2, -this.height / 2 + 20);
}
});
var labeledRect = new LabeledRect({
width: 100,
height: 50,
left: 100,
top: 100,
label: '1',
fill: '#fff'
});
labeledRect.set('data', {
first_item: 'Hello',
second_item: 'World',
next_itme: true
})
canv?.add(labeledRect);}
This code works just fine, the thing is when I want to copy and paste the object it doesn't work correctly.
How would I implement thefromObjectmethode so I am able to copy and paste the object?
I did some research and I found a solution, so I figured I would post it, maybe it is helpfull to somone.
This post helped me out a lot: Cloning selection of custom objects
I took me a while, but it turns out you have to assign the property to the fabric library for it to work:
fabric.LabeledRect = fabric.util.createClass(...)
and for typescript first you have to assign fabric to a variable of type any to bypass the type system and than it will work:
var Fabric: any = fabric;
Fabric.LabeledRect = fabric.util.createClass(...)
so I just had to assign fabric to a variable of type any and assign the property to the fabric library and add the fromObject function.
Now I can copy and paste my the labeledRect.
Here is how I solved it:
const addLabeledRect = (canv: fabric.Canvas | null) => {
var Fabric: any = fabric;
Fabric.LabeledRect = fabric.util.createClass(fabric.Rect, {
type: 'labeledRect',
initialize: function (options: { label?: any; }) {
options || (options = {});
this.callSuper('initialize', options);
this.set('label', options.label || '');
},
toObject: function () {
return fabric.util.object.extend(this.callSuper('toObject'), {
label: this.get('label')
});
},
_render: function (ctx: { font: string; fillStyle: string; fillText: (arg0: any, arg1: number, arg2: number) => void; }) {
this.callSuper('_render', ctx);
ctx.font = '20px Helvetica';
ctx.fillStyle = '#333';
ctx.fillText(this.label, -this.width / 2, -this.height / 2 + 20);
}
}
);
//added this part
Fabric.LabeledRect.fromObject = function (object: any, callback: any) {
var labeledRect = new Fabric.LabeledRect(object);
callback && callback(labeledRect);
return labeledRect;
};
var labeledRect = new Fabric.LabeledRect({
width: 100,
height: 50,
left: 100,
top: 100,
label: '1',
fill: '#fff'
});
labeledRect.set('data', {
first_item: 'Hello',
second_item: 'World',
next_itme: true
})
canv?.add(labeledRect);}
I need to achieve the tree view (Go JS Tree View). The respective tree view sample source code without React JS is at (Tree View Source Code). I'm trying to do the same using React JS and have the following code written. But somehow I'm missing something and the diagram/tree view is not rendering. Can you please help me to figure out the issue?
import React from 'react';
import * as go from 'gojs';
import { ReactDiagram } from 'gojs-react';
import '../../../App.css';
go.Shape.defineFigureGenerator("ExpandedLine", function(shape, w, h) {
return new go.Geometry()
.add(new go.PathFigure(0, 0.25*h, false)
.add(new go.PathSegment(go.PathSegment.Line, .5 * w, 0.75*h))
.add(new go.PathSegment(go.PathSegment.Line, w, 0.25*h)));
});
// use a sideways V figure instead of PlusLine in the TreeExpanderButton
go.Shape.defineFigureGenerator("CollapsedLine", function(shape, w, h) {
return new go.Geometry()
.add(new go.PathFigure(0.25*w, 0, false)
.add(new go.PathSegment(go.PathSegment.Line, 0.75*w, .5 * h))
.add(new go.PathSegment(go.PathSegment.Line, 0.25*w, h)));
});
let nodeDataArray = [{ key: 0 }];
const initDiagram = () => {
let $ = go.GraphObject.make;
const diagram =
$(go.Diagram, "myDiagramDiv",
{
allowMove: false,
allowCopy: false,
allowDelete: false,
allowHorizontalScroll: false,
layout:
$(go.TreeLayout,
{
alignment: go.TreeLayout.AlignmentStart,
angle: 0,
compaction: go.TreeLayout.CompactionNone,
layerSpacing: 16,
layerSpacingParentOverlap: 1,
nodeIndentPastParent: 1.0,
nodeSpacing: 0,
setsPortSpot: false,
setsChildPortSpot: false
})
});
diagram.nodeTemplate =
$(go.Node,
{ // no Adornment: instead change panel background color by binding to Node.isSelected
selectionAdorned: false,
// a custom function to allow expanding/collapsing on double-click
// this uses similar logic to a TreeExpanderButton
doubleClick: function(e, node) {
let cmd = diagram.commandHandler;
if (node.isTreeExpanded) {
if (!cmd.canCollapseTree(node)) return;
} else {
if (!cmd.canExpandTree(node)) return;
}
e.handled = true;
if (node.isTreeExpanded) {
cmd.collapseTree(node);
} else {
cmd.expandTree(node);
}
}
},
$("TreeExpanderButton",
{ // customize the button's appearance
"_treeExpandedFigure": "ExpandedLine",
"_treeCollapsedFigure": "CollapsedLine",
"ButtonBorder.fill": "whitesmoke",
"ButtonBorder.stroke": null,
"_buttonFillOver": "rgba(0,128,255,0.25)",
"_buttonStrokeOver": null
}),
$(go.Panel, "Horizontal",
{ position: new go.Point(18, 0) },
new go.Binding("background", "isSelected",
s => (s ? 'lightblue' : 'white')).ofObject(),
$(go.Picture,
{
width: 18, height: 18,
margin: new go.Margin(0, 4, 0, 0),
imageStretch: go.GraphObject.Uniform
},
// bind the picture source on two properties of the Node
// to display open folder, closed folder, or document
new go.Binding("source", "isTreeExpanded", imageConverter).ofObject(),
new go.Binding("source", "isTreeLeaf", imageConverter).ofObject()),
$(go.TextBlock,
{ font: '9pt Verdana, sans-serif' },
new go.Binding("text", "key", function(s) { return "item " + s; }))
) // end Horizontal Panel
); // end Node
diagram.linkTemplate = $(go.Link);
let max = 499;
let count = 0;
while (count < max) {
count = makeTree(3, count, max, nodeDataArray, nodeDataArray[0]);
}
diagram.model = new go.TreeModel(nodeDataArray);
return diagram;
}
function makeTree(level, count, max, nodeDataArray, parentData) {
let numChildren = Math.floor(Math.random() * 10);
for (let i = 0; i < numChildren; i++) {
if (count >= max) return count;
count++;
let childData = { key: count, parent: parentData.key };
nodeDataArray.push(childData);
if (level > 0 && Math.random() > 0.5) {
count = makeTree(level - 1, count, max, nodeDataArray, childData);
}
}
return count;
}
function imageConverter(prop, picture) {
let node = picture.part;
if (node.isTreeLeaf) {
return "images/document.svg";
} else {
if (node.isTreeExpanded) {
return "images/openFolder.svg";
} else {
return "images/closedFolder.svg";
}
}
}
window.addEventListener('DOMContentLoaded', initDiagram);
const TreeView = () => {
return (
<>
GO JS
<div id="myDiagramDiv">
<ReactDiagram
initDiagram={initDiagram}
divClassName='diagram-component'
nodeDataArray={nodeDataArray}
skipsDiagramUpdate={false}
/>
</div>
</>
);
}
export default TreeView;
When React start executing, the DOMContentLoaded event have already been fired. Instead try to call initDiagram in a useEffect hook
const TreeView = () => {
useEffect(initDiagram);
return (
<>
GO JS
<div id="myDiagramDiv">
<ReactDiagram
initDiagram={initDiagram}
divClassName='diagram-component'
nodeDataArray={nodeDataArray}
skipsDiagramUpdate={false}
/>
</div>
</>
);
}
I was define my chart as below (MainChart.vue).
import { Line, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
// const brandPrimary = '#20a8d8'
export default {
extends: Line,
mixins: [reactiveProp],
props: ['options', 'chartData', 'height'],
mounted () {
this.renderChart(this.chartData, this.options)
var elements = 1
}
}
I tested this code and confirmed that it worked well.
<line-chart :chartData="myChartData"></line-chart>
but, I tried rendering chart dynamically, it is not working.
import lineChart from './MainChart';
// ...
let chartClass = Vue.extend(lineChart)
let chartInstance = new chartClass({
propsData: {
chartData: myChartData
}
})
chartInstance.$mount()
console.log(chartInstance.$el)
console.log(chartInstance.$el.querySelector("canvas").toDataURL('image/png'))
console.log(chartInstance.$refs.canvas)
console.log(chartInstance.$refs.canvas.toDataURL('image/png'))
Console messages:
I checked from the console and found that nothing was drawn in the canvas area.
How can I do render my chart dynamically?
Similar questions:
Is it possible to print a chart with vue-chartjs?
To get full image data, you have to wait until the chart is finished. Using 'Promise' is helpful.
async function addChart(d, i, w, h) {
var canvas = document.createElement("canvas")
canvas.width = 765
canvas.height = 382
//canvas.style.width = "765px"
//canvas.style.height = "382px"
//canvas.style.display = "none"
canvas.id = "dynamicChart"
document.body.appendChild(canvas)
var ctx = document.getElementById("dynamicChart").getContext('2d');
var draw = () => new Promise((resolve, reject) => {
new Chart(ctx, {
type: 'bar',
data: d,
options: {
responsive: false
}
})
setTimeout(() => resolve(), 100)
})
await draw()
let imageData = document.getElementById("dynamicChart").toDataURL("image/png")
console.log(imageData)
addImage(imageData, i, w, h)
document.body.removeChild(canvas)
}
// ...
await addChart(myChartData, 0, 400, 300)
If you want draw multiple chart for in the loop, try this:
let chartFunctions = []
myList.forEach((item) => {
chartFunctions.push(async function() {
await addChart(myChartData, 3, 160, 80)
})
}
for(let k in chartFunctions) {
await chartFunctions[k]()
}
Console messages:
I am creating a React app and using chart.js to show historical bitcoin price data from https://www.coindesk.com/api
I want the background of the chart to be color coded depending on the data being displayed. If a particular point in the data is low the background should be red. If it is high, it should be green. Somewhere in the middle and it should be yellow.
I can get the screen to show the various colors but they don't line up with the points on the graph as expected.
The troughs should be red and the peaks should be green but they don't quite match up.
I have already looked at these two stack overflow posts but they don't quite have what I need.
Chart.js and gradient background color
Chart.js line chart set background color
I have also done more research into HTML canvas to see if maybe I am getting the width wrong. I don't think so but that's still a possibility.
class PriceIndexChart extends Component {
state = {
labels: [],
data: [],
}
async getPriceIndexData() {
const response = await fetch('https://api.coindesk.com/v1/bpi/historical/close.json?start=2019-05-11&end=2019-05-23');
let data = await response.json();
return data;
}
componentDidMount() {
this.getPriceIndexData()
.then(d => this.setState({
labels: Object.keys(d.bpi),
data: Object.values(d.bpi)
}))
.catch(err => console.log(err))
}
getColorStopColors = () => {
const colors = ['rgba(231, 76, 60, 1)', 'rgba(241, 196, 15, 1)', 'rgba(46, 204, 113, 1)'];
const numArr = [...this.state.data];
const sortArr = [...numArr].sort((a,b) => a - b);
const breakPoint = Math.floor(numArr.length / 3);
const firstThird = sortArr[breakPoint];
const secondThird = sortArr[breakPoint * 2];
const max = Math.max(...numArr);
let output = [];
for (let n of numArr) {
switch (true) {
case n <= firstThird:
output.push(this.getRGBA(n, firstThird, colors[0]));
break;
case n <= secondThird:
output.push(this.getRGBA(n, secondThird, colors[1]));
break;
case n > secondThird:
output.push(this.getRGBA(n, max, colors[2]));
break;
default:
console.log('something went wrong');
break;
}
}
return output;
}
getRGBA = (num, max, clr) => {
return `rgba(${chroma(clr).alpha(num / max)._rgb.join(', ')})`;
}
getColorStopStops = () => {
const length = this.state.data.length;
let output = [];
for (let i in this.state.data) {
output.push(i / length);
};
return output;
}
setColorGradient = (canvas) => {
const width = canvas.clientWidth;
const ctx = canvas.getContext('2d');
console.log(ctx);
const gradient = ctx.createLinearGradient(0, 0, width, 0);
const grdntStops = this.getColorStopStops();
const grdntColors = this.getColorStopColors();
for (let i = 0; i < this.state.data.length; i++) {
gradient.addColorStop(grdntStops[i], grdntColors[i]);
}
// console.log(grdntStops, grdntColors);
return gradient;
}
makeChart = canvas => {
const chartData = {
labels: this.state.labels,
datasets: [
{
label: 'bitcoin price',
data: this.state.data,
backgroundColor: this.setColorGradient(canvas),
pointHoverBackgroundColor: 'rgba(254, 202, 87, 1)',
borderColor: 'rgba(10, 189, 227, .8)',
borderWidth: 5,
borderJoinStyle: 'round',
lineTension: .2,
},
]
}
return chart;
}
With canvas.toJSON(); I cannot export the custom canvas attributes.
I have to use prototype of canvas. But I don't know how to build a prototype structure.
Existing (allowed) data:
var clipFath = this.clipPath, data = {
version: fabric.version,
objects: this._toObjects (methodName, propertiesToInclude),
};
I need it this way:
var clipFath = this.clipPath, data = {
version: fabric.version,
objects: this._toObjects (methodName, propertiesToInclude),
custom_settings_json: this.custom_settings /* <-- */
};
Original source line 7698:
In the following example, I can add custom settings to fabric objects. I need a similar structure for canvas. Source
fabric.Object.prototype.toObject = (function (toObject) {
return function (propertiesToInclude) {
propertiesToInclude = (propertiesToInclude || []).concat(
['custom_attr_1','custom_attr_2'] /* <- */
);
return toObject.apply(this, [propertiesToInclude]);
};
})(fabric.Object.prototype.toObject);
I tried this to set for canvas, as below
var canvas = new fabric.Canvas('canvas');
var custom = {
"data1": 1,
"data2": 2
}
canvas.custom_settings_json = custom;
var json_data = canvas.toJSON();
console.log(json_data);
// console log:
{
"version":"2.4.1",
"objects":[{.....}],
"custom_settings_json": {
"data1": 1,
"data2": 2
}
}
But I am getting this result without custom_settings_json in toJSON output.
// console log:
{
"version":"2.4.1",
"objects":[{.....}]
}
You can extend toJSON of canvas. As besically you need to add properties after getting the canvas data as json, just extend with your custom property after.
DEMO
fabric.Canvas.prototype.toJSON = (function(toJSON) {
return function(propertiesToInclude) {
return fabric.util.object.extend(toJSON.call(this,propertiesToInclude), {
custom_settings_json: this.custom_settings_json
});
}
})(fabric.Canvas.prototype.toJSON);
var canvas = new fabric.Canvas('c', {
"custom_settings_json": {
"data1": 1,
"data2": 2
}
});
canvas.add(new fabric.Circle({
left: 10,
top: 10,
radius: 50
}))
console.log(canvas.toJSON())
canvas{
border:2px solid;
}
<script src="https://rawgit.com/kangax/fabric.js/master/dist/fabric.js"></script>
<canvas id="c"></canvas>
You need to extend _toObjectMethod method from fabric.StaticCanvas.
I made a exemple jsfiddle.
Open the console and you will see the json from toJSON method with custom parameters data1,data2,data3.
//rewrite core
fabric.StaticCanvas.prototype._toObjectMethod = (function(toObjectMethod) {
return function(propertiesToInclude) {
return fabric.util.object.extend(toObjectMethod.call(this, "toDatalessObject", propertiesToInclude), {
data1: this.data1,
data2: this.data2,
data3: this.data3,
});
};
})(fabric.StaticCanvas.prototype._toObjectMethod);
//end
var myCanvas = new fabric.Canvas('my-canvas');
myCanvas.data1 = 1;
myCanvas.data2 = 2;
myCanvas.data3 = 4;
console.log(myCanvas.toJSON(['test']));
<script src='https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.6/fabric.min.js'></script>
<canvas id="my-canvas" width="550" height="550"> </canvas>