From c1d5945cae7a382921558d99c51b982b6cc5bb38 Mon Sep 17 00:00:00 2001 From: Kari Lavikka Date: Fri, 3 Jan 2025 14:00:05 +0200 Subject: [PATCH] refactor: extract default properties from jsdoc annotations --- extract-default-properties.mjs | 45 +++++++++++++++++ src/bellplot.ts | 21 ++++---- src/defaultProperties.ts | 37 ++++++++++++++ src/gui/gui.ts | 34 +++---------- src/jellyfish.ts | 2 +- src/layout.ts | 90 ++++++++++++++++++++++++++++++---- 6 files changed, 182 insertions(+), 47 deletions(-) create mode 100644 extract-default-properties.mjs create mode 100644 src/defaultProperties.ts diff --git a/extract-default-properties.mjs b/extract-default-properties.mjs new file mode 100644 index 0000000..f02f27f --- /dev/null +++ b/extract-default-properties.mjs @@ -0,0 +1,45 @@ +import { createGenerator } from "ts-json-schema-generator"; +import { writeFileSync } from "fs"; + +/** @type {import('ts-json-schema-generator/dist/src/Config').Config} */ +const config = { + tsconfig: "./tsconfig.json", + type: "InSchema", + sortProps: false, +}; + +const schema = createGenerator(config).createSchema(config.type); + +function generateDefaults(schema) { + function generateProps(properties) { + const lines = []; + for (const [key, value] of Object.entries(properties)) { + if (value.default !== undefined) { + lines.push(` ${key}: ${JSON.stringify(value.default)}`); + } else { + console.warn(`Warning: No default defined for '${key}'`); + } + } + return lines; + } + + const jsCode = ` +// Automatically generated by extract-default-properties.mjs + +import { + type LayoutProperties, + type CostWeights +} from "./layout.js"; + +export const DEFAULT_PROPERTIES = { +${generateProps(schema.definitions["LayoutProperties"].properties).join(",\n")} +} as LayoutProperties; + +export const DEFAULT_COST_WEIGHTS = { +${generateProps(schema.definitions["CostWeights"].properties).join(",\n")} +} as CostWeights; +`; + return jsCode; +} + +writeFileSync("src/defaultProperties.ts", generateDefaults(schema)); diff --git a/src/bellplot.ts b/src/bellplot.ts index e4d77c8..33667e5 100644 --- a/src/bellplot.ts +++ b/src/bellplot.ts @@ -10,40 +10,41 @@ import { drawArrowAndLabel } from "./utilityElements.js"; export interface BellPlotProperties { /** * The shape of the bell tip. 0 is a sharp tip, 1 is a blunt tip. + * * @minimum 0 * @maximum 1 + * @default 0.1 */ bellTipShape: number; /** * How much to spread nested bell tips. 0 is no spread, 1 is full spread. + * * @minimum 0 * @maximum 1 + * @default 0.5 */ bellTipSpread: number; /** * The width of strokes in the bell. + * * @minimum 0 * @maximum 10 + * @default 1 */ bellStrokeWidth: number; /** * Where the bell has fully appeared and the plateau starts. + * * @minimum 0 * @maximum 1 + * @default 0.75 */ - plateauPos: number; + bellPlateauPos: number; } -export const DEFAULT_BELL_PLOT_PROPERTIES: BellPlotProperties = { - bellTipShape: 0.1, - bellTipSpread: 0.5, - bellStrokeWidth: 1, - plateauPos: 0.75, -}; - /** * Adds the nested subclones into an SVG group. */ @@ -190,7 +191,7 @@ export function drawBellPlot( if (sampleTakenGuide != "none") { const sw = bellPlotProperties.bellStrokeWidth ?? 1; - const x = Math.round(width * bellPlotProperties.plateauPos); + const x = Math.round(width * bellPlotProperties.bellPlateauPos); g.line(x, sw, x, height - sw) .stroke({ color: "black", @@ -293,7 +294,7 @@ export function treeToShapers( const a = fractionalStep / 2; // Create a plateau at the end so that the right edge looks like // a stacked bar chart. - const b = 1 / props.plateauPos; + const b = 1 / props.bellPlateauPos; transformX = (x) => x * (b - a) + a; } diff --git a/src/defaultProperties.ts b/src/defaultProperties.ts new file mode 100644 index 0000000..591786b --- /dev/null +++ b/src/defaultProperties.ts @@ -0,0 +1,37 @@ + +// Automatically generated by extract-default-properties.mjs + +import { + type LayoutProperties, + type CostWeights +} from "./layout.js"; + +export const DEFAULT_PROPERTIES = { + bellTipShape: 0.1, + bellTipSpread: 0.5, + bellStrokeWidth: 1, + bellPlateauPos: 0.75, + sampleHeight: 110, + sampleWidth: 90, + inferredSampleHeight: 120, + gapHeight: 60, + sampleSpacing: 60, + columnSpacing: 90, + tentacleWidth: 2, + tentacleSpacing: 5, + inOutCPDistance: 0.3, + bundleCPDistance: 0.6, + sampleFontSize: 12, + showLegend: true, + phylogenyColorScheme: true, + phylogenyHueOffset: 0, + sampleTakenGuide: "text" +} as LayoutProperties; + +export const DEFAULT_COST_WEIGHTS = { + crossing: 10, + pathLength: 2, + orderMismatch: 2, + bundleMismatch: 3, + divergence: 4 +} as CostWeights; diff --git a/src/gui/gui.ts b/src/gui/gui.ts index 5f079fd..57f982b 100644 --- a/src/gui/gui.ts +++ b/src/gui/gui.ts @@ -1,15 +1,14 @@ import GUI, { Controller } from "lil-gui"; import { DataTables, filterDataTablesByPatient } from "../data.js"; import { tablesToJellyfish } from "../jellyfish.js"; -import { - CostWeights, - DEFAULT_COST_WEIGHTS, - LayoutProperties, -} from "../layout.js"; +import { CostWeights, LayoutProperties } from "../layout.js"; import { addInteractions } from "../interactions.js"; import { downloadSvg, downloadPng } from "./download.js"; -import { DEFAULT_BELL_PLOT_PROPERTIES } from "../bellplot.js"; import { escapeHtml } from "../utils.js"; +import { + DEFAULT_COST_WEIGHTS, + DEFAULT_PROPERTIES, +} from "../defaultProperties.js"; interface GeneralProperties { patient: string | null; @@ -21,25 +20,6 @@ const DEFAULT_GENERAL_PROPERTIES = { zoom: 1, } as GeneralProperties; -const DEFAULT_LAYOUT_PROPERTIES = { - sampleHeight: 110, - sampleWidth: 90, - inferredSampleHeight: 120, - gapHeight: 60, - sampleSpacing: 60, - columnSpacing: 90, - tentacleWidth: 2, - tentacleSpacing: 5, - inOutCPDistance: 0.3, - bundleCPDistance: 0.6, - sampleFontSize: 12, - showLegend: true, - phylogenyColorScheme: true, - phylogenyHueOffset: 0, - sampleTakenGuide: "text", - ...DEFAULT_BELL_PLOT_PROPERTIES, -} as LayoutProperties; - export function setupGui(container: HTMLElement, tables: DataTables) { container.innerHTML = HTML_TEMPLATE; const jellyfishGui = container.querySelector(".jellyfish-gui") as HTMLElement; @@ -93,7 +73,7 @@ export function setupGui(container: HTMLElement, tables: DataTables) { layoutFolder.add(layoutProps, "bellTipShape", 0, 1); layoutFolder.add(layoutProps, "bellTipSpread", 0, 1); layoutFolder.add(layoutProps, "bellStrokeWidth", 0, 3); - layoutFolder.add(layoutProps, "plateauPos", 0.2, 1); + layoutFolder.add(layoutProps, "bellPlateauPos", 0.2, 1); layoutFolder.add(layoutProps, "sampleFontSize", 8, 16); layoutFolder.add(layoutProps, "showLegend"); layoutFolder.add(layoutProps, "phylogenyColorScheme"); @@ -188,7 +168,7 @@ function getSavedOrDefaultSettings() { ...(settings.generalProps ?? {}), } as GeneralProperties, layoutProps: { - ...DEFAULT_LAYOUT_PROPERTIES, + ...DEFAULT_PROPERTIES, ...(settings.layoutProps ?? {}), } as LayoutProperties, costWeights: { diff --git a/src/jellyfish.ts b/src/jellyfish.ts index b6ca8b2..d8f4ef3 100644 --- a/src/jellyfish.ts +++ b/src/jellyfish.ts @@ -18,7 +18,6 @@ import { treeIterator, treeToNodeArray } from "./tree.js"; import * as d3 from "d3"; import { CostWeights, - DEFAULT_COST_WEIGHTS, findLegendPlacement, getNodePlacement, LayoutProperties, @@ -41,6 +40,7 @@ import { SubcloneMetricsMap, } from "./composition.js"; import { createDistanceMatrix, jsDivergence } from "./statistics.js"; +import { DEFAULT_COST_WEIGHTS } from "./defaultProperties.js"; /** * This is the main function that glues everything together. diff --git a/src/layout.ts b/src/layout.ts index 5484e73..7ee5aa2 100644 --- a/src/layout.ts +++ b/src/layout.ts @@ -1,9 +1,18 @@ import { BellPlotProperties } from "./bellplot.js"; +import { DEFAULT_COST_WEIGHTS } from "./defaultProperties.js"; import { getBoundingBox, isIntersecting, Rect } from "./geometry.js"; import { NODE_TYPES, SampleTreeNode } from "./sampleTree.js"; import { treeToNodeArray } from "./tree.js"; import { fisherYatesShuffle, SeededRNG } from "./utils.js"; +/** + * This is just an entry point for ts-json-schema-generator. + */ +export interface InSchema { + layoutProps: LayoutProperties; + costWeights: CostWeights; +} + export interface NodePosition { node: SampleTreeNode; top: number; @@ -13,85 +22,117 @@ export interface NodePosition { export interface LayoutProperties extends BellPlotProperties { /** * Height of real sample nodes + * * @minimum 10 + * @default 110 */ sampleHeight: number; /** * Width of sample nodes + * * @minimum 10 + * @default 90 */ sampleWidth: number; /** * Height of inferred sample nodes + * * @minimum 10 + * @default 120 */ inferredSampleHeight: number; /** * Height of gaps between samples. Gaps are routes for tentacle bundles. + * * @minimum 0 + * @default 60 */ gapHeight: number; /** * Vertical space between samples + * * @minimum 0 + * @default 60 */ sampleSpacing: number; /** * Horizontal space between columns + * * @minimum 10 + * @default 90 */ columnSpacing: number; /** * Width of tentacles in pixels + * * @minimum 0 + * @default 2 */ tentacleWidth: number; /** * Space between tentacles in a bundle, in pixels + * * @minimum 0 + * @default 5 */ tentacleSpacing: number; /** * Relative distance of tentacle control points from the edge of the sample node + * * @minimum 0 * @maximum 0.45 + * @default 0.3 */ inOutCPDistance: number; /** * Relative distance of tentacle bundle's control points. The higher the value, * the longer the individual tentacles stay together before diverging. + * * @minimum 0 * @maximum 1.2 + * @default 0.6 */ bundleCPDistance: number; /** * Font size for sample labels + * * @minimum 0 + * @default 12 */ sampleFontSize: number; /** * Whether to show the legend + + * @default true */ showLegend: boolean; /** * Whether to use a color scheme based on phylogeny + * + * @default true */ phylogenyColorScheme: boolean; /** - * Offset for the hue of the phylogeny color scheme + * Offset for the hue of the phylogeny color scheme. If the automatically generated + * hues are not to your liking, you can adjust the hue offset to get a different + * color scheme. + * + * @minimum 0 + * @maximum 360 + * @default 0 */ phylogenyHueOffset: number; @@ -101,26 +142,57 @@ export interface LayoutProperties extends BellPlotProperties { * `"none"` for no guides, * `"line"` for a faint dashed line in all samples, * `"text"` same as line, but with a text label in one of the samples. + * + * @default "text" */ sampleTakenGuide: "none" | "line" | "text"; } export interface CostWeights { + /** + * Weight for tentacle bundles between two pairs of samples crossing each other + * + * @minimum 0 + * @default 10 + */ crossing: number; + + /** + * Weight for the total length of the paths (tentacle bundles) connecting samples + * + * @minimum 0 + * @default 2 + */ pathLength: number; + + /** + * Weight for the mismatch in the order of samples. The order is based on the + * "phylogenetic center of mass" computed from the subclonal compositions. + * + * @minimum 0 + * @default 2 + */ orderMismatch: number; + + /** + * Weight for the mismatch in the placement of bundles. The "optimal" placement is + * based on the subclonal compositions, but such placement may produce excessively + * long tentacle bundles. + * + * @minimum 0 + * @default 3 + */ bundleMismatch: number; + + /** + * Weight for the sum of divergences between adjacent samples + * + * @minimum 0 + * @default 4 + */ divergence: number; } -export const DEFAULT_COST_WEIGHTS: CostWeights = { - crossing: 10, - pathLength: 2, - orderMismatch: 2, - divergence: 3, - bundleMismatch: 4, -}; - export function sampleTreeToColumns(sampleTree: SampleTreeNode) { const nodes = treeToNodeArray(sampleTree);