Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/frontend/src/diagram/diagram_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function DiagramDocumentEditor(props: {
const navigate = useNavigate();

const onCreateAnalysis = async (diagramRefId: string) => {
const newRef = createAnalysis("diagram", diagramRefId, api);
const newRef = await createAnalysis("diagram", diagramRefId, api);
navigate(`/analysis/${newRef}`);
};

Expand Down
193 changes: 193 additions & 0 deletions packages/frontend/src/stdlib/analyses/diagram_graph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import type * as Viz from "@viz-js/viz";
import { Show, createSignal } from "solid-js";
import { P, match } from "ts-pattern";

import type { Uuid } from "catlog-wasm";
import type { DiagramAnalysisProps } from "../../analysis";
import type { DiagramJudgment } from "../../diagram";
import type { DiagramAnalysisMeta, Theory } from "../../theory";
import { DownloadSVGButton, GraphvizSVG } from "../../visualization";
import {
type GraphContent,
type GraphvizAttributes,
defaultEdgeAttributes,
defaultGraphAttributes,
defaultNodeAttributes,
graphvizEngine,
graphvizFontname,
svgCssClasses,
} from "./graph";

import baseStyles from "./base_styles.module.css";

/** Configure a graph visualization for use with diagrams in a model. */
export function configureDiagramGraph(options: {
id: string;
name: string;
description?: string;
}): DiagramAnalysisMeta<GraphContent> {
const { id, name, description } = options;
return {
id,
name,
description,
component: (props) => <DiagramGraph title={name} {...props} />,
initialContent: () => ({
tag: "graph",
layout: "graphviz-directed",
}),
};
}

/** Visualize a diagram in a model as a graph.

Such a visualizations makes sense for any discrete double theory and is in
general restricted to basic objects. See `ModelGraph` for more.
*/
export function DiagramGraph(
props: {
title?: string;
} & DiagramAnalysisProps<GraphContent>,
) {
const [svgRef, setSvgRef] = createSignal<SVGSVGElement>();

const graphviz = () => {
const liveModel = props.liveDiagram.liveModel;
const theory = liveModel.theory();
return (
theory &&
diagramToGraphviz(props.liveDiagram.formalJudgments(), theory, {
baseObName(id) {
return liveModel.objectIndex().map.get(id);
},
baseMorName(id) {
return liveModel.morphismIndex().map.get(id);
},
})
);
};

const title = () => props.title ?? "Diagram";

return (
<div class="model-graph">
<div class={baseStyles.panel}>
<span class={baseStyles.title}>{title()}</span>
<span class={baseStyles.filler} />
<DownloadSVGButton
svg={svgRef()}
tooltip={`Export the ${title().toLowerCase()} as SVG`}
size={16}
/>
</div>
<Show when={graphviz()}>
{(graph) => (
<GraphvizSVG
graph={graph()}
options={{
engine: graphvizEngine(props.content.layout),
}}
ref={setSvgRef}
/>
)}
</Show>
</div>
);
}

/** Convert a diagram in a model into a Graphviz graph.
*/
export function diagramToGraphviz(
diagram: DiagramJudgment[],
theory: Theory,
options?: {
baseObName?: (id: Uuid) => string | undefined;
baseMorName: (id: Uuid) => string | undefined;
attributes?: GraphvizAttributes;
},
): Viz.Graph {
const nodes = new Map<string, Required<Viz.Graph>["nodes"][0]>();
for (const judgment of diagram) {
const matched = match(judgment)
.with(
{
tag: "object",
obType: P.select("obType"),
over: {
tag: "Basic",
content: P.select("overId"),
},
},
(matched) => matched,
)
.otherwise(() => null);
if (!matched) {
continue;
}
const { id, name } = judgment;
const { obType, overId } = matched;
const label = [name, options?.baseObName?.(overId)].filter((s) => s).join(" : ");
const meta = theory.instanceObTypeMeta(obType);
nodes.set(id, {
name: id,
attributes: {
id,
label,
class: svgCssClasses(meta).join(" "),
fontname: graphvizFontname(meta),
},
});
}

const edges: Required<Viz.Graph>["edges"] = [];
for (const judgment of diagram) {
const matched = match(judgment)
.with(
{
tag: "morphism",
morType: P.select("morType"),
over: {
tag: "Basic",
content: P.select("overId"),
},
dom: {
tag: "Basic",
content: P.select("domId"),
},
cod: {
tag: "Basic",
content: P.select("codId"),
},
},
(matched) => matched,
)
.otherwise(() => null);
if (!matched) {
continue;
}
const { id, name } = judgment;
const { morType, overId, codId, domId } = matched;
const label = [name, options?.baseMorName?.(overId)].filter((s) => s).join(" : ");
const meta = theory.instanceMorTypeMeta(morType);
edges.push({
head: codId,
tail: domId,
attributes: {
id,
label,
class: svgCssClasses(meta).join(" "),
fontname: graphvizFontname(meta),
},
});
}

const attributes = options?.attributes;
return {
directed: true,
nodes: Array.from(nodes.values()),
edges,
graphAttributes: { ...defaultGraphAttributes, ...attributes?.graph },
nodeAttributes: { ...defaultNodeAttributes, ...attributes?.node },
edgeAttributes: { ...defaultEdgeAttributes, ...attributes?.edge },
};
}
65 changes: 65 additions & 0 deletions packages/frontend/src/stdlib/analyses/graph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type * as Viz from "@viz-js/viz";

import type { BaseTypeMeta } from "../../theory";

import textStyles from "../text_styles.module.css";

/** Graph layout engine supported by CatColab.

Currently we just use Graphviz. In the future we may support other tools.
*/
type LayoutEngine = "graphviz-directed" | "graphviz-undirected";

/** Configuration for an analysis that visualizes a graph. */
export type GraphContent = {
tag: "graph";

/** Layout engine for graph. */
layout: LayoutEngine;
};

export function graphvizEngine(layout: LayoutEngine): Viz.RenderOptions["engine"] {
if (layout === "graphviz-directed") {
return "dot";
} else if (layout === "graphviz-undirected") {
return "neato";
}
}

/** Top-level attributes of a Graphviz graph. */
export type GraphvizAttributes = {
graph?: Viz.Graph["graphAttributes"];
node?: Viz.Graph["nodeAttributes"];
edge?: Viz.Graph["edgeAttributes"];
};

/** Default graph attributes for Graphviz. */
export const defaultGraphAttributes: Required<Viz.Graph>["graphAttributes"] = {
nodesep: "0.5",
};

/** Default node attributes for Graphviz. */
export const defaultNodeAttributes: Required<Viz.Graph>["nodeAttributes"] = {
// XXX: How to set the font size?
fontsize: "20",
shape: "box",
width: 0,
height: 0,
};

/** Default edge attributes for Graphviz. */
export const defaultEdgeAttributes: Required<Viz.Graph>["edgeAttributes"] = {
fontsize: "20",
sep: "5",
};

// XXX: Precise font matching seems impossible here but we'll at least give
// Graphviz a monospace font if and only if we're using one.
export const graphvizFontname = (meta?: BaseTypeMeta): string =>
meta?.textClasses?.includes(textStyles.code) ? "Courier" : "Helvetica";

// XXX: This should probably go somewhere else.
export const svgCssClasses = (meta?: BaseTypeMeta): string[] => [
...(meta?.svgClasses ?? []),
...(meta?.textClasses ?? []),
];
1 change: 1 addition & 0 deletions packages/frontend/src/stdlib/analyses/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./lotka_volterra";
export * from "./model_graph";
export * from "./diagram_graph";
export * from "./stock_flow_diagram";
export * from "./submodel_graphs";
Loading