Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C2S doesn't work with new version of Chart.js : Attempted to apply path command to node g #25

Open
denis-migdal opened this issue Nov 3, 2022 · 17 comments

Comments

@denis-migdal
Copy link

denis-migdal commented Nov 3, 2022

I got several issue trying to use C2S to export a graph generated with Chart.js

On stackoverflow, I found out that C2S was lacking some method (getContext, style, getAttribute, and addEventListener) (cf below).

However, new versions of Chart.js still seems to be doing something that C2S doesn't like.

Initially I used a npm package that seems to work (but without any git associated) at the exception of setTransform and resetTransform that weren't implemented ( https://www.npmjs.com/package/canvas-to-svg ). This npm package seems to be maintained by @rob-gordon (https://github.com/rob-gordon), and I think is from this git https://github.com/tone-row/canvas-to-svg

I then tried several versions of C2S. Yours seems to be one of the most maintained. Only the grid is drawn, and I don't see any error in the console (cf below) : "Attempted to apply path command to node g"

Would you know how I could make it works ?

Missing methods :


ctx.getContext = function (contextId) {
		  if (contextId=="2d" || contextId=="2D") {
		      return this;
		  }
		  return null;
}

ctx.style = function () {
		  return this.__canvas.style
}

ctx.getAttribute = function (name) {
		  return this[name];
}
//ctx.setTransform = function() {}
//ctx.resetTransform = function() {}

ctx.addEventListener =  function(type, listener, eventListenerOptions) {
		  console.log("canvas2svg.addEventListener() not implemented.")
}

Minimal example :

import {Chart, LinearScale, ScatterController, PointElement, LineElement} from 'chart.js';
Chart.register(ScatterController, PointElement, LineElement, LinearScale);

import { Context } from "./svgcanvas-issue-fill-path/";

let config = {
        data: {
        datasets: [{
        	type:'scatter',
        	showLine: true,
            data: [ [3,0], [3,1],[5,5],[7,6],[12,1], [19,2]]
        }],
    },

        options: {
        	animation: false,
            responsive:false,
            maintainAspectRatio: false
        }
    };
    
let ctx = new Context(500,500);

// @ts-ignore
ctx.getContext = function (contextId) {
		  if (contextId=="2d" || contextId=="2D") {
		      return this;
		  }
		  return null;
}

// @ts-ignore
ctx.style = function () {
		  return this.__canvas.style
}

// @ts-ignore
ctx.getAttribute = function (name) {
		  return this[name];
}
//ctx.setTransform = function() {}
//ctx.resetTransform = function() {}

// @ts-ignore
ctx.addEventListener =  function(type, listener, eventListenerOptions) {
		  console.log("canvas2svg.addEventListener() not implemented.")
}

//let canvas = document.getElementById('test');
//new Chart(canvas.getContext('2d'), config);

new Chart(ctx, config);

let str = ctx.getSerializedSvg();

//download(str, 'test.svg', 'image/svg+xml');

Expected :

canvas

Result :
test(52)

@denis-migdal
Copy link
Author

Seems to be related to this issue : gliffy#44

@denis-migdal
Copy link
Author

It seems that the issue is when Chart.js is trying to draw lines.

I found a dirty dirty workaroud : instead of just drawing a line, drawing 2 lines and ask Charts JS to fill the space between the 2 to make a line... that's dirty... but it works.

I'll post the workaround here, but I think fixing C2S still would be great.

@denis-migdal
Copy link
Author

denis-migdal commented Nov 4, 2022

A little workaround function that does the trick until C2S is fixed.

img

There are little artifacts as I need to do better segment merging

// @ts-ignore
import { Context } from "svgcanvas";


import {Chart, ChartConfigurationCustomTypesPerDataset, LinearScale, ScatterController, PointElement, LineElement} from 'chart.js';
Chart.register(ScatterController, PointElement, LineElement, LinearScale);

