src/d3/Graph.ts

   1/**

   2 * ## d3 Graphs

   3 * 

   4 * 

   5 * 'script.js'>

   6 * // create data set:

   7 * const data = new hsdatab.Data({

   8 *    colNames:['date''time''volume''costs'], 

   9 *    rows:[['1/1/14', -1,  0.2, 0.3], ['1/1/16', -0.2, 0.7, 0.2], ['9/1/16', 0.4, 0.1, 0.3],

  10 *          ['5/1/17', 0.6, 0,   0.1], ['7/1/18', 0.8, 0.3, 0.5], ['1/1/19', 1,   0.2, 0.4]]

  11 * });

  12 * 

  13 * // setup and plot the data:

  14 * const graph = new hsgraph.d3.Graph(root);

  15 * graph.addSeries('bubble''time''volume''costs');

  16 * graph.addSeries('bubble''time''volume''costs');

  17 * 

  18 * //with (graph.defaults.Scales('costs').scale) {

  19 * //     range.min = 0

  20 * //     range.min = 20

  21 * //}

  22 * 

  23 * with (graph.defaults.Graph.canvas) {

  24 *      fill.color = '#eee';

  25 *      stroke.width = 0;

  26 * }

  27 * graph.defaults.Axes.hor.tickLabel.font.weight = '200';

  28 * console.log(`size ${graph.defaults.Axes.hor.tickLabel.font.weight}`);

  29 * 

  30 * with (graph.defaults.Plot.area) {

  31 *      //fill.color = '#fcc';

  32 * }

  33 * graph.render(data);

  34 * 

  35 * // change values avery 2s:

  36 * setInterval(() => {

  37 *    data.getData().map(row => {

  38 *      //row[1] = Math.random()*graph.config.viewPort.width;

  39 *      row[2] = Math.random()*graph.config.viewPort.height;

  40 *      row[3] = Math.random()*50;

  41 *      graph.render(data);

  42 *    });

  43 *    data.sort('ascending''x');

  44 * }, 2000);

  45 * 

  46 * 

  47 * 

  48 * 

  49 */

  50

  51 /** */

  52

  53import { log as gLog } from 'hsutil';   const log = gLog('d3.Graph');

  54import * as d3 from "d3";

  55

  56import { Data, NumDomain }  from 'hsdatab';

  57import { GraphCfg, UnitVp } from './ConfigTypes';

  58import { d3Base }           from './ConfigTypes';

  59// import { Scale, scaleTypes }from './ConfigTypes';

  60import { Defaults }         from './Defaults';

  61import { Plot }             from './Plot';

  62import { Axis, Direction }  from './Axis';

  63import { GraphComponent }   from './GraphComponent';

  64

  65const margin:number = 10;

  66

  67/**

  68 * creates the base SVG element for the entire graph.

  69 * This function is intended to be called only once at creation of the graph.

  70 * @param cfg 

  71 */

  72function createBaseSVG(cfg: GraphCfg):d3Base {

  73    const base = d3.select(cfg.root);

  74    base.selectAll('div').remove();

  75    base.selectAll('svg').remove();

  76    const svg = base.append('svg')

  77        .classed('baseSVG', true)

  78        .attr('height''100%')

  79        .attr('width''100%')

  80        .attr('preserveAspectRatio''xMinYMin meet')

  81        ;

  82    cfg.baseSVG = svg;

  83    svg.append('rect')

  84        .classed('baseRect', true)

  85        .attr('x', 0)

  86        .attr('y', 0);

  87    return svg;

  88}

  89

  90/**

  91 * regualrly sets the graph's viewBox, typically after a window resize.

  92 * @param cfg 

  93 */

  94function updateBaseSVG(cfg: GraphCfg) {

  95    cfg.baseSVG

  96        .attr('viewBox', `0 0 ${cfg.viewPort.width} ${cfg.viewPort.height}`)

  97        .attr('font-size', '20px')

  98        ;

  99}

 100

 101

 102export class Graph extends GraphComponent {

 103    private plot:Plot;

 104    private axes:Axis[] = [];

 105    private cumulativeDomains: {[colName:string]: [number, number]} = {};

 106

 107    constructor(root:any) { 

 108        super();

 109        this.config.root = root;

 110        log.info('creating Graph');

 111        const base = createBaseSVG(this.config); 

 112        updateBaseSVG(this.config);

 113        this.plot = new Plot(this.config);

 114        this.axes.push(new Axis(this.config, Direction.Horizontal));

 115        this.axes.push(new Axis(this.config, Direction.Vertical));

 116        window.onresize = () => this.resize();

 117    }

 118

 119    public get defaults(): Defaults {

 120        return this.config.defaults;

 121    }

 122

 123    resize() {

 124        const cfg = this.config;

 125        if (cfg.root.clientWidth > 0) {

 126            if (cfg.root.clientWidth !== cfg.client.width || cfg.root.clientHeight !== cfg.client.height) {

 127                log.info(`resizing svg: [${cfg.client.width} x ${cfg.client.height}] -> [${cfg.root.clientWidth} x ${cfg.root.clientHeight}]`);

 128                cfg.client.width = cfg.root.clientWidth;

 129                cfg.client.height = cfg.root.clientHeight;

 130                cfg.viewPort.height = cfg.viewPort.width * cfg.root.clientHeight / cfg.root.clientWidth;

 131                updateBaseSVG(cfg);

 132            }

 133        }

 134    }

 135

 136    /**

 137     * renders all Graph elements using `data`

 138     * @param data 

 139     */

 140    public render(data:Data) {

 141        this.defaults.Axes.hor.tickLabel.font.size = 200;

 142        console.log(`size ${this.defaults.Axes.hor.tickLabel.font.size}`);

 143        this.setScales(data);

 144        this.drawCanvas(this.config);

 145        this.plot.setBorders(10, 10, 10, 10);

 146        this.plot.render(data);

 147        this.axes.forEach(a => a.render(data));

 148    }

 149

 150    /**

 151     * adds a series to the plot.

 152     * @param type type of plot to use, e.g. 'bubble' or 'scatter'

 153     * @param params the column name of the parameters used to plot the series

 154     */

 155    public addSeries(type:string, x:string, y:string, ...params:string[]) {

 156        this.resize();

 157        this.config.scales.hor.dataCol = x;

 158        this.config.scales.ver.dataCol = y;

 159        this.plot.addSeries(type, x, y, ...params);

 160    }

 161

 162    /**

 163     * renders the Graph's background canvas

 164     * @param cfg 

 165     */

 166    private drawCanvas(cfg: GraphCfg) {

 167        const canvas = cfg.defaults.Graph.canvas;

 168        d3.select('.baseRect')

 169        .attr('width', cfg.viewPort.width)

 170        .attr('height', cfg.viewPort.height)

 171        .attr('rx', canvas.rx)

 172        .attr('ry', canvas.ry)

 173        .attr('stroke', canvas.stroke.color)

 174        .attr('stroke-width', canvas.stroke.width)

 175        .attr('stroke-opacity', canvas.stroke.opacity)

 176        .attr('fill', canvas.fill.color)

 177        .attr('fill-opacity', canvas.fill.opacity);

 178    }

 179

 180    private setScales(data:Data) {

 181        /** expands `domains[colName]` to include the range of the current `data` domain. */

 182        function expandDomain(domains:{[colName:string]:NumDomain}, colName:string) {

 183            domains[colName] = domains[colName] || [1e90, -1e90];

 184            const dataDom:NumDomain = data.findDomain(colName);

 185            domains[colName][0] = Math.min(domains[colName][0], dataDom[0]);

 186            domains[colName][1] = Math.max(domains[colName][1], dataDom[1]);

 187            return domains[colName];

 188        }

 189        const hor = this.config.scales.hor;

 190        const ver = this.config.scales.ver;

 191        hor.scale = d3.scaleLinear()

 192            .domain(expandDomain(this.cumulativeDomains, hor.dataCol))

 193            .range([margin, this.config.viewPort.width-2*margin]);

 194        ver.scale = d3.scaleLinear()

 195            .domain(expandDomain(this.cumulativeDomains, ver.dataCol))

 196            .range([this.config.viewPort.height-2*margin, margin]);

 197    }

 198}