src/SVGElem.ts

   1export const m = require("mithril");

   2export type Vnode = typeof m.Vnode;

   3import { XYScale }  from './AxesTypes';

   4import { DataRow }  from 'hsdatab';

   5

   6/** svg primitive Point, measured in viewbox coordinates.  */

   7export interface Point {

   8    /** x-viewbox value of the point */

   9    x:   number;

  10    /** y-viewbox value of the point */

  11    y:   number;

  12    /** viewbox unit to use for x coordinate. Allowed values are 'px' or '%'; defaults to 'px' */

  13    xunit?: string;

  14    /** viewbox unit to use for y coordinate. Allowed values are 'px' or '%'; defaults to 'px' */

  15    yunit?: string;

  16}

  17

  18/** svg primitive Rect, measured in viewbox coordinates.  */

  19export interface Rect {

  20    /** top left point */

  21    tl: Point;

  22    /** bottom right point */

  23    br: Point;

  24}

  25

  26/** 

  27 * svg extended Point, measured in viewbox coordinates. 

  28 * Extends `Point` with optional `dx` and 'dy' offsets and optional units.

  29 */

  30export interface ExtendedPoint extends Point{

  31    dx?: number;

  32    dy?: number;

  33    dxunit?: string;

  34    dyunit?: string;

  35}

  36

  37

  38export interface Area {

  39    w: number;

  40    h: number;

  41    wunit?: string;

  42    hunit?: string;

  43}

  44

  45export interface TextElem {

  46    /** the text to show */

  47    text: string; 

  48

  49    /** a css class to set */

  50    cssClass?:  string;   

  51

  52    /** a style to set */

  53    style?:  string;   

  54

  55    /** optional absolute x positioning on the canvas, e.g. '50%' */

  56    x?:         string;

  57

  58    /** optional absolute y positioning on the canvas, e.g. '50%' */

  59    y?:         string;

  60    

  61    /** horizontal align: 'start' | 'middle' | 'end'; uses `text-align` attribute */

  62    xpos:       TextHAlign;

  63

  64    /** vertical align: 'top' | 'center' | 'bottom'; uses `dy` attribute */

  65    ypos:       TextVAlign;

  66

  67    /** horizontal label offset in 'em'; uses `dx` attribute */

  68    hOffset:    number;

  69

  70    /** vertical label offset in 'em'; uses `dy` attribute */

  71    vOffset:    number;

  72}

  73

  74export function round (num:number):string { 

  75    const result = num.toFixed(1);

  76    if (result === 'Infinity') {

  77        return '1e20';

  78    } 

  79    return result;

  80}

  81

  82export enum TextHAlign {

  83    start   = 'start',

  84    middle  = 'middle',

  85    end     = 'end'

  86}

  87

  88export enum TextVAlign {

  89    top     = 'top',

  90    center  = 'center',

  91    bottom  = 'bottom'

  92}

  93

  94export abstract class SVGElem {

  95    /**

  96     * plot some text 

  97     * @param cfg configures the text alignment and positioning

  98     * @param text the text to plot

  99     */

 100    text(cfg:TextElem, text:string):Vnode {

 101        let yShift = 0;

 102        let hAlign:TextHAlign;

 103        switch(cfg.xpos) {

 104            case TextHAlign.start:  hAlign = cfg.xpos; break;

 105            case TextHAlign.end:    hAlign = cfg.xpos; break;

 106            case TextHAlign.middle: 

 107            default:                hAlign = TextHAlign.middle;

 108        }

 109        switch(cfg.ypos) { // additional y 'em' shift

 110            case TextVAlign.top:    yShift = 0.7; break;

 111            case TextVAlign.center: yShift = 0.35; break;

 112            case TextVAlign.bottom: 

 113            default:                yShift =  0; break;

 114        }

 115        const param = { 

 116            x: cfg.x || ''

 117            y: cfg.y || '',

 118            dx:round(cfg.hOffset||0) + 'em',    

 119            dy:round((cfg.vOffset||0)+yShift) + 'em',

 120            style: `text-anchor:${hAlign}; ${cfg.style||''}`,

 121            class: cfg.cssClass,

 122        };

 123        return m('text', param, text);

 124    }

 125

 126    /**

 127     * plot a rectangle in domain coordinates

 128     * @param tl the top-left corner of the rect

 129     * @param area the width and height of the rect

 130     * @param style optional css style setting, such as stroke or stroke-width

 131     */

 132    rect(tl:Point, area:Area, style:string, title?:string):Vnode {

 133        if (area.w < 0) {

 134            tl.x += area.w;

 135            area.w = -area.w;

 136        }

 137        if (area.h < 0) {

 138            tl.y += area.h;

 139            area.h = -area.h;

 140        }

 141        const param = {

 142            x: round(tl.x),       y: round(tl.y),

 143            width: round(area.w)  + (area.wunit||''), 

 144            height: round(area.h) + (area.hunit||''),

 145            style: style

 146        };

 147        return m('rect', param); // , m('title', title);

 148    }

 149

 150    /**

 151     * plot a circle around the center domain point `c`, with radius `r`

 152     * @param c the circle's center point in domain coordinates

 153     * @param r the circle's radius, in domain coordinates

 154     * @param style optional css style setting, such as stroke or stroke-width

 155     */

 156    circle(c:Point, r:number, style:string, title?:string):Vnode {

 157        return m('circle'

 158            { cx: round(c.x), cy: round(c.y), r: round(r), style: style },

 159            m('title', title)

 160        );

 161    }

 162

 163    /**

 164     * defines a clip rect to apply to other elelements via the `id`

 165     * @param tl top-left corner of the `clipRect` in domain coordinates

 166     * @param area width and height of the `clipRect` in domain coordinates

 167     * @param id a unique clip id to reference the `clipRect` by

 168     */

 169    clipRect(tl:Point, area:Area, id:string):Vnode {

 170if (area.h < 0) { 

 171    console.log(area); 

 172}        

 173        const param = {

 174            x: round(tl.x),       y: round(tl.y),

 175            width: round(Math.abs(area.w))  + (area.wunit||''), 

 176            height: round(Math.abs(area.h)) + (area.hunit||'')

 177        };

 178        return m('defs', m('clipPath', {id: id}, m('rect', param)));

 179    }

 180

 181    /**

 182     * plots a straight line from `x0/y0` to `x1/y1`.

 183     * @param x0 starting point x domain coordinate 

 184     * @param x1 ending point x domain coordinate 

 185     * @param y0 starting point y domain coordinate 

 186     * @param y1 ending point y domain coordinate 

 187     * @param cssClass optional css class attribute

 188     */

 189    line(x0:number, x1:number, y0:number, y1:number, cssClass?:string):Vnode {

 190        const param = {

 191            x1: round(x0), y1: round(y0), 

 192            x2: round(x1),   y2: round(y1), 

 193            class: cssClass

 194        };

 195        return m('line', param);

 196    }

 197

 198    /**

 199     * plots a horizontal line from `x0/y` to `x1/y`.

 200     * @param x0 starting point x domain coordinate 

 201     * @param x1 ending point x domain coordinate 

 202     * @param y  starting and ending point y domain coordinate 

 203     * @param cssClass optional css class attribute

 204     */

 205    horLine(x0:number, x1:number, y:number, cssClass?:string):Vnode {

 206        const param = {

 207            x1: round(x0), y1: round(y), 

 208            x2: round(x1), y2: round(y), 

 209            class: cssClass

 210        };

 211        return m('line', param);

 212    }

 213

 214    /**

 215     * plots a vertical line from `x/y0` to `x/y1`.

 216     * @param x  starting and ending point x domain coordinate 

 217     * @param y0 starting point y domain coordinate 

 218     * @param y1 ending point y domain coordinate 

 219     * @param cssClass optional css class attribute

 220     */

 221    verLine(x:number, y0:number, y1:number, cssClass?:string):Vnode {

 222        const param = {

 223            x1: round(x), y1: round(y0), 

 224            x2: round(x), y2: round(y1), 

 225            class: cssClass

 226        };

 227        return m('line', param);

 228    }

 229

 230    /**

 231     * plots a polyline from points in `data`. `x` and `y` are the indices to reference 

 232     * the data for the x-axis, respectively the y-axis in each row in `data`. That is,

 233     * plot `data[row][x] / data[row][y]` for all rows. 

 234     * @param data an array of rows; each row is an array of data. The first row contains the 

 235     * series names and will be skipped.

 236     * @param x the index in each row to use as x coordinate

 237     * @param y the index in each row to use as y coordinate

 238     * @param scales the scales to use to convert coordinates into range values

 239     * @param id the unique clip-path id to use, or undefined

 240     * @param style an optional `style` attribute, e.g. to set the stroke and stroke-width.

 241     */

 242    polyline(data:DataRow[], x:number, y:number, scales:XYScale, id:string, style?:string, title?:string):Vnode {

 243        return m('polyline', { 

 244            'clip-path': id? `url(#${id})` : undefined,

 245            style: style,

 246            points: data.map((row:number[]) => 

 247                `${round(scales.x.convert(row[x]))},${round(scales.y.convert(row[y]))}`).join(' ')

 248        }, m('title', title)); 

 249    }

 250

 251    /**

 252     * plots a polygon from points in `data`. `x` and `y` are the indices to reference 

 253     * the data for the x-axis, respectively the y-axis in each row in `data`. That is,

 254     * plot `data[row][x] / data[row][y]` for all rows. 

 255     * @param data an array of rows; each row is an array of data. The first row contains the 

 256     * series names and will be skipped.

 257     * @param x the index in each row to use as x coordinate

 258     * @param y the index in each row to use as y coordinate

 259     * @param scales the scales to use to convert coordinates into range values

 260     * @param id the unique clip-path id to use, or undefined

 261     * @param style an optional `style` attribute, e.g. to set the stroke and stroke-width.

 262     */

 263    polygon(dataFore:DataRow[], dataBack:DataRow[], x:number, yFore:number, yBack:number, scales:XYScale, id:string, style?:string, title?:string):Vnode {

 264        const indexed = (x===undefined);

 265        const sx = (_x:number) => round(scales.x.convert(_x));

 266        const sy = (_y:number) => round(scales.y.convert(_y));

 267        const clip = id? `url(#${id})` : undefined;

 268        const points:string = 

 269                dataFore.map((row:number[], i:number) => 

 270                    `${sx(indexed?i:row[x])},${sy(row[yFore])}`)

 271        .concat(dataBack.map((row:number[], i:number) => 

 272                    `${sx(indexed?(dataBack.length-i-1):row[x])},${sy(yBack?row[yBack]:0)}`

 273        )).join(' ');

 274        return m('polygon', { 'clip-path': clip, style: style, points: points }, m('title', title));

 275    }

 276

 277    /**

 278     * plots a shape from points in `data`. `x` and `y` are the indices to reference 

 279     * the data for the x-axis, respectively the y-axis in each row in `data`. That is,

 280     * plot `data[row][x] / data[row][y]` for all rows. 

 281     * @param data an array of rows; each row is an array of data. The first row contains the 

 282     * series names and will be skipped.

 283     * @param x the index in each row to use as x coordinate

 284     * @param y the index in each row to use as y coordinate

 285     * @param scales the scales to use to convert coordinates into range values

 286     * @param id the unique clip-path id to use, or undefined

 287     * @param style an optional `style` attribute, e.g. to set the stroke and stroke-width.

 288     */

 289    shape(points:DataRow[], id:string, style:string, title?:string):Vnode {

 290        return m('polyline', { 

 291            'clip-path': id? `url(#${id})` : undefined,

 292            style: style,

 293            points: points.map((row:number[]) => 

 294                `${round(row[0])},${round(row[1])}`).join(' ')

 295            }, m('title', title)); 

 296    }

 297}

 298

 299