function patch_linedata(data: number[][], width: number): {upline: [number,number][], downline: [number,number][]} {

	let dw = width/2;

	let segments = data.map( (_,idx) => [data[idx], data[idx+1]]);
	--segments.length;

	let angles = segments.map( ([a, b]) => {

		let dx = b[0] - a[0];
		let dy = b[1] - a[1];

		return Math.atan(dy/dx);
	});

	let upline   = new Array(segments.length*2);
	let downline = new Array(segments.length*2);
	for(let i = 0; i < segments.length; ++i) {

		let langle = Math.PI - (angles[i] + Math.PI/2);

		let dx =   Math.cos(langle) * dw;
		let dy = - Math.sin(langle) * dw;

		for(let j = 0; j < segments[i].length; ++j) {
			let point = segments[i][j];
			upline  [2*i+j] = [point[0]+dx, point[1]+dy];
			downline[2*i+j] = [point[0]-dx, point[1]-dy];
		}
	}

	if( upline[0][0] < downline[0][0] )
		downline.unshift( upline[0] );
	else
		upline  .unshift( downline[0] );

	let last_up   = upline  [upline  .length-1];
	let last_down = downline[downline.length-1];
	if( last_up[0] > last_down[0] )
		downline.push( last_up );
	else
		upline  .push( last_down );

	return {
		upline,
		downline
	}
}
function patch_context(ctx: Context) {

	// @ts-ignore
	ctx.getContext = function (contextId) {
	    if (contextId=="2d" || contextId=="2D") {
	        return this;
	    }
	    return null;
	}

	// @ts-ignore
	ctx.style = function () {
	    return this.__canvas.style
	}

	// @ts-ignore
	ctx.getAttribute = function (name) {
	    return this[name];
	}
	//ctx.setTransform = function() {}
	//ctx.resetTransform = function() {}

	// @ts-ignore
	ctx.addEventListener =  function(type, listener, eventListenerOptions) {
	    console.log("canvas2svg.addEventListener() not implemented.")
	}
}
function patch_line(line_cfg: any,
					config: ChartConfigurationCustomTypesPerDataset,
					scales: any
					) {

	let data = line_cfg.data.map( (point: [number,number]) => [
									scales.x.getPixelForValue(point[0]),
									scales.y.getPixelForValue(point[1])
								]);

	let {upline, downline} = patch_linedata(data, line_cfg.borderWidth ?? 3); // 3 is default for line/scatter

	function px2val( data: [number, number][] ): [number,number][] {

		return data.map( point => [
				scales.x.getValueForPixel(point[0]),
				scales.y.getValueForPixel(point[1])
			]);
	}

	let upline_cfg    = Object.assign({}, line_cfg,{
			data: px2val(upline),
			pointRadius: 0,
			fill: {
				target: '+1',
				above: line_cfg.borderColor,
				below: line_cfg.borderColor,
			}
		});

	let downline_cfg  = Object.assign({}, line_cfg, {
		data: px2val(downline),
		pointRadius: 0,
		fill: {
			//target: 'origin',
			//above: 'blue'
		}
	});

	config.data.datasets.push(upline_cfg, downline_cfg);
}
function patch_graph(config: ChartConfigurationCustomTypesPerDataset, scales: any) {

	let lines = config.data.datasets.filter(d => (d as any).showLine === true);

	for(let line of lines)
		patch_line(line, config, scales);
}

export function G2S() {


	let config: ChartConfigurationCustomTypesPerDataset = {
	    data: {
		    datasets: [{
		    	type:'scatter',
		    	showLine: true,
		    	borderColor: 'red',
		    	//borderWidth: 10,
		        data: [ [3,0.2], [3,1],[5,5],[7,6],[12,1], [19,2], [20,2]]
		    }],
		},
	    options: {
	    	animation: false,
	        responsive:false,
	        maintainAspectRatio: false
	    }
	};

	// compute scales...
	let ctx_o = new Context(500,500);
	patch_context(ctx_o);
	let chart = new Chart(ctx_o, config);

	let scales = chart.scales;
	
	let ctx = new Context(500,500);
	patch_context(ctx);
	patch_graph(config, scales);
	new Chart(ctx, config);

	return ctx.getSerializedSvg();
}

