model-navigator-standalone
Version:
Standalone MDF graph model visualizer
438 lines (385 loc) • 13 kB
JavaScript
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 `${sizeStr} ${suffix}`;
};
export const getSubmitPath = (project) => {
const path = project.split('-');
const programName = path[0];
const projectCode = path.slice(1).join('-');
return `${submissionApiPath}/${programName}/${projectCode}`;
};
export const jsonToString = (data, schema = {}) => {
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(([k]) => 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(([k]) => k);
}
export const parseParamWidth = (width) => ((typeof width === 'number') ? `${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${dd}`; }
if (mm < 10) { mm = `0${mm}`; }
const todaysDate = `${yyyy}-${mm}-${dd}`;
let hours = date.getHours();
let minutes = date.getMinutes();
let seconds = date.getSeconds();
if (hours < 10) { hours = `0${hours}`; }
if (minutes < 10) { minutes = `0${minutes}`; }
if (seconds < 10) { seconds = `0${seconds}`; }
return filePreFix ? `${filePreFix}${fileName} ${todaysDate} ${hours}-${minutes}-${seconds}`
: `${fileName} ${todaysDate} ${hours}-${minutes}-${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'} ${c.target_type}.${c.targetId}`;
}
});
}
return line;
};
export const convertToTSV = (node) => {
let line = tsvMiddleware(node);
Object.keys(node.properties).forEach((key) => {
line += ('\t').concat(`${key}`);
});
const text = `${line}\r\n${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(([key, value]) => {
if (value.src !== 'Loader-derived') {
line += ('\t').concat(`${key}`);
}
});
const text = `${line}\r\n${node.title}`;
return text;
};
export const generateVocabFullDownload = (fullDictionary, format, prefix = "") => {
const c2nl = category2NodeList(fullDictionary);
const enumArr = [];
const zip = new JSZip();
Object.keys(c2nl).forEach((category) => {
const nodes = c2nl[category];
nodes.forEach(({ title, properties }) => {
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) => `${createFileName(`${title}-${propertyKey}`, prefix + 'Controlled_Vocabulary-')}.${format}`
switch (format) {
case 'TSV': {
const vocabTSVArr = enumArr.map(({ enums, title, propertyKey }) => {
let content = '';
if (enums && enums.length) {
enums.forEach((item, index) => {
content += (index === 0) ? item : `${'\n'}${item}`;
});
}
return { content, title, propertyKey };
});
vocabTSVArr.forEach(({ title, propertyKey, content }) => 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(({ title, enums, propertyKey }) => 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 (configUrl = "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(({ title, content, format }) => { zip.file(`${createFileName(title)}.${format}`, content); });
zip.generateAsync({ type: 'blob' }).then((thisContent) => {
saveAs(thisContent, createFileName(title));
});
} catch {
throw Error("Failed to fetch example files");
}
}
export const downloadLoadingExample = async (zipUrl = "") => {
window.open(zipUrl, '_blank');
};
export function category2NodeList(model) {
return Object.fromEntries(
model.tag_kvs('Category')
.map( ([key, value]) => {
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,
};
}