-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dh code review #1
base: empty
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General comments
The application works as intended and the code is readable and easy to understand and navigate (though there is a fair bit of commented out code here in and there that should be cleaned up). However, the code can be substantially optimized, but it will require some strategizing and potentially substantial refactoring. If time /resources are short, the code as is will be relatively safe, but I have some concerns about accessibility in terms of compatibility with even not-so-old browsers, and potential interaction with hard-to-predict user browser configurations and extensions. The comments throughout the code offer some potential solutions.
The JavaScript code would be best handled as a module (e.g. via Node) with a dedicated package.json. This will provide several advantages:
- you will have a centralized place to manage dependencies, including tools to notify you of security concerns and mechanisms for updating things manually
- you will have access to code bundling tools (such as rollup). This will take care of things like minimization to reduce the size of the code you send to clients, and tree-shaking to get rid of unnecessary code from libraries.
- code bundling will also help with avoiding polluting the global namespace, ensuring that your code can be used in other applications without conflict (see my comments on the code, starting from visDynamic.html)
- your pages will only need to import fewer scripts (likely just one), reducing HTTP requests
- if you chose to test your JS code, you will be able to able to access testing libraries that can run on Node, such as Mocha.
Interface and accessibility
This isn't the main focus of this review, so these are just some general observations as feedback.
- For accessibility and ease of use it would be good to include keyboard controls to navigate the visualization.
- Some menu buttons cannot be reached via tabbing, limiting interaction to mouse users only
- The hamburger menu on "Fields of research" is hard to see
import { CitationNet } from "{{ url_for('static', filename='force-citationNet.js') }}"; | ||
|
||
const jsondata = await '{{ jsondata|tojson }}'; | ||
window.net = new CitationNet(jsondata); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's generally not a good idea to add anything to the global namespace (including the window object) as this will reduce code re-usability. It may seem unnecessary if your code is strongly coupled with this specific page and plan to vet the libraries you include in order to avoid conflict. However, it also risks messing with people's browser extensions, which is unpredictable (just in case some extension does create a "net" global object, for example).
Doing this may need a fair bit of refactoring however. One challenge will be handling click events: they are better handled within the JavaScript code, by adding event listeners. For example:
// Attach event listener to the button
document.querySelector('#rngLayoutRadius button').addEventListener('click', readLayout);
Code bundlers like rollup can help with these operations.
import { CitationNet } from "{{ url_for('static', filename='force-citationNet.js') }}"; | ||
|
||
const jsondata = await '{{ jsondata|tojson }}'; | ||
window.net = new CitationNet(jsondata); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's generally not a good idea to add anything to the global namespace (including the window object) as this will reduce code re-usability. It may seem unnecessary if your code is strongly coupled with this specific page and you plan to vet the libraries you include in order to avoid conflict. However, it also risks messing with people's browser extensions, which is unpredictable (just in case some extension does create a "net" global object, for example).
Doing this may need a fair bit of refactoring however. One challenge will be handling click events: they are better handled within the JavaScript code, by adding event listeners. For example:
// Attach event listener to the button
document.querySelector('#rngLayoutRadius button').addEventListener('click', readLayout);
Code bundlers like rollup can help with these operations.
document.getElementById("sidebar").style.width = "300px"; | ||
|
||
window.adaptWindowSize = () => { | ||
var navHeight = document.getElementsByClassName("navbar")[0].scrollHeight; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of var
, it's safer to use const
for variables that do not need to be updated (constants) and let
for most other variables, unless you need to make them available outside of their immediate scope.
</div> | ||
</div> | ||
|
||
<script type="module"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is totally fine and in line with modern practices, many older browsers will not know what to do. Consulting "Can I Use," is a good way of getting a sense of which environments will support your code. If you want to make sure your application is more accessible, consider using tools to compile your code based on a target browser that can work as a common denominator. Babel is the right tool for this and it can be integrated with rollup (mentioned elsewhere).
try { | ||
if (!(jsondata)) jsondata = jsondata; | ||
} catch (error) { | ||
alert("no JSON containing citation data specified, graph cannot be displayed"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly recommend not to use alert()
. It blocks the browser and the javascript thread, forcing the user to interact to continue doing anything else at all. Some popup blockers may block it anyway. Instead, consider showing a UI component with this information. Since you're using Bootstrap, consider using an alert or a toast. You can generate the HTML dynamically and append it to the DOM (and remove it when dismissed or after a timeout). If that sounds too complicated, you may also consider setting a property to the class and have the code in visDynamic.html
monitor the property to react accordingly (e.g. by showing some text in the main <div>
...)
} | ||
|
||
async function loadFOR() { | ||
const stats = await window.net.getStats(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to avoid polluting the global namespace, I would recommend passing net
as a parameter instead
|
||
window.onresize = window.adaptWindowSize; | ||
|
||
await showFOR(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of relying on window
, pass net
to this function to make it accessible from loadFOR
in pieChart_vega-lite.js
|
||
console.log(vegaLiteSpec); | ||
|
||
vegaEmbed('#forChartDivvis', vegaLiteSpec); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider attaching the click events here in order to avoid polluting the global scope.
forChartDivloader.hidden = true; | ||
} | ||
|
||
function hideFOR() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you end up attaching click events in loadFOR
, make sure to remove them here.
document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown; | ||
} else { | ||
/* otherwise, move the DIV from anywhere inside the DIV:*/ | ||
elmnt.onmousedown = dragMouseDown; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine, but I'd recommend using addEventListener
instead for more control and reusability of the handler (as well as being able to detach it properly if/when needed!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just thought that you may also want to consider using the same handler to add a touch event listener (though I haven't tested the application with a touch screen, so I don't know to what extent it's worth supporting touch).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I think you expected from our initial conversation, there's a lot of cleanup that can be done here, and hopefully in our wrap-up conversation we can help you gauge what is most applicable and highest priority and impact.
@raffazizzi has already touched on the important javascript handling and embedding aspects, which I agree with. If you don't want to make the js a full-blown reusable package, at least integrating webpack into your flask should get you some good improvement in terms of compiling and minimizing the js code so you only have one file to include.
In general, the main higher-level problems I see are along the lines of encapsulation - there are connections both ways between html, force graph code, and pie chart code. I would focus on making the html more semantic (e.g., radio buttons and checkboxes for controls) and move the event handlers and logic bindings into the javascript. The pie chart code should not need to know about or interact with the controls or the force network code, but just take a containing element for display and a set of statistics to render.
Two suggestions on the UI of the application based on experimenting with it as I worked through the code:
- Display menu by default if screen is large enough
- Provide some sort of warning before linking out to the article, or an alternate display? that wasn't the behavior I expected in the context (I thought I would be interacting with the graph). Maybe a future enhancement could be to load the article or some preview version in a card on the page.
if (node.attributes.nodeyear >= this.inputNode.attributes.nodeyear) { | ||
node.fz = 5 * (node.attributes.nodeyear - this.inputNode.attributes.nodeyear); | ||
} else { | ||
node.fz = 5 * (node.attributes.nodeyear - this.inputNode.attributes.nodeyear); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This conditional seems unnecessary, since the logic is the same either way.
if (node.attributes.nodeyear >= this.inputNode.attributes.nodeyear) { | ||
node.fz = 5 * (node.attributes.nodeyear - this.inputNode.attributes.nodeyear + 20); | ||
} else { | ||
node.fz = 5 * (node.attributes.nodeyear - this.inputNode.attributes.nodeyear - 20); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, maybe the earlier conditional was based on this one and the adjustment got dropped.
Given that the logic is nearly the same I would collapse the larger if/else and add an adjustment amount that is +/-20 or +/- 0 depending on whether the spacing is enabled.
constructor(jsondata = null) { | ||
// if jsonPath is not provided try to get globJsonPath, show alert if fails | ||
try { | ||
if (!(jsondata)) jsondata = jsondata; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment indicates a global fallback, but I'm not sure it's working as described. If you need a global fallback it should have a different variable name and be clearly described/defined before this class.
Looking at the calling code in the html, I think if the passed in data doesn't work then the global won't either.
const forChartDiv = document.getElementById("forChartDiv"); | ||
const forChartDivvis = document.getElementById("forChartDivvis"); | ||
const forChartDivloader = document.getElementById("forChartDivloader"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given how tightly coupled this is to the html structure in your template, it would be cleaner and more maintainable if you passed in a container element and constructed the other elements using createElement
when the chart is first initialized.
There's a fair bit of code for making this element draggable, but I'm not sure how important that is - it wasn't obvious to me in the UI that it was draggable, I only found it after looking at the code and experimenting. You might consider whether that is actually necessary, or ask some users if they find themselves using that feature. If it's not essential you could simplify the code quite a bit by removing it.
Consider refactoring into a class.
const forChartDiv = document.getElementById("forChartDiv"); | ||
const forChartDivvis = document.getElementById("forChartDivvis"); | ||
const forChartDivloader = document.getElementById("forChartDivloader"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be cleaner to group into an object, e.g.
const forChartDiv = document.getElementById("forChartDiv"); | |
const forChartDivvis = document.getElementById("forChartDivvis"); | |
const forChartDivloader = document.getElementById("forChartDivloader"); | |
const chart = { | |
container: document.getElementById("forChartDiv"), | |
vis: document.getElementById("forChartDivvis"), | |
loader: document.getElementById("forChartDivloader") | |
}; |
Then you can reference container.chart
etc.
Also recommend more semantic names (container vs div), and maybe loader
should be loading
.
if (outerValue == 0) { | ||
this.graph.d3Force('radialOuter', d3.forceRadial(radius).strength(1.0)); | ||
} else { | ||
this.graph.d3Force('radialOuter', d3.forceRadial(radius).strength(CitationNet.strengthFuncFactory(0.0, 1.0, 0, outerValue))); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This another place you can consolidate mostly redundant code to make it easier to understand what changes based on the conditional, e.g.
let radialForceStrength = 1.0
if (outerValue) {
radialForceStrength = CitationNet.strengthFuncFactory(0.0, 1.0, 0, outerValue);
}
this.graph.d3Force('radialOuter', d3.forceRadial(radius).strength(radialForceStrength);
a.outgoingLinks.push(edge); | ||
|
||
!a.outgoingLinkTo && (a.outgoingLinkTo = []); | ||
a.outgoingLinkTo.push(b); | ||
|
||
!b.incomingLinks && (b.incomingLinks = []); | ||
b.incomingLinks.push(edge); | ||
|
||
!b.incomingLinkFrom && (b.incomingLinkFrom = []); | ||
b.incomingLinkFrom.push(a); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to store links and references in both directions? I wonder if there is a better data structure that would let you avoid the redundancy here and still do whatever calculations or operations you need to do.
I couldn't find where else these are referenced so I'm not sure what they are for.
delete edge.color; | ||
delete edge.size; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this in your example data, maybe the code is unneeded?
* Constructs a new CitationNet object, but does not initialize it. Call object.initialize() right after this. | ||
* @param {String} jsondata - path or url to citation data as JSON file/stream | ||
*/ | ||
constructor(jsondata = null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comments describe jsondata
as path/url to the JSON data, and the method above is called fetchJSON
, but the use of |tojson
in the flask template and from inspecting the running version (which seems slightly different), it looks like you are passing json data as a string. Based on some commented out code I suspect that an earlier implementation used a url and a fetch
call.
It would help to more clearly name the variables and methods to accurately reflect this, and perhaps distinguish between the json data as string and as object.
* @param {String} path - path or url to JSON file | ||
* @returns {object} parsed JSON object | ||
*/ | ||
async function fetchJSON(data) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only used once, not sure it merits a separate function.
Thanks a lot @raffazizzi and @rlskoeser for your extensive and valuable comments. The meetup at the end of the code review helped me a lot to understand the context of your comments. Based on your input it seems the most sustainable approach to cleaning up the code is to actually restart from scratch and develop a standalone js package. This way I can better address re-usability and namespace pollution and so on, and test the critical logic parts of the code for the network creation. Have a nice week and see you another time 👋 💙 |
The ticket for this code review is: Issue 8
This repository is ready for review