Now we've laid out the diagram, we can move on to the final step of drawing, or rendering it. This is mainly dull and technical, relating to getting what we have in mind onto the canvas. Inasmuch as there is anything interesting to the task, it is figuring out how much space to leave in the gaps between the shapes, and how to label both shapes and connectors so that nothing overlaps. And while that may not be boring, it is tricky.
In order to try and break up some of the complexity, I have added another layer into this, which abstracts away the canvas. This also has the benefit of making the render algorithm testable. The abstraction layer, of course, can only be tested using complex image-comparison tools.
Creating the Diagram Tabs
Anyway, first off, we need to dispose of something that's been hanging over us for a while, to wit, finding the tab and the canvas to draw into. As I said before, I want to make this as stable as possible, so we want each tab to be named according to the first node laid out in the diagram (at least until we have some better way of naming things), and then to have the diagrams remain in the same sequence based on this.Then, as we render, we reuse the tabs and canvas objects that already existed, and delete or add any to reflect bigger changes in the overall portfolio of diagrams.
ensureTabs(tabrow, tabdisplay) {(For complicated reasons, this code does not quite align with its history, and only works with the code that comes from polishing the UI which follows).
this.tabs = {};
var titles = tabrow.querySelectorAll(".diagram-tab");
var bodies = tabdisplay.querySelectorAll(".diagram-tab");
var toRemove = [];
for (var i=0;i<titles.length;i++) {
toRemove[titles[i].dataset["diagramFor"]] = { title: titles[i], body: bodies[i] };
}
for (var i=0;i<this.diagrams.length;i++) {
var d = this.diagrams[i];
var t = findTabFor(titles, d.named);
var b = findTabFor(bodies, d.named);
if (!t) {
t = addDiagramTab(tabrow, tabdisplay, d.named);
} else {
delete toRemove[d.named];
t = { title: t, diagram: b };
}
this.tabs[d.named] = t;
}
var keys = Object.keys(toRemove);
for (var i=0;i<keys.length;i++) {
tabrow.removeChild(toRemove[keys[i]]);
}
}
The function to add a new diagram tab looks like this:
function addDiagramTab(titles, display, name) {
var t = document.createElement("div");
t.className = "diagram-tab tab-title";
t.dataset["diagramFor"] = name;
t.appendChild(document.createTextNode(name));
titles.appendChild(t);
clickFor(ev => selectDiagramTab(name)) (t);
var d = document.createElement("div");
d.className = "diagram-tab tab-body";
d.dataset["diagramFor"] = name;
var cd = document.createElement("div");
var c = document.createElement("canvas");
c.className="diagram";
c.setAttribute("width", "1200");
c.setAttribute("height", "800");
cd.appendChild(c);
d.appendChild(cd);
display.appendChild(d);
return { title: t, diagram: d };
}
export default Portfolio;
Rendering the Diagram
Then we can come back and implement the render code. The first step is to implement the Render interface, and collect all the information from Layout about the shapes and connectors. As we do this, we collect metadata about the rows and columns which are implied in the layout.shape(x, y, s) {Ultimately, the connector method needs to do similar analysis, figuring out how wide individual canals will need to be, and thus the spacing between the grid. But we haven't got that far yet, so it's just a question of recording the fact that the connector needs to be drawn.
if (x >= this.maxx) this.maxx = x+1;
if (y >= this.maxy) this.maxy = y+1;
if (!this.rows[y]) {
this.rows[y] = new RowInfo();
}
if (!this.columns[x]) {
this.columns[x] = new ColumnInfo();
}
this.rows[y].include(s);
this.columns[x].include(s);
this.shapes.push({ x: x, y: y, s: s });
}
connector(pts) {When layout is complete, it calls the done method which figures out what the actual size of all the elements, including the overall diagram, will be and then draws all the shapes and connectors in the right places, using the canvas abstraction.
this.connectors.push(pts);
}
done() {
this.figureGrid();
// draw all the shapes
for (var i=0;i<this.shapes.length;i++) {
var si = this.shapes[i];
this.drawShape(si.x, si.y, si.s);
}
// draw the connectors
for (var i=0;i<this.connectors.length;i++) {
var pts = this.connectors[i];
this.drawConnector(pts);
}
}
figureGrid() {
// based on the rows and columns, we can figure out what the grid should be
// because it's a grid, which just need two sets of values, one that maps x units to x locations and one for y
var xpos = minCanal; // the left margin
for (var i=0;i<this.maxx;i++) {
if (!this.columns[i]) continue; // this should not be possible, but safety first ...
var c = this.columns[i];
c.from = xpos;
xpos += c.maxwidth;
c.to = xpos;
c.channels = [];
xpos += c.right;
}
this.totalWidth = xpos;
var ypos = minCanal; // the top margin
for (var i=0;i<this.maxy;i++) {
if (!this.rows[i]) continue; // this should not be possible, but safety first ...
var c = this.rows[i];
c.from = ypos;
ypos += c.maxheight;
c.to = ypos;
c.channels = [];
ypos += c.below;
}
this.totalHeight = ypos;
}
No comments:
Post a Comment