src/Series.ts

   1/**

   2 * # Series

   3 * renders the one or more series in a variety of styles.

   4 * 

   5 * ### Configurations and Defaults

   6 * See {@link Series.Series.defaultConfig Series.defaultConfig} for defaults

   7 * and {@link Series.SeriesDef SeriesDef} for defining configuration details.

   8 * 

   9 * ### Attributes

  10 * The `Series` class is called by {@link Graph.Graph `Graph`} as 

  11 * `m(Series, { cfg:cfg.series, scales:scales, data:this.data })`

  12 * with the following attributes:

  13 * - cfg: {@link Series.SeriesConfig `SeriesConfig`} configuration parameters for series

  14 * - scales: {@link Axes.XYScale `XYScale`} the scales to use

  15 * - data: {@link hsdatab:Data.Data `Data`} array of `Data` sets to use, indexed by cfg[].dataIndex

  16 * 

  17 * @module Series

  18 */

  19

  20/** */

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

  22export type Vnode = typeof m.Vnode;

  23import { Config,

  24         VisibleCfg }   from './Graph';

  25import { Data, 

  26         DataSet }      from 'hsdatab';

  27import { Condition }    from 'hsdatab';

  28import { SVGElem }      from './SVGElem';

  29import { Axes }         from './Axes';

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

  31import { PlotLine }     from './PlotLine';

  32import { PlotMarkers }  from './PlotMarkers';

  33import { PlotBar }      from './PlotBar';

  34import { PlotArea }     from './PlotArea';

  35

  36let gClipID = 0;

  37

  38function copyDefault(target:any, source:any, defaults:any) {

  39    Object.keys(source).forEach((key:string) => {

  40        if (typeof source[key] === 'object') { 

  41            if (target[key] === undefined) { target[key] = {}; }

  42            copyDefault(target[key], source[key], defaults); 

  43        } else {

  44            if (target[key] === undefined) { target[key] = source[key]; }

  45            if (target[key] === 'default') { target[key] = defaults[key]; }

  46        }

  47    });

  48}

  49

  50/**

  51 * 

  52 */

  53export class Series extends SVGElem { 

  54    /**

  55     * Defines available styles for series marker:

  56     * - circle

  57     * - square

  58     * - diamond

  59     * - upTriangle

  60     * - downTriangle

  61     */

  62    static marker = {

  63        circle:         Symbol('circle marker'),

  64        square:         Symbol('square marker'),

  65        diamond:        Symbol('diamond marker'),

  66        upTriangle:     Symbol('upward triangle marker'),

  67        downTriangle:   Symbol('downward triangle marker')

  68    };

  69

  70    /**

  71     * Defines available plot types:

  72     * - line

  73     * - bar

  74     */

  75    static plot = {

  76        line:    new PlotLine(),

  77        marker:  new PlotMarkers(),

  78        bar:     new PlotBar(),

  79        area:    new PlotArea()

  80    };

  81

  82    static map = {

  83        stacked: 'stacked',

  84        shared:  'shared'

  85    };

  86

  87    /** 

  88     * determines the default style applied to each series.

  89     * Colors will be chosen by series index from `defaultColors`.

  90     */

  91    public static defaultStyle:SeriesStyle = {

  92        line:   { color:'default', visible: true, width: 2},

  93        marker: { color:'default', visible: false, size: 10, shape: Series.marker.circle},

  94        label:  { color:'default', visible: false },

  95        fill:   { color:'default', visible: false },

  96        bar:    { color:'default', visible: false, width: 50, offset: 30 }

  97    };       

  98           

  99    /** determines the default color for the first couple of series */

 100    public static defaultColors = ['#f00''#0f0''#00f''#ff0''#0ff''#f0f''#000''#444''#888''#ccc'];

 101

 102    /** 

 103     * Defines default values for all configurable parameters in `Series`

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

 105     * 

 106     * ### Configurations and Defaults

 107     * ```

 108     *  cfg.series = {@link Series.SeriesConfig }{

 109     *      data: {@link hsdatab:Data.DataSet }[...],    // pool of `Data` sets to be plotted, initialized as `[]`

 110     *      clip: true,              // series an markers are clipped to the plot area

 111     *      series: {@link Series.SeriesDef }[...] // array of series descriptors, initialized to empty array (no series)

 112     *  } 

 113     * 

 114     * // sets the default colors that will be assigend to series by index

 115     * Series.defaultColors:

 116     *          ['#f00''#0f0''#00f''#ff0''#0ff''#f0f''#000''#444''#888''#ccc'];

 117     * 

 118     * // sets the default style to be applied to series

 119     * Series.defaultStyle: {@link Series.SeriesStyle } {

 120     *          line:   { 

 121     *              color: // the line color to use, preset from defaultColors

 122     *              width: 5,       // the line width in viewbox units

 123     *              visible: true   // whether line is draw or not

 124     *          },

 125     *          marker: { 

 126     *              color: // the marker color to use, preset from defaultColors

 127     *              size: 10,       // the marker size in viewbox coordinates

 128     *              shape: Series.marker.circle, // the marker shaper, See {@link Series.Series.marker Series.marker}

 129     *              visible: true 

 130     *          },

 131     *          fill: { 

 132     *              color: // the fill color to use, preset from defaultColors

 133     *              visible: true 

 134     *          },

 135     *          bar: { 

 136     *              color: // the bar color to use, preset from defaultColors

 137     *              width: 50,

 138     *              offset: 30,

 139     *              visible: false 

 140     *          }

 141     *      }

 142     * ``` 

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

 144     * previously configured components.

 145     */

 146    static defaultConfig(cfg:Config) {

 147        cfg.series = new SeriesConfig();

 148    }

 149

 150    /**

 151     * Makes adjustments to cfg based on current settings.

 152     * For series with missing x-values: 

 153     * - remove title 

 154     * - set axis type to index

 155     * - disable minor grid lines

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

 157     */

 158    static adjustConfig(cfg:Config) { 

 159        cfg.series.series.forEach((s:SeriesDef) => {

 160            if (s.x === undefined) { // undefined x-value -> use index as x-value

 161                cfg.axes.primary.x.title.hOffset = 0;

 162                cfg.axes.primary.x.scale.type = Axes.type.index;

 163                cfg.grid.minor.ver.visible = false;

 164            }

 165        });

 166    }

 167    

 168    drawClipRect(clipID:string, scales:XYScale) {

 169        return !clipID? m('') : this.clipRect(

 170            {   x:scales.x.range()[0], y:scales.y.range()[1]}, 

 171            {

 172                w:scales.x.range()[1] - scales.x.range()[0], 

 173                h:scales.y.range()[0] - scales.y.range()[1]

 174            }, 

 175            clipID);

 176    }

 177

 178    view(node?: Vnode): Vnode {

 179        const cfg:SeriesConfig  = node.attrs.cfg;

 180        const scales:XYScale = node.attrs.scales.primary;

 181        const data:Data[]    = node.attrs.data;

 182        const clipID = cfg.clip? 'hs'+gClipID++ : undefined;

 183        // const clipID = cfg.clip? 'hs'+Math.floor(Math.random()*10000) : undefined;

 184        cfg.series.map((s:SeriesDef) => {

 185            if (s.map === Series.map.shared) {  // reset ySum if needed

 186                s.ySum = '$sum';

 187                data[s.dataIndex].colAdd(s.ySum);            // add $max if not present

 188                data[s.dataIndex].colInitialize(s.ySum, 0);  // and initialize to 0

 189            }

 190        });

 191        cfg.series.map((s:SeriesDef) => {

 192            const dt = data[s.dataIndex];

 193            if (s.map===Series.map.shared) {    // aggregate ySum over series

 194                const valCol = dt.colNumber(s.y);                       

 195                dt.colInitialize(s.ySum, (v:number, i:number, row:number[])=>{ return v+row[valCol]; });

 196            }

 197            if (s.map) {

 198                s.yBase = '$'+ s.map;

 199                dt.colAdd(s.yBase);             // add $stacked or $shared if not present

 200                dt.colInitialize(s.yBase, 0);   // and initialize to 0

 201            }

 202        });

 203        return m('svg', { class:'hs-graph-series'}, [

 204            this.drawClipRect(clipID, scales),

 205            m('svg', cfg.series.map((s:SeriesDef, i:number) => { 

 206                const dt = data[s.dataIndex];

 207                const type = Series.plot[s.type] || Series.plot.line;

 208                type.setDefaults(dt, s, scales);

 209                const d = s.cond? dt.filter(s.cond) : dt;

 210                const plot = type.plot(d, s, scales, i, clipID);  // plot y above yBase; 

 211                if (s.map) {                                      // if 'stacked' or 'shared' -> accumulate y

 212                    const valCol = d.colNumber(s.y);                       

 213                    d.colInitialize(s.yBase, (v:number, i:number, row:number[])=>{ return v+row[valCol]; });

 214                }

 215                return m('svg', {class:`hs-graph-series-${i}`}, plot);

 216            }))

 217        ]);

 218    }

 219}

 220

 221export interface ColoredCfg extends VisibleCfg {

 222    /** the color in hex */

 223    color: string;

 224}

 225export interface LineStyle extends ColoredCfg {

 226    /** the stroke width in px */

 227    width: number; 

 228}

 229

 230export interface MarkerStyle extends ColoredCfg {

 231    /** the stroke width in px */

 232    size:  number;      

 233

 234    /** the marker shape, selected from {@link Series.Series.marker Series.marker} */

 235    shape: Symbol;              

 236}

 237

 238export interface FillStyle extends ColoredCfg {

 239}

 240

 241export interface TextStyle extends ColoredCfg {

 242}

 243

 244export interface BarStyle extends ColoredCfg {

 245    /** width of bars in % of space between bars */

 246    width: number;  

 247    

 248    /** offset between column series in % between bars */

 249    offset:number; 

 250}

 251

 252export interface SeriesStyle {

 253    line:   LineStyle;

 254    marker: MarkerStyle;

 255    fill:   FillStyle;

 256    bar:    BarStyle;

 257    label:  TextStyle;

 258}

 259

 260/** 

 261 * Defines Series columns and values to use, as well as the plot type to apply.

 262 * The following settings are available for configuration:

 263 * ```

 264 * cfg.series.series = [{

 265 *    type: TYPE,        // the series type, e.g. 'line', etc. See below.

 266 *    dataIndex: number, // the `Data` set to use. The index refers to the position in `series.data`

 267 *    x: string,         // the column name or index of the x-coordinate to use for drawing

 268 *    y: string,         // the column name or index of the y-coordinate to use for drawing

 269 *    yBase: string,     // if specified, used as lower series for filling the area

 270 *    l: string,         // the column name or index to use for series labels

 271 *    hOffset: number;   // horizontal label offset in em o

 272 *    vOffset?: number;  // vertical label offset in em

 273 *    map?: 'stacked' | 'shared'// stack series, or show the share (normalize to 100%)

 274 *    style: {@link Series.SeriesStyle SeriesStyle},  // allows overriding a default style setting

 275 *    cond: {@link hsdatab:DataFilters.Condition Condition} // allows specifying a filter applied to data before rendering.

 276 * }]

 277 * ```

 278 * The following series *TYPE*s are available. For configuration details, see:

 279 * - 'line':  (or omitted): {@link PlotLine PlotLine}

 280 * - 'markers': {@link PlotMarkers PlotMarkers}

 281 * - 'area':    {@link PlotArea PlotArea}

 282 * - 'bar':     {@link PlotBar PlotBar}

 283 */

 284export interface SeriesDef {

 285    /** 

 286     * required column names or indices. 

 287     * [0] is reserved for the x direction. 

 288     * Further elements are dependent on the {@link Series.type plot} type. 

 289     */

 290    x:string;               // x values

 291    y?:string;              // y values

 292    yBase?:string;          // if specified, treats (y/yBase) as (high/low) series

 293    ySum?:string;           // internal support column for 'shared' mode;

 294    l?:string;              // labels

 295    hOffset?: number;       // offset in em

 296    vOffset?: number;       // offset in em

 297    map?: string;           // accepts 'stacked'and 'shared'

 298    /** An index into the `Data[]` pool, identifying the `Data` set to use. defaults to `0` */

 299    dataIndex?: number;

 300    /** optional plot type, selected from {@link Series.Series.plot Series.plot} as string; defaults to  'line' */

 301    type?:string;   

 302    /** style information to use for plotting; if ommitted, a `type`-dependent default is used */

 303    style?:SeriesStyle;

 304    /** optinal filter condition on the data prior to drawing */

 305    cond?: Condition;

 306}

 307

 308

 309/** 

 310 * Defines the default settings. 

 311 * Implemented as a class rather than interface to allow for a getter/setter implementation

 312 * of `series`. This allows for postprocessing user configurations while maintaining 

 313 * convenient notation, e.g.

 314 * ```

 315 *  cfg.series.series = [           // invoke the setter

 316 *      { x:'time', y:'volume'},    // behind the scenes, adds 

 317 *      { x:'time', y:'price']}     // missing fields such as .style

 318 *  ];

 319 *  fg.series.series[0].style.marker.visible = true;    // invoke the getter

 320 * ```

 321 */

 322export class SeriesConfig {

 323    private seriesDefs:SeriesDef[] = [];

 324    private _data:DataSet[];

 325

 326

 327    /** 

 328     * the `DataSet`s to be used for plots.

 329     * Each set describes the column names and the rows-of-columns data.

 330     * The `SeriesDef.dataIndex` determines which set to use.

 331     */

 332    public set data(d:DataSet[]) {

 333        this._data = d; 

 334        // m.redraw();

 335    }

 336

 337    public get data():DataSet[] {

 338        return this._data; 

 339    }

 340

 341    /** determines if seires plot will be clipped to the chart area */

 342    public clip = true;   

 343

 344    /** 

 345     * `series` accessor method: array of series definitions to define plots. 

 346     * 

 347     */

 348    public set series(cfg: SeriesDef[]) {    // array of series descriptions

 349        const defStyle = Series.defaultStyle;

 350        const defColors = Series.defaultColors;

 351        cfg.forEach((s:SeriesDef) => {

 352            s.type = s.type || 'line';

 353            s.style = s.style || {};

 354            s.dataIndex = s.dataIndex || 0;

 355            const defaults = {

 356                color:defColors[this.seriesDefs.length]

 357            };

 358            copyDefault(s.style, defStyle, defaults);

 359            this.seriesDefs.push(s);

 360            switch (s.type) {

 361                case 'line'

 362                    s.style.line.visible = true; 

 363                    break;

 364                case 'marker'

 365                    s.style.marker.visible = true; 

 366                    break;

 367                case 'area'

 368                    s.style.fill.visible = true; 

 369                    break;

 370                case 'bar'

 371                    s.style.fill.visible = true; 

 372                    break;

 373            }

 374        });

 375    }

 376    public get series():SeriesDef[] { return this.seriesDefs; }

 377}

 378

 379