Open denis-migdal opened 2 years ago
Seems to be related to this issue : https://github.com/gliffy/canvas2svg/issues/44
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.
A little workaround function that does the trick until C2S is fixed.
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();
}
With better segment merging to remove the artifacts.
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
}
}
@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.
@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,
@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 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.
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,
The data is coming as,
Another approach, I tried to initailized scales with, let scales = config.options.scales; But this give me this error,
Could you please help me understand what should be passed as inside scales for this logic to work correctly in my case?
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.
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.
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.
Sure will try to figure out the exact problem and solve it. Thanks!
See my comment in the original repo for an alternative on how to hotfix this: https://github.com/gliffy/canvas2svg/issues/44#issuecomment-1961132569
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.
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.
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.
It's a bit clearer in this version https://github.com/insinfo/canvas2svg/blob/b877064ae0e4c8d05be4a4aa0fec6cd6b449a8f1/canvas2svg.js#L1131
I think the error messages are a red herring and that the issue is chart.js is communicating lines via a Path2D object passed via canvas.stroke(path) rather than directly calling moveTo/lineTo on the canvas. Path2D is opaque - and C2S doesn't have a way of interpreting and so does nothing. I've monkey patched Path2D in my local project to record invocations to subsequently be applied by C2S, but it's not really a proper way to go about it.
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 :
Minimal example :
Expected :
Result :