views.py
HeatmapTool, HeatmapToolElement, HeatmapDataParser are all “abstract” classes because we want to create specialized versions of them, while ensuring a constant behaviour at runtime. To do so, we must also define abstract methods, which the concrete children classes will be forced to implement, lest the program return a TypeError and crash.
To define an abstract class in JavaScript, we simply indicate that the calling constructor cannot be of the abstract class:
//In the Abstract Class' ctor:
if (this.constructor === AbstractClassName) {throw new TypeError("Cannot instantiate abstract class AbstractClassName")}
This ensures that the following works:
If the first instruction of the ConcreteClass’s constructor is super()
, while the following:
Returns the TypeError defined above.
Defining abstract methods is just as easy:
//In the Abstract Class' ctor:
if (this.abstractMethod === undefined) {throw new TypeError("Concrete Class must define an override for abstractMethod()")}
//In the Concrete class:
abtractMethod(){}
This ensures that all concrete children classes of the abstract class must provide a definition of the abstractMethod
, which in turn allows the parent abstract class to call this method without knowing its specifics.
With this in mind, what are the abstract methods of our classes?
Note: PresetField behaves in a special way: we do not know if all tools will have a PresetParser. Forcing all Tools to define a PresetParser would be fundamentally wrong. In OO-languages, this is where an interface would come in. However, JavaScript (to my knowledge) supports neither interfaces or (thank God) multiple inheritance. Thus, this backward abstraction workaround seems to work fine.
Right now, if you’re thinking: “Wow Clem, that’s a handy little trick you found”, I’d like to point out that I only just now checked to make sure JavaScript didn’t not have a “inherit” or “implements” keyword, which is about 2 weeks after I actually wrote much of HeatmapTool.js. So yeah, keep your expectations low.
Typical method structure:
activePreset
propertyaddElement()
methodready
property to true
Example using HeatmapToolDisplayer:
define()
{
let THIS = this;
$(document).ready(function ()
{
// --- 1 ---
$("#" + THIS.name).append(
`<p class="fieldFluff">
<b>Presets</b>
<span class="FluffTooltip">
Presets modify the options availaible in the dropdown fields you see. They are made to facilitate getting the results you want quickly.
</span>
</p>
<div id="dsp-presetSelect" class="darkDisplayer"></div>
<hr>
<p class="fieldFluff">
<b>Y</b>
<span class="FluffTooltip">
In this area, you can select which snoRNAs you wish to display in the heatmap,
along with their denomination, and other qualities you deem relevant.
</span>
</p>
<div id="dsp-major" class="lightDisplayer">Show snoRNA on Y axis as: </div>
<div id="dsp-set" class="darkDisplayer">snoRNAs to display:</div>
<div id="dsp-minors" class="lightDisplayer">Additional characteristics to display: </div>
<hr>
<p class="fieldFluff">
<b>X</b>
<span class="FluffTooltip">
In this area, you can select which columns you want to display the values of for each entry.
</span>
</p>
<div id="dsp-columns" class="darkDisplayer">Tissues and/or Cell lines to display:</div>
<hr>
<div id="dsp-options"></div>
`
)
});
// --- 2 ---
var presetSelect = new PresetField("dsp-presetSelect", "dsp_preset", this, "heatmap/getPresetList/displayer", "heatmap/fetchPreset/displayer/");
// --- 3 ---
var maj = new SelectField("dsp-major", "dsp_major", presetSelect.activePreset.Major);
var min = new ManySelectField("dsp-minors", "dsp_minors", presetSelect.activePreset.Minor, 3);
var columns = new ManySelectField("dsp-columns", "dsp_columns", presetSelect.activePreset.Param, -1);
var set = new DatasetField("dsp-set", "dsp_set", this.tableID, "symbol", "id");
var options = new MiscField("dsp-options", "dsp_options", ["normalize", "logScale"], "lightDisplayer", "darkDisplayer");
// --- 4 ---
this.addElements([presetSelect, maj, min, columns, set, options]);
// --- 5 ---
this.ready = true;
}
NOTE: let THIS=this
is used often throughout the code because once you enter a JQuery… query, this
stops referring to the current object instance. Using let THIS=this
circumvents this issue while preserving the verbose appeal of reffering to the current instance as this
.
Typical method structure:
ready
property is set to true (break otherwise)HeatmapElements
with new instances for each one that needs to be changed.Example using HeatmapToolDisplayer:
presetParser(preset)
{
// --- 1 ---
if (!this.ready)
return;
// --- 2 ---
this.heatmapElements[1] = new SelectField("dsp-major", "dsp_major", preset.Major);
this.heatmapElements[2] = new ManySelectField("dsp-minors", "dsp_minors", preset.Minor, -1);
this.heatmapElements[3] = new ManySelectField("dsp-columns", "dsp_columns", preset.Param, -1);
}
NOTE: This implies that you either know the indices of each heatmapElement. Alternatively, I believe you could use an object to represent heatmapElements instead of an array to make you use of some verbose indices. I haven’t tried it, but I believe it would either work out of the box or require very little modifications to make work.
Typical method structure:
HeatmapTool.DATA()
Example using HeatmapToolDisplayer:
parseData()
{
// --- 1 ---
var dataTool = this.tool.DATA();
// --- 2 ---
var dataOut = {
"LabelsY": [dataTool.dsp_major.value],
"LabelsX": this.parseSpecialOperands(dataTool.dsp_columns.values),
"LabelValuesY": [],
"Data": [],
"DataAmount": 0,
"Normalize": dataTool.dsp_options.normalize ? 1 : 0,
"LogScale": 0
};
// --- 3 ---
if (dataTool.dsp_options.logScale !== undefined)
dataOut.LogScale = dataTool.dsp_options.logScale;
dataTool.dsp_minors.values.forEach(function (label)
{
dataOut.LabelsY.push(label);
});
let THIS = this;
var TABLE = null;
var validPks = [];
var pks = dataTool.dsp_set.values;
$(document).ready(function ()
{
TABLE = $("#" + THIS.tool.tableID).DataTable().rows().data();
});
for (var cpt = 0; cpt < TABLE.length; cpt++)
{
validPks.push(TABLE[cpt]["id"]);
}
pks.forEach(function (value)
{
var index = validPks.indexOf(parseInt(value));
if (index != -1)
{
var labelValues = [];
dataOut.LabelsY.forEach(function (label)
{
labelValues.push(
(TABLE[index][label] == null) ? "N/A" : TABLE[index][label]
);
});
dataOut.LabelValuesY.push(labelValues);
var dataRow = [];
dataOut.LabelsX.forEach(function (column)
{
dataRow.push((TABLE[index][column] == null) ? 0 : parseInt(TABLE[index][column]))
});
dataOut.Data.push(dataRow);
dataOut.DataAmount += 1;
}
});
return dataOut;
}
}
NOTE: Though not required by abstraction, I recommend using a dedicated method for handling special Params like EXP
or ALLEXPS
as seen on Line 8.
This method is a fancy getter that doesn’t have the get
keyword in front of it because there was probably a reason but I forgot.
Example using MiscField:
DATA()
{
var obj = {};
let THIS = this;
$(document).ready(function ()
{
if (THIS.normalize) { obj.normalize = $("#miscNormalize").is(":checked") }
if (THIS.logScale) { obj.logScale = $("#miscLogScale").is(":checked") ? $("#" + THIS.htmlID + "-logscale").val() : 0 }
});
return obj;
}
The JSON’s formatting is at your discretion since you need to handle it in HeatmapDataParser.parseData()
anyhow.
A HeatmapToolElement’s HTML should always be contained within a div with an ID equal to the ToolElement’s htmlID
property. Other than that, it’s yet another fancy getter.
Example using ManySelectField:
toHTML()
{
var str = `<div id="${this.htmlID}" class="selectMany">`
this.fields.forEach(function (field)
{
str += field.toHTML();
});
str +=
`<button class="heatmapMsfBtn" id="${this.htmlID + "-add"}">+</button>
<button class="heatmapMsfBtn" id="${this.htmlID + "-remove"}">-</button>`
return str + `</div>`;
}
This method is a collection of JQuery event handling methods. The following syntax is mandatory to the good behaviour of the program:
// Example in SelectField
initListeners()
{
let THIS = this;
$(document).ready(function ()
{
$("#" + THIS.htmlID).on
(
'change',
{
'caller': THIS
},
THIS.changeValue
);
});
}
changeValue(event)
{
let THIS = event.data.caller;
$(document).ready(function ()
{
THIS.value = $("#" + THIS.htmlID).val();
});
}
It is crucial that you use $.on()
and not $.change()
or anything like that. Your event listeners will not work if you make “direct listeners”. Incidentally, it also makes for cleaner and more separated code, which is a feature.
The way the code is structured right now, a concrete HeatmapToolElement calls its super
constructor as the first instruction of his. This behaviour is mandatory. However, a problem arises when the HeatmapToolElement
constructor calls the put()
method at the end of its contructor. This is also mandatory, for reasons more cryptic. If we try to put elements on the page that are still undefined, the application will crash. To circumvent this, the beforePut()
can be defined in a children concrete class to define the missing elements before the parent constructor goes through with putting the elements on the page.
Example using ManySelectField:
class ManySelectField extends HeatmapToolElement
{
constructor(destination, htmlID, setOfValues, capacity = -1)
{
super(destination, htmlID, { "setOfValues": setOfValues, "fields": [], "capacity": capacity });
}
beforePut(data)
{
this.setOfValues = data.setOfValues;
this.fields = data.fields;
this.capacity = (data.capacity == -1) ? data.setOfValues.length : data.capacity;
}
...
Creates a dropdown field.
SelectField(destination, htmlID, setOfValues)
The values in setOfValues should be of format: {“value”:“xyz”,“verbose”:“EksWhyZee”}
Creates an area where you can dynamically create and remove SelectFields. The SelectFields cannot share a same value between them.
ManySelectField(destination, htmlID, setOfValues, capacity=-1)
The values in setOfValues should be of format: {“value”:“xyz”,“verbose”:“EksWhyZee”}
Creates an area where you can collect data from the table and display what entries are selected for vizualization.
DatasetField(destination, htmlID, tableID, displayColumn, pkColumn)
Creates a field for additional data processing parameters
MiscField(destination, htmlID, options, light, dark)
Creates a list of checkboxable options
CheckboxField(destination, htmlID, setOfValues)