DEV Community

Masui Masanori
Masui Masanori

Posted on • Updated on

[C3.js][TypeScript] Draw line charts 3

Intro

In this time, I will try adding multiple line data, format grid lines, and etc.
After creating charts, I will save them as images.

Use real numbers as scale span

I tried to create the value of the scale of the grid.
But when I used real numbers, some values would become unexpected.

    private getValueList(range: { min: number, max: number},
            scaleSpan: { main: number, auxiliary: number }): readonly number[] {
        const results: number[] = [];
        // min: -2, max: 10, auxiliary: 0.1
        for(let i = range.min; i <= range.max; i += scaleSpan.auxiliary) {
            console.log(`value: ${i}`);
            results.push(i);
        }
        return results;
    }
Enter fullscreen mode Exit fullscreen mode

Result

Image description

So I adjust them first.

    private getValueList(range: ChartRange,
            scaleSpan: ScaleSpan): readonly number[] {
        const results: number[] = [];
        const adjustedAuxiliary = scaleSpan.auxiliary * scaleSpan.adjuster;
        // min: -2, max: 10, auxiliary: 0.1, adjuster: 10
        for(let i = range.min * scaleSpan.adjuster; i <= range.max * scaleSpan.adjuster; i += adjustedAuxiliary) {
            console.log(`value: ${(i / scaleSpan.adjuster)}`);
            results.push(i / scaleSpan.adjuster);
        }
        return results;
    }
Enter fullscreen mode Exit fullscreen mode

Result

Image description

Samples

chart.type.ts

export enum LineType {
    default = 0,
    dashedLine,
}
export type ImageSize = {
    width: number,
    height: number,
};
export type ChartRange = {
    min: number,
    max: number
};
export type ScaleSpan = {
    main: number,
    auxiliary: number,
    adjuster: number
};
export type ChartValue = {
    type: LineType,
    color: string,
    values: readonly Point[],
}
export type Point = {
    x: number,
    y: number,
};
export type ChartValues = {
    size: ImageSize,
    fullSize: ImageSize,
    dpi: number,
    xRange: ChartRange,
    yRange: ChartRange,
    xScaleSpan: ScaleSpan,
    yScaleSpan: ScaleSpan,
    charts: readonly ChartValue[],
};
Enter fullscreen mode Exit fullscreen mode

main.page.ts

import { ChartValues, LineType } from "./charts/chart.type";
import { ChartViewer } from "./charts/chartViewer";

export function generate(): void {
    const chartRoot = document.getElementById("chart_root") as HTMLElement;
    const view = new ChartViewer(chartRoot);
    view.draw(generateSampleData());
    setTimeout(() => {
        view.saveImage();
    }, 100);
}
function generateSampleData(): ChartValues {
    const charts = [
        {
            type: LineType.default,
            color: "#ff0000",
            values: [{ x: 0.2, y: 0 },
                { x: 1, y: 1.2 },
                { x: 3.2, y: 2.5 },
                { x: 5.6, y: 6.7 },
                { x: 7, y: 7.8 },
                { x: 8.0 , y: 9 }],
        },
        {
            type: LineType.dashedLine,
            color: "#00ff00",
            values: [{ x: 1.2, y: 10 },
                { x: 4.1, y: 5.2 },
                { x: 5.3, y: 6.5 },
                { x: 6.6, y: -1.7 },
                { x: 7.5, y: 1.8 },
                { x: 7.8 , y: 4 }]
        }];
    return {
        size: { width: 1200, height: 800 },
        fullSize: { width: 1400, height: 800 },
        dpi: 600,
        xRange: { min: -2, max: 10 },
        yRange: { min: 0, max: 10 },
        xScaleSpan: { main: 1, auxiliary: 0.2, adjuster: 10 },
        yScaleSpan: { main: 1, auxiliary: 0.2, adjuster: 10 },
        charts,
    };
}
Enter fullscreen mode Exit fullscreen mode

Add multiple line data

chartViewer.ts

import c3, { ChartType } from "c3";
import { ChartRange, ChartValues, ScaleSpan } from "./chart.type";

export class ChartViewer {
    private chartElement: HTMLElement;

