From 6c1d57fdb5dadcff6ae18c3907eff787c0e1cc9a Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 3 Oct 2023 11:56:57 -0400 Subject: [PATCH] add example links for best "variant callers" layouts --- package-lock.json | 10 ++--- package.json | 2 +- pages/index.tsx | 105 +++++++++++++++++++++++++++++++--------------- src/lib/layout.ts | 2 +- 4 files changed, 78 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6abb676..368d44d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "buffer": "^6.0.3", "lodash": "^4.17.21", "next": "^13.2.4", - "next-utils": "https://gitpkg.now.sh/runsascoded/next-utils/dist?ab15477", + "next-utils": "https://gitpkg.now.sh/runsascoded/next-utils/dist?2865e86", "react": "^18.2.0", "react-bootstrap": "^2.8.0", "react-dom": "^18.0.0", @@ -9899,8 +9899,8 @@ }, "node_modules/next-utils": { "version": "0.1.2", - "resolved": "https://gitpkg.now.sh/runsascoded/next-utils/dist?ab15477", - "integrity": "sha512-VgMwmgkL4bZsqgMSXR8iZUnFvvUeXHgW+Y41nEPnXCpmuSAMOfIQr5169f7kYAqVcb9n8Rj+NAjqEYmxnvc81Q==", + "resolved": "https://gitpkg.now.sh/runsascoded/next-utils/dist?2865e86", + "integrity": "sha512-QZW5Q04bcJST1I8Ki1q7bRXPGzJNzvwixKe2lYvo9yaqptyyImZ4OP/VnYn+aSuu9WKOaC6R/4ntcjbQ/eRZpQ==", "dependencies": { "@duckdb/duckdb-wasm": "^1.24.0", "@mdx-js/loader": "^2.3.0", @@ -21136,8 +21136,8 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, "next-utils": { - "version": "https://gitpkg.now.sh/runsascoded/next-utils/dist?ab15477", - "integrity": "sha512-VgMwmgkL4bZsqgMSXR8iZUnFvvUeXHgW+Y41nEPnXCpmuSAMOfIQr5169f7kYAqVcb9n8Rj+NAjqEYmxnvc81Q==", + "version": "https://gitpkg.now.sh/runsascoded/next-utils/dist?2865e86", + "integrity": "sha512-QZW5Q04bcJST1I8Ki1q7bRXPGzJNzvwixKe2lYvo9yaqptyyImZ4OP/VnYn+aSuu9WKOaC6R/4ntcjbQ/eRZpQ==", "requires": { "@duckdb/duckdb-wasm": "^1.24.0", "@mdx-js/loader": "^2.3.0", diff --git a/package.json b/package.json index 20ad6f4..ecdb0ec 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "buffer": "^6.0.3", "lodash": "^4.17.21", "next": "^13.2.4", - "next-utils": "https://gitpkg.now.sh/runsascoded/next-utils/dist?ab15477", + "next-utils": "https://gitpkg.now.sh/runsascoded/next-utils/dist?2865e86", "react": "^18.2.0", "react-bootstrap": "^2.8.0", "react-dom": "^18.0.0", diff --git a/pages/index.tsx b/pages/index.tsx index 65771a8..224a38d 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -27,11 +27,12 @@ import {CircleCoords, Coord, makeVars, Vars, XYRRCoords, XYRRTCoords} from "../s import {ShapesTable} from "../src/components/tables/shapes"; import useLocalStorageState from 'use-local-storage-state' import _ from "lodash" -import {getHashMap, getHistoryStateHash, Param, ParsedParam, parseHashParams, updatedHash, updateHashParams} from "next-utils/params"; +import {getHashMap, getHistoryStateHash, HashMapVal, Param, ParsedParam, parseHashParams, updatedHash, updateHashParams} from "next-utils/params"; import CopyLayout from "../src/components/copy-layout" import {precisionSchemes, ShapesParam} from "../src/lib/shapes-buffer"; import {Checkbox, Number, Select} from "../src/components/controls"; import {useRouter} from "next/router"; +import Link from "next/link"; const Plot = dynamic(() => import("react-plotly.js"), { ssr: false }) @@ -161,12 +162,12 @@ export function DetailsSection({ title, tooltip, open, toggle, className, childr ) } -export type LinkItem = { name: string, val: Val, description: ReactNode } -export function Links({ links, cur, setVal, activeVisited, }: { +export type LinkItem = { name: string, val: Val | string, description: ReactNode } +export function Links({ links, cur, setVal, setHash, }: { links: LinkItem[] - cur: Val + cur: Val | string setVal: Dispatch - activeVisited?: boolean + setHash: (hash: string) => void }): [ () => void, ReactNode ] { const [ showTooltip, setShowTooltip ] = useState(null) return [ @@ -175,25 +176,28 @@ export function Links({ links, cur, setVal, activeVisited, }: { links.map(({ name, val, description }, idx) => { const overlay = console.log("tooltip click:", name)}>{description} const isCurVal = _.isEqual(cur, val) - // console.log("link:", isCurVal, cur, val) - const a = (className?: string) => { - setVal(val) - setShowTooltip(null) - e.preventDefault() - e.stopPropagation() - console.log(`clicked link: ${name}`) - }}>{name} + console.log("link:", isCurVal, cur, val) + const a = ( + typeof val === 'string' + ? { + console.log("Setting hash:", val) + setHash(val) + setShowTooltip(null) + }} + >{name} + : { + setVal(val) + setShowTooltip(null) + e.preventDefault() + e.stopPropagation() + console.log(`clicked link: ${name}`) + }}>{name} + ) return (
  • - { - isCurVal - ? ( - activeVisited - ? a(css.activeLink) - : {name} - ) : a() - } - {' '} + {a}{' '} { @@ -235,26 +239,35 @@ export const initialLayoutKey = "initialLayout" export const shapesKey = "shapes" export const targetsKey = "targets" +export const VariantCallersPaperLink = Roberts et al (2013) + const layouts: LinkItem[] = [ { name: "Ellipses", val: Ellipses4t, description: "4 ellipses intersecting to form all 15 possible regions, rotated -45°", }, { name: "Ellipses (axis-aligned)", val: Ellipses4, description: "Same as above, but ellipse axes are horizontal/vertical (and rotation is disabled)", }, { name: "Circles", val: Circles, description: "4 circles in a diamond shape, such that 2 different subsets (of 3) are symmetric, and 11 of 15 possible regions are represented (missing 2 4C2's and 2 4C3's).", }, - { name: "Disjoint", val: Disjoint, description: "4 disjoint circles" }, + { name: "Disjoint", val: Disjoint, description: "4 disjoint circles. When two (or more) sets are supposed to intersect, but don't, a synthetic penalty is added to the error computation, which is proportional to: 1) each involved set's distance to the centroid of the centers of the sets that are supposed to intersect, as well as 2) the size of the target subset. This \"disjoint\" initial layout serves demonstrate/test this behavior. More sophisticated heuristics would be useful here, as the current scheme is generally insufficient to coerce all sets into intersecting as they should." }, + { name: "Variant callers (best)", val: "#s=Mzxv4Cc95664TAhIgtTaZ1wTbpB32hca6RnYrxzN5QRgbF4oaXr5MStC6KxNYYZy5g5IuzaS1moF4lLWtIXXY-VOO2f8wNvsQk9Jqqfg0B-RDkXMZTCTpTaymPnuwF-vswFGRVwFE4hgScC1ofXRaBdnvzm84fjZ8wtEkWHaqiifUM4TVEtIbh8&t=633,618,112,187,0,14,1,319,13,55,17,21,0,9,36", description: <>Best computed layout for the "variant callers" example, from {VariantCallersPaperLink}. ≈50,000 steps beginning from the "Circles" layout above, error <0.176%.}, + { name: "Variant callers (alternate)", val: "#s=MzC1VAFocttl2gbaDkR1obVIOSo-npdk8mfAn4j0s68wpq4FE4o0YIptFI5hupi525mqCJLTS0BbLsnqcJ0oFOtaun28Afy9HfyAHhdHhtsAsLO8mNdyKFNwt4op_97d4DXguxY3S4k7RxPbNbPIu_2XIvm5qJ0NJn5qsgeVxEhvcgoRO8FFnpU&t=633,618,112,187,0,14,1,319,13,55,17,21,0,9,36", description: <>Another layout for the "variant callers" example, from {VariantCallersPaperLink}. ≈20,000 steps beginning from the "Ellipses" layout above, error ≈2.27%.} // { name: "CircleLattice", layout: SymmetricCircleLattice, description: "4 circles centered at (0,0), (0,1), (1,0), (1,1)", }, ] const layoutsMap = new Map(layouts.map(({ name, val }) => [ name, val ])) -type Params = { +export type Params = { s: Param t: Param } -type ParsedParams = { +export type ParsedParams = { s: ParsedParam t: ParsedParam } -type HistoryState = { +export type HashMap = { + s: HashMapVal + t: HashMapVal +} + +export type HistoryState = { s: ShapesParam t: Targets } @@ -265,8 +278,8 @@ export function Body() { { name: "Fizz Buzz", val: FizzBuzz, description: <>2 circles, of size 1/3 and 1/5, representing integers divisible by 3 and by 5. Inspired by {fizzBuzzLink}. }, { name: "Fizz Buzz Bazz", val: FizzBuzzBazz, description: <>Extended version of {fizzBuzzLink} above, with 3 sets, representing integers divisible by 3, 5, or 7. This is impossible to model accurately with 3 circles, but possible with ellipses. }, { name: "Fizz Buzz Bazz Qux", val: FizzBuzzBazzQux, description: <>Extended version of {fizzBuzzLink} above, with 4 sets, representing integers divisible by 2, 3, 5, or 7. Impossible to model exactly even with 4 ellipses (AFAIK!), but gradient descent gets as close as it can. }, - { name: "3 symmetric sets", val: ThreeEqualCircles, description: <>Simple test case, 3 circles, one starts slightly off-center from the other two, "target" ratios require the 3 circles to be in perfectly symmetric position with each other. }, - { name: "Variant callers", val: VariantCallers, description: <>Values from Roberts et al (2013), "A comparative analysis of algorithms for somatic SNV detection + // { name: "3 symmetric sets", val: ThreeEqualCircles, description: <>Simple test case, 3 circles, one starts slightly off-center from the other two, "target" ratios require the 3 circles to be in perfectly symmetric position with each other. }, + { name: "Variant callers", val: VariantCallers, description: <>Values from {VariantCallersPaperLink}, "A comparative analysis of algorithms for somatic SNV detection in cancer," Fig. 3} ].map(({ name, val, description }) => ({ name, val: makeTargets(val), description })) @@ -484,16 +497,21 @@ export function Body() { [ model,] ) + const getHistoryState = useCallback( + (hash: string) => mapEntries(getHashMap(params, hash), (k, { val }) => [ k, val ]) as HistoryState, + [ params ] + ) + useEffect( () => { - const fn = (e: PopStateEvent) => { + const popStateFn = (e: PopStateEvent) => { const hash = getHistoryStateHash() console.log("popstate: hash", hash, "e.state", e.state, "history.state", history.state) if (!hash) { console.warn(`no hash in history state url ${history.state.url} or as ${history.state.as}`) return } - const { s, t } = mapEntries(getHashMap(params, hash), (k, { val }) => [ k, val ]) as HistoryState + const { s, t } = getHistoryState(hash) if (s) { console.log("setting shapes from history state:", s) setInitialShapes(s.shapes) @@ -508,8 +526,15 @@ export function Body() { console.warn("no targets in history state") } } - window.addEventListener('popstate', fn) - return () => window.removeEventListener('popstate', fn) + const hashChangeFn = (e: HashChangeEvent) => { + console.log("hashchange: oldURL", e.oldURL, "newURL", e.newURL, e) + } + window.addEventListener('popstate', popStateFn) + window.addEventListener('hashchange', hashChangeFn) + return () => { + window.removeEventListener('popstate', popStateFn) + window.removeEventListener('hashchange', hashChangeFn) + } }, [ setInitialShapes, setUrlShapesPrecisionScheme, setTargets, ] ) @@ -1218,6 +1243,18 @@ export function Body() { [ shapes ] ) + const setHash = useCallback( + (hash: string) => { + const { s, t } = getHistoryState(hash) + if (!s) { console.warn(`no s in hash ${hash}`); return } + if (!t) { console.warn(`no t in hash ${hash}`); return } + console.log(`setting shapes and targets from hash ${hash}:`, s.shapes, `(${s.precisionSchemeId})`, t) + setInitialShapes(s.shapes) + setUrlShapesPrecisionScheme(s.precisionSchemeId) + setTargets(t) + }, + [ params, setInitialShapes, setTargets, ] + ) const [ clearExampleTooltip, exampleLinks ] = Links({ links: exampleTargets, cur: targets, @@ -1228,7 +1265,7 @@ export function Body() { setInitialShapes(newShapes) pushHistoryState(newShapes, newTargets, true) }, - activeVisited: true, + setHash, }) const [ clearLayoutTooltip, layoutLinks ] = Links({ links: layouts, @@ -1239,7 +1276,7 @@ export function Body() { setInitialShapes(newShapes) pushHistoryState(newShapes, targets, true) }, - activeVisited: true, + setHash, }) // Clear URL fragment state if `stateInUrlFragment` has been set to `false` diff --git a/src/lib/layout.ts b/src/lib/layout.ts index ef4a36d..b838780 100644 --- a/src/lib/layout.ts +++ b/src/lib/layout.ts @@ -40,8 +40,8 @@ export const Circles: InitialLayout = [ export const Disjoint: InitialLayout = [ { c: { x: 0, y: 0, }, r: { x: 1, y: 1 }, t: 0, }, { c: { x: 3, y: 0, }, r: { x: 1, y: 1 }, t: 0, }, - { c: { x: 0, y: 3, }, r: { x: 1, y: 1 }, t: 0, }, { c: { x: 3, y: 3, }, r: { x: 1, y: 1 }, t: 0, }, + { c: { x: 0, y: 3, }, r: { x: 1, y: 1 }, t: 0, }, ] export const SymmetricCircleLattice: InitialLayout = [ { c: { x: 0, y: 0, }, r: { x: 1, y: 1 }, t: 0, },