-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathTabularChunkedView.vue
More file actions
206 lines (183 loc) · 6.36 KB
/
TabularChunkedView.vue
File metadata and controls
206 lines (183 loc) · 6.36 KB
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
<script setup lang="ts">
import { useWindowScroll } from "@vueuse/core";
import axios from "axios";
import { parse } from "csv-parse/sync";
import { computed, onMounted, reactive, ref, watch } from "vue";
import type { HDADetailed } from "@/api";
import type { TableField } from "@/components/Common/GTable.types";
import { getAppRoot } from "@/onload/loadConfig";
import GTable from "@/components/Common/GTable.vue";
interface TabularChunk {
ck_data: string;
offset: number;
data_line_offset: number;
}
interface TabularDataset extends HDADetailed {
metadata_columns?: number;
metadata_column_types?: string[];
metadata_column_names?: string[];
}
interface TabularChunkedViewProps {
options: TabularDataset;
}
const props = defineProps<TabularChunkedViewProps>();
const offset = ref(0);
const loading = ref(true);
// TODO: add visual loading indicator
const atEOF = ref(false);
const tabularData = reactive<{ rows: string[][] }>({
rows: [],
});
const columns = computed(() => {
const columns = Array(props.options.metadata_columns);
// for each column_name, inject header
if (props.options.metadata_column_names && props.options.metadata_column_names?.length > 0) {
props.options.metadata_column_names.forEach((column_name, index) => {
columns[index] = column_name;
});
}
return columns;
});
const columnStyle = computed(() => {
const columnStyle = Array(props.options.metadata_columns);
if (props.options.metadata_column_types && props.options.metadata_column_types?.length > 0) {
props.options.metadata_column_types.forEach((column_type, index) => {
columnStyle[index] = column_type === "str" || column_type === "list" ? "text-left" : "text-right";
});
}
return columnStyle;
});
const fieldKeys = computed(() => {
return Array.from({ length: columns.value.length }, (_, index) => `column_${index}`);
});
const fields = computed<TableField[]>(() => {
return fieldKeys.value.map((key, index) => ({
key,
label: columns.value[index] || `Column ${index + 1}`,
cellClass: columnStyle.value[index],
}));
});
const tableRows = computed(() => {
return tabularData.rows.map((row) => {
const paddedRow =
row.length < columns.value.length ? row.concat(Array(columns.value.length - row.length).fill("")) : row;
return Object.fromEntries(fieldKeys.value.map((key, index) => [key, paddedRow[index] ?? ""]));
});
});
const delimiter = computed(() => {
return props.options.file_ext === "csv" ? "," : "\t";
});
const chunkUrl = computed(() => {
return `${getAppRoot()}dataset/display?dataset_id=${props.options.id}`;
});
// Loading more data on user scroll to (near) bottom.
const { y } = useWindowScroll();
watch(y, (newY) => {
if (
atEOF.value !== true &&
loading.value === false &&
newY > document.body.scrollHeight - window.innerHeight - 100
) {
nextChunk();
}
});
function processChunk(chunk: TabularChunk) {
// parsedChunk is a 2d array of strings
let parsedChunk = [];
try {
parsedChunk = parse(chunk.ck_data, { delimiter: delimiter.value, relax_quotes: true });
} catch (error) {
// If this blows up it's likely data in a comment or header line
// (e.g. VCF files) so just split it by newline first then parse
// each line individually.
parsedChunk = chunk.ck_data.trim().split("\n");
parsedChunk = parsedChunk.map((line) => {
try {
const parsedLine = parse(line, { delimiter: delimiter.value })[0];
return parsedLine || [line];
} catch (error) {
// Failing lines get passed through intact for row-level
// rendering/parsing.
return [line];
}
});
}
parsedChunk.forEach((row: string[], index: number) => {
if (index >= (chunk.data_line_offset || 0)) {
// TODO test perf of a batch update instead of individual row pushes?
tabularData.rows.push(processRow(row));
}
});
// update new offset
offset.value = chunk.offset;
}
function processRow(row: string[]) {
const num_columns = columns.value.length;
if (row.length === num_columns) {
// pass through
return row;
} else if (row.length > num_columns) {
// SAM file or like format with optional metadata included.
return row.slice(0, num_columns - 1).concat([row.slice(num_columns - 1).join("\t")]);
} else if (row.length === 1) {
// Try to split by comma first
let rowDataSplit = row[0]!.split(",");
if (rowDataSplit.length === num_columns) {
return rowDataSplit;
}
// Try to split by tab
rowDataSplit = row[0]!.split("\t");
if (rowDataSplit.length === num_columns) {
return rowDataSplit;
}
// Try to split by space
rowDataSplit = row[0]!.split(" ");
if (rowDataSplit.length === num_columns) {
return rowDataSplit;
}
return row;
} else {
// rowData.length is greater than one, but less than num_columns. Render cells and pad tds.
// Possibly a SAM file or like format with optional metadata missing.
// Could also be a tabular file with a line with missing columns.
return row.concat(Array(num_columns - row.length).fill(""));
}
}
function nextChunk() {
// Attempt to fetch next chunk, given the current offset.
loading.value = true;
axios
.get(chunkUrl.value, {
params: {
offset: offset.value,
},
})
.then((response) => {
if (response.data.ck_data === "") {
// Galaxy returns an empty chunk if there's no more.
atEOF.value = true;
} else {
// Otherwise process the chunk.
processChunk(response.data);
}
loading.value = false;
});
}
onMounted(() => {
// Fetch and render first chunk
nextChunk();
});
</script>
<template>
<div>
<!-- TODO loading spinner locked to top right -->
<GTable
compact
hover
striped
head-variant="dark"
:fields="fields"
:items="tableRows"
:load-more-loading="loading" />
</div>
</template>