34
[C3.js][TypeScript] Save C3.js charts as images
Last time, I save C3.js charts as images.
But all of the CSS values were ignored.
So this time, I will try using CSS to save images.
I can't get stylesheet values by TypeScript.
<!DOCTYPE html>
<html lang="en">
<head>
<title>chart sample</title>
<meta charset="utf-8">
<link href="css/sample.css" rel="stylesheet" />
</head>
<body>
<div id="sample_target">Hello</div>
<script src="./js/sample.js"></script>
<script>Page.init();</script>
</body>
</html>
#sample_target {
background-color: red;
}
export function init(): void {
const sample = document.getElementById("sample_target") as HTMLElement;
console.log(sample.style.background);
console.log(sample.style.backgroundColor);
...
}
This is because the images what are created from SVG don't have grid lines.
To get CSS values, I can use "document.styleSheets".
Each elements have stylesheet values per file.
Because I can get null from "document.styleSheets[0].title", I use "document.styleSheets[0].href" to find the target stylesheet file.
<!DOCTYPE html>
<html lang="en">
<head>
<title>chart sample</title>
<meta charset="utf-8">
<link href="css/chart.page.css" rel="stylesheet" />
<link href="css/c3.css" rel="stylesheet" />
</head>
<body>
<div id="chart_root"></div>
<script src="./js/main.page.js"></script>
<script>Page.init();</script>
</body>
</html>
.solid_line line {
stroke: #000000;
stroke-dasharray: 1 0;
stroke-linecap: round;
}
.dashed_line line {
stroke: #9f9f9f;
stroke-dasharray: 2 5;
stroke-linecap: round;
}
@import url("../../node_modules/c3/c3.min.css");
...
public saveImage(): void {
...
const styleSheet = this.getStyleSheet("chart.page.css");
...
}
...
private getStyleSheet(targetName: string): CSSStyleSheet|null {
for(let i = 0; i < document.styleSheets.length; i++) {
const styleSheet = document.styleSheets[i];
if(styleSheet?.href == null) {
continue;
}
if(styleSheet.href.endsWith(targetName)) {
return styleSheet;
}
}
return null;
}
...
Last time, I set styles to all lines of the chart.
In this time, I try separating them to set each styles.
I have to get class names and ids from "CSSStyleSheet" to find DOM targets.
I can get them from "document.styleSheets[0].cssRules".
One problem is because I only can get "CSSRule" from "document.styleSheets[0].cssRules[0]" and the type doesn't have "selectorText".
So I have to use type narrowing of TypeScript.
...
public saveImage(): void {
const svg = this.getSvgRoot();
if(svg == null) {
console.error("svg was null");
return;
}
const styleSheet = this.getStyleSheet("chart.page.css");
if(styleSheet == null) {
console.error("styleSheet was null");
return;
}
this.setLineStyles(svg, styleSheet, "solid_line");
this.setLineStyles(svg, styleSheet, "dashed_line");
...
}
...
private setLineStyles(svg: SVGElement, styleSheet: CSSStyleSheet, targetName: string): void {
const lineElements = svg.querySelectorAll(`.c3 .${targetName} line`);
if(lineElements.length <= 0) {
return;
}
for(let i = 0; i < styleSheet.cssRules.length; i++) {
const rule = styleSheet.cssRules[i];
if(this.isCSSDeclaration(rule)) {
if(rule.selectorText.includes(targetName)) {
for(let k = 0; k < lineElements.length; k++) {
const line = lineElements[k] as any;
if(line == null) {
continue;
}
if("style" in line &&
line.style instanceof CSSStyleDeclaration) {
line.style.stroke = rule.style.stroke;
line.style.strokeDasharray = rule.style.strokeDasharray;
line.style.strokeLinecap = rule.style.strokeLinecap;
}
}
}
}
}
}
private isCSSDeclaration(obj: any): obj is CSSStyleRule {
if(obj == null) {
return false;
}
if(((obj instanceof Object) &&
("selectorText" in obj) &&
("style" in obj)) == false) {
return false;
}
if(typeof obj.selectorText !== "string") {
return false;
}
if(obj.style instanceof CSSStyleDeclaration == false) {
return false;
}
return true;
}
...
import c3 from "c3";
import { ChartValues } from "./chart.type";
export class ChartViewer {
private chartElement: HTMLElement;
private chart: c3.ChartAPI|null = null;
public constructor(root: HTMLElement) {
this.chartElement = document.createElement("div");
root.appendChild(this.chartElement);
}
public draw(value: ChartValues): void {
const valueXList = this.getValueX(0, 10);
const ticksX = this.getTicks(valueXList);
const valueYList = value.values.map(v => v.y);
const gridLineY = valueYList.map(t => this.generateGridLine(t));
const gridLines = valueXList.map(t => this.generateGridLine(t));
this.chart = c3.generate({
bindto: this.chartElement,
data: {
x: "x",
columns: [
["data1", ...value.values.map(v => v.y)],
["x", ...value.values.map(v => v.x)],
],
types: {
data1: "line"
},
},
axis: {
x: {
min: 0,
max: 10,
tick: {
values: [...ticksX],
outer: false,
},
padding: { left: 0, }
},
y: {
min: 0,
padding: { bottom: 0, }
}
},
grid: {
x: {
show: false,
lines: [...gridLines],
},
y: {
show: false,
lines: [...gridLineY],
}
},
interaction: {
enabled: false,
},
});
}
public saveImage(): void {
const svg = this.getSvgRoot();
if(svg == null) {
console.error("svg was null");
return;
}
const styleSheet = this.getStyleSheet("chart.page.css");
if(styleSheet == null) {
console.error("styleSheet was null");
return;
}
this.setLineStyles(svg, styleSheet, "solid_line");
this.setLineStyles(svg, styleSheet, "dashed_line");
const chartPaths = svg.querySelectorAll(".c3-chart path");
for(let i = 0; i < chartPaths.length; i++) {
const path: any = chartPaths[i];
if(this.hasStyle(path)) {
path.style.fill = "none";
path.style.stroke = "green";
}
}
const lines = svg.querySelectorAll(".c3-axis line");
const nodes = svg.querySelectorAll(".c3-axis path");
const gridLines = Array.from(nodes).concat(Array.from(lines));
for(let i = 0; i < gridLines.length; i++) {
const line: any = gridLines[i];
if(this.hasStyle(line)) {
line.style.fill = "none";
line.style.stroke = "red";
}
}
const serializedImage = new XMLSerializer().serializeToString(svg);
const image = new Image();
image.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = this.chartElement.clientWidth;
canvas.height = this.chartElement.clientHeight;
const ctx = canvas.getContext("2d");
if(ctx == null) {
console.error("ctx was null");
return;
}
ctx.drawImage(image, 0, 0);
document.body.appendChild(canvas);
};
image.src = "data:image/svg+xml;charset=utf-8;base64," + window.btoa(serializedImage);
}
private getStyleSheet(targetName: string): CSSStyleSheet|null {
for(let i = 0; i < document.styleSheets.length; i++) {
const styleSheet = document.styleSheets[i];
if(styleSheet?.href == null) {
continue;
}
if(styleSheet.href.endsWith(targetName)) {
return styleSheet;
}
}
return null;
}
private setLineStyles(svg: SVGElement, styleSheet: CSSStyleSheet, targetName: string): void {
const lineElements = svg.querySelectorAll(`.c3 .${targetName} line`);
if(lineElements.length <= 0) {
return;
}
for(let i = 0; i < styleSheet.cssRules.length; i++) {
const rule = styleSheet.cssRules[i];
if(this.isCSSDeclaration(rule)) {
if(rule.selectorText.includes(targetName)) {
for(let k = 0; k < lineElements.length; k++) {
const line = lineElements[k] as any;
if(line == null) {
continue;
}
if("style" in line &&
line.style instanceof CSSStyleDeclaration) {
line.style.stroke = rule.style.stroke;
line.style.strokeDasharray = rule.style.strokeDasharray;
line.style.strokeLinecap = rule.style.strokeLinecap;
}
}
}
}
}
}
private getValueX(from: number, to: number): readonly number[] {
const results: number[] = [];
for(let i = from; i <= to; i++) {
if(i < to) {
for(let j = 0.0; j < 1.0; j += 0.1) {
results.push(i + j);
}
}
}
return results;
}
private getTicks(values: readonly number[]): readonly string[] {
const results: string[] = [];
for(const v of values) {
if(v === (Math.trunc(v))) {
results.push(v.toString());
} else {
results.push("");
}
}
return results;
}
private generateGridLine(value: number): { value: string, class: string } {
let lineClass = "";
if(value === (Math.trunc(value))) {
lineClass = "solid_line";
} else {
lineClass = "dashed_line";
}
return {
value: value.toString(),
class: lineClass,
};
}
private getSvgRoot(): SVGElement|null {
for(let i = 0; i < this.chartElement.children.length; i++) {
if(this.chartElement.children[0] == null) {
continue;
}
if(this.chartElement.children[0].tagName === "svg") {
return this.chartElement.children[0] as SVGElement;
}
}
return null;
}
private isCSSDeclaration(obj: any): obj is CSSStyleRule {
if(obj == null) {
return false;
}
if(((obj instanceof Object) &&
("selectorText" in obj) &&
("style" in obj)) == false) {
return false;
}
if(typeof obj.selectorText !== "string") {
return false;
}
if(obj.style instanceof CSSStyleDeclaration == false) {
return false;
}
return true;
}
private hasStyle(obj: any): obj is { style: CSSStyleDeclaration } {
if((obj instanceof Object &&
"style" in obj) === false ) {
return false;
}
return (obj.style instanceof CSSStyleDeclaration);
}
}
34