namespace SvgDrawing { export interface Rect { x: number; y: number; width: number; height: number; } export interface Point { x: number; y: number; } export class Point { public static add(a: Point, b: Point, out_result: Point) { out_result.x = a.x + b.x; out_result.y = a.y + b.y; } public static subtract(a: Point, b: Point, out_result: Point) { out_result.x = a.x - b.x; out_result.y = a.y - b.y; } public static normalize(v: Point) { let invN = 1.0 / Math.sqrt(v.x*v.x + v.y*v.y); v.x *= invN; v.y *= invN; } public static dot(a: Point, b: Point) { return a.x*b.x + a.y*b.y; } public static normalizedDot(a: Point, b: Point) { let NA = Math.sqrt(a.x*a.x + a.y*a.y); let NB = Math.sqrt(b.x*b.x + b.y*b.y); return (a.x*b.x + a.y*b.y) / NA / NB; } } export class Viewport { private invDataW: number = 0; private invDataH: number = 0; constructor(private data: Rect, private view: Rect) { this.update(); } setData(r: Rect) { this.data = r; this.update(); } setView(r: Rect) { this.view = r; this.update(); } xDataToView(x: number) { return (x - this.data.x) / this.data.width * this.view.width + this.view.x; } yDataToView(y: number) { return (y - this.data.y) / this.data.height * this.view.height + this.view.y; } dataToView(p: Point, out_point: Point) { out_point.x = (p.x - this.data.x) * this.invDataW * this.view.width + this.view.x; out_point.y = (p.y - this.data.y) * this.invDataH * this.view.height + this.view.y; } private update() { this.invDataW = 1.0 / this.data.width; this.invDataH = 1.0 / this.data.height; } } export interface LineStyle { className: string; } export class SvgElement { public viewport: Viewport; public width: number; public height: number; constructor(private htmlElement: HTMLElement) { let viewBox = htmlElement.getAttribute('viewBox').split(' '); let r: Rect = { x: Number(viewBox[0]), y: Number(viewBox[1]), width: Number(viewBox[2]), height: Number(viewBox[3]) }; this.viewport = new Viewport(r, { x: r.x, y: r.y + r.height, width: r.width, height: -r.height }); this.width = r.width; this.height = r.height; } clear() { this.htmlElement.querySelectorAll('*').forEach(elt => elt.remove()); } line(start: Point, end: Point): SVGLineElement { let line = document.createElementNS('http://www.w3.org/2000/svg','line'); line.setAttribute('x1', this.viewport.xDataToView(start.x).toString()); line.setAttribute('y1', this.viewport.yDataToView(start.y).toString()); line.setAttribute('x2', this.viewport.xDataToView(end.x).toString()); line.setAttribute('y2', this.viewport.yDataToView(end.y).toString()); this.htmlElement.append(line); return line; } text(position: Point, value: string, anchor?: string, verticalAlign?: number): SVGTextElement { let text = document.createElementNS('http://www.w3.org/2000/svg','text'); text.setAttribute('x', this.viewport.xDataToView(position.x).toString()); text.setAttribute('y', this.viewport.yDataToView(position.y).toString()); if(anchor !== undefined) text.setAttribute('text-anchor', anchor); if(verticalAlign !== undefined) text.setAttribute('dy', (1.0-verticalAlign) + 'em'); text.appendChild(document.createTextNode(value)); this.htmlElement.append(text); return text; } graph(y: number[]): SVGPathElement[]; graph(x: number[], y: number[]): SVGPathElement[]; graph(y: number[], indices: number[], styles: LineStyle[]): SVGPathElement[]; graph(x: number[], y: number[], indices: number[], styles: LineStyle[]): SVGPathElement[]; graph(arg1: number[], arg2?: number[], arg3?: number[] | LineStyle[], arg4?: LineStyle[]): SVGPathElement[] { let indices: number[] | null = null; let styles: LineStyle[] | null = null; if(arg3) { if(arg4) { indices = arg3; styles = arg4; } else { indices = arg2; styles = arg3; } } if(styles == null) { styles = [{className: ''}]; } let read = (idx: number, out_point: Point) => { out_point.x = arg1[idx]; out_point.y = arg2[idx]; return true; }; let reset = (idx: number) => {}; if(!arg2 || (arg3 && typeof(arg3[0]) !== 'number')) { read = (idx: number, out_point: Point) => { out_point.x = idx; out_point.y = arg1[idx]; return true; }; } let num = arg1.length; console.assert(!arg2 || num == arg2.length); if(num <= 1) return null; let optimizeCurveDist = 5; let optimizeCurveAngle = 45.0; if(optimizeCurveDist > 0 || optimizeCurveAngle < 0.0) { let rawRead = read; let dp = Math.cos(optimizeCurveAngle/180*Math.PI); let lastDrawnPoint: Point = { x: 0, y: 0 }; let nextPoint: Point = { x: 0, y: 0 }; let dir: Point = { x: 0, y: 0 }; let nextDir: Point = { x: 0, y: 0 }; let nextSegDir: Point = { x: 0, y: 0 }; let perp: Point = { x: 0, y: 0 }; let rawReset = reset; reset = (idx: number) => { rawReset(idx); read(idx, lastDrawnPoint); }; read = (idx: number, out_point: Point) => { rawRead(idx, out_point); if(idx == 0 || idx == num - 1) return true; rawRead(idx + 1, nextPoint); Point.subtract(out_point, lastDrawnPoint, dir); Point.subtract(nextPoint, out_point, nextSegDir); if(Point.normalizedDot(dir, nextSegDir) < dp) { lastDrawnPoint = { ...out_point }; return true; } Point.subtract(nextPoint, lastDrawnPoint, nextDir); perp.x = -nextDir.y; perp.y = nextDir.x; Point.normalize(perp); let d = Math.abs(Point.dot(perp, dir)); if(d > optimizeCurveDist) { lastDrawnPoint = { ...out_point }; return true; } return false; } } let startTime = performance.now(); let count = 0; let paths = []; for(let styleIdx = 0; styleIdx < styles.length; ++styleIdx) { let dataPoint: Point = { x: 0, y: 0 }; let viewPoint: Point = { x: 0, y: 0 }; let coordinates = ''; let open = false; for(let idx = 0; idx < num; ++idx) { let includePoint = !indices || indices[idx] == styleIdx; if(!open && includePoint) { reset(idx); read(idx, dataPoint); this.viewport.dataToView(dataPoint, viewPoint); let space = coordinates == '' ? '' : ' '; coordinates += space+'M'+Math.round(viewPoint.x)+','+Math.round(viewPoint.y) + ' L'; count += 1; } else if(open && (read(idx, dataPoint) || !includePoint)) { this.viewport.dataToView(dataPoint, viewPoint); coordinates += ' '+Math.round(viewPoint.x)+','+Math.round(viewPoint.y); count += 1; } open = includePoint; } let style = styles[styleIdx]; let path = document.createElementNS('http://www.w3.org/2000/svg','path'); path.setAttribute('class', 'graph' + (style.className == '' ? '' : ' ' + style.className)); path.setAttribute('d', coordinates); this.htmlElement.append(path); paths.push(path); } let endTime = performance.now(); console.log("graph: " + count + " points, " + (endTime - startTime) + "ms"); return paths; } } }