    public constructor(root: HTMLElement) {
        this.chartElement = document.createElement("div");
        root.appendChild(this.chartElement);
    }
    public draw(values: ChartValues): void {
        const valueXList = this.getValueList(values.xRange, values.xScaleSpan);
        const valueYList = this.getValueList(values.yRange, values.yScaleSpan);
        const data = this.generateChartData(values);
        if(data == null) {
            return;
        }        
        c3.generate({
            bindto: this.chartElement,
            data,
            axis: {
                x: {
                    min: values.xRange.min,
                    max: values.xRange.max,
                    tick: {
                        values: this.getTicks(valueXList, values.xRange, values.xScaleSpan),
                        outer: false,
                    },
                },
                y: {
                    show: true,
                    min: values.yRange.min,
                    max: values.yRange.max,
                    tick: {
                        values: this.getTicks(valueYList, values.yRange, values.yScaleSpan),
                        outer: false,
                    },
                },
            },
            grid: {
                x: {
                    show: false,
                    lines: this.generateGridLines(valueXList, values.xRange, values.xScaleSpan),
                },
                y: {
                    show: false,
                    lines: this.generateGridLines(valueYList, values.yRange, values.yScaleSpan),
                }
            },
            interaction: {
                enabled: true,
            },
            size: values.size,
        });
    }
    public saveImage(): void {
...
    }
    /** generate data for "c3.generate"  */
    private generateChartData(values: ChartValues): c3.Data|null {
        if(values.charts.length <= 0) {
            return null;
        }
        /* if all x values of every chart data are same, you can use only one "x" value */
        const xs: { [key: string]: string } = {};
        const columns: [string, ...c3.Primitive[]][] = [];
        const types: { [key: string]: ChartType } = {};
        for(let i = 0; i < values.charts.length; i++) {
            const chartValues = values.charts[i]?.values;
            if(chartValues == null ||
                chartValues.length <= 0) {
                continue;
            }
            xs[`data${i + 1}`] = `x${i + 1}`;
            columns.push([`data${i + 1}`, ...chartValues.map(v => v.y)]);
            columns.push([`x${i + 1}`, ...chartValues.map(v => v.x)]);
            types[`data${i + 1}`] = "line";
        }
        return {
            xs,
            columns,
            types,
        };
    }
    private getValueList(range: ChartRange,
            scaleSpan: ScaleSpan): number[] {
        const results: number[] = [];
        const adjustedAuxiliary = scaleSpan.auxiliary * scaleSpan.adjuster;
        for(let i = range.min * scaleSpan.adjuster; i <= range.max * scaleSpan.adjuster; i += adjustedAuxiliary) {
            results.push(i / scaleSpan.adjuster);
        }
        return results;
    }
    private getTicks(values: readonly number[],
        range: ChartRange,
        scaleSpan: ScaleSpan): string[] {
        const results: string[] = [];
        let count = range.min;
        for(const v of values) {
            if(v === (scaleSpan.main * count)) {
                results.push(v.toString());            
                count += 1;
            }
        }
        return results;
    }
    private generateGridLines(values: readonly number[],
        range: ChartRange,
        scaleSpan: ScaleSpan): { value: string, class: string }[] {
            const results = new Array<{ value: string, class: string }>();
            let count = range.min;
        for(const v of values) {
            if(v === (scaleSpan.main * count)) {
                results.push({
                    value: v.toString(),
                    class: "solid_line",
                });        
                count += 1;
            } else {
                results.push({
                    value: v.toString(),
                    class: "dashed_line",
                });
            }
        }
        return results;
    }
...
}
Enter fullscreen mode Exit fullscreen mode

Changing lines styles

I want to change colors, widths, styles of lines.
I can set colors from "c3.Data".

chartViewer.ts

...
    /** generate data for "c3.generate"  */
    private generateChartData(values: ChartValues): c3.Data|null {
        if(values.charts.length <= 0) {
            return null;
        }
        const xs: { [key: string]: string } = {};
        const columns: [string, ...c3.Primitive[]][] = [];
        const types: { [key: string]: ChartType } = {};
        const colors: { [key: string]: string } = {};
        for(let i = 0; i < values.charts.length; i++) {
            const chartValues = values.charts[i]?.values;
            if(chartValues == null ||
                chartValues.length <= 0) {
                continue;
            }
            xs[`data${i + 1}`] = `x${i + 1}`;
            columns.push([`data${i + 1}`, ...chartValues.map(v => v.y)]);
            columns.push([`x${i + 1}`, ...chartValues.map(v => v.x)]);
            types[`data${i + 1}`] = "line";
            colors[`data${i + 1}`] = values.charts[i]?.color ?? "#000000";
        }
        return {
            xs,
            columns,
            types,
            colors,
        };
    }
...
Enter fullscreen mode Exit fullscreen mode

But I can't find setting widths and styles.
So I set by myself.

chartViewer.ts

...
    public draw(values: ChartValues): void {
...     
        c3.generate({
...
        });
        this.setLineStyles(values);
    }
...
    private setLineStyles(values: ChartValues): void {
        if(values.charts.length <= 0) {
            return;
        }
        for(let i = 0; i < values.charts.length; i++) {
            // the line elements have "c3-line-{data name}" class. 
            const solidLines = this.chartElement.getElementsByClassName(`c3-line-data${i + 1}`);
            const chartValue = values.charts[i];
            if(chartValue == null) {
                continue;
            }
            for(let i = 0; i < solidLines.length; i++) {
                const path = solidLines[i];
                if(this.hasStyle(path)){
                    path.style.fill = "none";
                    path.style.strokeWidth = "3px";
                    path.style.strokeLinecap = "round";
                    switch(chartValue.type) {
                        case LineType.dashedLine:
                            path.style.strokeDasharray = "2 5";
                            break;
                        default:
                            path.style.strokeDasharray = "1 0";
                            break;
                    }                    
                }
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Grid lines styles

Z-Index

By default, the grid lines are drawn in front of lines.

Image description

I can change the orders.

...
    public draw(values: ChartValues): void {
...  
        c3.generate({
            bindto: this.chartElement,
            data,
            axis: {
...
            },
            grid: {
                x: {
                    show: false,
                    lines: this.generateGridLines(valueXList, values.xRange, values.xScaleSpan),
                },
                y: {
                    show: false,
                    lines: this.generateGridLines(valueYList, values.yRange, values.yScaleSpan),
                },
                lines: {
                    front: false,
                },
            },
            point: {
                show: false,
            },
            legend: {
                show: false,
            },
            interaction: {
                enabled: true,
            },
            size: values.size,
        });
        this.setLineStyles(values);
    }
...
Enter fullscreen mode Exit fullscreen mode

Result

Image description

Result

Image description

Latest comments (0)