src/Axes.ts

   1/**

   2 * # Axes

   3 * renders the x- and y-axis with title, tick marks and labels.

   4 * 

   5 * ### Attributes

   6 * The `Axes` class is called by {@link Graph.Graph `Graph`} as 

   7 * `m(Axes, { cfg:cfg.axes, scales:scales })`

   8 * with the following attributes:

   9 * - cfg: a {@link Axes.AxesConfig AxesConfig} object

  10 * - scales: a {@link Axes.Scales Scales } object

  11 *

  12 * ### Configurations and Defaults

  13 * See {@link Axes.Axes.defaultConfig Axes.defaultConfig}

  14 * 

  15 * ### Example

  16 * 

  17 * 'script.js'>

  18 * let series = {

  19 *    colNames:['time''volume''price'],

  20 *    rows:[

  21 *      [0.2, 0.7, 0.87],

  22 *      [0.4, 0.015, 0.7],

  23 *      [0.6, 0.01, 0.7],

  24 *      [0.7, 5, 0.6],

  25 *      [0.8, 10, 0.75]

  26 * ]};

  27 * 

  28 * function myConfig(cfg) {

  29 *      cfg.series.data   = [series];

  30 *      cfg.series.series = [

  31 *          { x:'time', y:'volume'},

  32 *          { x:'time', y:'price'}

  33 *      ];

  34 *      cfg.series.series[0].style.marker.visible = true;

  35 *      cfg.series.series[1].style.marker.visible = true;

  36 *      cfg.series.series[1].style.marker.shape = hsgraph.Series.marker.square;

  37 *      cfg.chart.title.text          = 'Volume over Time';

  38 *      cfg.chart.title.xpos          = 'end';

  39 *      cfg.chart.title.ypos          = 'top';

  40 *      cfg.chart.title.vOffset       = -1.5;

  41 *      cfg.grid.minor.hor.visible    = true;

  42 * 

  43 *      const axes = cfg.axes.primary;

  44 *      axes.x.title.text = 'time';

  45 *      axes.y.title.text = 'volume';

  46 *      axes.y.scale.type = hsgraph.Axes.type.log;

  47 * }

  48 * 

  49 * m.mount(root, { 

  50 *      view:() => m(hsgraph.Graph, {cfgFn: myConfig })

  51 * });

  52 *

  53 * 

  54 * 'style.css'>

  55 * .hs-graph-chart { fill: #fff; }

  56 * .hs-graph-series { stroke-width: 5; }

  57 * 

  58 * 

  59 */

  60

  61 /** */

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

  63export type Vnode = typeof m.Vnode;

  64

  65import { XYScale,

  66         ScaleCfg, 

  67         Scales,

  68         MarkCfg,

  69         AxisCfg,

  70         AxisType,

  71         AxesConfig,

  72         TickStruct,

  73         TickLabel,

  74         TickDefs } from './AxesTypes';

  75

  76import { Config, 

  77         LabelCfg } from './Graph';

  78import { Scale }    from './Scale';

  79import { Domain }   from 'hsdatab';

  80import { SVGElem, 

  81         TextHAlign,

  82         TextVAlign,

  83         Area }     from './SVGElem';

  84

  85

  86/** 

  87 * calculates the range value of axis crossing from a domain value.

  88 * @param cross the domain value where the axis crosses. Either 'min''max', or a numeric domain value

  89 * @param scale the Scale object for the perpendicular axis.

  90 */

  91function getCrossAt(cross:string|number, scale:Scale):number {

  92    let crossesAt:number;

  93    switch (cross) {

  94        case 'min': crossesAt = scale.domain()[0]; break;

  95        case 'max': crossesAt = scale.domain()[1]; break;

  96        default:    crossesAt = cross || 0;

  97    }

  98    return scale.convert(crossesAt);

  99}

 100

 101export class Axes extends SVGElem {

 102    /**

 103     * Defines available axis types:

 104     * - linear

 105     * - log

 106     * - date

 107     * - index

 108     * - percent

 109     * - ordinal

 110     * - nominal

 111     */

 112    static type = {

 113        linear:     'linear axis',

 114        log:        'log axis',

 115        date:       'date axis',

 116        index:      'index axis',

 117        percent:    'percent axis',

 118        ordinal:    'ordinal axis',

 119        nominal:    'nominal axis',

 120        auto:       'auto'

 121    };

 122

 123    /** 

 124    * Defines default values for display elements in `Axes`

 125    * sets the default configuration for the primary and secondary axes.

 126    * See {@link Graph.Graph.makeConfig Graph.makeConfig} for the sequence of initializations. 

 127    * 

 128    * ### Configurations and Defaults

 129    * ```

 130    * cfg.axes = {@link AxesTypes.AxesConfig } {

 131    *    primary: {                // Primary axis:

 132    *       x: axisCfg(true, true),

 133    *       y: axisCfg(true, false)

 134    *    },

 135    *    secondary: {               // Secondary axis:

 136    *       x: axisCfg(false, true),

 137    *       y: axisCfg(false, false)

 138    *    }

 139    *  }

 140    * ```

 141    * #### axisCfg(primary:boolean, x:boolean):

 142    * ```

 143    *  cfg.axes.[primary|secondary].[x|y] = {@link AxesTypes.AxisCfg }{

 144    *     visible:    primary? true : false,   // hide secondary axes

 145    *     crossesAt:  primary? 'min':'max',    // default axis crossing

 146    *     scale:     {@link AxesTypes.ScaleCfg }scaleCfg(),     // scale type and domain

 147    *     title:     {@link Graph.LabelCfg }titleCfg(primary, x),

 148    *     ticks:     {@link AxesTypes.TicksCfg }{                    

 149    *         major: {@link AxesTypes.TickStruct }{                

 150    *             marks:  {@link AxesTypes.MarkCfg }markCfg(primary, true),  

 151    *             labels: {@link Graph.LabelCfg }labelCfg(primary, x, true),     

 152    *             labelFmt: undefined 

 153    *         },

 154    *         minor: {@link AxesTypes.TickStruct }{ 

 155    *             marks:  markCfg(primary, false),

 156    *             labels: labelCfg(primary, x, false),     

 157    *             labelFmt: undefined 

 158    *         }

 159    *     } 

 160    *  }

 161    * ```

 162    * #### scaleCfg():

 163    * ```

 164    *  cfg.axes.[primary|secondary].[x|y].scale = {@link AxesTypes.ScaleCfg }{

 165    *      type:   {@link Axes.Axes.type } Axes.type.linear,  

 166    *      domain: {@link Data.Domain }['auto''auto']    // : min/max of domain; 'auto''tight', or a domain value

 167    *  }

 168    * ```

 169    * #### titleCfg(primary:boolean, x:boolean):

 170    * ```

 171    *  cfg.axes.[primary|secondary].[x|y].title = {@link SVGElem.TextElem }{

 172    *     visible: true,  

 173    *     text:    (x? 'x' : 'y') + (primary? '' : '2'),    // 'x' / 'y' or 'x2' / 'y2'

 174    *     xpos:    x? 'end' : (primary? 'middle' : 'start'),          

 175    *     ypos:    x? 'top' : (primary? 'bottom' : 'top'),           

 176    *     hOffset: x? -2 : (primary? 0 : 0.3),            

 177    *     vOffset: x? (primary? 0.4 : -1.2) : (primary? -0.5 : 0.7) 

 178    *  }      

 179    * ```

 180    * #### markCfg(primary:boolean, major:boolean):

 181    * ```

 182    *  cfg.axes.[primary|secondary].[x|y].ticks.[major|minor].marks = {@link AxesTypes.MarkCfg }{

 183    *     visible: major, 

 184    *     length: (primary? 1 : -1) * (major? 10 : 5) 

 185    *  }      

 186    * ```

 187    * #### labelCfg(primary:boolean, x:boolean, major:boolean):

 188    * ```

 189    *  cfg.axes.[primary|secondary].[x|y].ticks.[major|minor].labels = {@link SVGElem.TextElem }{

 190    *     visible: major, 

 191    *     xpos: x? 'middle' : (primary? 'end' : 'start')

 192    *     ypos: x? (primary? 'top' : 'bottom') : 'center'

 193    *     hOffset: x? 0 (primary? -0.7 : 0.7), 

 194    *     vOffset: x? (primary? 0.7 : -0.7) : 0

 195    *  }      

 196    * ```

 197    * @param cfg the configuration object, containing default settings for all 

 198    * previously configured components.

 199    */

 200    static defaultConfig(cfg:Config) {

 201        function scaleCfg():ScaleCfg {

 202            return {                             // axis scaling information

 203                type: 'auto',                    //    scale type

 204                domain:['auto''auto']  //    min/max of domain; 'auto', or a domain value

 205            };

 206        }

 207        function labelCfg(primary:boolean, x:boolean, major:boolean):LabelCfg {

 208            return { 

 209                visible: major, text: '',

 210                xpos: x? TextHAlign.middle : (primary? TextHAlign.end : TextHAlign.start),

 211                ypos: x? (primary? TextVAlign.top : TextVAlign.bottom) : TextVAlign.center, 

 212                hOffset: x? 0 : (primary? -1 : 0.7), 

 213                vOffset: x? (primary? 0.7 : -0.7) : 0

 214            }; 

 215        }

 216        function markCfg(primary: boolean, major:boolean):MarkCfg {

 217            return { 

 218                visible: major, 

 219                length: (primary? 1 : -1) * (major? 10 : 5) 

 220            };

 221        }

 222        function titleCfg(primary:boolean, x:boolean):LabelCfg {

 223            return {

 224                visible: true,  text: (x? 'x' : 'y') + (primary? '' : '2'),    

 225                xpos:  x? TextHAlign.end : (primary? TextHAlign.middle : TextHAlign.start),          

 226                ypos:  x? TextVAlign.top : (primary? TextVAlign.bottom : TextVAlign.top),           

 227                hOffset: x? -2 : (primary? 0 : 0.3),            

 228                vOffset: x? (primary? 0.4 : -1.2) : (primary? -0.5 : 0.7)       

 229            };

 230        }

 231        function axisCfg(primary:boolean, x:boolean):AxisCfg {

 232            return {

 233                visible:    primary? true : false, 

 234                crossesAt:  primary?'min':'max'

 235                scale:      scaleCfg(),

 236                title:      titleCfg(primary, x),

 237                ticks: {                    

 238                    major: {                

 239                        marks:  markCfg(primary, true),  

 240                        labels: labelCfg(primary, x, true),

 241                        labelFmt: undefined    

 242                    },

 243                    minor: { 

 244                        marks:  markCfg(primary, false),

 245                        labels: labelCfg(primary, x, false),    

 246                        labelFmt: undefined    

 247                    }

 248                } 

 249            };

 250        }

 251        cfg.axes = {

 252            primary: {

 253                x: axisCfg(true, true),

 254                y: axisCfg(true, false)

 255            },

 256            secondary: {

 257                x: axisCfg(false, true),

 258                y: axisCfg(false, false)

 259            }

 260        };

 261    }

 262

 263    /**

 264     * Makes adjustments to cfg based on current settings

 265     * @param cfg the configuration object, containing default settings for all components

 266     */

 267    static adjustConfig(cfg:Config) { 

 268    }

 269    

 270    /**

 271     * draws the axis line

 272     */

 273    drawAxisLine(x:boolean, range:Area, cross:number) {

 274        return x? this.horLine(range[0], range[1], cross, 'hs-graph-axis-line') :

 275                  this.verLine(cross, range[0], range[1], 'hs-graph-axis-line');

 276    }

 277

 278    /**

 279     * draws the axis title

 280     */

 281    drawTitle(x:boolean, ttlCfg:LabelCfg, type: string, range:Area, cross:number):Vnode {

 282        ttlCfg.cssClass = 'hs-graph-axis-title';

 283        const xy = { transform:`translate(${x?range[1]:cross}, ${x?cross:range[1]})` };

 284        return !ttlCfg.visible? undefined : 

 285            m('g', xy, this.text(ttlCfg, ttlCfg.text));

 286    }

 287

 288    /**

 289     * draws the tick marks. Labels are plotted for major tick marks only.

 290     */

 291    drawTickMarks(x:boolean, type:string, crossesAt:number, scale:Scale, ticks:TickDefs, cfg:TickStruct):Vnode {

 292        return m('svg', { class:`hs-graph-axis-${type}-tick-marks`}, 

 293            !cfg.marks.visible? '' : ticks.marks.map((t:number) => { 

 294                return x? this.verLine(scale.convert(t), crossesAt, crossesAt+cfg.marks.length) :

 295                          this.horLine(crossesAt, crossesAt-cfg.marks.length, scale.convert(t));

 296            })

 297        );

 298    }

 299

 300    /**

 301     * draws the tick labels. Labels are plotted for major tick marks only.

 302     */

 303    drawTickLabels(x:boolean, type:string, crossesAt:number, scale:Scale, ticks:TickDefs, cfg:TickStruct):Vnode {

 304        scale.setLabelFormat(cfg.labelFmt);

 305        const labelCfg:any = {};

 306        Object.keys(cfg.labels).forEach((k:string) => labelCfg[k] = cfg.labels[k]);

 307        return m('svg', {class:`hs-graph-axis-${type}-tick-label`}, 

 308            !labelCfg.visible? '' : ticks.labels.map((t:TickLabel, i:number) => { 

 309                const v = scale.convert(t.pos);

 310                const xy = { transform:`translate(${x?v:crossesAt}, ${x?crossesAt:v})` };

 311                if (x && i===0) { 

 312                    labelCfg.xpos = TextHAlign.start; 

 313                } else if (x && i===ticks.labels.length-1) { 

 314                    labelCfg.xpos = TextHAlign.end; 

 315                } else {

 316                    labelCfg.xpos = TextHAlign.middle; 

 317                }

 318                return m('g', xy, this.text(labelCfg, t.text));

 319            })

 320        );

 321    }

 322

 323    /**

 324     * draws a single axis

 325     * @param dir axis to draw: 'x' or 'y'

 326     * @param attrs attributes required for rendering:

 327     * - type: 'primary' or 'secondary'

 328     * - scales:

 329     * - cfg: 

 330     */

 331    drawAxis(dir:string, scales: XYScale, type:string, axisCfg:AxesConfig):Vnode {

 332        const x = dir==='x';

 333        const range = scales[dir].range();

 334        const cfg   = axisCfg[type][dir];

 335        // scales[dir].scaleType(cfg.scale.type);

 336        const crossesAt:number = getCrossAt(cfg.crossesAt, scales[x?'y':'x']);

 337        const ticks = scales[dir].ticks();

 338        return !cfg.visible? undefined : m('svg', { class:`hs-graph-axis-${dir} hs-graph-axis-${type}`}, [

 339            this.drawAxisLine(x, range, crossesAt),

 340            this.drawTitle(x, cfg.title, type, range, crossesAt),

 341            this.drawTickMarks(x, 'minor', crossesAt, scales[dir], ticks.minor, cfg.ticks.minor),

 342            this.drawTickMarks(x, 'major', crossesAt, scales[dir], ticks.major, cfg.ticks.major),

 343            this.drawTickLabels(x, 'minor', crossesAt, scales[dir], ticks.minor, cfg.ticks.minor),

 344            this.drawTickLabels(x, 'major', crossesAt, scales[dir], ticks.major, cfg.ticks.major)

 345        ]);

 346    }

 347

 348    view(node?: Vnode): Vnode {

 349        const cfg:AxesConfig = node.attrs.cfg;

 350        const scales:Scales  = node.attrs.scales;

 351        return m('svg', {class:'hs-graph-axis'}, [

 352            this.drawAxis('x', scales.primary, 'primary', cfg),

 353            this.drawAxis('y', scales.primary, 'primary', cfg),

 354            this.drawAxis('x', scales.secondary, 'secondary', cfg),

 355            this.drawAxis('y', scales.secondary, 'secondary', cfg)

 356        ]);

 357    }

 358}

 359

 360/**

 361 * ### Simple Example

 362 * 

 363 * 'script.js'>

 364 * let series = {

 365 *    colNames:['time''volume'],

 366 *    rows:[

 367 *      [-1, 0.2],

 368 *      [0.2, 0.7],

 369 *      [0.4, -0.2],

 370 *      [0.6, 0],

 371 *      [0.8, 0.5],

 372 *      [1, 0.7]

 373 * ]};

 374 * 

 375 * m.mount(root, { 

 376 *      view:() => m(hsgraph.Graph, {cfgFn: cfg => {

 377 *          cfg.chart.title.text          = 'Simple Example';

 378 *          cfg.series.data   = [series];

 379 *          cfg.series.series = [{ x:'time', y:'volume' }];

 380 *      }})

 381 * });

 382 *

 383 * 

 384 * 'style.css'>

 385 * .hs-graph-chart { fill: #fff; }

 386 * .hs-graph-series { stroke-width: 5; }

 387 * 

 388 * 

 389 */

 390class ExampleLinearAxis {}

 391

 392/**

 393* ### Logarithmic Axis

 394

 395'script.js'>

 396* let series = {

 397*    colNames:['time''volume'],

 398*    rows:[[0.3, 0.2], [0.32, 0.7], [0.4, 8], [0.56, 10], [0.7, 0.5], [0.8, 15]]

 399* };

 400

 401* m.mount(root, { 

 402*      view:() => m(hsgraph.Graph, {cfgFn: cfg => {

 403*          cfg.chart.title.text = 'Log Y Axis';

 404*          cfg.series.data   = [series];

 405*          cfg.series.series = [{ x'time', y:'volume' }];

 406*          cfg.axes.primary.x.scale.type = hsgraph.Axes.type.log;

 407*          cfg.axes.primary.x.scale.domain = ['tight''tight'];

 408*          cfg.axes.primary.y.scale.type = hsgraph.Axes.type.log;

 409*          cfg.axes.primary.y.scale.domain = ['auto''auto'];

 410*          cfg.grid.minor.hor.visible = true;

 411*          cfg.grid.minor.ver.visible = true;

 412*      }})

 413* });

 414*

 415

 416

 417*/

 418class ExampleLogAxis {}

 419

 420/**

 421* ### Date Axis

 422

 423'script.js'>

 424* let series = {

 425*    colNames:['time''volume'],

 426*    rows:[['2/6/17', 0.2], ['3/18/17', 0.7], ['5/1/17', 8], ['11/20/17', 10], ['1/15/18', 0.5]]

 427* };

 428

 429* m.mount(root, { 

 430*      view:() => m(hsgraph.Graph, {cfgFn: cfg => {

 431*          cfg.chart.title.text = 'Date X Axis';

 432*          cfg.series.data   = [series];

 433*          cfg.series.series = [{ x:'time', y:'volume' }];

 434*          cfg.axes.primary.x.scale.type = hsgraph.Axes.type.date;

 435*          cfg.axes.primary.x.ticks.major.labelFmt = '%MMM %YY';

 436*      }})

 437* });

 438*

 439

 440'style.css'>

 441* .hs-graph-series { stroke-width: 5; }

 442

 443

 444*/

 445class ExampleDateAxis {}

 446