model-navigator-standalone
Version:
Standalone MDF graph model visualizer
473 lines (461 loc) • 15 kB
JavaScript
import "core-js/modules/es.array.reduce.js";
import "core-js/modules/es.array.sort.js";
import "core-js/modules/es.json.stringify.js";
import "core-js/modules/es.number.to-fixed.js";
import "core-js/modules/es.object.from-entries.js";
import "core-js/modules/es.promise.js";
import "core-js/modules/es.regexp.exec.js";
import "core-js/modules/es.string.replace.js";
import "core-js/modules/es.string.starts-with.js";
import "core-js/modules/es.string.trim.js";
import "core-js/modules/esnext.iterator.constructor.js";
import "core-js/modules/esnext.iterator.filter.js";
import "core-js/modules/esnext.iterator.for-each.js";
import "core-js/modules/esnext.iterator.map.js";
import "core-js/modules/esnext.iterator.reduce.js";
import "core-js/modules/web.dom-collections.iterator.js";
import * as d3 from 'd3-scale';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
import { fileManifestDownload } from '../../../config/file-manifest-config';
import axios from 'axios';
const submissionApiPath = 'FIXME-submissionApiPath';
export const humanFileSize = size => {
if (typeof size !== 'number') {
return '';
}
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
const sizeStr = (size / 1024 ** i).toFixed(2) * 1;
const suffix = ['B', 'KB', 'MB', 'GB', 'TB'][i];
return "".concat(sizeStr, " ").concat(suffix);
};
export const getSubmitPath = project => {
const path = project.split('-');
const programName = path[0];
const projectCode = path.slice(1).join('-');
return "".concat(submissionApiPath, "/").concat(programName, "/").concat(projectCode);
};
export const jsonToString = function jsonToString(data) {
let schema = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
const replacer = (key, value) => {
if (value === null) {
return undefined;
}
if (schema[key] === 'number') {
const castedValue = Number(value);
if (Number.isNaN(castedValue)) {
return value;
}
return castedValue;
}
return value;
};
return JSON.stringify(data, replacer, ' ');
};
export const predictFileType = (dirtyData, fileType) => {
const predictType = fileType;
const jsonType = 'application/json';
const tsvType = 'text/tab-separated-values';
const data = dirtyData.trim();
if (data.indexOf('{') !== -1 || data.indexOf('}') !== -1) {
return jsonType;
}
if (data.indexOf('\t') !== -1) {
return tsvType;
}
return predictType;
};
/**
* Little wrapper around setinterval with a guard to prevent an async function
* from being invoked multiple times.
*
* @param {()=>Promise} lambda callback should return a Promise
* @param {int} timeoutMs passed through to setinterval
* @return the setinterval id (can be passed to clearinterval)
*/
export async function asyncSetInterval(lambda, timeoutMs) {
let isRunningGuard = false;
return setInterval(() => {
if (!isRunningGuard) {
isRunningGuard = true;
lambda().then(() => {
isRunningGuard = false;
});
}
}, timeoutMs);
}
// export const getCategoryColor = (category) => {
// const colorMap = {
// clinical: '#05B8EE',
// biospecimen: '#27AE60',
// data_file: '#7EC500',
// metadata_file: '#F4B940',
// analysis: '#FF7ABC',
// administrative: '#AD91FF',
// notation: '#E74C3C',
// index_file: '#26D9B1',
// clinical_assessment: '#3283C8',
// medical_history: '#05B8EE',
// satellite: d3.schemeCategory20[11],
// radar: d3.schemeCategory20[16],
// stream_gauge: d3.schemeCategory20[19],
// weather_station: d3.schemeCategory20[10],
// data_observations: d3.schemeCategory20[3],
// experimental_methods: d3.schemeCategory20[4],
// Imaging: d3.schemeCategory20[5],
// study_administration: d3.schemeCategory20[6],
// subject_characteristics: d3.schemeCategory20[7],
// };
// const defaultColor = '#9B9B9B';
// return colorMap[category] ? colorMap[category] : defaultColor;
// };
// export function legendCreator(legendGroup, nodes, legendWidth) {
// // Find all unique categories
// const uniqueCategoriesList = nodes.reduce((acc, elem) => {
// if (acc.indexOf(elem.category) === -1) {
// acc.push(elem.category);
// }
// return acc;
// }, []);
// uniqueCategoriesList.sort((aIn, bIn) => {
// const a = aIn.toLowerCase();
// const b = bIn.toLowerCase();
// if (a < b) {
// return -1;
// } if (a > b) {
// return 1;
// }
// return 0;
// });
// const legendFontSize = '0.9em';
// // Make Legend
// legendGroup.selectAll('text')
// .data(uniqueCategoriesList)
// .enter().append('text')
// .attr('x', legendWidth / 2)
// .attr('y', (d, i) => `${1.5 * (2.5 + i)}em`)
// .attr('text-anchor', 'middle')
// .attr('fill', (d) => getCategoryColor(d))
// .style('font-size', legendFontSize)
// .text((d) => d);
// legendGroup.append('text')
// .attr('x', legendWidth / 2)
// .attr('y', `${2}em`)
// .attr('text-anchor', 'middle')
// .text('Categories')
// .style('font-size', legendFontSize)
// .style('text-decoration', 'underline');
// }
// export function addArrows(graphSvg) {
// graphSvg.append('svg:defs')
// .append('svg:marker')
// .attr('id', 'end-arrow')
// .attr('viewBox', '0 -5 10 10')
// .attr('fill', 'darkgray')
// .attr('refX', 0)
// .attr('refY', 0)
// .attr('markerWidth', 6)
// .attr('markerHeight', 6)
// .attr('orient', 'auto')
// .append('svg:path')
// .attr('d', 'M0,-5L10,0L0,5');
// }
// export function addLinks(graphSvg, edges) {
// return graphSvg.append('g')
// .selectAll('path')
// .data(edges)
// .enter()
// .append('path')
// .attr('stroke-width', 2)
// .attr('marker-mid', 'url(#end-arrow)')
// .attr('stroke', 'darkgray')
// .attr('fill', 'none');
// }
/**
* Compute SVG coordinates fx, fy for each node in nodes.
* Decorate each node with .fx and .fy property as side effect.
*
* @param {Array<Node>} nodes each decorated with a position [width,height] in [0,1]
* @param {*} graphWidth
* @param {*} graphHeight
*/
export function calculatePosition(nodes, graphWidth, graphHeight) {
// Calculate the appropriate position of each node on the graph
const fyVals = [];
const retNodes = nodes;
for (let i = 0; i < nodes.length; i += 1) {
retNodes[i].fx = retNodes[i].position[0] * graphWidth;
retNodes[i].fy = retNodes[i].position[1] * graphHeight;
if (fyVals.indexOf(retNodes[i].fy) === -1) {
fyVals.push(retNodes[i].fy);
}
}
return {
retNodes,
fyValsLength: fyVals.length
};
}
/**
* Type agnostic compare thunk for Array.sort
* @param {*} a
* @param {*} b
*/
export function sortCompare(a, b) {
if (a === b) {
return 0;
}
return a < b ? -1 : 1;
}
export function computeLastPageSizes(filesMap, pageSize) {
return Object.keys(filesMap).reduce((d, key) => {
const result = d;
result[key] = filesMap[key].length % pageSize;
return result;
}, {});
}
export function capitalizeFirstLetter(str) {
const res = str.replace(/_/gi, ' ');
return res.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
}
/**
* Avoid importing underscore just for this ... export for testing
* @method intersection
* @param aList {Array<String>}
* @param bList {Array<String>}
* @return list of intersecting elements
*/
export function intersection(aList, bList) {
const key2Count = aList.concat(bList).reduce((db, it) => {
const res = db;
if (res[it]) {
res[it] += 1;
} else {
res[it] = 1;
}
return res;
}, {});
return Object.entries(key2Count).filter(kv => kv[1] > 1).map(_ref => {
let [k] = _ref;
return k;
});
}
export function minus(aList, bList) {
const key2Count = aList.concat(bList).concat(aList).reduce((db, it) => {
const res = db;
if (res[it]) {
res[it] += 1;
} else {
res[it] = 1;
}
return res;
}, {});
return Object.entries(key2Count).filter(kv => kv[1] === 2).map(_ref2 => {
let [k] = _ref2;
return k;
});
}
export const parseParamWidth = width => typeof width === 'number' ? "".concat(width, "px") : width;
export const isPageFullScreen = pathname => !!(pathname && (pathname.toLowerCase() === '/dd' || pathname.toLowerCase().startsWith('/dd/') || pathname.toLowerCase() === '/cohort-tools' || pathname.toLowerCase().startsWith('/cohort-tools/')));
export const isFooterHidden = pathname => !!(pathname && (pathname.toLowerCase() === '/dd' || pathname.toLowerCase().startsWith('/dd/')));
export function createFileName(fileName, filePreFix) {
const date = new Date();
const yyyy = date.getFullYear();
let dd = date.getDate();
let mm = date.getMonth() + 1;
if (dd < 10) {
dd = "0".concat(dd);
}
if (mm < 10) {
mm = "0".concat(mm);
}
const todaysDate = "".concat(yyyy, "-").concat(mm, "-").concat(dd);
let hours = date.getHours();
let minutes = date.getMinutes();
let seconds = date.getSeconds();
if (hours < 10) {
hours = "0".concat(hours);
}
if (minutes < 10) {
minutes = "0".concat(minutes);
}
if (seconds < 10) {
seconds = "0".concat(seconds);
}
return filePreFix ? "".concat(filePreFix).concat(fileName, " ").concat(todaysDate, " ").concat(hours, "-").concat(minutes, "-").concat(seconds) : "".concat(fileName, " ").concat(todaysDate, " ").concat(hours, "-").concat(minutes, "-").concat(seconds);
}
export const tsvMiddleware = node => {
let line = 'type';
const {
links
} = node;
if (links && links.length) {
links.forEach(c => {
if (c.targetId && String(c.generatedType).toLowerCase() !== 'loader-generated') {
line += '\t'.concat(" ", c.target_type, ".").concat(c.targetId);
}
});
}
return line;
};
export const convertToTSV = node => {
let line = tsvMiddleware(node);
Object.keys(node.properties).forEach(key => {
line += '\t'.concat("".concat(key));
});
const text = "".concat(line, "\r\n").concat(node.title);
return text;
};
export const isFileManifest = node => node.id === 'file';
export const generateFileManifest = node => {
let line = tsvMiddleware(node);
const arr = Object.entries(node.properties);
const mergedArr = arr.concat(fileManifestDownload);
mergedArr.forEach(_ref3 => {
let [key, value] = _ref3;
if (value.src !== 'Loader-derived') {
line += '\t'.concat("".concat(key));
}
});
const text = "".concat(line, "\r\n").concat(node.title);
return text;
};
export const generateVocabFullDownload = function generateVocabFullDownload(fullDictionary, format) {
let prefix = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
const c2nl = category2NodeList(fullDictionary);
const enumArr = [];
const zip = new JSZip();
Object.keys(c2nl).forEach(category => {
const nodes = c2nl[category];
nodes.forEach(_ref4 => {
let {
title,
properties
} = _ref4;
const propertyKeyList = Object.keys(properties);
propertyKeyList.forEach(propertyKey => {
const property = properties[propertyKey];
if (property.enum) {
enumArr.push({
title,
enums: property.enum,
propertyKey
});
}
});
});
});
const zipFileName = createFileName(prefix + 'Controlled_Vocabularies', '');
const getFileName = (title, propertyKey, format) => "".concat(createFileName("".concat(title, "-").concat(propertyKey), prefix + 'Controlled_Vocabulary-'), ".").concat(format);
switch (format) {
case 'TSV':
{
const vocabTSVArr = enumArr.map(_ref5 => {
let {
enums,
title,
propertyKey
} = _ref5;
let content = '';
if (enums && enums.length) {
enums.forEach((item, index) => {
content += index === 0 ? item : '\n'.concat(item);
});
}
return {
content,
title,
propertyKey
};
});
vocabTSVArr.forEach(_ref6 => {
let {
title,
propertyKey,
content
} = _ref6;
return zip.file(getFileName(title, propertyKey, 'tsv'), content);
});
zip.generateAsync({
type: 'blob'
}).then(thisContent => {
saveAs(thisContent, zipFileName);
});
}
break;
// eslint-disable-next-line no-lone-blocks
case 'JSON':
{
enumArr.forEach(_ref7 => {
let {
title,
enums,
propertyKey
} = _ref7;
return zip.file(getFileName(title, propertyKey, 'json'), JSON.stringify(enums));
});
zip.generateAsync({
type: 'blob'
}).then(thisContent => {
saveAs(thisContent, zipFileName);
});
}
break;
default:
break;
}
};
export const generateLoadingExample = async function generateLoadingExample() {
let configUrl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "https://raw.githubusercontent.com/CBIIT/icdc-data-loading-example-sets/main/config.json";
const zip = new JSZip();
// fetch config
const {
loadingExamples,
title
} = await (await axios.get(configUrl)).data;
try {
const titleArr = Object.keys(loadingExamples);
const res = await Promise.all(Object.values(loadingExamples).map(example => axios.get(example)));
const data = res.map((res, index) => ({
title: titleArr[index],
content: res.data,
format: titleArr[index].split('.')[1]
}));
data.forEach(_ref8 => {
let {
title,
content,
format
} = _ref8;
zip.file("".concat(createFileName(title), ".").concat(format), content);
});
zip.generateAsync({
type: 'blob'
}).then(thisContent => {
saveAs(thisContent, createFileName(title));
});
} catch (_unused) {
throw Error("Failed to fetch example files");
}
};
export const downloadLoadingExample = async function downloadLoadingExample() {
let zipUrl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "";
window.open(zipUrl, '_blank');
};
export function category2NodeList(model) {
return Object.fromEntries(model.tag_kvs('Category').map(_ref9 => {
let [key, value] = _ref9;
let cat = value;
return [cat.toLowerCase(), model.tagged_items('Category', cat).filter(item => item._kind === 'Node')];
}));
}
/** cluster props according to the category for PDF download */
export function sortByCategory(c2nl, model) {
const keys = Object.keys(c2nl);
return model.nodes().sort((a, b) => keys.indexOf(a.tags('Category') - keys.indexOf(b.tags('Category'))));
}
export function getNodePropertyCount(model) {
return {
nodesCount: model.nodes().length,
propertiesCount: model.props().length
};
}