Location: A review of cardiac cellular electrophysiology models @ c47db6b2fedb / dojo-presentation / js / andre / Chart.js

Author:
David Nickerson <david.nickerson@gmail.com>
Date:
2021-09-17 15:39:51+12:00
Desc:
tweak html formatting
Permanent Source URI:
https://models.fieldml.org/workspace/a1/rawfile/c47db6b2fedb368422c7f4d5191aeb9f319ad684/dojo-presentation/js/andre/Chart.js

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is "Andre's Reference Description Framework".
 *
 * The Initial Developer of the Original Code is
 * David Nickerson <nickerso@users.sourceforge.net>.
 * Portions created by the Initial Developer are Copyright (C) 2007-2008
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */
dojo.provide("andre.Chart");
dojo.require("dijit.layout.SplitContainer");
dojo.require("dijit.layout.ContentPane");
dojo.require("dojox.data.CsvStore");
dojo.require("dojox.charting.Chart2D");
dojo.require("dojox.charting.widget.Legend");
dojo.require("dojox.gfx");
dojo.require("andre.ChartData");
dojo.require("andre.utils");
dojo.require("dojox.charting.themes.PlotKit.blue");
dojo.declare("andre.Chart", null, {
  /*
  Summary:
          A single object used to handle the drawing of and interaction with graphs defined using CellML graphing metadata. Will create the full display with a chart, legend, and trace definition list.
          
          FIXME: initially simply reading in data from CSV generated by CellMLSimulator.
  
          FIXME: should maybe look at ignoring the metadata colours and going with the defaults for the themes - they look way cooler.
  
          TODO: With Dojo 1.2 there should be support for events and other cool stuff with charts. This should make it easy to enhance the charts with tool tips and maybe a Chart Legend widget.
          TODO: update: will be unlikely to have events for line charts any time soon, although possible some time in the future.
  */
  
  // id: String
  //  Unique name of this chart
  id: "",
  
  // parentContainerId: String
  //   The ID of the parent content item, needed to help insert new content items into the display order correctly
  parentContainerId: "",
  
  // titles: Object
  //  Titles to use in labelling the chart.
  titles: null,
  
  // chart: dojox.charting.Chart2D
  //  The actual chart object
  chart: null,
  
  // chartData: andre.ChartData
  //  The actual data store used to retrieve the data to be charted.
  chartData: null,
  
  // traceList: Node
  //   The container node for the trace list which will be built up as data series are added to the chart.
  traceList: null,
  
  // traceLabelMap: Object
  //   Maps trace labels to the full label string used to make the legend
  traceLabelMap: null,
  
  // data: Object
  //  Temporary store for the data retrieved from the chartData before it is applied to the chart itself.
  data: null,
  
  // topicAppendData: String
  //  The topic which the chart will listen for in order to append data
  //  FIXME: make it unique so each chart only gets events relevant to itself
  topicAppendData: "",
  
  // _idList: Static array used to ensure unique names for each chart
  _idList: [],
  
  // listenerAppendData: function
  listenerAppendData: null,
  
  // inProgress: Integer
  //  incremented for each series added to the chart and then decremented as each series' data fetch completes. Chart is rendered when final series completes its fetch.
  inProgress: 0,
  
  constructor: function (/*String*/parentContainerId, /*String*/dataURL, /*Object*/style, /*Object*/titles) {
    // summary: initialise the Chart object
    // parentContainerId: String
    //  the ID of the parent content item.
    // dataURL: String
    //  the url of the CSV data source from where the chart data will come from.
    // style: Object
    //  The styles to use for the chart.
    // titles: Object
    //  titles.title  - main title for the chart
    //  titles.x      - label for x-axis
    //  titles.y      - label for y-axis
    
    this.parentContainerId = parentContainerId;
    this.titles = titles;
    
    // create the unique id for the chart
    this._idList.push("");
    this.id = "andreChart-" + this._idList.length;
    
    // initialise the traceLabelMap
    this.traceLabelMap = {};
    
    // create a node to hold the graph - must set the size for the chart
    // FIXME: looks like charts need to be attached to the document in order to work properly, so rather than force the issue we'll create a dummy div off screen and then the user can call place() to add the chart to its appropriate location once that is added to the document
    var node = dojo.body().appendChild(dojo.doc.createElement("div"));
    node.setAttribute("id",this.id);
    dojo.addClass(node,"andreChartContainer");
    var containerStyle = {};
    /*// ensure some styles match the chart
    if (style.backgroundColor) containerStyle.backgroundColor = style.backgroundColor;
    if (style.color) containerStyle.color = style.color;*/
    // and force some more to hide the node offscreen (FIXME: these shouldn't be set by the user?)
    containerStyle.position = "absolute";
    containerStyle.visibility = "hidden",
    containerStyle.top = "-9999px",
    dojo.style(node,containerStyle);
    // make a table for the layout?
    var table = node.appendChild(dojo.doc.createElement("table"));
    var tr = table.appendChild(dojo.doc.createElement("tr"));
    var td = tr.appendChild(dojo.doc.createElement("td"));
    // create the legend node
    var legendNode = td.appendChild(dojo.doc.createElement("div"));
    legendNode.setAttribute("id",this.id + "-legend");
    dojo.addClass(legendNode,"andreChartLegendNode");
    // check for required style for the chart node
    if (!style.width) style.width = "300px";
    if (!style.height) style.height = "300px";
    var chartNode = td.appendChild(dojo.doc.createElement("div"));
    // FIXME: overriding all colours for now...
    style.backgroundColor = "";
    style.color = "";
    dojo.style(chartNode,style);
    console.log("set chart node style: " + dojo.toJson(style));
    this.chart = new dojox.charting.Chart2D(chartNode);
    // Set the theme
    var theme = dojox.charting.themes.PlotKit.blue;
    /*if (style.backgroundColor) {
      console.log("Setting theme background colour to: " + style.backgroundColor);
      theme.plotarea.fill = style.backgroundColor;
      theme.chart.fill = style.backgroundColor;
      // ensure the gridlines are visible?
      var bc = dojo.colorFromString(style.backgroundColor).toHex();
      var gc = dojo.colorFromString(theme.axis.majorTick.color).toHex();
      if (bc == gc) {
        console.log("Background matches gridlines: " + bc + "/" + gc);
        var c = dojo.colorFromHex(gc).toRgb();
        for (var i=0;i<3;i++) {
          if (c[i]<127) c[i] = 191;
          else c[i] = 63;
        }
        c = dojo.colorFromArray(c).toHex();
        console.log("Changing gridlines colour to: " + c);
        theme.axis.majorTick.color = c;
      } else {
        console.log("Background different colour to gridlines: " + bc + "/" + gc);
      }
    }
    if (style.color) {
      console.log("Setting theme colour to: " + style.color);
      theme.axis.fontColor = style.color;
      theme.series.fontColor = style.color;
      theme.marker.fill = style.color;
    }*/
    // FIXME: use my colours? the defaults all look too similar...
    theme.defineColors({
      colors: [
          dojo.colorFromHex("#ff0000"),
          dojo.colorFromHex("#0000ff"),
          dojo.colorFromHex("#008000"),
          dojo.colorFromHex("#000000"),
          dojo.colorFromHex("#808000"),
          dojo.colorFromHex("#800080"),
          dojo.colorFromHex("#008080"),
          dojo.colorFromHex("#800000"),
          dojo.colorFromHex("#000080"),
          dojo.colorFromHex("#ff00ff"),
          dojo.colorFromHex("#808080"),
          dojo.colorFromHex("#00ff00"),
          dojo.colorFromHex("#00ffff"),
          dojo.colorFromHex("#ffff00"),
          ]
    });
    this.chart.setTheme(theme);
    // Add a default plot (lines)
    this.chart.addPlot("default", {type: "Default", lines: true, shadows: {dx: 2, dy: 2, dw: 2}});
    // Add a grid on the X&Y major ticks
    this.chart.addPlot("grid", {type: "Grid"});
    // Create an x-axis
    this.chart.addAxis("x", {htmlLabels: false, stroke: {stroke: "black"}, majorTick: {stroke: "black", length: 5}, minorTick: {stroke: "gray", length: 2}});
    // Create a y-axis
    this.chart.addAxis("y", {htmlLabels: false, stroke: {stroke: "black"}, vertical: true, majorTick: {stroke: "black", length: 5}, minorTick: {stroke: "red", length: 2}});
    // initialise the data
    this.data = {};
    // and the topic for the data listener
    this.topicAppendData = this.id + "-appendData";
    // subscribe for new data
    this.listenerAppendData = dojo.subscribe(this.topicAppendData, this, "_appendData");
    // and create the chart data object
    this.chartData = new andre.ChartData(dataURL);
    // and now the trace list
    this.traceList = tr.appendChild(dojo.doc.createElement("td"));
    this.traceList.setAttribute("id",this.id + "-trace-list");
    dojo.addClass(this.traceList,"traceListContainer");
  },
  
  addSeriesByPosition: function (/*String*/series, /*Object*/style) {
    // FIXME: doesn't like trying to render with no data points so initialise with something in case the fetch fails/returns no data
    // FIXME: this also means we don't need to pass style around as well
    var data = [{'x': 0, 'y': 0}];
    this.chart.addSeries(series,data/*,style*/);
    console.log("added series (by position) '" + series + "' to the chart with the style: " + dojo.toJson(style));
    // initialise the data cache for this series
    this.data[series] = [];
    this.inProgress++;
    // and initiate the fetch of data, which will be handled by this.appendData
    this.chartData.fetchDataByPosition(series,this.topicAppendData);
  },
  
  addSeriesByLabel: function (/*Data store*/store,/*Data store item*/item, /*Object*/style) {
    /* grab the label */
    var series = store.getValue(item,"label");
    /* and the id of the corresponding entry in the trace list */
    var traceListId = store.getIdentity(item);
    // FIXME: doesn't like trying to render with no data points so initialise with something in case the fetch fails/returns no data
    // FIXME: this also means we don't need to pass style around as well
    var data = [{'x': 0, 'y': 0}];
    // add an entry to the traceLabelMap for this label
    this.makeLegendLabel(series,traceListId);
    // and get the label
    var legendLabel = this.getLegendLabel(series);
    this.chart.addSeries(legendLabel,data,style);
    console.log("added series (by label) '" + legendLabel + "' to the chart with the style: " + dojo.toJson(style));
    // add the entry into the trace list
    var t = this.traceList.appendChild(dojo.doc.createElement("div"));
    t.setAttribute("id",traceListId);
    dojo.addClass(t,"traceListTrace");
    if (store.hasAttribute(item,"colour")) {
      dojo.style(t,"color",store.getValue(item,"colour"));
    }
    // use the label for a heading
    var e = t.appendChild(dojo.doc.createElement("div"));
    var heading = e.appendChild(dojo.doc.createElement("span"));
    dojo.addClass(heading,"traceListHeading");
    heading.innerHTML = series;
    if (store.hasAttribute(item,"xVariable")) {
      e = this.makeTraceVariable(store,store.getValue(item,"xVariable"),"x-axis");
      t.appendChild(e);
    }
    if (store.hasAttribute(item,"yVariable")) {
      e = this.makeTraceVariable(store,store.getValue(item,"yVariable"),"y-axis");
      t.appendChild(e);
    }
    if (store.hasAttribute(item,"y2Variable")) {
      e = this.makeTraceVariable(store,store.getValue(item, "y2Variable"), "y2-axis");
      t.appendChild(e);
    }
    // initialise the data cache for this series
    this.data[series] = [];
    this.inProgress++;
    // and initiate the fetch of data, which will be handled by this.appendData
    this.chartData.fetchDataByLabel(series,this.topicAppendData);
  },
  
  _appendData: function(message) {
    // summary: a listener for the chart.topicAppendData message
    // FIXME: might need to look at more frequent updates if the data store fetch takes a really long time.
    // FIXME: write this out to remember what fields should be present
    var series = message.series;
    var request = message.request;
    var complete = message.complete;
    var error = message.error;
    var msg = message.errorMessage;
    var x = message.x;
    var y = message.y;
    if (error) {
      console.log("andre.Chart._appendData: Error occurred so not appending any data: " + msg);
      this.data[series] = [];
      this.inProgress--;
    } else if (complete) {
      this.inProgress--;
      console.log("andre.Chart._appendData: Fetch complete, tidy up time (" + this.inProgress + " other fetches in progress)");
      if (this.data[series].length > 0) {
        var legendLabel = this.getLegendLabel(series);
        this.chart.updateSeries(legendLabel,this.data[series]);
        if (this.inProgress == 0) {
          this._render();
        }
        this.data[series] = [];
      } else {
        console.log("ERROR: no data points to plot for series: " + series);
      }
    } else {
      //console.log("andre.Chart._appendData: appending the data point: {" + x + "," + y + "} to the series: '" + series + "'");
      this.data[series].push({'x': x, 'y': y});
    }
  },
  
  _render: function() {
    // summary: call this to render the chart after it is attached to the document dom and the data has been altered.
    
    /* fullRender() seems to recalculate graph axes */
    this.chart.fullRender();
    //console.dir(this.chart);
    
    // add in the titles
    if (this.titles) {
      if (this.titles.x) this.makeTitle('x',this.titles.x);
      if (this.titles.y) this.makeTitle('y',this.titles.y);
    }
    
    // add in the legend - after all the series have been added and the chart is rendered
    var legend = dijit.byId(this.id + "-legend");
    if (legend) {
      // need to destroy any existing legend and redraw it?
      legend.destroy();
    }
    var legendNode = dojo.byId(this.id + "-legend");
    if (legendNode) {
      legend = new dojox.charting.widget.Legend({chart: this.chart, horizontal: true}, legendNode);
    } else {
      console.log("ERROR: where has the legend node got to?");
    }
  },
  
  makeTitle: function(/*String*/axis, /*String*/title) {
    // summary: add the given title to the specified axis as a label
    //  should be called after the chart is rendered...
    // based on information from: http://dojotoolkit.org/forum/dojo-foundation/general-discussion/how-add-name-axes-im-not-able
    
    // FIXME: what about chart title?
    var axis = this.chart.axes[axis];
    var dim = this.chart.dim;
    var offsets = this.chart.offsets;
    var ta = this.chart.theme.axis;
    var taFont = "font" in axis.opt ? axis.opt.font : ta.font;
    var x;
    var y;
    var rotate=0;
    if(axis.vertical){
      rotate = 270;
      y=dim.height/2  - offsets.b/2;
      x=0+12;
    } else {
      x=dim.width/2 + offsets.l/2;
      y=dim.height - 12/2;
    }
    var m = dojox.gfx.matrix;
    var elem = axis.group.createText({x:x, y:y, text:title, align: "middle"});
    //console.log("family: " + taFont.family + "; size: " + taFont.size + ";");
    elem.setFont({family:taFont.family, size: "10pt", weight: "normal"});
    /*var colour = dojo.style(this.chart.node,"color");
    if (colour) {
      elem.setFill(colour);
    } else {*/
      // FIXME: a good default?
      elem.setFill('black');
    /*}*/
    elem.setTransform(m.rotategAt(rotate, x,y));
  },
  
  place: function(/*DomNode*/refNode,/*String*/position) {
    // summary: since the chart is initially drawn offscreen, here we reparent it as defined by the refNode and position attributes. (see dojo.place())
    var node = dojo.byId(this.id);
    dojo.place(node,refNode,position);
    dojo.style(node,{
      position: "",
      visibility: "",
      top: "",
    });
  },
  
  destroy: function() {
    // summary: clean up and destory this chart
    dojo.unsubscribe(this.listenerAppendData);
    this.chart.destroy();
  },
  
  makeLegendLabel: function(/*String*/label,/*String*/traceListId) {
    // summary: for a given label return the legend label, used to encode extra data into the HTML code used as the series label in the chart legend.
    this.traceLabelMap[label] = "<span class=\"traceLinkText\" onclick=\"andre.utils.highlightNode('" + traceListId + "');\">" + label + "</span>";
  },
  
  getLegendLabel: function(/*String*/label) {
    if (this.traceLabelMap[label]) {
      return this.traceLabelMap[label];
    } else {
      console.log("ERROR: traceLabelMap entry doesn't exist for: " + label);
      return label;
    }
  },
  
  makeTraceVariable: function(/*Data store*/store, /*data store item*/item, /*String*/label) {
    // summary: for a given graph trace variable create a Node holding the desription.
    var element = dojo.doc.createElement("div");
    dojo.addClass(element,"traceListVariable");
    element.appendChild(dojo.doc.createElement("span")).innerHTML = label;
    /* must have a simulation */
    var simulation = store.getValue(item,"simulation");
    var e = element.appendChild(dojo.doc.createElement("div"));
    e.appendChild(andre.utils.makeContentItemLink(store.getIdentity(simulation), this.parentContainerId, "simulation", "simulation"));
    /* must have a variable */
    var variable = store.getValue(item,"variable");
    e = element.appendChild(dojo.doc.createElement("div"));
    //e.appendChild(andre.utils.makeContentItemLink(store.getValue(variable, "uri"), this.parentContainerId, "source variable", "variable"));
    // FIXME: no variable yet
    e.appendChild(andre.utils.makeContentItemLinkFromURI(store, variable, this.parentContainerId));
    /* may have a range specified */
    var maxSet = false;
    var minSet = false;
    var maximim;
    var minimum;
    if (store.hasAttribute(item,"minimum")) {
      minSet = true;
      minimum = store.getValue(item,"minimum");
    }
    if (store.hasAttribute(item,"maximum")) {
      maxSet = true;
      maximum = store.getValue(item,"maximum");
    }
    if (minSet || maxSet) {
      e = element.appendChild(dojo.doc.createElement("div"));
      var label = e.appendChild(dojo.doc.createElement("span"));
      dojo.addClass(label,"traceListValueRange");
      var string = "Range restricted to: ";
      if (minSet) {
        string += "[" + minimum.toString();
      } else {
        string += "(-∞";
      }
      string += ",";
      if (maxSet) {
        string += maximum.toString() + "]";
      } else {
        string += "∞)";
      }
      label.innerHTML = string;
    }
    return(element);
  },
});