- Quick Overview of JavaScript Files
- Details for the More Complex Files
-
main.js
- Controls the loading and parsing of the.nii
and.pial
meshes. Also callselectrodes.js
, which handles all of the loading and event setting for the 3D electrode rendering. -
color.js
- Has aCOLOR
object which contains semantic names for XTK color vectors. Also contains function for translating seizure type names to colors (e.g."onset" : COLOR.red"
) -
DOM.js
- Contains most of the DOM elements as an object. This helps reduce the amount ofdocument.getElementById('long-element-name')
required in the other parts of the program -
electrodecanvas.js
- Contains the classes for electrode canvases. Uses XTK's volume parser and the canvas APIsdrawImageData
to dynamically render 2D electrode colors. -
electrodes.js
- A large file containing all of the functions that have to do with the 3D electrodes. Fetches the electrode and signal header json files, and then uses the data to add events to menus and sliders. -
gfx.js
- Wraps some of XTKs 3D geometry code (i.e.X.sphere
) into a singleGFX
object that can be called in multiple places.electrodes.js
usesGFX
to render the spheres on the scene -
mapInterval.js
- A single function that takes a coordinate on the interval[a, b]
and maps it to a coordinate on the interval[c, d]
. This is how we take the 3D coordinates, which are usually on the interval[-128, 127]
to the interval[0, 255]
. -
search.js
- Handles the search page at the beginning of the application. -
signaldisplay.js
- Contains all of the code for displaying the signal graph. -
sliceRenderer.js
- Contains all of the code for adding drag and zoom functionality to 2D elements.
The electrodes.js
file contains all of the functionality for rendering the 3D electrode sphere objects onto the scene. In order to add or change to this file, it is important to have a sense of all the data structures it uses.
This is the JSON parsed as a regular JavaScript object. You can get the electrode data specifically by using data.electrodes
, or functional maps by using data.functionalMaps
.
- The function
getAttrArray(data, attr)
can be especially useful for plucking specific properties from the electrodes or functional maps. For example, if I want to get an array of just theelecID
, similar to the old way N-Tools JSON's were stored, I can dogetAttrArray(data.electrodes, 'elecID')
This is the XTK 3D renderer. It contains several methods that we make use of in various functions
-
pick(x, y)
- Takes an(x, y)
pair in screen coordinates and finds theuniqueID
of the object clicked. By default, only the spheres and cylinders are pickable; everything else has.pickable
set to false. -
get(ID)
- Takes the ID returned from pick and finds the actual object in the renderer itself. An ID of 0 indicates no ID has been found. -
camera.view
- Gets the view matrix for the renderers camera. This will then be used to create the perspective matrix, which we then use to get the screen coordinates for a 2D electrode tag
The XTK volume object. Because we linked the minified XTK-Edge as a <script>
tag, many of the object properties can be a bit difficult to understand. However, the one we use for the electrode canvas is volume.K
, as this contains the NIfTI image array.
The X.sphere
objects that cover the brain surface.
The X.sphere
objects that are opaque and blue and surround an electrode when selected. They are invisible by default, and work by turning the one corresponding to the clicked electrode visible. In future versions, it might be helpful to find another way to highlight spheres, or remove the spheres when not in use rather than relying on making them visible/invisible.
The X.cylinder
objects that represent a connection between two electrodes.
The X.cylinder
objects that represent a highlighted functional map. The reasoning is the same as above.
We put the three canvases into a single array to make it easier to pass to the different functions. You will often see code that calls methods on all three of them like so:
slices.forEach(s => s.drawCanvas())
The canvas has to be redrawn like this every time an edit change happens. The sliceMap for each slice has to be re-created as well.
The bounding box is a vector that contains the offset from the origin. Graphics objects generated need to be offset by the bounding box in order to appear at the origin. There is an XTK renderer function called resetBoundingBox()
. However, this seems to have unintended side effects, such as causing the 2D electrode tags to appear off center. There are many things within XTK that appear tightly coupled. An example of this would be not adding the volume to the renderer at all, which causes the electrodes to be rendered way off center. It would be good to figure this out in the future, but for now it solves the problem.
Any new function added to N-Tools that wants to work with these data must pass a reference to the new function. If future developers can simplify the code so that the functions need less arguments, this would be a great improvement.
Some of the functions in this file are admittedly quite complex and could be named better. This presents great opportunities for refactoring. Here is an overview of some of the more involved ones.
This function adds a click event to the 3D canvas. Once this function has been called, the click handler contains a reference to the data, so it does not need to be called multiple times as the program runs. It begins by using the renderers pick
and get
functions to get a reference to the clicked XTK object. It then finds the index of this object in the electrodeSpheres
array.
It is important to remember that the data.electrodes
array and the electrodeSpheres
array are parallel; that is, data.electrodes[i]
has a graphical representation in electrodeSpheres[i]
. Since electrodes cannot be deleted, this should never change.
Note that the .g
property in minified XTK corresponds to the type of 3D object XTK is representing, e.g. 'sphere' or 'cylinder'. If the object is found, it calls updateView
. If an fmap is found, it calls getAttrArray
to get two arrays for the Threshold
and AfterDischarge
, and uses that index to display results on the fmap menu.
This function is similar to jumpSlicesOnClick, but it does not move the slice view. It instead creates a context menu when the user right clicks. The getting and picking with the renderer is the same.
We currently are injecting an HTML markdown directly into the scene with a template literal. When we tried making an already formed HTML menu in view.html
, it would either only edit the most recently clicked electrode, or edit every electrode at the same time
When this menu is visible, all of its inner HTML elements can be accessed with document.getElementById
and related methods.
It adds a click handler to the update button. This click handler calls editElectrode
and addFmap
, and then updates the colors of the 3D electrodes and 2D slices.
Takes all of the properties from the fields in the electrode menu and edits the data.electrodes
array. It does this by creating a copy of the old electrode with only the elecType, intPopulation, and seizType changed. Then, it calls updateLabels
to reflect the new change on the view.
This function was added late and is a bit messy. It begins by gathering all of the data present in the edit menu fields, and finding the electrode specified for the connection. It then creates a new X.cylinder
object, and a new fmap data object which will be appended to the data.functionalMaps
array.
The electrodeCanvas is a rather bloated class that keeps track of the state of the 2D renderers. The parent class is simply electrodeCanvas
, and it has three children: sagittalCanvas
, coronalCanvas
, and axialCanvas
. We thought we were being clever by having three sub-classes, since before, electrodeCanvas
would have to constantly assess itself to understand what orientation it was in. This is important, because depending on the orientation it is going to draw electrodes very differently.
However, adding subclasses also introduced a lot of repetitious code, which we will explain below. Each canvas manipulates a different set of coordinates, depending on which coordinate we treat as the "slice index."
This method creates a new ES6 Map that has a slice index as the key, and an array of electrodes mapped to that slice. It does this by making use of mapInterval
. When it is completed, it will have several entries such as
0: {87 => Array(3)}
1: {77 => Array(3)}
2: {66 => Array(3)}
3: {60 => Array(2)}
4: {57 => Array(3)}
5: {55 => Array(1)}
6: {56 => Array(2)}
7: {88 => Array(2)}
8: {68 => Array(1)}
9: {63 => Array(1)}
10: {58 => Array(2)}
...
It is normal for these to not be in order, and there should not be any duplicate keys.
Creates the events for the scroll wheel on the canvas. This enables images to cycle through by updating the current slice, and calling
this.drawCanvas()
Calculate offset is a bit confusing if you are unfamiliar with how NIfTI files are stored. This document goes into all the detail one could want, but essentially if you have an array buffer containing the NIfTI data, and want the (x, y, z)
coordinate, you use the formula,
(i + j ∗ dim[1] + k ∗ dim[1] ∗ dim[2]) ∗ (bitpix/8)
i
, j
, and k
can be the slice, row, or column depending on how you use it. We also use dim - i
, since otherwise the NIfTI will appear upside down. This was a ton of brute force experimentation.
The method begins by getting the current dimensions, and then uses the pixel data from the NIfTI to draw the image with two nested for loops. It then checks if the current slice has any adjacent electrodes. Since we have been going from a continuos to discrete interval, there will never be a perfect match, so this helps make it look a bit better. The previous and next electrodes have half the radius, to give the illusion of them growing and shrinking as the image passes through.
This function iterates through all of the electrodes in a current slice, and draws their relative coordinates. Since the origin for screen coordinates is in the top left, they often have to be flipped about the X or Y axis in order to appear normally, which is why you might see lines such as
this.dims[0] - Math.round(mapInterval ...
This was my best attempt at giving each canvas two notions of its current slice. The currentSlice
is the slice currently visible to the user. The relativeSlice
is whichever slice the user clicked last. That way, when they user wants to sync the slices back, it sets
this.currentSlice = this.relativeSlice