- Author:
- David Nickerson <david.nickerson@gmail.com>
- Date:
- 2021-09-17 15:50:49+12:00
- Desc:
- tweak html formatting
- Permanent Source URI:
- https://models.fieldml.org/workspace/a1/rawfile/1b3862589abf79ae9119ee0b5e99a8b785d762e1/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);
},
});