diff --git a/docs/docs/loading-data/data-formats.md b/docs/docs/loading-data/data-formats.md index 908a8159..1f847d18 100644 --- a/docs/docs/loading-data/data-formats.md +++ b/docs/docs/loading-data/data-formats.md @@ -9,7 +9,7 @@ This page describes file formats used in Chromoscope. To find a list of required ## Structural Variants (BEDPE) -The structural variants are stored in a BEDPE file. The following columns are used in the browser: +The structural variants are stored in a headed BEDPE file. The order of the columns does not need to be in the exact same order. This is a The following columns are used in the browser: | Property | Type | Note | |---|---|---| @@ -43,7 +43,7 @@ In Chromosope, strands are mapped with the following types of SVs. ## CNV (TSV) -The CNV is stored in a tab-delimited file that is visualized as three tracks: CNV, Gain, and LOH. +The CNV is stored in a headed tab-delimited file that is visualized as three tracks: CNV, Gain, and LOH. The order of the columns does not need to be in the exact same order. | Property | Type | Note | |---|---|---| @@ -63,7 +63,7 @@ https://s3.amazonaws.com/gosling-lang.org/data/SV/7a921087-8e62-4a93-a757-fd8cdb ## Drivers (TSV or JSON) -The drivers are stored in a tab-delimited file. When this file is present, the browser will show drivers that are included in the file only. +The drivers are stored in a headed tab-delimited file. When this file is present, the browser will show drivers that are included in the file only. The order of the columns does not need to be in the exact same order. diff --git a/src/App.tsx b/src/App.tsx index e307c00a..5c41c0f6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,7 @@ import HorizontalLine from './ui/horizontal-line'; import SampleConfigForm from './ui/sample-config-form'; import { BrowserDatabase } from './browser-log'; import legend from './legend.png'; +import UrlsafeCodec from './lib/urlsafe-codec'; const db = new Database(); const log = new BrowserDatabase(); @@ -167,24 +168,39 @@ function App(props: RouteComponentProps) { rightReads.current = []; }, [demo]); + function isWebAddress(url) { + return url.startsWith('http://') || url.startsWith('https://'); + } + useEffect(() => { + const fetchData = async (url) => { + let responseText; + let externalDemo; + if (isWebAddress(url)) { + responseText = await fetch(url).then(response => response.text()); + externalDemo = JSON.parse(responseText); + } else { + externalDemo = await UrlsafeCodec.decode(url); + } + processDemoData(externalDemo); + }; + + function processDemoData(demoData){ + if (Array.isArray(demoData) && demoData.length >= 0) { + setFilteredSamples(demoData); + demoData = demoData[demoIndex.current < demoData.length ? demoIndex.current : 0]; + } else { + setFilteredSamples([demoData]); + } + if (demoData) { + setDemo(demoData); + } + setShowSmallMultiples(true); + setReady(true); + }; + if (externalUrl) { - fetch(externalUrl).then(response => - response.text().then(d => { - let externalDemo = JSON.parse(d); - if (Array.isArray(externalDemo) && externalDemo.length >= 0) { - setFilteredSamples(externalDemo); - externalDemo = externalDemo[demoIndex.current < externalDemo.length ? demoIndex.current : 0]; - } else { - setFilteredSamples([externalDemo]); - } - if (externalDemo) { - setDemo(externalDemo); - } - setShowSmallMultiples(true); - setReady(true); - }) - ); + fetchData(externalUrl); } }, []); @@ -493,6 +509,7 @@ function App(props: RouteComponentProps) { currentSpec.current = JSON.stringify(spec); // console.log('spec', spec); return ( +
+
); // !! Removed `demo` not to update twice since `drivers` are updated right after a demo update. }, [ready, xDomain, visPanelWidth, drivers, showOverview, showPutativeDriver, selectedSvId, breakpoints, svReads]); diff --git a/src/data/driver.custom.json b/src/data/driver.custom.json index 6e59c89d..66ee7b96 100644 --- a/src/data/driver.custom.json +++ b/src/data/driver.custom.json @@ -1,7 +1,7 @@ [ { "chr": "chr2", - "pos": 47806320, + "pos": 47795017, "ref": "G", "alt": "A", "gene": "MSH6", @@ -13,7 +13,7 @@ }, { "chr": "chr6", - "pos": 157133136, + "pos": 156993402, "ref": "G", "alt": "C", "gene": "ARID1B", @@ -26,7 +26,7 @@ }, { "chr": "chr8", - "pos": 38428420, + "pos": 38439986, "ref": "G", "alt": "A", "gene": "FGFR1", @@ -38,7 +38,7 @@ }, { "chr": "chr13", - "pos": 32339132, + "pos": 32357888, "ref": "G", "alt": "T", "gene": "BRCA2", @@ -51,7 +51,7 @@ }, { "chr": "chr17", - "pos": 7675088, + "pos": 7677976, "ref": "C", "alt": "T", "gene": "TP53", @@ -64,7 +64,7 @@ }, { "chr": "chrX", - "pos": 77681733, + "pos": 77645546, "ref": "T", "alt": "C", "gene": "ATRX", @@ -78,19 +78,19 @@ { "gene": "CDKN2A", "chr": "chr9", - "pos": 21981527, + "pos": 21981538, "category": "deletion" }, { "gene": "MET", "chr": "chr7", - "pos": 116735291, + "pos": 116735286, "category": "amplification" }, { "gene": "PTEN", "chr": "chr10", - "pos": 89677278, + "pos": 87917777, "category": "deletion", "biallelic": "yes" } diff --git a/src/data/samples.ts b/src/data/samples.ts index c4bc19ea..53bea3f1 100644 --- a/src/data/samples.ts +++ b/src/data/samples.ts @@ -1,4 +1,4 @@ -import { Assembly } from 'gosling.js/dist/src/core/gosling.schema'; +import { Assembly } from 'gosling.js/dist/src/gosling-schema'; import _7a921087 from '../script/img/7a921087-8e62-4a93-a757-fd8cdbe1eb8f.jpeg'; import _84ca6ab0 from '../script/img/84ca6ab0-9edc-4636-9d27-55cdba334d7d.jpeg'; import _7d332cb1 from '../script/img/7d332cb1-ba25-47e4-8bf8-d25e14f40d59.jpeg'; diff --git a/src/lib/urlsafe-codec.ts b/src/lib/urlsafe-codec.ts new file mode 100644 index 00000000..14d93478 --- /dev/null +++ b/src/lib/urlsafe-codec.ts @@ -0,0 +1,72 @@ +import pako from 'pako'; +import base64Js from 'base64-js'; + +/** + * UrlsafeCodec provides static methods to encode and decode samples + * to and from a URL-safe base64 encoded string. It uses JSON for serialization, + * pako for compression, and base64-js for handling base64 encoding. + */ +class UrlsafeCodec { + /** + * Encodes a sample object into a URL-safe base64 string. + * + * The method serializes the sample to a JSON string, compresses it using pako, + * By default, the header check is turned off. If headerCheck is set to true, the method + * will perform zlib/gzip header and checksum verification during decompression using pako. + * converts the compressed data to a base64 string, and then modifies the base64 string + * to make it URL-safe by replacing '+' with '.', '/' with '_', and '=' with '-'. + * + * @param {Object} sample - The sample object to encode. + * @param {boolean} headerCheck - Optional parameter to enable header and checksum verification. + * @returns {string} A URL-safe base64 encoded string representing the sample. + */ + static encode(sample, headerCheck = false) { + try { + const string = JSON.stringify(sample); + const encoder = new TextEncoder(); + const stringAsUint8Array = encoder.encode(string); + // Set 'raw' to true if headerCheck is false + const compressedUint8Array = pako.deflate(stringAsUint8Array, {raw: !headerCheck }); + const base64Bytes = base64Js.fromByteArray(compressedUint8Array); + const base64Blob = base64Bytes.toString(); + const base64UrlsafeBlob = base64Blob.replace(/\+/g, '.').replace(/\//g, '_').replace(/=/g, '-'); + return base64UrlsafeBlob; + } catch (error) { + console.error('Error encoding sample:', error); + // Handle the error or rethrow, depending on your needs + throw error; + } + } + + /** + * Decodes a URL-safe base64 string back into a sample object. + * + * The method reverses the URL-safe transformation by replacing '.', '_', and '-' + * with '+', '/', and '=' respectively. It then converts the base64 string back to bytes. + * By default, the header check is turned off. If headerCheck is set to true, the method + * will perform zlib/gzip header and checksum verification during decompression using pako. + * Finally, it parses the JSON string to reconstruct the original sample object. + * + * @param {string} encodedString - The URL-safe base64 encoded string to decode. + * @param {boolean} headerCheck - Optional parameter to enable header and checksum verification. + * @returns {Object} The original sample object. + */ + static decode(encodedString, headerCheck = false) { + try { + const base64Blob = encodedString.replace(/\./g, '+').replace(/_/g, '/').replace(/-/g, '='); + const compressedUint8Array = base64Js.toByteArray(base64Blob); + // Set 'raw' to true if headerCheck is false + const bytes = pako.inflate(compressedUint8Array, {raw: !headerCheck }); + const decoder = new TextDecoder(); + const string = decoder.decode(bytes); + const sample = JSON.parse(string); + return sample; + } catch (error) { + console.error('Error decoding string:', error); + // Handle the error or rethrow, depending on your needs + throw error; + } + } +} + +export default UrlsafeCodec; diff --git a/src/main.tsx b/src/main.tsx index 687d69a6..0d8c7dc7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,10 +2,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Route } from 'react-router-dom'; import App from './App'; +import JsonBase64Converter from './ui/json-base64-converter'; ReactDOM.render( - + + , document.getElementById('root') ); diff --git a/src/track/sv.ts b/src/track/sv.ts index 93e71ff5..c18f9bb5 100644 --- a/src/track/sv.ts +++ b/src/track/sv.ts @@ -102,18 +102,6 @@ export default function sv( url, type: 'csv', separator: '\t', - headerNames: [ - 'chrom1', - 'start1', - 'end1', - 'chrom2', - 'start2', - 'end2', - 'sv_id', - 'pe_support', - 'strand1', - 'strand2' - ], genomicFieldsToConvert: [ { chromosomeField: 'chrom1', diff --git a/src/ui/json-base64-converter.tsx b/src/ui/json-base64-converter.tsx new file mode 100644 index 00000000..482bf73d --- /dev/null +++ b/src/ui/json-base64-converter.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import UrlsafeCodec from '../lib/urlsafe-codec'; + +const JsonBase64Converter = () => { + const [jsonText, setJsonText] = useState(''); + const [base64Text, setBase64Text] = useState(''); + const [error, setError] = useState(''); + + const handleEncode = () => { + try { + const jsonObject = JSON.parse(jsonText); + const encoded = UrlsafeCodec.encode(jsonObject); + setBase64Text(encoded); + setError(''); + } catch (error) { + setError('Error parsing JSON. Please enter valid JSON.'); + } + }; + + const handleDecode = () => { + try { + const decoded = UrlsafeCodec.decode(base64Text); + const jsonString = JSON.stringify(decoded, null, 4); + setJsonText(jsonString); + setError(''); + } catch (error) { + setError('Error decoding base64 string. Please enter a valid base64-encoded string.'); + } + }; + + return ( +
+
+

JSON Text

+