@denis-migdal
Copy link
Author

With better segment merging to remove the artifacts.

result

Hopes it'll help someone someday ;)


function patch_linedata(data: number[][], width: number): {upline: [number,number][], downline: [number,number][]} {

	let dw = width/2;

	let segments = data.map( (_,idx) => [data[idx], data[idx+1]]);
	--segments.length;

	let angles = segments.map( ([a, b]) => {

		let dx = b[0] - a[0];
		let dy = b[1] - a[1];

		return Math.atan(dy/dx);
	});

	let upline   = new Array(segments.length*2);
	let downline = new Array(segments.length*2);
	for(let i = 0; i < segments.length; ++i) {

		let langle = Math.PI - (angles[i] + Math.PI/2);

		let dx =   Math.cos(langle) * dw;
		let dy = - Math.sin(langle) * dw;

		for(let j = 0; j < segments[i].length; ++j) {
			let point = segments[i][j];
			upline  [2*i+j] = [point[0]+dx, point[1]+dy];
			downline[2*i+j] = [point[0]-dx, point[1]-dy];
		}
	}

	function merge_segments(line : [number,number][]) {

		let result: [number,number][] = new Array(line.length/2+1);

		result[0] = line[0];
		result[result.length-1] = line[line.length-1];

		for(let i = 1; i < line.length/2; ++i) {

			type Segment = [ [number,number],[number,number] ];

			let seg1: Segment = [line[(i-1)*2], line[(i-1)*2+1]];
			let seg2: Segment = [line[i*2]    , line[i*2+1]];

			// y = ax + b
			function calcParams(seg: Segment) {

				let dx = seg[1][0] - seg[0][0];
				let dy = seg[1][1] - seg[0][1];

				let a = dy/dx;
				let b = seg[0][1] - a * seg[0][0];

				return [a, b];
			}

			let [a1, b1] = calcParams(seg1);
			let [a2, b2] = calcParams(seg2);

			if(a1 === a2 || Number.isNaN(a1 - b2) ) {
				result[i] = seg2[0];
				continue;
			}

			if( a1 === Number.POSITIVE_INFINITY || a1 === Number.NEGATIVE_INFINITY ) {
				let x = seg1[0][0];
				let y = a2 * x + b2;
				result[i] = [ x , y ];
				continue;
			}
			if( a2 === Number.POSITIVE_INFINITY || a2 === Number.NEGATIVE_INFINITY ) {
				let x = seg2[0][0];
				let y = a1 * x + b1;
				result[i] = [ x , y ];
				continue;
			}

			let x = -(b1-b2)/(a1-a2);
			let y = a1 * x + b1;

			result[i] = [ x, y ];
		}

		return result;
	}

	upline   = merge_segments(upline);
	downline = merge_segments(downline);

	if( upline[0][0] < downline[0][0] )
		downline.unshift( upline[0] );
	else
		upline  .unshift( downline[0] );

	let last_up   = upline  [upline  .length-1];
	let last_down = downline[downline.length-1];
	if( last_up[0] > last_down[0] )
		downline.push( last_up );
	else
		upline  .push( last_down );

	return {
		upline,
		downline
	}
}

@Dean-NC
Copy link

Dean-NC commented Feb 14, 2023

@denis-migdal Thanks very much for posting this. I'm using chart.js and jsPdf, and the PDF quality is good when printed (I use chart.js option devicePixelRatio: 2), but not great when viewing PDF on screen and zooming PDF. I'm considering SvgCanvas, but I see you found issues. Did your latest patch code work OK, or did you find more issues? I have scatter and bar charts, and only 1 chart with line (bar/line combo).
However, if the only way to get SVG to PDF is going through canvas, then this might not provide any benefit to me.

