-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathscript.js
390 lines (377 loc) · 15.9 KB
/
script.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
const io = {
upload(event) {
switch (event.target.dataset.type) {
case "csv":
event.preventDefault();
fetch("index.php", {method: "POST", body: new FormData(document.getElementById("form_csv"))})
.then(response => {return response.json()})
.then(data => {
memory.data(data);
table.init(memory.data());
});
document.getElementById("csv").value = "";
break;
case "json":
const file = document.getElementById("json").files[0];
const reader = new FileReader();
reader.addEventListener("load", (event) => {
const j = JSON.parse(event.target.result);
memory.data(j.data);
memory.options = j.options;
memory.sortable = j.sortable;
memory.theme(j.theme);
memory.title(j.title); // override the default one assigned in the previous statement
// todo decode j.options and use them
table.init(memory.data());
});
reader.readAsText(file);
document.getElementById("json").value = "";
break;
}
},
url() {
const file = location.search.substring(1);
fetch(file, {method: "GET"})
.then(response => {return response.json()})
.then(data => {
memory.options = data.options;
memory.sortable = data.sortable;
memory.theme(data.theme);
menu.title(data.title);
table.init(JSON.stringify(data.data));
});
},
download(target) {
if (memory.fullUI) {
memory.options = menu.options();
memory.sortable = menu.sortable();
}
const json_string =
"{\"data\": " + memory.data() +
", \"title\": \"" + memory.title() +
"\", \"options\": " + JSON.stringify(memory.options) +
", \"rows\": " + JSON.stringify(menu.selected_rows()) +
", \"sortable\": " + memory.sortable +
", \"theme\": \"" + memory.theme() +
"\"}";
const blob = new Blob([json_string], {type: "text/json" });
target.download = memory.title() + ".json";
target.href = window.URL.createObjectURL(blob);
}
}
const memory = {
default_title: "Time Line",
fullUI: true,
options: undefined,
sortable: undefined,
exists() {
return localStorage.getItem("data") !== null;
},
data(data) {
if (data === undefined) {
return localStorage.getItem("data");
}
else {
this.title(this.default_title);
localStorage.setItem("data", JSON.stringify(data));
}
},
theme(theme) {
if (theme === undefined) {
return document.documentElement.getAttribute("data-bs-theme");
}
else {
document.documentElement.setAttribute("data-bs-theme", theme);
}
},
title(title) {
if (title === undefined) {
return localStorage.getItem("title");
}
else {
if (title === null || title.trim() === "") {title = this.default_title;}
localStorage.setItem("title", title);
menu.title(title);
}
}
}
const menu = {
all_rows: ".dropdown-item[data-menu='rows']",
checked_options: ".dropdown-item[data-menu='options'][data-checked='true']",
checked_rows: ".dropdown-item[data-menu='rows'][data-checked='true']",
color_picker: document.getElementById("color_picker"),
custom_title: document.getElementById("custom_title"),
end_of_row_list: "#view_menu .dropdown-menu",
page_title: document.getElementById("page_title"),
save_menu: document.querySelector(".dropdown-item[data-menu='file'][data-item='save']"),
sort_option: document.querySelector(".dropdown-item[data-menu='options'][data-item='nosort']"),
unchecked_rows: ".dropdown-item[data-menu='rows'][data-checked='false']",
view_menu: document.querySelector("#view_menu .dropdown-toggle"),
add_rows() {
document.querySelectorAll(this.all_rows).forEach(element => {
element.remove(); // get rid of menu items left over from a previous chart, if any
});
const insertion_point = document.querySelector(this.end_of_row_list);
table.master.getDistinctValues(0).forEach(category => {
var badge_count = table.master.getFilteredRows([{column: 0, value: category}]).length.toString();
var li = document.createElement("LI");
li.setAttribute("class", "position-relative");
var a = document.createElement("A");
a.href = "#";
a.setAttribute("onclick", "menu.toggle(event.target);");
a.setAttribute("class", "dropdown-item");
a.dataset.menu = "rows";
a.dataset.checked = "true";
a.dataset.label = "Show " + category;
a.dataset.value = category;
a.innerText = "Hide " + category;
var s = document.createElement("SPAN");
s.setAttribute("class", "badge bg-info rounded-pill");
s.innerText = badge_count;
li.append(a);
li.append(s);
// want to add them before the divider and reset menu choice at the bottom
insertion_point.insertBefore(li, insertion_point.children.item(insertion_point.children.length - 2));
});
},
options() {
const option_object = {};
if (!timeline.zoomable) {return option_object;} // when making chart from zoomed data, don't respect any of the other options user has set
option_object["timeline"] = {};
document.querySelectorAll(this.checked_options).forEach(element => {
switch (element.dataset.item) {
case "expand":
Object.assign(option_object["timeline"], {colorByRowLabel: true, groupByRowLabel: false});
break;
case "hide":
Object.assign(option_object["timeline"], {showRowLabels: false});
break;
case "same":
Object.assign(option_object["timeline"], {colorByRowLabel: true});
break;
case "single":
// Object.assign(option_object["timeline"], {singleColor: this.color_picker.value});
let interactivity = true;
option_object["colors"] = table.colors().map(element => {
interactivity = interactivity ? !element : interactivity;
return element ? "transparent" : this.color_picker.value;
});
option_object["enableInteractivity"] = interactivity;
break;
case "sort":
break;
case "wider":
option_object["width"] = timeline.element.offsetWidth * 2;
break;
}
});
return option_object;
},
reset() {
document.querySelectorAll(this.checked_options).forEach(element => { // set options menu items to default/initial state
element.innerText = [element.dataset.label, element.dataset.label = element.innerText][0];
element.dataset.checked = "false";
});
document.querySelectorAll(this.unchecked_rows).forEach(element => { // set rows menu items to default/initial state
element.innerText = [element.dataset.label, element.dataset.label = element.innerText][0];
element.dataset.checked = "true";
});
table.transform();
},
selected_rows() {
const rows = [];
document.querySelectorAll(this.checked_rows).forEach(element => {
rows.push(element.dataset.value);
});
return rows;
},
show() {
this.title(memory.title());
// on file menu
this.save_menu.classList.remove("disabled");
this.view(true);
},
sortable() {
return this.sort_option.dataset.checked === "false";
},
title(title) {
this.page_title.innerHTML = title;
this.custom_title.value = title;
},
toggle(menu_item) {
if (menu_item.dataset.menu === "theme") {
memory.theme(menu_item.dataset.item);
document.querySelectorAll("[data-menu='theme']").forEach(element => {
element.classList.toggle("d-none"); // flip selection between only two choices, light and dark
});
timeline.x_axis(document.querySelector(".offcanvas-body header"));
timeline.x_axis(timeline.element);
}
else {
menu_item.innerText = [menu_item.dataset.label, menu_item.dataset.label = menu_item.innerText][0]; // swap data-label with innerText
menu_item.dataset.checked = menu_item.dataset.checked === "false"; // set true to false and false to true
table.transform(); // make a new timeline with these changed settings in mind
}
},
view(state) {
const cl = this.view_menu.classList;
if (state) {
cl.remove("disabled");
}
else {
cl.add("disabled");
}
}
}
const table = {
master: undefined,
copy: undefined,
colors() {
const start_date_column = this.copy.getNumberOfColumns() - 2;
const end_date_column = start_date_column + 1;
const color_list = [];
for (let index = 0; index < this.copy.getNumberOfRows(); index++) {
var current_start = this.copy.getValue(index, start_date_column);
var current_end = this.copy.getValue(index, end_date_column);
color_list.push(current_start == current_end);
}
return color_list;
},
convert(input) {
input.forEach(row => {
var end = row.length - 1; // if 3 columns, end date is 3rd; if 4, end date is 4th (0-based)
if (!row[end]) { // end date is empty (false)
row[end] = Date.now(); // so change it to 'now'
}
});
return new google.visualization.arrayToDataTable(input);
},
filter() {
if (!memory.fullUI) {return;}
const rows = this.copy.getFilteredRows(
[{column: 0, test: (value, rowId, columnId, datatable) => {return menu.selected_rows().includes(value);}}]
);
const view = new google.visualization.DataView(this.copy);
view.setRows(rows);
this.copy = view.toDataTable();
},
init(data) { // every time data is found in memory, via File/New, Imported from JSON, etc.
this.master = this.convert(JSON.parse(data));
if (memory.fullUI) {
menu.add_rows();
menu.reset(); // duplicates table.transform below
menu.show();
}
this.transform();
},
sort(test) {
if (!test) {return;} // "don't sort" was specified in options
// either by row label, then start date; or by row label, then start date, then bar label
const columns = this.copy.getNumberOfColumns() == 3 ? [0, 1] : [0, 2, 1];
const rows = this.copy.getSortedRows(columns);
const view = new google.visualization.DataView(this.copy);
view.setRows(rows);
this.copy = view.toDataTable();
},
transform() {
this.copy = this.master.clone(); // always use a copy, because some optional timeline actions change the underlying data
this.filter();
if (memory.fullUI) {
memory.options = menu.options();
memory.sortable = menu.sortable();
}
this.sort(memory.sortable);
// document.documentElement.setAttribute("data-bs-theme")
timeline.draw(this.copy);
},
zoom() {
const target = timeline.chart.getSelection()[0].row;
if (isNaN(target)) {return;}
const start_date_column = this.copy.getNumberOfColumns() - 2;
const end_date_column = start_date_column + 1;
const target_start = this.copy.getValue(target, start_date_column);
const target_end = this.copy.getValue(target, end_date_column);
for (let index = 0; index < this.copy.getNumberOfRows(); index++) {
var current_start = this.copy.getValue(index, start_date_column);
var current_end = this.copy.getValue(index, end_date_column);
if (current_start < target_start) {
if (current_end > target_start) {
current_start = target_start;
}
}
if (current_end > target_end) {
if (current_start < target_end) {
current_end = target_end;
}
}
this.copy.setValue(index, start_date_column, current_start);
this.copy.setValue(index, end_date_column, current_end);
}
const view = new google.visualization.DataView(this.copy);
view.setRows(this.copy.getFilteredRows([
{column: start_date_column, test: (value, rowId, columnId, datatable) => {return value < target_end;}},
{column: end_date_column, test: (value, rowId, columnId, datatable) => {return value > target_start;}}
]));
document.querySelector("#view_menu .dropdown-menu").classList.remove("show"); // in case it was showing, dismiss it
timeline.draw(view);
}
}
const timeline = {
chart: undefined,
element: document.getElementById("timeline"),
selectable: true,
zoomable: true,
load() { // this is what starts everything
google.charts.load("current", {"packages": memory.fullUI ? ["table", "timeline"] : ["timeline"]});
google.charts.setOnLoadCallback(() => {this.init()});
},
init() {
if (memory.fullUI) {initialize_help();}
this.chart = new google.visualization.Timeline(this.element);
this.when_ready();
if (memory.fullUI) {
document.getElementById("whole_menu").classList.remove("invisible");
google.visualization.events.addListener(this.chart, "select", function() {
if (timeline.selectable) {
if (timeline.zoomable) {
menu.view(false); // hide the view menu so as to disallow modifications to the about-to-be-zoomed chart
table.zoom();
} else {
menu.view(true);
table.transform();
}
timeline.zoomable = !timeline.zoomable;
}
});
if (memory.exists()) {
table.init(memory.data());
}
else {
document.getElementById("help").classList.add("show");
}
}
else {
io.url();
}
},
draw(data) {
document.querySelectorAll(".google-visualization-tooltip").forEach(element => {
element.remove(); // removes any stray lingering tooltips that didn't disappear off screen automatically
});
this.chart.draw(data, memory.options);
},
when_ready(chart = this.chart, container = this.element) {
// correct x-axis text colors on transform, in case dark mode is set at top level
google.visualization.events.addListener(chart, 'ready', function () {
timeline.x_axis(container);
});
},
x_axis(container) { // fix colors on timeline x-axis to contract with screen's background color (light or dark)
var color = memory.theme() === "dark" ? "#ffffff" : "#000000";
container.querySelectorAll("text").forEach(element => {
if (element.getAttribute('text-anchor') === 'middle') {
element.setAttribute('fill', color);
}
});
}
}