@ShreyaseeKamble
Copy link

ShreyaseeKamble commented Dec 21, 2023

@denis-migdal I tried to replicate the same solution, but did not work in my case. Still the library gives same error for line and Radar chart.

This is the SVG which is getting generated in my case,

g path missing

@denis-migdal
Copy link
Author

@denis-migdal Did your latest patch code work OK, or did you find more issues?

For my usage it seemed to work OK.

@denis-migdal
Copy link
Author

@denis-migdal I tried to replicate the same solution, but did not work in my case. Still the library gives same error for line and Radar chart.

Can you provide a minimal example of your code reproducing it so I can test it ?
It seems to work for my usage, but maybe there are other things causing issues.

@ShreyaseeKamble
Copy link

ShreyaseeKamble commented Dec 22, 2023

I won't be able to share the code here but when I tried to debug this code further in my case, I found following,

As we have referred scales as,
let scales = chart.scales;

Inside patch_line function,
issue

The data is coming as,
issue2

Another approach, I tried to initailized scales with,
let scales = config.options.scales;
But this give me this error,

issue4

Could you please help me understand what should be passed as inside scales for this logic to work correctly in my case?

@denis-migdal
Copy link
Author

I think you made a mistake when copying my code.

Indeed, I do :

let data = line_cfg.data.map( (point: [number,number]) => [
									scales.x.getPixelForValue(point[0]),
									scales.y.getPixelForValue(point[1])
								]);

But you wrote :

let data = line_cfg.data.map( (point: [number,number]) => {[
									scales.x.getPixelForValue(point[0]),
									scales.y.getPixelForValue(point[1])
								]});

Yeah took me quite sometime to see it xD.

@ShreyaseeKamble
Copy link

Not a mistake, I was debugging the code and to add the debugger Ihad added brackets to make it a function but anyways even after removing, It gives same error.

@denis-migdal
Copy link
Author

Not a mistake, I was debugging the code and to add the debugger Ihad added brackets to make it a function

If you add bracket you also need to add a return.

after removing, It gives same error.

To be fair I do not really understand how C2S is working internally, I just noticed lines weren't printed but area fill was, so I replaced the lines by area filling.

I'll advise you to try using it on empty graph, then adding elements little by little to see exactly what is causing the issue.

@ShreyaseeKamble
Copy link

Sure will try to figure out the exact problem and solve it. Thanks!

@jiayihu
Copy link

jiayihu commented Feb 23, 2024

See my comment in the original repo for an alternative on how to hotfix this: gliffy#44 (comment)

@denis-migdal
Copy link
Author

See my comment in the original repo for an alternative on how to hotfix this: gliffy#44 (comment)

Thanks, I'll try to test that someday.
My temporary fix was a very dirty one as I do not really know how C2S is working, I'm happy a cleaner version has been found.

I think gliffy gave up on C2S, as it's been years since the repo has been updated. I don't know which repository is the new maintained one.

@jiayihu
Copy link

jiayihu commented Feb 23, 2024

Yes maintining a repo is time consuming and not worth it if you don't use it anymore in your job. zenozeng also has pretty much abandoned the repo. Can't blame any of them.

I'd like to fork and create a TS version of it, so at least contributing would be easier but it would probably become an additional unmaintained repo over time 😄

Back to the fix, my alternative is just an hotfix, not a proper fix, but it seems to work well for me and I work on a complex Miro-like collaborative canvas.

@denis-migdal
Copy link
Author

I'd like to fork and create a TS version of it, so at least contributing would be easier but it would probably become an additional unmaintained repo over time 😄

It'll be easier if someone created a C2S organization so that the owner(s) would just have to manage the rights, i.e. add or remove users to the organization members.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants